1: <?php
2: /**
3: * Klutz Comic Class.
4: *
5: * @author Marcus I. Ryan <marcus@riboflavin.net>
6: * @package Klutz
7: */
8: class Klutz_Comic
9: {
10: /**
11: * The title of the comics (Dilbert, The 5th Wave, etc.)
12: *
13: * @var string
14: */
15: var $name = null;
16:
17: /**
18: * The author or authors of the comic (the byline)
19: *
20: * @var string
21: */
22: var $author = null;
23:
24: /**
25: * The URL for the official homepage (not necessarily where we
26: * get the comic from.
27: *
28: * @var string
29: */
30: var $homepage = null;
31:
32: /**
33: * Days (lowercase, three-letter english abbreviation) that this comic is
34: * is available.
35: *
36: * @var array
37: */
38: var $days = array('mon','tue','wed','thu','fri','sat','sun');
39:
40: /**
41: * Some comment to display for this comic
42: *
43: * @var string
44: */
45: var $comment = null;
46:
47: /**
48: * Days behind the current date this comic is published
49: *
50: * @var string
51: */
52: var $offset = 0;
53:
54: /**
55: * Are past episodes available? Some comics are difficult or
56: * impossible to retrieve other than the day it's published.
57: *
58: * @var boolean
59: */
60: var $nohistory = false;
61:
62: /*
63: * Parameters specific to fetching or otherwise processing the comic
64: */
65:
66: /**
67: * Web browser object used to fetch pages
68: *
69: * @var HTTP_Request
70: */
71: var $http = null;
72:
73: /**
74: * The first url we need to hit to get the comic we want.
75: *
76: * @var string
77: */
78: var $url;
79:
80: /**
81: * Headers we need to pass/override to be able to get the comic.
82: * These are passed to HTTP_Request.
83: */
84:
85: /**
86: * The referral URL to use when fetching the comic.
87: *
88: * @var string
89: */
90: var $referer = null;
91:
92: /**
93: * The user-agent to use when fetching the comic.
94: *
95: * @var string
96: */
97: var $agent = null;
98:
99: /**
100: * The username to use when fetching the comic.
101: *
102: * @var string
103: */
104: var $user = null;
105:
106: /**
107: * The password to use when fetching the comic.
108: *
109: * @var string
110: */
111: var $pass = null;
112:
113: /**
114: * Cookies to set when fetching the comic.
115: *
116: * @var array
117: */
118: var $cookies = array();
119:
120: /**
121: * Headers to set when fetching the comic.
122: *
123: * @var array
124: */
125: var $headers = array();
126:
127: /**
128: * An array of the fields we need to do substitution on.
129: *
130: * @var array
131: */
132: var $subs = null;
133:
134: //
135: // used for the {i} construct (for sites that id comics by "instance")
136: //
137:
138: /**
139: * Method for counting instances (when using the 'i' construct in
140: * substitutions.
141: *
142: * @var string
143: */
144: var $itype = null;
145:
146: /**
147: * Format string for the instance construct (printf string)
148: *
149: * @var string
150: */
151: var $iformat = '%d';
152:
153: /**
154: * The number of the "first" instance of the comic (the reference
155: * number) when using the reference-based instance type
156: *
157: * @var integer
158: */
159: var $icount = 0;
160:
161: /**
162: * The date for which the reference is icount.
163: *
164: * @var date
165: */
166: var $idate = null;
167:
168: /**
169: * The day the "week" starts for instance type weekly.
170: * Abbreviated day name in english, lowercase.
171: *
172: * @var string
173: */
174: var $isow = 'sun';
175:
176: /**
177: * The array of overrides by weekday. If sun_url exists, then
178: * when trying to fetch the sunday edition of this comic, it will
179: * fetch it from the specified url instead of $url.
180: *
181: * @var array
182: */
183: var $override = array();
184:
185: /**
186: * Loads the $comics[$comic] array into this object
187: *
188: * @param string $comic The comic to create this object from
189: */
190: function Klutz_Comic(&$comic)
191: {
192: // what variables should we try to set directly from the comic array?
193: $vars = array('name', 'author', 'homepage', 'comment', 'offset',
194: 'url', 'itype', 'iformat', 'icount', 'idate', 'isow',
195: 'referer', 'agent', 'user', 'pass', 'nohistory');
196:
197: // set the variables for this object that we've been passed
198: foreach ($vars as $field) {
199: if (!empty($comic[$field]) && !is_array($comic[$field])) {
200: $this->$field = $comic[$field];
201: unset($comic[$field]);
202: }
203: }
204:
205: if (!is_null($this->idate) && !is_numeric($this->idate)) {
206: $this->idate = strtotime($this->idate);
207: }
208:
209: // What arrays should we try to set from the comic array, and
210: // do we want to perform a function?
211: $arrays = array('days' => 'strtolower', 'headers' => null,
212: 'subs' => 'strtolower', 'cookies' => null);
213:
214: // set the arrays - make sure each is an array & values lowercased!
215: foreach ($arrays as $field => $function) {
216: if (isset($comic[$field]) && is_array($comic[$field])) {
217: if (is_null($function)) {
218: $this->$field = $comic[$field];
219: } else {
220: $this->$field = array_map($function, $comic[$field]);
221: }
222: unset($comic[$field]);
223: }
224: }
225:
226: // Set any override strings in $this->override[]. Capitalize
227: // and shorten the day keys to match date('D').
228: if (isset($comic['override']) && is_array($comic['override']) && count($comic['override'])) {
229: foreach ($comic['override'] as $dow => $value) {
230: if (strlen($dow) >= 3) {
231: $this->override[ucfirst(substr($dow,0,3))] = $value;
232: }
233: }
234:
235: }
236:
237: // Anything left should be specific to the fetch driver.
238: // Let the derivative class handle any extra parsing
239: }
240:
241: /**
242: * Create an HTTP_Request object and set all parameters necessary to
243: * perform fetches for this comic.
244: *
245: * @param timestamp $date Date of the comic to retrieve (default today)
246: */
247: function _initHTTP($date, $url)
248: {
249: if (is_null($this->http)) {
250: $options = array();
251: if (isset($GLOBALS['conf']['http']['proxy']) && !empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
252: $options = array_merge($options, $GLOBALS['conf']['http']['proxy']);
253: }
254:
255: require_once 'HTTP/Request.php';
256: $this->http = new HTTP_Request($url, $options);
257:
258: $v = $this->getOverride("referer", $date);
259: if (!is_null($v)) {
260: $this->http->addHeader('Referer', $v);
261: }
262:
263: $v = $this->getOverride("agent");
264: if (!is_null($v)) {
265: $this->http->addHeader('User-Agent', $v);
266: }
267:
268: $user = $this->getOverride("user", $date);
269: $pass = $this->getOverride("pass", $date);
270: if (!is_null($user) and !is_null($pass)) {
271: $this->http->setBasicAuth($user, $pass);
272: }
273:
274: foreach ($this->getOverride('cookies', $date) as $name => $value) {
275: $this->http->addCookie($name, $value);
276: }
277: foreach ($this->getOverride('headers', $date) as $name => $value) {
278: $this->addHeader($name, $value);
279: }
280: }
281: }
282:
283: /**
284: * Turn the search strings from the configuration file into
285: * preg_match-formatted strings
286: *
287: * @param array $comic An array containing the content portion of Perl
288: * regular expressions
289: *
290: * @return array Search strings properly formatted to be used with
291: * preg_match
292: *
293: * @access private
294: */
295: function _prepareSearch($search)
296: {
297: foreach (array_keys($search) as $i) {
298: if (is_array($search[$i])) {
299: $search[$i] = $this->_prepareSearch($search[$i]);
300: } elseif (substr($search[$i], 0, 1) != '|') {
301: $search[$i] = '|' . $search[$i] . '|i';
302: }
303: }
304: return $search;
305: }
306:
307: /**
308: * Check for "override" settings - settings that override other
309: * settings depending on the day on which the comic appears
310: *
311: * @param string $setting The name of the setting to override
312: * @param timestamp $date The date to check for overrides
313: * @param string $array_map Filter to be used with array_map
314: *
315: * @return mixed If the setting is an array, returns the setting passed
316: * through array_map if array_map was passed. Otherwise,
317: * returns the value of the setting, overridden if an
318: * override is present
319: */
320: function getOverride($setting, $date = null, $array_map = null)
321: {
322: if (is_null($date)) {
323: $date = mktime(0, 0, 0);
324: }
325:
326: $day = date('D', $date);
327:
328: if (isset($this->override[$day][$setting])) {
329: if ((isset($this->$setting) && is_array($this->$setting)) ||
330: !is_null($array_map)) {
331: if (is_array($this->$setting)) {
332: return array_map($array_map,
333: array($this->override[$day][$setting]));
334: } else {
335: return array_map($array_map, $this->override[$day][$setting]);
336: }
337: } else {
338: return $this->override[$day][$setting];
339: }
340: } else {
341: return $this->$setting;
342: }
343: }
344:
345: /**
346: * Process known substitutions in a string. Currently known options:<br />
347: * o {dow(int day, string format)} day is numeric day of the week, format
348: * format is an strftime string (e.g. '%Y%m%d'), replaced with the
349: * formatted date for the requested day of the week
350: * o {i} replaced with the instance of this comic as determined by
351: * the various instance configuration options<br />
352: * o {format} format is an strftime string, replaced with todays date
353: * formatted according to the format string<br />
354: * o {lc(string)} replaces string with string lowercased<br />
355: * o {uc(string)} replaces string with string uppercased<br />
356: * o {t(string)} removes extra space surrounding string<br />
357: * o {tl0(string)} removes leading zeroes from string
358: *
359: * @param string $string String to process
360: * @param timestamp $date Date to use when processing subs
361: *
362: * @return string A string with all substitutions made
363: */
364: function substitute($string, $date = null)
365: {
366: if (is_null($date)) {
367: $date = mktime(0, 0, 0);
368: }
369: $d = getdate($date);
370:
371: if (is_array($string)) {
372: foreach (array_keys($string) as $i) {
373: $string[$i] = $this->substitute($string[$i], $date);
374: }
375: return $string;
376: }
377:
378: while (preg_match('/\{dow\((\d)\,\s*(.*?)\)\}/ie',$string,$dow) > 0) {
379: $s = strftime($dow[2], mktime(0, 0, 0, $d['mon'],
380: $d['mday'] - ($d['wday'] - $dow[1]),
381: $d['year']));
382: // $s = strftime($dow[2], $date+3600-(86400*($d['wday'] - $dow[1])));
383: $string = str_replace($dow[0], $s, $string);
384: }
385: $string = preg_replace('/\{i\}/i',
386: $this->getInstance($date),
387: $string);
388: $string = preg_replace('/(?<![\134]\w)(\{[^\}]+\})/e',"strftime('\\1', $date)", $string);
389: $string = preg_replace('/\{lc\((.*?)\)\}/ie',"strtolower('\\1')", $string);
390: $string = preg_replace('/\{uc\((.*?)\)\}/ie',"strtoupper('\\1')", $string);
391: $string = preg_replace('/\{t\((.*?)\)\}/ie',"trim('\\1')", $string);
392: $string = preg_replace('/\{tl0\((.*?)\)\}/ie',"ltrim('\\1','0')", $string);
393: $string = preg_replace('/(?<![\134]\w)\{(.*?)\}/', "\\1\\2", $string);
394:
395: return $string;
396: }
397:
398: /**
399: * Get the instance requested based on the date. The instance is
400: * determined by itype, iformat, idate, isow
401: *
402: * @param timestamp $date The date the instance occurs on
403: *
404: * @return string An strftime-formatted string based on
405: * the iformat parameter
406: */
407: function getInstance($date)
408: {
409: $itype = $this->getOverride('itype', $date);
410: $iformat = $this->getOverride('iformat', $date);
411:
412: // get an instance if needed
413: $method = 'getInstance_' . $itype;
414: if (method_exists($this, $method)) {
415: $instance = $this->$method($date);
416: } else {
417: $instance = '';
418: }
419:
420: return sprintf($iformat, $instance);
421: }
422:
423: /**
424: * Get an instance number for a comic that appears monthly
425: *
426: * @param timestamp $date The date the comic appears
427: *
428: * @return integer The instance number (unformatted)
429: */
430: function getInstance_monthly($date)
431: {
432: // get the timestamp for the first day of the month and
433: // make sure time for $date is midnight
434: $d = getdate($date);
435: $date = mktime(0, 0, 0, $d['mon'], $d['mday'], $d['year']);
436: $d = mktime(0, 0, 0, $d['mon'], 1, $d['year']);
437:
438: $days = $this->getOverride('days', $date, 'strtolower');
439:
440: // figure out how many times the comic should have appeared this month
441: $instance = 0;
442: while ($d <= $date) {
443: $dow = getdate($d);
444: $dow = substr(Horde_String::lower($d['weekday']),0,3);
445: if (in_array($dow, $days)) {
446: $instance++;
447: }
448: $d = mktime(0, 0, 0, $dow['mon'], $dow['mday'] + 1, $dow['year']);
449: }
450:
451: return $instance;
452: }
453:
454: /**
455: * Get an instance number for a comic that appears weekly
456: *
457: * @param timestamp $date The date the comic appears
458: *
459: * @return integer The instance number (unformatted)
460: */
461: function getInstance_weekly($date)
462: {
463: return '';
464: }
465:
466: /**
467: * Get an instance number for a comic that appears yearly (NOT IMPLEMENTED!)
468: *
469: * @param timestamp $date The date the comic appears
470: *
471: * @return integer The instance number (unformatted)
472: */
473: function getInstance_yearly($date)
474: {
475: return '';
476: }
477:
478: /**
479: * Get an instance number for a comic based on a date reference.
480: * This takes the idate option as a reference date, then uses the
481: * 'days' setting to determine how often it appears. Using this
482: * information it extrapolates which instance will occur on the
483: * date requested.
484: *
485: * @param timestamp $date The date the comic appears
486: *
487: * @return integer The instance number (unformatted)
488: */
489: function getInstance_ref($date)
490: {
491: $d = $this->getOverride('idate', $date);
492: $c = $this->getOverride('icount', $date);
493: $days = $this->getOverride('days', $date);
494:
495: if ($d < $date) {
496: // The reference date is older than the requested date
497:
498: // how many full weeks can we jump?
499: $j = floor(($date - $d)/604800);
500: $c += $j * count($days);
501: $d += $j * 604800;
502: while ($d <= $date) {
503: $d = getdate($d);
504: $d = mktime(0, 0, 0, $d['mon'], $d['mday'] + 1, $d['year']);
505: if (in_array(Horde_String::lower(date('D', $d)), $days)) {
506: $c++;
507: }
508: }
509: } else {
510: // The reference date is newer than the requested date
511:
512: // how many full weeks can we jump?
513: $j = floor(($d - $date)/604800);
514: $c -= $j * count($days);
515: $date += $j * 604800;
516: while ($date < $d) {
517: $d = getdate($d);
518: $d = mktime(0, 0, 0, $d['mon'], $d['mday'] - 1, $d['year']);
519: if (in_array(Horde_String::lower(date('D', $d)), $days)) {
520: $c--;
521: }
522: }
523: }
524:
525: return $c;
526: }
527: }
528: