root/hacks/trunk/feedphp/feed.php

Revision 80, 10.7 kB (checked in by verbosus, 12 months ago)

Fixed issue with bloody PHP mktime and yyyy-mm-dd pubDates

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1<?php
2  /* feed.php
3   *
4   * A collection of simple classes to output Atom 1.0 and RSS 2.0
5   * feeds from PHP 5.
6   *
7   * author: Antonio Cavedoni <http://cavedoni.com/>
8   * version: 0.4
9   * revision: $Id$
10   * license: BSD
11   *
12   * acknowledgements: Ludovico Magnocavallo, Simon Willison, Adrian
13   * Holovaty, Jacob Kaplan-Moss and the Django project, Federico
14   * Marzocchi
15   *
16   * Feed::serve() outputs the feed with the proper MIME type and
17   * using conditional GETs to save bandwidth/resources, Feed::write()
18   * just prints the generated string.
19   *
20   * When creating the feed you may use the AtomFeed or Rss20Feed
21   * classes, both share a similar API.
22   *
23   * An example:
24   *
25   * require_once('feed.php');
26   *
27   * $feed = new AtomFeed(array(
28   *     'title' => 'My feed',
29   *     'link' => 'http://localhost/myfeed.php',
30   *     'description' => 'My feed description'
31   * ));
32   *
33   * $feed->addItem(array(
34   *    'title' => 'mytitle',
35   *    'link' => 'http://example.org/1',
36   *    'description' => 'my post description',
37   *    'pubDate' => '2007-05-01',
38   *    'uniqueId' => 'http://example.org/1',
39   *    'author' => 'Alan Smithee',
40   *    'authorEmail' => 'alan@example.org'
41   * ));
42   *
43   * $feed->addItem(array(
44   *     'title' => 'another title',
45   *     'link' => 'http://example.org/2',
46   *     'description' => 'another description',
47   *     'pubDate' => '2007-05-05',
48   *     'uniqueId' => 'http://example.org/2',
49   *     'author' => 'Joe Bloggs',
50   *     'authorEmail' => 'joe@example.org'
51   * ));
52   *
53   * $feed->serve();
54   *
55   */
56
57class FeedWriter extends XMLWriter {
58    public function elementWithAttrs($name, $attrs, $content=false) {
59        $this->startElement($name);
60        foreach ($attrs as $key => $value) {
61            $this->writeAttribute($key, $value);
62        }
63        if ($content) $this->text($content);
64        $this->endElement();
65    }
66}
67
68class Feed {
69    public $items = array();
70    public $writer;
71    public $charset = 'UTF-8';
72    public $title;
73    public $link;
74    public $description;
75    public $mimeType;
76
77    public function __construct($params)
78    {
79        if (array_key_exists('charset', $params)) {
80            $this->charset = $params['charset'];
81        }
82        if (array_key_exists('title', $params)) {
83            $this->title = $params['title'];
84        }
85        if (array_key_exists('link', $params)) {
86            $this->link = $params['link'];
87        }
88        if (array_key_exists('description', $params)) {
89            $this->description = $params['description'];
90        }
91    }
92
93    public function startWriter() 
94    {
95        $this->writer = new FeedWriter();
96        $this->writer->openMemory();
97        $this->writer->startDocument('1.0', $this->charset);
98    }
99
100    public function latestPostDate() 
101    {
102        $updates = array();
103        foreach ($this->items as $item) {
104            $updates[] = $item['pubDate'];
105        }
106        rsort($updates);
107        return $updates[0];
108    }
109
110    public function addItem($params) 
111    {
112        if (strlen($params['pubDate']) == 10) {
113            // yyyy-mm-dd
114            $d = strptime($params['pubDate'], "%Y-%m-%d");
115            $itemDate = mktime(
116                0, 0, 0, 
117                $d['tm_mon'] + 1, // bloody zero-indexed PHP month numbers!
118                $d['tm_mday'],
119                $d['tm_year']
120            );
121
122        } else if (strlen($params['pubDate']) == 19) {
123            // yyyy-mm-dd hh:mm:ss
124            $d = strptime($params['pubDate'], "%Y-%m-%d %H:%M:%S");           
125            $itemDate = mktime(
126                $d['tm_hour'], 
127                $d['tm_min'],
128                $d['tm_sec'],
129                $d['tm_mon'] + 1, // bloody zero-indexed PHP month numbers!
130                $d['tm_mday'],
131                $d['tm_year']
132           );
133        }
134
135        $item = array(
136            'title' => $params['title'], 
137            'link' => $params['link'], 
138            'description' => $params['description'], 
139            'pubDate' => $itemDate
140        );
141
142        if (array_key_exists('author', $params)) {
143            $item['author'] = $params['author'];
144        }
145        if (array_key_exists('authorEmail', $params)) {
146            $item['authorEmail'] = $params['authorEmail'];
147        }
148        if (array_key_exists('uniqueId', $params)) {
149            $item['uniqueId'] = $params['uniqueId'];
150        }
151        array_push($this->items, $item);
152    }
153
154    public function numItems() 
155    {
156        return sizeof($this->items);
157    }
158
159    public function rfc3339Date($date) 
160    {
161        return date('Y-m-d\TH:I:S\Z', $date);
162    }
163
164    public function rfc2822Date($date) 
165    {
166        return date('r', $date);
167    }
168
169    public function doConditionalGet($tstamp) 
170    {
171        /*
172         * A PHP implementation of conditional GET, see also:
173         * http://lightpress.org/post/php-http11-dates-and-conditional-get
174         * http://simon.incutio.com/archive/2003/04/23/conditionalGet
175         * http://fishbowl.pastiche.org/archives/001132.html
176         */
177
178        // ETag is any quoted string
179        $etag = '"'. $tstamp .'"';
180
181        // RFC1123 date, see http://bugs.php.net/bug.php?id=31842
182        if (version_compare(PHP_VERSION, "4.3.11", ">=")) {
183            $format = 'r';
184        } else {
185            $format = 'D, d M Y H:i:s O';
186        }
187        $rfc1123 = substr(gmdate('r', $tstamp), 0, -5) . 'GMT';
188
189        // RFC1036 date
190        $rfc1036 = gmdate('l, d-M-y H:i:s ', $tstamp) . 'GMT';
191
192        // asctime
193        $ctime = gmdate('D M j H:i:s', $tstamp);
194
195        // Send the headers
196        header("Last-Modified: $rfc1123");
197        header("ETag: $etag");
198
199        // See if the client has provided the required headers
200        $if_modified_since = $if_none_match = false;
201        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
202            $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
203        }
204        if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
205            $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
206        }
207        if (!$if_modified_since && !$if_none_match) {
208            // both are missing
209            return $rfc1123;
210        }
211
212        // At least one of the headers is there - check them
213        // check etag if it's there and there's no if-modified-since
214        if ($if_none_match) {
215            if ($if_none_match != $etag) {
216                // etag is there but doesn't match
217                return $rfc1123;
218            }
219            if (!$if_modified_since && ($if_none_match == $etag)) {
220                header('HTTP/1.0 304 Not Modified');
221                exit;
222            }
223        }
224        if ($if_modified_since) {
225
226            // check if-modified-since
227            foreach (array($rfc1123, $rfc1036, $ctime) as $d) {
228                if ($d == $if_modified_since) {
229
230                    // Nothing has changed since their last request
231                    // serve a 304 and exit
232                    header('HTTP/1.0 304 Not Modified');
233                    exit;
234                }
235            }
236        }
237        // return $rfc1123 as it may be useful later
238        // eg 'lastBuildDate' for RSS2
239        return $rfc1123;
240    }
241
242    public function write() { }
243
244    public function serve() 
245    { 
246        header('Content-type: '. $this->mimeType .'; charset='. $this->charset);
247        $this->doConditionalGet(date('U', $this->latestPostDate()));
248        print $this->write();
249    }
250
251}
252
253class AtomFeed extends Feed {
254    public $mimeType = 'application/atom+xml';
255    public $nameSpace = 'http://www.w3.org/2005/Atom';
256
257    public function write() 
258    {
259        $this->startWriter();
260        $this->writer->startElement('feed');
261        $this->writer->writeAttribute('xmlns', $this->nameSpace);
262        $this->writer->writeElement('id', $this->link);
263        $this->writer->writeElement('title', $this->title);
264        $this->writer->writeElement('updated', 
265            $this->rfc3339Date($this->latestPostDate()));
266        $this->writer->elementWithAttrs('link', 
267            array('rel' => 'self', 'href' => $this->link));
268        $this->writeItems();
269        $this->writer->endElement();
270        return $this->writer->outputMemory(true);
271    }
272 
273    public function writeItems() 
274    {
275        foreach ($this->items as $item) {
276            $this->writer->startElement('entry');
277            $this->writer->writeElement('title', $item['title']);
278            $this->writer->elementWithAttrs('link', 
279                array('rel' => 'alternate', 'href' => $item['link']));
280            $this->writer->elementWithAttrs('content', 
281                array('type' => 'html'), $item['description']);
282            $this->writer->writeElement('id', $item['link']);
283            $this->writer->writeElement('updated', 
284                $this->rfc3339Date($item['pubDate']));
285            if ($item['author']) {
286                $this->writer->startElement('author');
287                $this->writer->writeElement('name', $item['author']);
288                if (array_key_exists('authorEmail', $item)) {
289                    $this->writer->writeElement('email', $item['authorEmail']);
290                }
291                $this->writer->endElement();
292            }
293            $this->writer->endElement();
294        }
295    }
296}
297
298class Rss20Feed extends Feed {
299    public $mimeType = 'application/rss+xml';
300    public $rssVersion = '2.0';
301
302    public function write() 
303    {
304        $this->startWriter();
305        $this->writer->startElement('rss');
306        $this->writer->writeAttribute('version', $this->rssVersion);
307        $this->writer->startElement('channel');
308        $this->writer->writeElement('link', $this->link);
309        $this->writer->writeElement('description', $this->description);
310        $this->writer->writeElement('title', $this->title);
311        $this->writer->writeElement('lastBuildDate', 
312            $this->rfc2822Date($this->latestPostDate()));
313        $this->writeItems();
314        $this->writer->endElement();
315        $this->writer->endElement();
316        return $this->writer->outputMemory(true);
317    }
318 
319    public function writeItems() 
320    {
321        foreach ($this->items as $item) {
322            $this->writer->startElement('item');
323            $this->writer->writeElement('title', $item['title']);
324            $this->writer->writeElement('link', $item['link']);
325            $this->writer->writeElement('guid', $item['uniqueId']);
326            $this->writer->writeElement('description', $item['description']);
327            $this->writer->writeElement('pubDate', 
328                $this->rfc2822Date($item['pubDate']));
329            if (array_key_exists('author', $item)) {
330                $this->writer->writeElement('author', 
331                    "{$item['authorEmail']} ({$item['author']})");
332            }
333            $this->writer->endElement();
334        }
335    }
336}
337?>
Note: See TracBrowser for help on using the browser.