<?php
  /* feed.php 
   * 
   * A collection of simple classes to output Atom 1.0 and RSS 2.0
   * feeds from PHP 5.
   * 
   * author: Antonio Cavedoni <http://cavedoni.com/>
   * version: 0.4
   * revision: $Id$
   * license: BSD
   * 
   * acknowledgements: Ludovico Magnocavallo, Simon Willison, Adrian
   * Holovaty, Jacob Kaplan-Moss and the Django project, Federico
   * Marzocchi
   * 
   * Feed::serve() outputs the feed with the proper MIME type and
   * using conditional GETs to save bandwidth/resources, Feed::write()
   * just prints the generated string.
   * 
   * When creating the feed you may use the AtomFeed or Rss20Feed
   * classes, both share a similar API.
   * 
   * An example:
   * 
   * require_once('feed.php');
   * 
   * $feed = new AtomFeed(array(
   *     'title' => 'My feed', 
   *     'link' => 'http://localhost/myfeed.php', 
   *     'description' => 'My feed description'
   * ));
   * 
   * $feed->addItem(array(
   *    'title' => 'mytitle', 
   *    'link' => 'http://example.org/1', 
   *    'description' => 'my post description', 
   *    'pubDate' => '2007-05-01', 
   *    'uniqueId' => 'http://example.org/1', 
   *    'author' => 'Alan Smithee', 
   *    'authorEmail' => 'alan@example.org'
   * ));
   * 
   * $feed->addItem(array(
   *     'title' => 'another title', 
   *     'link' => 'http://example.org/2', 
   *     'description' => 'another description', 
   *     'pubDate' => '2007-05-05', 
   *     'uniqueId' => 'http://example.org/2', 
   *     'author' => 'Joe Bloggs', 
   *     'authorEmail' => 'joe@example.org'
   * ));
   * 
   * $feed->serve();
   * 
   */

class FeedWriter extends XMLWriter {
    public function elementWithAttrs($name, $attrs, $content=false) {
        $this->startElement($name);
        foreach ($attrs as $key => $value) {
            $this->writeAttribute($key, $value);
        }
        if ($content) $this->text($content);
        $this->endElement();
    }
}

class Feed {
    public $items = array();
    public $writer;
    public $charset = 'UTF-8';
    public $title;
    public $link;
    public $description;
    public $mimeType;

    public function __construct($params)
    {
        if (array_key_exists('charset', $params)) {
            $this->charset = $params['charset'];
        }
        if (array_key_exists('title', $params)) {
            $this->title = $params['title'];
        }
        if (array_key_exists('link', $params)) {
            $this->link = $params['link'];
        }
        if (array_key_exists('description', $params)) {
            $this->description = $params['description'];
        }
    }

    public function startWriter() 
    {
        $this->writer = new FeedWriter();
        $this->writer->openMemory();
        $this->writer->startDocument('1.0', $this->charset);
    }

    public function latestPostDate() 
    {
        $updates = array();
        foreach ($this->items as $item) {
            $updates[] = $item['pubDate'];
        }
        rsort($updates);
        return $updates[0];
    }

    public function addItem($params) 
    {
        if (strlen($params['pubDate']) == 10) {
            // yyyy-mm-dd
            $d = strptime($params['pubDate'], "%Y-%m-%d");
            $itemDate = mktime(
                0, 0, 0, 
                $d['tm_mon'] + 1, // bloody zero-indexed PHP month numbers!
                $d['tm_mday'],
                $d['tm_year']
            );

        } else if (strlen($params['pubDate']) == 19) {
            // yyyy-mm-dd hh:mm:ss
            $d = strptime($params['pubDate'], "%Y-%m-%d %H:%M:%S");            
            $itemDate = mktime(
                $d['tm_hour'], 
                $d['tm_min'],
                $d['tm_sec'],
                $d['tm_mon'] + 1, // bloody zero-indexed PHP month numbers!
                $d['tm_mday'],
                $d['tm_year']
           );
        }

        $item = array(
            'title' => $params['title'], 
            'link' => $params['link'], 
            'description' => $params['description'], 
            'pubDate' => $itemDate
        );

        if (array_key_exists('author', $params)) {
            $item['author'] = $params['author'];
        }
        if (array_key_exists('authorEmail', $params)) {
            $item['authorEmail'] = $params['authorEmail'];
        }
        if (array_key_exists('uniqueId', $params)) {
            $item['uniqueId'] = $params['uniqueId'];
        }
        array_push($this->items, $item);
    }

    public function numItems() 
    {
        return sizeof($this->items);
    }

    public function rfc3339Date($date) 
    {
        return date('Y-m-d\TH:I:S\Z', $date);
    }

    public function rfc2822Date($date) 
    {
        return date('r', $date);
    }

    public function doConditionalGet($tstamp) 
    {
        /*
         * A PHP implementation of conditional GET, see also: 
         * http://lightpress.org/post/php-http11-dates-and-conditional-get
         * http://simon.incutio.com/archive/2003/04/23/conditionalGet
         * http://fishbowl.pastiche.org/archives/001132.html
         */

        // ETag is any quoted string
        $etag = '"'. $tstamp .'"';

        // RFC1123 date, see http://bugs.php.net/bug.php?id=31842
        if (version_compare(PHP_VERSION, "4.3.11", ">=")) {
            $format = 'r';
        } else {
            $format = 'D, d M Y H:i:s O';
        }
        $rfc1123 = substr(gmdate('r', $tstamp), 0, -5) . 'GMT';

        // RFC1036 date
        $rfc1036 = gmdate('l, d-M-y H:i:s ', $tstamp) . 'GMT';

        // asctime
        $ctime = gmdate('D M j H:i:s', $tstamp);

        // Send the headers
        header("Last-Modified: $rfc1123");
        header("ETag: $etag");

        // See if the client has provided the required headers
        $if_modified_since = $if_none_match = false;
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
            $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
        }
        if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
            $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
        }
        if (!$if_modified_since && !$if_none_match) {
            // both are missing
            return $rfc1123;
        }

        // At least one of the headers is there - check them
        // check etag if it's there and there's no if-modified-since
        if ($if_none_match) {
            if ($if_none_match != $etag) {
                // etag is there but doesn't match
                return $rfc1123;
            }
            if (!$if_modified_since && ($if_none_match == $etag)) {
                header('HTTP/1.0 304 Not Modified');
                exit;
            }
        }
        if ($if_modified_since) {

            // check if-modified-since
            foreach (array($rfc1123, $rfc1036, $ctime) as $d) {
                if ($d == $if_modified_since) {

                    // Nothing has changed since their last request
                    // serve a 304 and exit
                    header('HTTP/1.0 304 Not Modified');
                    exit;
                }
            }
        }
        // return $rfc1123 as it may be useful later
        // eg 'lastBuildDate' for RSS2
        return $rfc1123;
    }

    public function write() { }

    public function serve() 
    { 
        header('Content-type: '. $this->mimeType .'; charset='. $this->charset);
        $this->doConditionalGet(date('U', $this->latestPostDate()));
        print $this->write();
    }

}

class AtomFeed extends Feed {
    public $mimeType = 'application/atom+xml';
    public $nameSpace = 'http://www.w3.org/2005/Atom';

    public function write() 
    {
        $this->startWriter();
        $this->writer->startElement('feed');
        $this->writer->writeAttribute('xmlns', $this->nameSpace);
        $this->writer->writeElement('id', $this->link);
        $this->writer->writeElement('title', $this->title);
        $this->writer->writeElement('updated', 
            $this->rfc3339Date($this->latestPostDate()));
        $this->writer->elementWithAttrs('link', 
            array('rel' => 'self', 'href' => $this->link));
        $this->writeItems();
        $this->writer->endElement();
        return $this->writer->outputMemory(true);
    }
  
    public function writeItems() 
    {
        foreach ($this->items as $item) {
            $this->writer->startElement('entry');
            $this->writer->writeElement('title', $item['title']);
            $this->writer->elementWithAttrs('link', 
                array('rel' => 'alternate', 'href' => $item['link']));
            $this->writer->elementWithAttrs('content', 
                array('type' => 'html'), $item['description']);
            $this->writer->writeElement('id', $item['link']);
            $this->writer->writeElement('updated', 
                $this->rfc3339Date($item['pubDate']));
            if ($item['author']) {
                $this->writer->startElement('author');
                $this->writer->writeElement('name', $item['author']);
                if (array_key_exists('authorEmail', $item)) {
                    $this->writer->writeElement('email', $item['authorEmail']);
                }
                $this->writer->endElement();
            }
            $this->writer->endElement();
        }
    }
}

class Rss20Feed extends Feed {
    public $mimeType = 'application/rss+xml';
    public $rssVersion = '2.0';

    public function write() 
    {
        $this->startWriter();
        $this->writer->startElement('rss');
        $this->writer->writeAttribute('version', $this->rssVersion);
        $this->writer->startElement('channel');
        $this->writer->writeElement('link', $this->link);
        $this->writer->writeElement('description', $this->description);
        $this->writer->writeElement('title', $this->title);
        $this->writer->writeElement('lastBuildDate', 
            $this->rfc2822Date($this->latestPostDate()));
        $this->writeItems();
        $this->writer->endElement();
        $this->writer->endElement();
        return $this->writer->outputMemory(true);
    }
  
    public function writeItems() 
    {
        foreach ($this->items as $item) {
            $this->writer->startElement('item');
            $this->writer->writeElement('title', $item['title']);
            $this->writer->writeElement('link', $item['link']);
            $this->writer->writeElement('guid', $item['uniqueId']);
            $this->writer->writeElement('description', $item['description']);
            $this->writer->writeElement('pubDate', 
                $this->rfc2822Date($item['pubDate']));
            if (array_key_exists('author', $item)) {
                $this->writer->writeElement('author', 
                    "{$item['authorEmail']} ({$item['author']})");
            }
            $this->writer->endElement();
        }
    }
}
?>