Rev Author Line No. Line
185 miho 1 <?php
2 # vim:et:ts=4:sts=4:sw=4:fdm=marker:
3 # {{{ Info
4 /***************************************************************************
5  
6 FeedCreator class v1.6
7 originally (c) Kai Blankenhorn
8 www.bitfolge.de
9 kaib@bitfolge.de
10 v1.3 work by Scott Reynen (scott@randomchaos.com) and Kai Blankenhorn
11 v1.5 OPML support by Dirk Clemens
12  
13 This program is free software; you can redistribute it and/or
14 modify it under the terms of the GNU General Public License
15 as published by the Free Software Foundation; either version 2
16 of the License, or (at your option) any later version.
17  
18 This program is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details: <http://www.gnu.org/licenses/gpl.txt>
22  
23 ****************************************************************************
24  
25  
26 Changelog:
27  
28 Modifications for WebSVN:
29 The main description link wasn't put through htmlspecialcharacters
30 Output encoding now defined by $config
31 Remove hardcoded time zone
32  
33 v1.6 05-10-04
34 added stylesheet to RSS 1.0 feeds
35 fixed generator comment (thanks Kevin L. Papendick and Tanguy Pruvot)
36 fixed RFC822 date bug (thanks Tanguy Pruvot)
37 added TimeZone customization for RFC8601 (thanks Tanguy Pruvot)
38 fixed Content-type could be empty (thanks Tanguy Pruvot)
39 fixed author/creator in RSS1.0 (thanks Tanguy Pruvot)
40  
41  
42 v1.6 beta 02-28-04
43 added Atom 0.3 support (not all features, though)
44 improved OPML 1.0 support (hopefully - added more elements)
45 added support for arbitrary additional elements (use with caution)
46 code beautification :-)
47 considered beta due to some internal changes
48  
49 v1.5.1 01-27-04
50 fixed some RSS 1.0 glitches (thanks to Stéphane Vanpoperynghe)
51 fixed some inconsistencies between documentation and code (thanks to Timothy Martin)
52  
53 v1.5 01-06-04
54 added support for OPML 1.0
55 added more documentation
56  
57 v1.4 11-11-03
58 optional feed saving and caching
59 improved documentation
60 minor improvements
61  
62 v1.3 10-02-03
63 renamed to FeedCreator, as it not only creates RSS anymore
64 added support for mbox
65 tentative support for echo/necho/atom/pie/???
66  
67 v1.2 07-20-03
68 intelligent auto-truncating of RSS 0.91 attributes
69 don't create some attributes when they're not set
70 documentation improved
71 fixed a real and a possible bug with date conversions
72 code cleanup
73  
74 v1.1 06-29-03
75 added images to feeds
76 now includes most RSS 0.91 attributes
77 added RSS 2.0 feeds
78  
79 v1.0 06-24-03
80 initial release
81  
82  
83  
84 ***************************************************************************/
85  
86 /*** GENERAL USAGE *********************************************************
87  
88 include("feedcreator.class.php");
89  
90 $rss = new UniversalFeedCreator();
91 $rss->useCached(); // use cached version if age<1 hour
92 $rss->title = "PHP news";
93 $rss->description = "daily news from the PHP scripting world";
94 $rss->link = "http://www.dailyphp.net/news";
95 $rss->syndicationURL = "http://www.dailyphp.net/".$_SERVER["PHP_SELF"];
96  
97 $image = new FeedImage();
98 $image->title = "dailyphp.net logo";
99 $image->url = "http://www.dailyphp.net/images/logo.gif";
100 $image->link = "http://www.dailyphp.net";
101 $image->description = "Feed provided by dailyphp.net. Click to visit.";
102 $rss->image = $image;
103  
104 // get your news items from somewhere, e.g. your database:
105 mysql_select_db($dbHost, $dbUser, $dbPass);
106 $res = mysql_query("SELECT * FROM news ORDER BY newsdate DESC");
107 while ($data = mysql_fetch_object($res)) {
108 $item = new FeedItem();
109 $item->title = $data->title;
110 $item->link = $data->url;
111 $item->description = $data->short;
112 $item->date = $data->newsdate;
113 $item->source = "http://www.dailyphp.net";
114 $item->author = "John Doe";
115  
116 $rss->addItem($item);
117 }
118  
119 // valid format strings are: RSS0.91, RSS1.0, RSS2.0, PIE0.1 (deprecated),
120 // MBOX, OPML, ATOM0.3
121 echo $rss->saveFeed("RSS1.0", "news/feed.xml");
122  
123 # }}}
124  
125 ***************************************************************************
126 * A little setup *
127 **************************************************************************/
128  
129 /**
130 * Version string.
131 **/
132 define("FEEDCREATOR_VERSION", "FeedCreator 1.6");
133  
134  
135  
136 /**
137 * A FeedItem is a part of a FeedCreator feed.
138 *
139 * @author Kai Blankenhorn <kaib@bitfolge.de>
140 * @since 1.3
141 */
142 class FeedItem {
143 # {{{ Properties
144  
145 /**
146 * Mandatory attributes of an item.
147 */
148 var $title, $description, $link;
149  
150 /**
151 * Optional attributes of an item.
152 */
153 var $author, $authorEmail, $image, $category, $comments, $guid, $source, $creator;
154  
155 /**
156 * Publishing date of an item. May be in one of the following formats:
157 *
158 * RFC 822:
159 * "Mon, 20 Jan 03 18:05:41 +0400"
160 * "20 Jan 03 18:05:41 +0000"
161 *
162 * ISO 8601:
163 * "2003-01-20T18:05:41+04:00"
164 *
165 * Unix:
166 * 1043082341
167 */
168 var $date;
169  
170 /**
171 * Any additional elements to include as an assiciated array. All $key => $value pairs
172 * will be included unencoded in the feed item in the form
173 * <$key>$value</$key>
174 * Again: No encoding will be used! This means you can invalidate or enhance the feed
175 * if $value contains markup. This may be abused to embed tags not implemented by
176 * the FeedCreator class used.
177 */
178 var $additionalElements = Array();
179  
180 // on hold
181 // var $source;
182  
183 # }}}
184 }
185  
186  
187  
188 /**
189 * An FeedImage may be added to a FeedCreator feed.
190 * @author Kai Blankenhorn <kaib@bitfolge.de>
191 * @since 1.3
192 */
193 class FeedImage {
194 # {{{ Properties
195  
196 /**
197 * Mandatory attributes of an image.
198 */
199 var $title, $url, $link;
200  
201 /**
202 * Optional attributes of an image.
203 */
204 var $width, $height, $description;
205  
206 # }}}
207 }
208  
209  
210 /**
211 * UniversalFeedCreator lets you choose during runtime which
212 * format to build.
213 * For general usage of a feed class, see the FeedCreator class
214 * below or the example above.
215 *
216 * @since 1.3
217 * @author Kai Blankenhorn <kaib@bitfolge.de>
218 */
219 class UniversalFeedCreator extends FeedCreator {
220 var $_feed;
221  
222 # {{{ _setFormat
223 function _setFormat($format) {
224 switch (strtoupper($format)) {
225  
226 case "2.0":
227 // fall through
228 case "RSS2.0":
229 $this->_feed = new RSSCreator20();
230 break;
231  
232 case "1.0":
233 // fall through
234 case "RSS1.0":
235 $this->_feed = new RSSCreator10();
236 break;
237  
238 case "0.91":
239 // fall through
240 case "RSS0.91":
241 $this->_feed = new RSSCreator091();
242 break;
243  
244 case "PIE0.1":
245 $this->_feed = new PIECreator01();
246 break;
247  
248 case "MBOX":
249 $this->_feed = new MBOXCreator();
250 break;
251  
252 case "OPML":
253 $this->_feed = new OPMLCreator();
254 break;
255  
256 case "ATOM0.3":
257 $this->_feed = new AtomCreator03();
258 break;
259  
260 default:
261 $this->_feed = new RSSCreator091();
262 break;
263 }
264  
265 $vars = get_object_vars($this);
266 foreach ($vars as $key => $value) {
267 if ($key!="feed") {
268 $this->_feed->{$key} = $this->{$key};
269 }
270 }
271 }
272 # }}}
273  
274 # {{{ createFeed
275 /**
276 * Creates a syndication feed based on the items previously added.
277 *
278 * @see FeedCreator::addItem()
279 * @param string format format the feed should comply to. Valid values are:
280 * "PIE0.1", "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML".
281 * @return string the contents of the feed.
282 */
283 function createFeed($format = "RSS0.91") {
284 $this->_setFormat($format);
285 return $this->_feed->createFeed();
286 }
287 # }}}
288  
289 # {{{ saveFeed
290 /**
291 * Saves this feed as a file on the local disk. After the file is saved, an HTTP redirect
292 * header may be sent to redirect the use to the newly created file.
293 * @since 1.4
294 *
295 * @param string format format the feed should comply to. Valid values are:
296 * "PIE0.1" (deprecated), "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM0.3".
297 * @param string filename optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()).
298 * @param boolean displayContents optional send the content of the file or not. If true, the file will be sent in the body of the response.
299 */
300 function saveFeed($format="RSS0.91", $filename="", $displayContents=true) {
301 $this->_setFormat($format);
302 $this->_feed->saveFeed($filename, $displayContents);
303 }
304 # }}}
305  
306 }
307  
308  
309 /**
310 * FeedCreator is the abstract base implementation for concrete
311 * implementations that implement a specific format of syndication.
312 *
313 * @abstract
314 * @author Kai Blankenhorn <kaib@bitfolge.de>
315 * @since 1.4
316 */
317 class FeedCreator {
318 # {{{ Properties
319  
320 /**
321 * Mandatory attributes of a feed.
322 */
323 var $title, $description, $link;
324  
325  
326 /**
327 * Optional attributes of a feed.
328 */
329 var $syndicationURL, $image, $language, $copyright, $pubDate, $lastBuildDate, $editor, $editorEmail, $webmaster, $category, $docs, $ttl, $rating, $skipHours, $skipDays;
330  
331  
332 /**
333 * @access private
334 */
335 var $items = Array();
336  
337  
338 /**
339 * This feed's MIME content type.
340 * @since 1.4
341 * @access private
342 */
343 var $contentType = "text/xml";
344  
345  
346 /**
347 * Any additional elements to include as an assiciated array. All $key => $value pairs
348 * will be included unencoded in the feed in the form
349 * <$key>$value</$key>
350 * Again: No encoding will be used! This means you can invalidate or enhance the feed
351 * if $value contains markup. This may be abused to embed tags not implemented by
352 * the FeedCreator class used.
353 */
354 var $additionalElements = Array();
355  
356 # }}}
357  
358 # {{{ addItem
359 /**
360 * Adds an FeedItem to the feed.
361 *
362 * @param object FeedItem $item The FeedItem to add to the feed.
363 * @access public
364 */
365 function addItem($item) {
366 $this->items[] = $item;
367 }
368 # }}}
369  
370 # {{{ iTrunc
371 /**
372 * Truncates a string to a certain length at the most sensible point.
373 * First, if there's a '.' character near the end of the string, the string is truncated after this character.
374 * If there is no '.', the string is truncated after the last ' ' character.
375 * If the string is truncated, " ..." is appended.
376 * If the string is already shorter than $length, it is returned unchanged.
377 *
378 * @static
379 * @param string string A string to be truncated.
380 * @param int length the maximum length the string should be truncated to
381 * @return string the truncated string
382 */
383 function iTrunc($string, $length) {
384 if (strlen($string)<=$length) {
385 return $string;
386 }
387  
388 $pos = strrpos($string,".");
389 if ($pos>=$length-4) {
390 $string = substr($string,0,$length-4);
391 $pos = strrpos($string,".");
392 }
393 if ($pos>=$length*0.4) {
394 return substr($string,0,$pos+1)." ...";
395 }
396  
397 $pos = strrpos($string," ");
398 if ($pos>=$length-4) {
399 $string = substr($string,0,$length-4);
400 $pos = strrpos($string," ");
401 }
402 if ($pos>=$length*0.4) {
403 return substr($string,0,$pos)." ...";
404 }
405  
406 return substr($string,0,$length-4)." ...";
407  
408 }
409 # }}}
410  
411 # {{{ _createGeneratorComment
412 /**
413 * Creates a comment indicating the generator of this feed.
414 * The format of this comment seems to be recognized by
415 * Syndic8.com.
416 */
417 function _createGeneratorComment() {
418 return "<!-- generator=\"".FEEDCREATOR_VERSION."\" -->\n";
419 }
420 # }}}
421  
422 # {{{ _createAdditionalElements
423 /**
424 * Creates a string containing all additional elements specified in
425 * $additionalElements.
426 * @param elements array an associative array containing key => value pairs
427 * @param indentString string a string that will be inserted before every generated line
428 * @return string the XML tags corresponding to $additionalElements
429 */
430 function _createAdditionalElements($elements, $indentString="") {
431 $ae = "";
432 if (is_array($elements)) {
433 foreach($elements AS $key => $value) {
434 $ae.= $indentString."<$key>$value</$key>\n";
435 }
436 }
437 return $ae;
438 }
439 # }}}
440  
441 # {{{ createFeed
442 /**
443 * Builds the feed's text.
444 * @abstract
445 * @return string the feed's complete text
446 */
447 function createFeed() {
448 }
449 # }}}
450  
451 # {{{ _generateFilename
452 /**
453 * Generate a filename for the feed cache file. The result will be $_SERVER["PHP_SELF"] with the extension changed to .xml.
454 * For example:
455 *
456 * echo $_SERVER["PHP_SELF"]."\n";
457 * echo FeedCreator::_generateFilename();
458 *
459 * would produce:
460 *
461 * /rss/latestnews.php
462 * latestnews.xml
463 *
464 * @return string the feed cache filename
465 * @since 1.4
466 * @access private
467 */
468 function _generateFilename() {
469 $fileInfo = pathinfo($_SERVER["PHP_SELF"]);
470 return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".xml";
471 }
472 # }}}
473  
474 # {{{ _redirect
475 /**
476 * @since 1.4
477 * @access private
478 */
479 function _redirect($filename) {
480 // attention, heavily-commented-out-area
481  
482 // maybe use this in addition to file time checking
483 //Header("Expires: ".date("r",time()+$this->_timeout));
484  
485 /* no caching at all, doesn't seem to work as good:
486 Header("Cache-Control: no-cache");
487 Header("Pragma: no-cache");
488 */
489  
490 // HTTP redirect, some feed readers' simple HTTP implementations don't follow it
491 //Header("Location: ".$filename);
492  
493 Header("Content-Type: ".$this->contentType."; filename=".basename($filename));
494 Header("Content-Disposition: inline; filename=".basename($filename));
495 readfile($filename, "r");
496 die();
497 }
498 # }}}
499  
500 # {{{ useCached
501 /**
502 * Turns on caching and checks if there is a recent version of this feed in the cache.
503 * If there is, an HTTP redirect header is sent.
504 * To effectively use caching, you should create the FeedCreator object and call this method
505 * before anything else, especially before you do the time consuming task to build the feed
506 * (web fetching, for example).
507 * @since 1.4
508 * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()).
509 * @param timeout int optional the timeout in seconds before a cached version is refreshed (defaults to 3600 = 1 hour)
510 */
511 function useCached($filename="", $timeout=3600) {
512 $this->_timeout = $timeout;
513 if ($filename=="") {
514 $filename = $this->_generateFilename();
515 }
516 if (file_exists($filename) AND (time()-filemtime($filename) < $timeout)) {
517 $this->_redirect($filename);
518 }
519 }
520 # }}}
521  
522 # {{{ saveFeed
523 /**
524 * Saves this feed as a file on the local disk. After the file is saved, a redirect
525 * header may be sent to redirect the user to the newly created file.
526 * @since 1.4
527 *
528 * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()).
529 * @param redirect boolean optional send an HTTP redirect header or not. If true, the user will be automatically redirected to the created file.
530 */
531 function saveFeed($filename="", $displayContents=true) {
532 if ($filename=="") {
533 $filename = $this->_generateFilename();
534 }
535 $feedFile = fopen($filename, "w+");
536 if ($feedFile) {
537 fputs($feedFile,$this->createFeed());
538 fclose($feedFile);
539 if ($displayContents) {
540 $this->_redirect($filename);
541 }
542 } else {
543 echo "<br /><b>Error creating feed file, please check write permissions.</b><br />";
544 }
545 }
546 # }}}
547 }
548  
549  
550 /**
551 * FeedDate is an internal class that stores a date for a feed or feed item.
552 * Usually, you won't need to use this.
553 */
554 class FeedDate {
555 var $unix;
556  
557 # {{{ __construct
558 /**
559 * Creates a new instance of FeedDate representing a given date.
560 * Accepts RFC 822, ISO 8601 date formats as well as unix time stamps.
561 * @param mixed $dateString optional the date this FeedDate will represent. If not specified, the current date and time is used.
562 */
563 function FeedDate($dateString="") {
564 if ($dateString=="") $dateString = date("r");
565  
566 if (is_integer($dateString)) {
567 $this->unix = $dateString;
568 return;
569 }
570 if (preg_match("~(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+)?(\\d{1,2})\\s+([a-zA-Z]{3})\\s+(\\d{4})\\s+(\\d{2}):(\\d{2}):(\\d{2})\\s+(.*)~",$dateString,$matches)) {
571 $months = Array("Jan"=>1,"Feb"=>2,"Mar"=>3,"Apr"=>4,"May"=>5,"Jun"=>6,"Jul"=>7,"Aug"=>8,"Sep"=>9,"Oct"=>10,"Nov"=>11,"Dec"=>12);
572 $this->unix = mktime($matches[4],$matches[5],$matches[6],$months[$matches[2]],$matches[1],$matches[3]);
573 if (substr($matches[7],0,1)=='+' OR substr($matches[7],0,1)=='-') {
574 $tzOffset = (substr($matches[7],0,3) * 60 + substr($matches[7],-2)) * 60;
575 } else {
576 if (strlen($matches[7])==1) {
577 $oneHour = 3600;
578 $ord = ord($matches[7]);
579 if ($ord < ord("M")) {
580 $tzOffset = (ord("A") - $ord - 1) * $oneHour;
581 } elseif ($ord >= ord("M") AND $matches[7]!="Z") {
582 $tzOffset = ($ord - ord("M")) * $oneHour;
583 } elseif ($matches[7]=="Z") {
584 $tzOffset = 0;
585 }
586 }
587 switch ($matches[7]) {
588 case "UT":
589 case "GMT": $tzOffset = 0;
590 }
591 }
592 $this->unix += $tzOffset;
593 return;
594 }
595 if (preg_match("~(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(.*)~",$dateString,$matches)) {
596 $this->unix = mktime($matches[4],$matches[5],$matches[6],$matches[2],$matches[3],$matches[1]);
597 if (substr($matches[7],0,1)=='+' OR substr($matches[7],0,1)=='-') {
598 $tzOffset = (substr($matches[7],0,3) * 60 + substr($matches[7],-2)) * 60;
599 } else {
600 if ($matches[7]=="Z") {
601 $tzOffset = 0;
602 }
603 }
604 $this->unix += $tzOffset;
605 return;
606 }
607 $this->unix = 0;
608 }
609 # }}}
610  
611 # {{{ rfc822
612 /**
613 * Gets the date stored in this FeedDate as an RFC 822 date.
614 *
615 * @return a date in RFC 822 format
616 */
617 function rfc822() {
618 return gmdate("r",$this->unix);
619 }
620 # }}}
621  
622 # {{{ iso8601
623 /**
624 * Gets the date stored in this FeedDate as an ISO 8601 date.
625 *
626 * @return a date in ISO 8601 format
627 */
628 function iso8601() {
629 $date = gmdate("Y-m-d\TH:i:sO",$this->unix);
630 $date = substr($date,0,22) . ':' . substr($date,-2);
631 return $date;
632 }
633 # }}}
634  
635 # {{{ unix
636 /**
637 * Gets the date stored in this FeedDate as unix time stamp.
638 *
639 * @return a date as a unix time stamp
640 */
641 function unix() {
642 return $this->unix;
643 }
644 # }}}
645 }
646  
647  
648 /**
649 * RSSCreator10 is a FeedCreator that implements RDF Site Summary (RSS) 1.0.
650 *
651 * @see http://www.purl.org/rss/1.0/
652 * @since 1.3
653 * @author Kai Blankenhorn <kaib@bitfolge.de>
654 */
655 class RSSCreator10 extends FeedCreator {
656  
657 # {{{ createFeed
658 /**
659 * Builds the RSS feed's text. The feed will be compliant to RDF Site Summary (RSS) 1.0.
660 * The feed will contain all items previously added in the same order.
661 * @return string the feed's complete text
662 */
663 function createFeed() {
664 global $config;
665 $feed = "<?xml version=\"1.0\" encoding=\"".$config->outputEnc."\"?>\n";
666 $feed.= "<?xml-stylesheet href=\"http://www.w3.org/2000/08/w3c-synd/style.css\" type=\"text/css\"?>\n";
667 $feed.= $this->_createGeneratorComment();
668 $feed.= "<rdf:RDF\n";
669 $feed.= " xmlns=\"http://purl.org/rss/1.0/\"\n";
670 $feed.= " xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
671 $feed.= " xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
672 $feed.= " <channel rdf:about=\"".htmlspecialchars($this->syndicationURL)."\">\n";
673 $feed.= " <title>".htmlspecialchars($this->title)."</title>\n";
674 $feed.= " <description>".htmlspecialchars($this->description)."</description>\n";
675 $feed.= " <link>".htmlspecialchars($this->link)."</link>\n";
676 if ($this->image!=null) {
677 $feed.= " <image rdf:resource=\"".$this->image->url."\" />\n";
678 }
679 $now = new FeedDate();
680 $feed.= " <dc:date>".htmlspecialchars($now->iso8601())."</dc:date>\n";
681 $feed.= " <items>\n";
682 $feed.= " <rdf:Seq>\n";
683 for ($i=0;$i<count($this->items);$i++) {
684 $feed.= " <rdf:li rdf:resource=\"".htmlspecialchars($this->items[$i]->link)."\"/>\n";
685 }
686 $feed.= " </rdf:Seq>\n";
687 $feed.= " </items>\n";
688 $feed.= " </channel>\n";
689 if ($this->image!=null) {
690 $feed.= " <image rdf:about=\"".$this->image->url."\">\n";
691 $feed.= " <title>".$this->image->title."</title>\n";
692 $feed.= " <link>".$this->image->link."</link>\n";
693 $feed.= " <url>".$this->image->url."</url>\n";
694 $feed.= " </image>\n";
695 }
696 //$feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " ");
697  
698 for ($i=0;$i<count($this->items);$i++) {
699 $feed.= " <item rdf:about=\"".htmlspecialchars($this->items[$i]->link)."\">\n";
700 //$feed.= " <dc:type>Posting</dc:type>\n";
701 $feed.= " <dc:format>text/html</dc:format>\n";
702 if ($this->items[$i]->date!=null) {
703 $itemDate = new FeedDate($this->items[$i]->date);
704 $feed.= " <dc:date>".htmlspecialchars($itemDate->iso8601())."</dc:date>\n";
705 }
706 if ($this->items[$i]->source!="") {
707 $feed.= " <dc:source>".htmlspecialchars($this->items[$i]->source)."</dc:source>\n";
708 }
709 if ($this->items[$i]->author!="") {
710 $feed.= " <dc:creator>".htmlspecialchars($this->items[$i]->author)."</dc:creator>\n";
711 }
712 $feed.= " <title>".htmlspecialchars(strip_tags(strtr($this->items[$i]->title,"\n\r"," ")))."</title>\n";
713 $feed.= " <link>".htmlspecialchars($this->items[$i]->link)."</link>\n";
714 $feed.= " <description>".htmlspecialchars($this->items[$i]->description)."</description>\n";
715 $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " ");
716 $feed.= " </item>\n";
717 }
718 $feed.= "</rdf:RDF>\n";
719 return $feed;
720 }
721 # }}}
722 }
723  
724  
725  
726 /**
727 * RSSCreator091 is a FeedCreator that implements RSS 0.91 Spec, revision 3.
728 *
729 * @see http://my.netscape.com/publish/formats/rss-spec-0.91.html
730 * @since 1.3
731 * @author Kai Blankenhorn <kaib@bitfolge.de>
732 */
733 class RSSCreator091 extends FeedCreator {
734  
735 /**
736 * Stores this RSS feed's version number.
737 * @access private
738 */
739 var $RSSVersion;
740  
741 # {{{ __construct
742 function RSSCreator091() {
743 $this->_setRSSVersion("0.91");
744 $this->contentType = "application/rss+xml";
745 }
746 # }}}
747  
748 # {{{ _setRSSVersion
749 /**
750 * Sets this RSS feed's version number.
751 * @access private
752 */
753 function _setRSSVersion($version) {
754 $this->RSSVersion = $version;
755 }
756 # }}}
757  
758 # {{{ createFeed
759 /**
760 * Builds the RSS feed's text. The feed will be compliant to RDF Site Summary (RSS) 1.0.
761 * The feed will contain all items previously added in the same order.
762 * @return string the feed's complete text
763 */
764 function createFeed() {
765 global $config;
766 $feed = "<?xml version=\"1.0\" encoding=\"".$config->outputEnc."\"?>\n";
767 $feed.= $this->_createGeneratorComment();
768 $feed.= "<rss version=\"".$this->RSSVersion."\">\n";
769 $feed.= " <channel>\n";
770 $feed.= " <title>".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."</title>\n";
771 $feed.= " <description>".FeedCreator::iTrunc(htmlspecialchars($this->description),500)."</description>\n";
772 $feed.= " <link>".htmlspecialchars($this->link)."</link>\n";
773 $now = new FeedDate();
774 $feed.= " <lastBuildDate>".htmlspecialchars($now->rfc822())."</lastBuildDate>\n";
775 $feed.= " <generator>".FEEDCREATOR_VERSION."</generator>\n";
776  
777 if ($this->image!=null) {
778 $feed.= " <image>\n";
779 $feed.= " <url>".$this->image->url."</url>\n";
780 $feed.= " <title>".FeedCreator::iTrunc(htmlspecialchars($this->image->title),100)."</title>\n";
781 $feed.= " <link>".$this->image->link."</link>\n";
782 if ($this->image->width!="") {
783 $feed.= " <width>".$this->image->width."</width>\n";
784 }
785 if ($this->image->height!="") {
786 $feed.= " <height>".$this->image->height."</height>\n";
787 }
788 if ($this->image->description!="") {
789 $feed.= " <description>".htmlspecialchars($this->image->description)."</description>\n";
790 }
791 $feed.= " </image>\n";
792 }
793 if ($this->language!="") {
794 $feed.= " <language>".$this->language."</language>\n";
795 }
796 if ($this->copyright!="") {
797 $feed.= " <copyright>".FeedCreator::iTrunc(htmlspecialchars($this->copyright),100)."</copyright>\n";
798 }
799 if ($this->editor!="") {
800 $feed.= " <managingEditor>".FeedCreator::iTrunc(htmlspecialchars($this->editor),100)."</managingEditor>\n";
801 }
802 if ($this->webmaster!="") {
803 $feed.= " <webMaster>".FeedCreator::iTrunc(htmlspecialchars($this->webmaster),100)."</webMaster>\n";
804 }
805 if ($this->pubDate!="") {
806 $pubDate = new FeedDate($this->pubDate);
807 $feed.= " <pubDate>".htmlspecialchars($pubDate->rfc822())."</pubDate>\n";
808 }
809 if ($this->category!="") {
810 $feed.= " <category>".htmlspecialchars($this->category)."</category>\n";
811 }
812 if ($this->docs!="") {
813 $feed.= " <docs>".FeedCreator::iTrunc(htmlspecialchars($this->docs),500)."</docs>\n";
814 }
815 if ($this->ttl!="") {
816 $feed.= " <ttl>".htmlspecialchars($this->ttl)."</ttl>\n";
817 }
818 if ($this->rating!="") {
819 $feed.= " <rating>".FeedCreator::iTrunc(htmlspecialchars($this->rating),500)."</rating>\n";
820 }
821 if ($this->skipHours!="") {
822 $feed.= " <skipHours>".htmlspecialchars($this->skipHours)."</skipHours>\n";
823 }
824 if ($this->skipDays!="") {
825 $feed.= " <skipDays>".htmlspecialchars($this->skipDays)."</skipDays>\n";
826 }
827 $feed.= $this->_createAdditionalElements($this->additionalElements, " ");
828  
829 for ($i=0;$i<count($this->items);$i++) {
830 $feed.= " <item>\n";
831 $feed.= " <title>".FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100)."</title>\n";
832 $feed.= " <link>".htmlspecialchars($this->items[$i]->link)."</link>\n";
833 $feed.= " <description>".htmlspecialchars($this->items[$i]->description)."</description>\n";
834 if ($this->items[$i]->author!="") {
835 $feed.= " <author>".htmlspecialchars($this->items[$i]->author)."</author>\n";
836 }
837 /*
838 // on hold
839 if ($this->items[$i]->source!="") {
840 $feed.= " <source>".htmlspecialchars($this->items[$i]->source)."</source>\n";
841 }
842 */
843 if ($this->items[$i]->category!="") {
844 $feed.= " <category>".htmlspecialchars($this->items[$i]->category)."</category>\n";
845 }
846 if ($this->items[$i]->comments!="") {
847 $feed.= " <comments>".$this->items[$i]->comments."</comments>\n";
848 }
849 if ($this->items[$i]->date!="") {
850 $itemDate = new FeedDate($this->items[$i]->date);
851 $feed.= " <pubDate>".htmlspecialchars($itemDate->rfc822())."</pubDate>\n";
852 }
853 if ($this->items[$i]->guid!="") {
854 $feed.= " <guid>".$this->items[$i]->guid."</guid>\n";
855 }
856 $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " ");
857 $feed.= " </item>\n";
858 }
859 $feed.= " </channel>\n";
860 $feed.= "</rss>\n";
861 return $feed;
862 }
863 # }}}
864 }
865  
866  
867  
868 /**
869 * RSSCreator20 is a FeedCreator that implements RDF Site Summary (RSS) 2.0.
870 *
871 * @see http://backend.userland.com/rss
872 * @since 1.3
873 * @author Kai Blankenhorn <kaib@bitfolge.de>
874 */
875 class RSSCreator20 extends RSSCreator091 {
876  
877 # {{{ __construct
878 function RSSCreator20() {
879 parent::_setRSSVersion("2.0");
880 }
881 # }}}
882  
883 }
884  
885  
886 /**
887 * PIECreator01 is a FeedCreator that implements the emerging PIE specification,
888 * as in http://intertwingly.net/wiki/pie/Syntax.
889 *
890 * @deprecated
891 * @since 1.3
892 * @author Scott Reynen <scott@randomchaos.com> and Kai Blankenhorn <kaib@bitfolge.de>
893 */
894 class PIECreator01 extends FeedCreator {
895  
896 # {{{ createFeed
897 function createFeed() {
898 $feed = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
899 $feed.= "<feed version=\"0.1\" xmlns=\"http://example.com/newformat#\">\n";
900 $feed.= " <title>".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."</title>\n";
901 $feed.= " <subtitle>".FeedCreator::iTrunc(htmlspecialchars($this->description),500)."</subtitle>\n";
902 $feed.= " <link>".$this->link."</link>\n";
903 for ($i=0;$i<count($this->items);$i++) {
904 $feed.= " <entry>\n";
905 $feed.= " <title>".FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100)."</title>\n";
906 $feed.= " <link>".htmlspecialchars($this->items[$i]->link)."</link>\n";
907 $itemDate = new FeedDate($this->items[$i]->date);
908 $feed.= " <created>".htmlspecialchars($itemDate->iso8601())."</created>\n";
909 $feed.= " <issued>".htmlspecialchars($itemDate->iso8601())."</issued>\n";
910 $feed.= " <modified>".htmlspecialchars($itemDate->iso8601())."</modified>\n";
911 $feed.= " <id>".$this->items[$i]->guid."</id>\n";
912 if ($this->items[$i]->author!="") {
913 $feed.= " <author>\n";
914 $feed.= " <name>".htmlspecialchars($this->items[$i]->author)."</name>\n";
915 if ($this->items[$i]->authorEmail!="") {
916 $feed.= " <email>".$this->items[$i]->authorEmail."</email>\n";
917 }
918 $feed.=" </author>\n";
919 }
920 $feed.= " <content type=\"text/html\" xml:lang=\"en-us\">\n";
921 $feed.= " <div xmlns=\"http://www.w3.org/1999/xhtml\">".$this->items[$i]->description."</div>\n";
922 $feed.= " </content>\n";
923 $feed.= " </entry>\n";
924 }
925 $feed.= "</feed>\n";
926 return $feed;
927 }
928 # }}}
929 }
930  
931  
932 /**
933 * AtomCreator03 is a FeedCreator that implements the atom specification,
934 * as in http://www.intertwingly.net/wiki/pie/FrontPage.
935 * Please note that just by using AtomCreator03 you won't automatically
936 * produce valid atom files. For example, you have to specify either an editor
937 * for the feed or an author for every single feed item.
938 *
939 * Some elements have not been implemented yet. These are (incomplete list):
940 * author URL, item author's email and URL, item contents, alternate links,
941 * other link content types than text/html. Some of them may be created with
942 * AtomCreator03::additionalElements.
943 *
944 * @see FeedCreator#additionalElements
945 * @since 1.6
946 * @author Kai Blankenhorn <kaib@bitfolge.de>, Scott Reynen <scott@randomchaos.com>
947 */
948 class AtomCreator03 extends FeedCreator {
949  
950 # {{{ __construct
951 function AtomCreator03() {
952 $this->contentType = "application/atom+xml";
953 }
954 # }}}
955  
956 # {{{ createFeed
957 function createFeed() {
958 $feed = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
959 $feed.= $this->_createGeneratorComment();
960 $feed.= "<feed version=\"0.3\" xmlns=\"http://purl.org/atom/ns#\"";
961 if ($this->language!="") {
962 $feed.= " xml:lang:\"".$this->language."\"";
963 }
964 $feed.= ">\n";
965 $feed.= " <title>".htmlspecialchars($this->title)."</title>\n";
966 $feed.= " <tagline>".htmlspecialchars($this->description)."</tagline>\n";
967 $feed.= " <link rel=\"alternate\" type=\"text/html\" href=\"".htmlspecialchars($this->link)."\"/>\n";
968 $feed.= " <id>".$this->link."</id>\n";
969 $now = new FeedDate();
970 $feed.= " <modified>".htmlspecialchars($now->iso8601())."</modified>\n";
971 if ($this->editor!="") {
972 $feed.= " <author>\n";
973 $feed.= " <name>".$this->editor."</name>\n";
974 if ($this->editorEmail!="") {
975 $feed.= " <email>".$this->editorEmail."</email>\n";
976 }
977 $feed.= " </author>\n";
978 }
979 $feed.= " <generator>".FEEDCREATOR_VERSION."</generator>\n";
980 $feed.= $this->_createAdditionalElements($this->additionalElements, " ");
981 for ($i=0;$i<count($this->items);$i++) {
982 $feed.= " <entry>\n";
983 $feed.= " <title>".htmlspecialchars(strip_tags($this->items[$i]->title))."</title>\n";
984 $feed.= " <link rel=\"alternate\" type=\"text/html\" href=\"".htmlspecialchars($this->items[$i]->link)."\"/>\n";
985 if ($this->items[$i]->date=="") {
986 $this->items[$i]->date = time();
987 }
988 $itemDate = new FeedDate($this->items[$i]->date);
989 $feed.= " <created>".htmlspecialchars($itemDate->iso8601())."</created>\n";
990 $feed.= " <issued>".htmlspecialchars($itemDate->iso8601())."</issued>\n";
991 $feed.= " <modified>".htmlspecialchars($itemDate->iso8601())."</modified>\n";
992 $feed.= " <id>".$this->items[$i]->link."</id>\n";
993 $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " ");
994 if ($this->items[$i]->author!="") {
995 $feed.= " <author>\n";
996 $feed.= " <name>".htmlspecialchars($this->items[$i]->author)."</name>\n";
997 $feed.= " </author>\n";
998 }
999 if ($this->items[$i]->description!="") {
1000 $feed.= " <summary>".htmlspecialchars($this->items[$i]->description)."</summary>\n";
1001 }
1002 $feed.= " </entry>\n";
1003 }
1004 $feed.= "</feed>\n";
1005 return $feed;
1006 }
1007 # }}}
1008 }
1009  
1010  
1011 /**
1012 * MBOXCreator is a FeedCreator that implements the mbox format
1013 * as described in http://www.qmail.org/man/man5/mbox.html
1014 *
1015 * @since 1.3
1016 * @author Kai Blankenhorn <kaib@bitfolge.de>
1017 */
1018 class MBOXCreator extends FeedCreator {
1019  
1020 # {{{ __construct
1021 function MBOXCreator() {
1022 $this->contentType = "text/plain";
1023 }
1024 # }}}
1025  
1026 # {{{ qp_enc
1027 function qp_enc($input = "", $line_max = 76) {
1028 $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
1029 $lines = preg_split("/(?:\r\n|\r|\n)/", $input);
1030 $eol = "\r\n";
1031 $escape = "=";
1032 $output = "";
1033 while( list(, $line) = each($lines) ) {
1034 //$line = rtrim($line); // remove trailing white space -> no =20\r\n necessary
1035 $linlen = strlen($line);
1036 $newline = "";
1037 for($i = 0; $i < $linlen; $i++) {
1038 $c = substr($line, $i, 1);
1039 $dec = ord($c);
1040 if ( ($dec == 32) && ($i == ($linlen - 1)) ) { // convert space at eol only
1041 $c = "=20";
1042 } elseif ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) ) { // always encode "\t", which is *not* required
1043 $h2 = floor($dec/16); $h1 = floor($dec%16);
1044 $c = $escape.$hex["$h2"].$hex["$h1"];
1045 }
1046 if ( (strlen($newline) + strlen($c)) >= $line_max ) { // CRLF is not counted
1047 $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay
1048 $newline = "";
1049 }
1050 $newline .= $c;
1051 } // end of for
1052 $output .= $newline.$eol;
1053 }
1054 return trim($output);
1055 }
1056 # }}}
1057  
1058 # {{{ createFeed
1059 /**
1060 * Builds the MBOX contents.
1061 * @return string the feed's complete text
1062 */
1063 function createFeed() {
1064 global $config;
1065 for ($i=0;$i<count($this->items);$i++) {
1066 if ($this->items[$i]->author!="") {
1067 $from = $this->items[$i]->author;
1068 } else {
1069 $from = $this->title;
1070 }
1071 $itemDate = new FeedDate($this->items[$i]->date);
1072 $feed.= "From ".strtr(MBOXCreator::qp_enc($from)," ","_")." ".date("D M d H:i:s Y",$itemDate->unix())."\n";
1073 $feed.= "Content-Type: text/plain;\n";
1074 $feed.= " charset=\"".$config->outputEnc."\"\n";
1075 $feed.= "Content-Transfer-Encoding: quoted-printable\n";
1076 $feed.= "Content-Type: text/plain\n";
1077 $feed.= "From: \"".MBOXCreator::qp_enc($from)."\"\n";
1078 $feed.= "Date: ".$itemDate->rfc822()."\n";
1079 $feed.= "Subject: ".MBOXCreator::qp_enc(FeedCreator::iTrunc($this->items[$i]->title,100))."\n";
1080 $feed.= "\n";
1081 $body = chunk_split(MBOXCreator::qp_enc($this->items[$i]->description));
1082 $feed.= preg_replace("~\nFrom ([^\n]*)(\n?)~","\n>From $1$2\n",$body);
1083 $feed.= "\n";
1084 $feed.= "\n";
1085 }
1086 return $feed;
1087 }
1088 # }}}
1089  
1090 # {{{ _generateFilename
1091 /**
1092 * Generate a filename for the feed cache file. Overridden from FeedCreator to prevent XML data types.
1093 * @return string the feed cache filename
1094 * @since 1.4
1095 * @access private
1096 */
1097 function _generateFilename() {
1098 $fileInfo = pathinfo($_SERVER["PHP_SELF"]);
1099 return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".mbox";
1100 }
1101 # }}}
1102 }
1103  
1104  
1105 /**
1106 * OPMLCreator is a FeedCreator that implements OPML 1.0.
1107 *
1108 * @see http://opml.scripting.com/spec
1109 * @author Dirk Clemens, Kai Blankenhorn
1110 * @since 1.5
1111 */
1112 class OPMLCreator extends FeedCreator {
1113  
1114 # {{{ createFeed
1115 function createFeed() {
1116 $feed = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1117 $feed.= $this->_createGeneratorComment();
1118 $feed.= "<opml xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n";
1119 $feed.= " <head>\n";
1120 $feed.= " <title>".htmlspecialchars($this->title)."</title>\n";
1121 if ($this->pubDate!="") {
1122 $date = new FeedDate($this->pubDate);
1123 $feed.= " <dateCreated>".$date->rfc822()."</dateCreated>\n";
1124 }
1125 if ($this->lastBuildDate!="") {
1126 $date = new FeedDate($this->lastBuildDate);
1127 $feed.= " <dateModified>".$date->rfc822()."</dateModified>\n";
1128 }
1129 if ($this->editor!="") {
1130 $feed.= " <ownerName>".$this->editor."</ownerName>\n";
1131 }
1132 if ($this->editorEmail!="") {
1133 $feed.= " <ownerEmail>".$this->editorEmail."</ownerEmail>\n";
1134 }
1135 $feed.= " </head>\n";
1136 $feed.= " <body>\n";
1137 for ($i=0;$i<count($this->items);$i++) {
1138 $feed.= " <outline type=\"rss\" ";
1139 $title = htmlspecialchars(strip_tags(strtr($this->items[$i]->title,"\n\r"," ")));
1140 $feed.= " title=\"".$title."\"";
1141 $feed.= " text=\"".$title."\"";
1142 //$feed.= " description=\"".htmlspecialchars($this->items[$i]->description)."\"";
1143 $feed.= " url=\"".htmlspecialchars($this->items[$i]->link)."\"";
1144 $feed.= "/>\n";
1145 }
1146 $feed.= " </body>\n";
1147 $feed.= "</opml>\n";
1148 return $feed;
1149 }
1150 # }}}
1151 }
1152  
1153 ?>