2
0
Просмотр исходного кода

Zend_Feed_Writer promoted to trunk

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@19728 44c647ce-9c0f-0410-b52a-842ac1e357ba
padraic 16 лет назад
Родитель
Сommit
82046f3cff
34 измененных файлов с 7568 добавлено и 0 удалено
  1. 1 0
      documentation/manual/en/manual.xml.in
  2. 180 0
      documentation/manual/en/module_specs/Zend_Feed_Writer.xml
  3. 274 0
      library/Zend/Feed/Writer.php
  4. 667 0
      library/Zend/Feed/Writer/Entry.php
  5. 41 0
      library/Zend/Feed/Writer/Exception/InvalidMethodException.php
  6. 87 0
      library/Zend/Feed/Writer/Extension/Atom/Renderer/Feed.php
  7. 67 0
      library/Zend/Feed/Writer/Extension/Content/Renderer/Entry.php
  8. 67 0
      library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Entry.php
  9. 67 0
      library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Feed.php
  10. 171 0
      library/Zend/Feed/Writer/Extension/ITunes/Entry.php
  11. 255 0
      library/Zend/Feed/Writer/Extension/ITunes/Feed.php
  12. 142 0
      library/Zend/Feed/Writer/Extension/ITunes/Renderer/Entry.php
  13. 222 0
      library/Zend/Feed/Writer/Extension/ITunes/Renderer/Feed.php
  14. 103 0
      library/Zend/Feed/Writer/Extension/RendererAbstract.php
  15. 38 0
      library/Zend/Feed/Writer/Extension/RendererInterface.php
  16. 63 0
      library/Zend/Feed/Writer/Extension/Slash/Renderer/Entry.php
  17. 103 0
      library/Zend/Feed/Writer/Extension/Threading/Renderer/Entry.php
  18. 67 0
      library/Zend/Feed/Writer/Extension/WellFormedWeb/Renderer/Entry.php
  19. 900 0
      library/Zend/Feed/Writer/Feed.php
  20. 248 0
      library/Zend/Feed/Writer/Renderer/Entry/Atom.php
  21. 214 0
      library/Zend/Feed/Writer/Renderer/Entry/Rss.php
  22. 348 0
      library/Zend/Feed/Writer/Renderer/Feed/Atom.php
  23. 277 0
      library/Zend/Feed/Writer/Renderer/Feed/Rss.php
  24. 181 0
      library/Zend/Feed/Writer/Renderer/RendererAbstract.php
  25. 78 0
      library/Zend/Feed/Writer/Renderer/RendererInterface.php
  26. 20 0
      tests/Zend/Feed/AllTests.php
  27. 571 0
      tests/Zend/Feed/Writer/EntryTest.php
  28. 201 0
      tests/Zend/Feed/Writer/Extension/ITunes/EntryTest.php
  29. 288 0
      tests/Zend/Feed/Writer/Extension/ITunes/FeedTest.php
  30. 609 0
      tests/Zend/Feed/Writer/FeedTest.php
  31. 247 0
      tests/Zend/Feed/Writer/Renderer/Entry/AtomTest.php
  32. 217 0
      tests/Zend/Feed/Writer/Renderer/Entry/RssTest.php
  33. 296 0
      tests/Zend/Feed/Writer/Renderer/Feed/AtomTest.php
  34. 258 0
      tests/Zend/Feed/Writer/Renderer/Feed/RssTest.php

+ 1 - 0
documentation/manual/en/manual.xml.in

@@ -203,6 +203,7 @@
         <xi:include href="module_specs/Zend_Feed-ModifyingFeed.xml" />
         <xi:include href="module_specs/Zend_Feed-CustomFeed.xml" />
         <xi:include href="module_specs/Zend_Feed_Reader.xml" />
+        <xi:include href="module_specs/Zend_Feed_Writer.xml" />
     </chapter>
 
     <chapter id="zend.file">

+ 180 - 0
documentation/manual/en/module_specs/Zend_Feed_Writer.xml

@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<sect1 id="zend.feed.writer">
+    <title>Zend_Feed_Writer</title>
+
+    <sect2 id="zend.feed.writer.introduction">
+        <title>Introduction</title>
+
+        <para><classname>Zend_Feed_Writer</classname> is the sibling component to
+        <classname>Zend_Feed_Reader</classname> responsible for generating feeds
+        for output. It supports the Atom 1.0 specification (RFC 4287) and RSS 2.0
+        as specified by the RSS Advisory Board (RSS 2.0.11). It does not deviate
+        from these standards. It does, however, offer a simple Extension system
+        which allows for any extension/module for either of these two specifications
+        to be implemented if they are not provided out of the box.</para>
+        
+        <para>In many ways, <classname>Zend_Feed_Writer</classname> is the inverse
+        of <classname>Zend_Feed_Reader</classname>. Where
+        <classname>Zend_Feed_Reader</classname> focused on providing an easy to
+        use architecture fronted by getter methods, <classname>Zend_Feed_Writer</classname>
+        is fronted by similarly named setters or mutators. This ensures the API
+        won't pose a learning curve to anyone familiar with
+        <classname>Zend_Feed_Reader</classname>.</para>
+        
+        <para>As a result of this design, the rest may even be obvious. Behind the
+        scenes, data set on any <classname>Zend_Feed_Reader</classname> object is
+        translated at render time onto a <classname>DOMDocument</classname> object
+        using the necessary feed elements. For each supported feed type there is
+        both an Atom 1.0 and RSS 2.0 renderer. Using a <classname>DOMDocument</classname>
+        rather a templating solution has numerous advantages, the most obvious being
+        the ability to export the <classname>DOMDocument</classname> for additional
+        processing and relying on PHP DOM for correct and valid rendering.</para>
+        
+        <para>As with <classname>Zend_Feed_Reader</classname>,
+        <classname>Zend_Feed_Writer</classname> is a standalone replacement for
+        <classname>Zend_Feed</classname>'s Builder architecture and is not compatible
+        with those classes.</para>
+        
+    </sect2>
+    
+    <sect2 id="zend.feed.writer.architecture">
+        <title>Architecture</title>
+        
+        <para>The architecture of <classname>Zend_Feed_Writer</classname> is very
+        simple. It has two core sets of classes: containers and renderers.</para>
+        
+        <para>The containers include the <classname>Zend_Feed_Writer_Feed</classname>
+        and <classname>Zend_Feed_Writer_Entry</classname> classes. The Entry classes
+        can be attached to any Feed class. The sole purpose of these containers is
+        to collect data about the feed to generate using a simple interface of setter
+        methods. These methods perform some data validity testing. For example, it
+        will validate any passed URIs, dates, etc. These checks are not tied to any
+        of the feed standards. The container objects also contain methods to allow
+        for fast rendering and export of the final feed, and these can be reused at will.</para>
+        
+        <para>While there are two data containers, there are four renderers - two
+        matching container renderers per support feed type. The renderer accept a
+        container, and based on its content attempt to generate a valid feed. If
+        the renderer is unable to generate a valid feed, perhaps due to the container
+        missing an obligatory data point, it will report this by throwing an
+        <classname>Exception</classname>. While it is possible to ignore
+        <classname>Exception</classname>s, this removes the default safeguard of
+        ensuring you have sufficient data set to render a wholly valid feed.</para>
+        
+        <para>Due to the system being divided between data containers and
+        renderers, it can make Extensions somewhat interesting. A typical Extension
+        offering namespaced feed and entry level elements, must itself reflect the
+        exact same architecture, i.e. offer feed and entry level data containers,
+        and matching renderers. There is, fortunately, no complex integration work
+        required since all Extension classes are simply registered and automatically
+        used by the core classes. We'll meet Extensions in more detail at the end
+        of this section.</para>
+        
+    </sect2>
+    
+    <sect2 id="zend.feed.writer.getting.started">
+        <title>Getting Started</title>
+    
+        <para>Using <classname>Zend_Feed_Reader</classname> is as simple as
+        setting data and triggering the renderer. Here is an example to generate
+        a minimal Atom 1.0 feed.</para>
+        
+        <programlisting language="php"><![CDATA[
+require_once 'Zend/Feed/Writer/Feed.php';
+
+/**
+ * Create the parent feed
+ */
+
+$feed = new Zend_Feed_Writer_Feed;
+$feed->setTitle('Paddy\'s Blog');
+$feed->setLink('http://www.example.com');
+$feed->setFeedLink('http://www.example.com/atom', 'atom');
+$feed->addAuthor(array(
+    'name' => 'Paddy',
+    'email' => 'paddy@example.com',
+    'uri' => 'http://www.example.com'
+));
+$feed->setDateModified(time());
+$feed->addHub('http://pubsubhubbub.appspot.com/');
+
+/**
+ * Add one or more entries. Note that entries must
+ * be manually added once created.
+ */
+
+$entry = $feed->createEntry();
+$entry->setTitle('All Your Base Are Belong To Us');
+$entry->setLink('http://www.example.com/all-your-base-are-belong-to-us');
+$entry->addAuthor(array(
+    'name' => 'Paddy',
+    'email' => 'paddy@example.com',
+    'uri' => 'http://www.example.com'
+));
+$entry->setDateModified(time());
+$entry->setDateCreated(time());
+$entry->setDescription('Exposing the difficultly of porting games to English.');
+$entry->setContent('I am not writing the article. The example is long enough as is ;).');
+$feed->addEntry($entry);
+
+/**
+ * Render the resulting feed to Atom 1.0 and assign to $out.
+ * You can substitute "atom" with "rss" to generate an RSS 2.0 feed.
+ */
+
+$out = $feed->export('atom');
+]]></programlisting>
+
+        <para>The output rendered should be as follows:</para>
+        
+                <programlisting language="xml">
+&#60;?xml version="1.0" encoding="utf-8"?&#62;
+&#60;feed xmlns="http://www.w3.org/2005/Atom"&#62;
+    &#60;title type="text"&#62;Paddy's Blog&#60;/title&#62;
+    &#60;subtitle type="text"&#62;Writing about PC Games since 176 BC.&#60;/subtitle&#62;
+    &#60;updated&#62;2009-12-14T20:28:18+00:00&#60;/updated&#62;
+    &#60;generator uri="http://framework.zend.com" version="1.10.0alpha"&#62;
+        Zend_Feed_Writer
+    &#60;/generator&#62;
+    &#60;link rel="alternate" type="text/html" href="http://www.example.com"/&#62;
+    &#60;link rel="self" type="application/atom+xml" href="http://www.example.com/atom"/&#62;
+    &#60;id&#62;http://www.example.com&#60;/id&#62;
+    &#60;author&#62;
+        &#60;name&#62;Paddy&#60;/name&#62;
+        &#60;email&#62;paddy@example.com&#60;/email&#62;
+        &#60;uri&#62;http://www.example.com&#60;/uri&#62;
+    &#60;/author&#62;
+    &#60;link rel="hub" href="http://pubsubhubbub.appspot.com/"/&#62;
+    &#60;entry&#62;
+        &#60;title type="html"&#62;&#60;![CDATA[All Your Base Are Belong To Us]]&#62;&#60;/title&#62;
+        &#60;summary type="html"&#62;
+            &#60;![CDATA[Exposing the difficultly of porting games to English.]]&#62;
+        &#60;/summary&#62;
+        &#60;published&#62;2009-12-14T20:28:18+00:00&#60;/published&#62;
+        &#60;updated&#62;2009-12-14T20:28:18+00:00&#60;/updated&#62;
+        &#60;link rel="alternate" type="text/html" href="http://www.example.com/all-your-base-are-belong-to-us"/&#62;
+        &#60;id&#62;http://www.example.com/all-your-base-are-belong-to-us&#60;/id&#62;
+        &#60;author&#62;
+        &#60;name&#62;Paddy&#60;/name&#62;
+        &#60;email&#62;paddy@example.com&#60;/email&#62;
+        &#60;uri&#62;http://www.example.com&#60;/uri&#62;
+        &#60;/author&#62;
+        &#60;content type="html"&#62;
+            &#60;![CDATA[I am not writing the article. The example is long enough as is ;).]]&#62;
+        &#60;/content&#62;
+    &#60;/entry&#62;
+&#60;/feed&#62;
+</programlisting>
+
+        <para>This is a perfect Atom 1.0 example. It should be noted that omitting
+        an obligatory point of data, such as a title, will trigger an
+        <classname>Exception</classname> when rendering as Atom 1.0. This will
+        differ for RSS 2.0 since a title may be omitted so long as a description
+        is present. This gives rise to Exceptions that differ between the two standards
+        depending on the renderer in use. By design, <classname>Zend_Feed_Writer</classname>
+        will not render an invalid feed for either standard unless the end-user
+        deliberately elects to ignore all Exceptions.</para>
+    
+    </sect2>
+
+</sect1>

+ 274 - 0
library/Zend/Feed/Writer.php

@@ -0,0 +1,274 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer
+{
+	/**
+	 * Namespace constants
+	 */
+	const NAMESPACE_ATOM_03  = 'http://purl.org/atom/ns#';
+    const NAMESPACE_ATOM_10  = 'http://www.w3.org/2005/Atom';
+    const NAMESPACE_RDF      = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
+    const NAMESPACE_RSS_090  = 'http://my.netscape.com/rdf/simple/0.9/';
+    const NAMESPACE_RSS_10   = 'http://purl.org/rss/1.0/';
+
+    /**
+	 * Feed type constants
+	 */
+	const TYPE_ANY              = 'any';
+	const TYPE_ATOM_03          = 'atom-03';
+    const TYPE_ATOM_10          = 'atom-10';
+    const TYPE_ATOM_ANY         = 'atom';
+    const TYPE_RSS_090          = 'rss-090';
+    const TYPE_RSS_091          = 'rss-091';
+    const TYPE_RSS_091_NETSCAPE = 'rss-091n';
+    const TYPE_RSS_091_USERLAND = 'rss-091u';
+    const TYPE_RSS_092          = 'rss-092';
+    const TYPE_RSS_093          = 'rss-093';
+    const TYPE_RSS_094          = 'rss-094';
+    const TYPE_RSS_10           = 'rss-10';
+    const TYPE_RSS_20           = 'rss-20';
+    const TYPE_RSS_ANY          = 'rss';
+    
+    /**
+     * PluginLoader instance used by component
+     *
+     * @var Zend_Loader_PluginLoader_Interface
+     */
+    protected static $_pluginLoader = null;
+
+    /**
+     * Path on which to search for Extension classes
+     *
+     * @var array
+     */
+    protected static $_prefixPaths = array();
+
+    /**
+     * Array of registered extensions by class postfix (after the base class
+     * name) across four categories - data containers and renderers for entry
+     * and feed levels.
+     *
+     * @var array
+     */
+    protected static $_extensions = array(
+        'entry' => array(),
+        'feed' => array(),
+        'entryRenderer' => array(),
+        'feedRenderer' => array()
+    );
+    
+    /**
+     * Set plugin loader for use with Extensions
+     *
+     * @param  Zend_Loader_PluginLoader_Interface
+     */
+    public static function setPluginLoader(Zend_Loader_PluginLoader_Interface $loader)
+    {
+        self::$_pluginLoader = $loader;
+    }
+
+    /**
+     * Get plugin loader for use with Extensions
+     *
+     * @return  Zend_Loader_PluginLoader_Interface
+     */
+    public static function getPluginLoader()
+    {
+        if (!isset(self::$_pluginLoader)) {
+            require_once 'Zend/Loader/PluginLoader.php';
+            self::$_pluginLoader = new Zend_Loader_PluginLoader(array(
+                'Zend_Feed_Writer_Extension_' => 'Zend/Feed/Writer/Extension/',
+            ));
+        }
+        return self::$_pluginLoader;
+    }
+
+    /**
+     * Add prefix path for loading Extensions
+     *
+     * @param  string $prefix
+     * @param  string $path
+     * @return void
+     */
+    public static function addPrefixPath($prefix, $path)
+    {
+        $prefix = rtrim($prefix, '_');
+        $path   = rtrim($path, DIRECTORY_SEPARATOR);
+        self::getPluginLoader()->addPrefixPath($prefix, $path);
+    }
+
+    /**
+     * Add multiple Extension prefix paths at once
+     *
+     * @param  array $spec
+     * @return void
+     */
+    public static function addPrefixPaths(array $spec)
+    {
+        if (isset($spec['prefix']) && isset($spec['path'])) {
+            self::addPrefixPath($spec['prefix'], $spec['path']);
+        }
+        foreach ($spec as $prefixPath) {
+            if (isset($prefixPath['prefix']) && isset($prefixPath['path'])) {
+                self::addPrefixPath($prefixPath['prefix'], $prefixPath['path']);
+            }
+        }
+    }
+
+    /**
+     * Register an Extension by name
+     *
+     * @param  string $name
+     * @return void
+     * @throws Zend_Feed_Exception if unable to resolve Extension class
+     */
+    public static function registerExtension($name)
+    {
+        $feedName  = $name . '_Feed';
+        $entryName = $name . '_Entry';
+        $feedRendererName  = $name . '_Renderer_Feed';
+        $entryRendererName = $name . '_Renderer_Entry';
+        if (self::isRegistered($name)) {
+            if (self::getPluginLoader()->isLoaded($feedName)
+            || self::getPluginLoader()->isLoaded($entryName)
+            || self::getPluginLoader()->isLoaded($feedRendererName)
+            || self::getPluginLoader()->isLoaded($entryRendererName)) {
+                return;
+            }
+        }
+        try {
+            self::getPluginLoader()->load($feedName);
+            self::$_extensions['feed'][] = $feedName;
+        } catch (Zend_Loader_PluginLoader_Exception $e) {
+        }
+        try {
+            self::getPluginLoader()->load($entryName);
+            self::$_extensions['entry'][] = $entryName;
+        } catch (Zend_Loader_PluginLoader_Exception $e) {
+        }
+        try {
+            self::getPluginLoader()->load($feedRendererName);
+            self::$_extensions['feedRenderer'][] = $feedRendererName;
+        } catch (Zend_Loader_PluginLoader_Exception $e) {
+        }
+        try {
+            self::getPluginLoader()->load($entryRendererName);
+            self::$_extensions['entryRenderer'][] = $entryRendererName;
+        } catch (Zend_Loader_PluginLoader_Exception $e) {
+        }
+        if (!self::getPluginLoader()->isLoaded($feedName)
+        && !self::getPluginLoader()->isLoaded($entryName)
+        && !self::getPluginLoader()->isLoaded($feedRendererName)
+        && !self::getPluginLoader()->isLoaded($entryRendererName)
+        ) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Could not load extension: ' . $name
+                . 'using Plugin Loader. Check prefix paths are configured and extension exists.');
+        }
+    }
+
+    /**
+     * Is a given named Extension registered?
+     *
+     * @param  string $extensionName
+     * @return boolean
+     */
+    public static function isRegistered($extensionName)
+    {
+        $feedName  = $extensionName . '_Feed';
+        $entryName = $extensionName . '_Entry';
+        $feedRendererName  = $extensionName . '_Renderer_Feed';
+        $entryRendererName = $extensionName . '_Renderer_Entry';
+        if (in_array($feedName, self::$_extensions['feed'])
+        || in_array($entryName, self::$_extensions['entry'])
+        || in_array($feedRendererName, self::$_extensions['feedRenderer'])
+        || in_array($entryRendererName, self::$_extensions['entryRenderer'])
+        ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get a list of extensions
+     *
+     * @return array
+     */
+    public static function getExtensions()
+    {
+        return self::$_extensions;
+    }
+
+    /**
+     * Reset class state to defaults
+     *
+     * @return void
+     */
+    public static function reset()
+    {
+        self::$_pluginLoader       = null;
+        self::$_prefixPaths        = array();
+        self::$_extensions         = array(
+            'entry' => array(),
+            'feed' => array(),
+            'entryRenderer' => array(),
+            'feedRenderer' => array()
+        );
+    }
+
+    /**
+     * Register core (default) extensions
+     *
+     * @return void
+     */
+    public static function registerCoreExtensions()
+    {
+        self::registerExtension('DublinCore');
+        self::registerExtension('Content');
+        self::registerExtension('Atom');
+        self::registerExtension('Slash');
+        self::registerExtension('WellFormedWeb');
+        self::registerExtension('Threading');
+        self::registerExtension('ITunes');
+    }
+    
+    /**
+     * Replaces XML special characters with entities.
+     *
+     * @param string $string
+     * @param string $encoding
+     * @return string
+     */
+    public static function xmlentities($string, $encoding)
+    {
+        return str_replace('&#039;', '&apos;', htmlspecialchars(
+            $string, ENT_QUOTES, $encoding
+        ));
+    }
+
+}

+ 667 - 0
library/Zend/Feed/Writer/Entry.php

@@ -0,0 +1,667 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Date
+ */
+require_once 'Zend/Date.php';
+
+/**
+ * @see Zend_Date
+ */
+require_once 'Zend/Uri.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Entry
+{
+
+    /**
+     * Internal array containing all data associated with this entry or item.
+     *
+     * @var array
+     */
+    protected $_data = array();
+    
+    /**
+     * Registered extensions
+     *
+     * @var array
+     */
+    protected $_extensions = array();
+    
+    /**
+     * Holds the value "atom" or "rss" depending on the feed type set when
+     * when last exported.
+     *
+     * @var string
+     */
+    protected $_type = null;
+    
+    /**
+     * Constructor: Primarily triggers the registration of core extensions and
+     * loads those appropriate to this data container.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        Zend_Feed_Writer::registerCoreExtensions();
+        $this->_loadExtensions();
+    }
+
+    /**
+     * Set a single author
+     *
+     * @param  int $index
+     * @return string|null
+     */
+    public function addAuthor($name, $email = null, $uri = null)
+    {
+        $author = array();
+        if (is_array($name)) {
+            if (!array_key_exists('name', $name) || empty($name['name']) || !is_string($name['name'])) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('Invalid parameter: author array must include a "name" key with a non-empty string value');
+            }
+            $author['name'] = $name['name'];
+            if (isset($name['email'])) {
+                if (empty($name['email']) || !is_string($name['email'])) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "email" array value must be a non-empty string');
+                }
+                $author['email'] = $name['email'];
+            }
+            if (isset($name['uri'])) {
+                if (empty($name['uri']) || !is_string($name['uri']) || !Zend_Uri::check($name['uri'])) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "uri" array value must be a non-empty string and valid URI/IRI');
+                }
+                $author['uri'] = $name['uri'];
+            }
+        } else {
+            if (empty($name['name']) || !is_string($name['name'])) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('Invalid parameter: "name" must be a non-empty string value');
+            }
+            $author['name'] = $name;
+            if (isset($email)) {
+                if (empty($email) || !is_string($email)) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "email" value must be a non-empty string');
+                }
+                $author['email'] = $email;
+            }
+            if (isset($uri)) {
+                if (empty($uri) || !is_string($uri) || !Zend_Uri::check($uri)) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "uri" value must be a non-empty string and valid URI/IRI');
+                }
+                $author['uri'] = $uri;
+            }
+        }
+        $this->_data['authors'][] = $author;
+    }
+
+    /**
+     * Set an array with feed authors
+     *
+     * @return array
+     */
+    public function addAuthors(array $authors)
+    {
+        foreach($authors as $author) {
+            $this->addAuthor($author);
+        }
+    }
+    
+    /**
+     * Set the feed character encoding
+     *
+     * @return string|null
+     */
+    public function setEncoding($encoding)
+    {
+        if (empty($encoding) || !is_string($encoding)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['encoding'] = $encoding;
+    }
+
+    /**
+     * Get the feed character encoding
+     *
+     * @return string|null
+     */
+    public function getEncoding()
+    {
+        if (!array_key_exists('encoding', $this->_data)) {
+            return 'UTF-8';
+        }
+        return $this->_data['encoding'];
+    }
+
+    /**
+     * Set the copyright entry
+     *
+     * @return string|null
+     */
+    public function setCopyright($copyright)
+    {
+        if (empty($copyright) || !is_string($copyright)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['copyright'] = $copyright;
+    }
+
+    /**
+     * Set the entry's content
+     *
+     * @return string|null
+     */
+    public function setContent($content)
+    {
+        if (empty($content) || !is_string($content)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['content'] = $content;
+    }
+
+    /**
+     * Set the feed creation date
+     *
+     * @return string|null
+     */
+    public function setDateCreated($date = null)
+    {
+        $zdate = null;
+        if (is_null($date)) {
+            $zdate = new Zend_Date;
+        } elseif (ctype_digit($date) && strlen($date) == 10) {
+            $zdate = new Zend_Date($date, Zend_Date::TIMESTAMP);
+        } elseif ($date instanceof Zend_Date) {
+            $zdate = $date;
+        } else {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid Zend_Date object or UNIX Timestamp passed as parameter');
+        }
+        $this->_data['dateCreated'] = $zdate;
+    }
+
+    /**
+     * Set the feed modification date
+     *
+     * @return string|null
+     */
+    public function setDateModified($date = null)
+    {
+        $zdate = null;
+        if (is_null($date)) {
+            $zdate = new Zend_Date;
+        } elseif (ctype_digit($date) && strlen($date) == 10) {
+            $zdate = new Zend_Date($date, Zend_Date::TIMESTAMP);
+        } elseif ($date instanceof Zend_Date) {
+            $zdate = $date;
+        } else {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid Zend_Date object or UNIX Timestamp passed as parameter');
+        }
+        $this->_data['dateModified'] = $zdate;
+    }
+
+    /**
+     * Set the feed description
+     *
+     * @return string|null
+     */
+    public function setDescription($description)
+    {
+        if (empty($description) || !is_string($description)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['description'] = $description;
+    }
+
+    /**
+     * Set the feed ID
+     *
+     * @return string|null
+     */
+    public function setId($id)
+    {
+        if (empty($id) || !is_string($id)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['id'] = $id;
+    }
+
+    /**
+     * Set a link to the HTML source of this entry
+     *
+     * @return string|null
+     */
+    public function setLink($link)
+    {
+        if (empty($link) || !is_string($link) || !Zend_Uri::check($link)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string and valid URI/IRI');
+        }
+        $this->_data['link'] = $link;
+    }
+
+    /**
+     * Set the number of comments associated with this entry
+     *
+     * @return string|null
+     */
+    public function setCommentCount($count)
+    {
+        if (empty($count) || !is_numeric($count) || (int) $count < 0) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "count" must be a non-empty integer number');
+        }
+        $this->_data['commentCount'] = (int) $count;
+    }
+
+    /**
+     * Set a link to a HTML page containing comments associated with this entry
+     *
+     * @return string|null
+     */
+    public function setCommentLink($link)
+    {
+        if (empty($link) || !is_string($link) || !Zend_Uri::check($link)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "link" must be a non-empty string and valid URI/IRI');
+        }
+        $this->_data['commentLink'] = $link;
+    }
+
+    /**
+     * Set a link to an XML feed for any comments associated with this entry
+     *
+     * @return string|null
+     */
+    public function setCommentFeedLink(array $link)
+    {
+        if (!isset($link['uri']) || !is_string($link['uri']) || !Zend_Uri::check($link['uri'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "link" must be a non-empty string and valid URI/IRI');
+        }
+        if (!isset($link['type']) || !in_array($link['type'], array('atom', 'rss', 'rdf'))) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "type" must be one'
+            . ' of "atom", "rss" or "rdf"');
+        }
+        if (!isset($this->_data['commentFeedLinks'])) {
+            $this->_data['commentFeedLinks'] = array();
+        }
+        $this->_data['commentFeedLinks'][] = $link;
+    }
+    
+    /**
+     * Set a links to an XML feed for any comments associated with this entry.
+     * Each link is an array with keys "uri" and "type", where type is one of:
+     * "atom", "rss" or "rdf".
+     *
+     * @return string|null
+     */
+    public function setCommentFeedLinks(array $links)
+    {
+        foreach ($links as $link) {
+            $this->setCommentFeedLink($link);
+        }
+    }
+
+    /**
+     * Set the feed title
+     *
+     * @return string|null
+     */
+    public function setTitle($title)
+    {
+        if (empty($title) || !is_string($title)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['title'] = $title;
+    }
+
+    /**
+     * Get an array with feed authors
+     *
+     * @return array
+     */
+    public function getAuthors()
+    {
+        if (!array_key_exists('authors', $this->_data)) {
+            return null;
+        }
+        return $this->_data['authors'];
+    }
+
+    /**
+     * Get the entry content
+     *
+     * @return string
+     */
+    public function getContent()
+    {
+        if (!array_key_exists('content', $this->_data)) {
+            return null;
+        }
+        return $this->_data['content'];
+    }
+
+    /**
+     * Get the entry copyright information
+     *
+     * @return string
+     */
+    public function getCopyright()
+    {
+        if (!array_key_exists('copyright', $this->_data)) {
+            return null;
+        }
+        return $this->_data['copyright'];
+    }
+
+    /**
+     * Get the entry creation date
+     *
+     * @return string
+     */
+    public function getDateCreated()
+    {
+        if (!array_key_exists('dateCreated', $this->_data)) {
+            return null;
+        }
+        return $this->_data['dateCreated'];
+    }
+
+    /**
+     * Get the entry modification date
+     *
+     * @return string
+     */
+    public function getDateModified()
+    {
+        if (!array_key_exists('dateModified', $this->_data)) {
+            return null;
+        }
+        return $this->_data['dateModified'];
+    }
+
+    /**
+     * Get the entry description
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+        if (!array_key_exists('description', $this->_data)) {
+            return null;
+        }
+        return $this->_data['description'];
+    }
+
+    /**
+     * Get the entry ID
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        if (!array_key_exists('id', $this->_data)) {
+            return null;
+        }
+        return $this->_data['id'];
+    }
+    
+    /**
+     * Get a link to the HTML source
+     *
+     * @return string|null
+     */
+    public function getLink()
+    {
+        if (!array_key_exists('link', $this->_data)) {
+            return null;
+        }
+        return $this->_data['link'];
+    }
+
+
+    /**
+     * Get all links
+     *
+     * @return array
+     */
+    public function getLinks()
+    {
+        if (!array_key_exists('links', $this->_data)) {
+            return null;
+        }
+        return $this->_data['links'];
+    }
+
+    /**
+     * Get the entry title
+     *
+     * @return string
+     */
+    public function getTitle()
+    {
+        if (!array_key_exists('title', $this->_data)) {
+            return null;
+        }
+        return $this->_data['title'];
+    }
+
+    /**
+     * Get the number of comments/replies for current entry
+     *
+     * @return integer
+     */
+    public function getCommentCount()
+    {
+        if (!array_key_exists('commentCount', $this->_data)) {
+            return null;
+        }
+        return $this->_data['commentCount'];
+    }
+
+    /**
+     * Returns a URI pointing to the HTML page where comments can be made on this entry
+     *
+     * @return string
+     */
+    public function getCommentLink()
+    {
+        if (!array_key_exists('commentLink', $this->_data)) {
+            return null;
+        }
+        return $this->_data['commentLink'];
+    }
+
+    /**
+     * Returns an array of URIs pointing to a feed of all comments for this entry
+     * where the array keys indicate the feed type (atom, rss or rdf).
+     *
+     * @return string
+     */
+    public function getCommentFeedLinks()
+    {
+        if (!array_key_exists('commentFeedLinks', $this->_data)) {
+            return null;
+        }
+        return $this->_data['commentFeedLinks'];
+    }
+    
+    /**
+     * Adds an enclosure to the entry.
+     *
+     * @param array $enclosures
+     */
+    public function setEnclosure(array $enclosure)
+    {
+        if (!isset($enclosure['type'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Enclosure "type" is not set');
+        }
+        if (!isset($enclosure['length'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Enclosure "length" is not set');
+        }
+        if (!isset($enclosure['uri'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Enclosure "uri" is not set');
+        }
+        if (!Zend_Uri::check($enclosure['uri'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Enclosure "uri" is not a valid URI/IRI');
+        }
+        if ((int) $enclosure['length'] <= 0) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Enclosure "length" must be an integer'
+            . ' indicating the content\'s length in bytes');
+        }
+        $this->_data['enclosure'] = $enclosure;
+    }
+    
+    /**
+     * Retrieve an array of all enclosures to be added to entry.
+     *
+     * @return array
+     */
+    public function getEnclosure()
+    {
+        if (!array_key_exists('enclosure', $this->_data)) {
+            return null;
+        }
+        return $this->_data['enclosure'];
+    }
+    
+    /**
+     * Unset a specific data point
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (isset($this->_data[$name])) {
+            unset($this->_data[$name]);
+        }
+    }
+    
+    /**
+     * Get registered extensions
+     *
+     * @return array
+     */
+    public function getExtensions()
+    {
+        return $this->_extensions;
+    }
+
+    /**
+     * Return an Extension object with the matching name (postfixed with _Entry)
+     *
+     * @param string $name
+     * @return object
+     */
+    public function getExtension($name)
+    {
+        if (array_key_exists($name . '_Entry', $this->_extensions)) {
+            return $this->_extensions[$name . '_Entry'];
+        }
+        return null;
+    }
+    
+    /**
+     * Set the current feed type being exported to "rss" or "atom". This allows
+     * other objects to gracefully choose whether to execute or not, depending
+     * on their appropriateness for the current type, e.g. renderers.
+     *
+     * @param string $type
+     */
+    public function setType($type)
+    {
+        $this->_type = $type;
+    }
+    
+    /**
+     * Retrieve the current or last feed type exported.
+     *
+     * @return string Value will be "rss" or "atom"
+     */
+    public function getType()
+    {
+        return $this->_type;
+    }
+
+    /**
+     * Method overloading: call given method on first extension implementing it
+     *
+     * @param  string $method
+     * @param  array $args
+     * @return mixed
+     * @throws Zend_Feed_Exception if no extensions implements the method
+     */
+    public function __call($method, $args)
+    {
+        foreach ($this->_extensions as $extension) {
+            try {
+                return call_user_func_array(array($extension, $method), $args);
+            } catch (Zend_Feed_Writer_Exception_InvalidMethodException $e) {
+            }
+        }
+        require_once 'Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Method: ' . $method
+            . ' does not exist and could not be located on a registered Extension');
+    }
+
+    /**
+     * Load extensions from Zend_Feed_Writer
+     *
+     * @return void
+     */
+    protected function _loadExtensions()
+    {
+        $all = Zend_Feed_Writer::getExtensions();
+        $exts = $all['entry'];
+        foreach ($exts as $ext) {
+            $className = Zend_Feed_Writer::getPluginLoader()->getClassName($ext);
+            $this->_extensions[$ext] = new $className();
+            $this->_extensions[$ext]->setEncoding($this->getEncoding());
+        }
+    }
+
+}

+ 41 - 0
library/Zend/Feed/Writer/Exception/InvalidMethodException.php

@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+
+/**
+ * @see Zend_Feed_Exception
+ */
+require_once 'Zend/Feed/Exception.php';
+
+
+/**
+ * Feed exceptions
+ *
+ * Class to represent exceptions that occur during Feed operations.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Exception_InvalidMethodException extends Zend_Exception
+{}

+ 87 - 0
library/Zend/Feed/Writer/Extension/Atom/Renderer/Feed.php

@@ -0,0 +1,87 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_Atom_Renderer_Feed
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        /**
+         * RSS 2.0 only. Used mainly to include Atom links and
+         * Pubsubhubbub Hub endpoint URIs under the Atom namespace
+         */
+        if (strtolower($this->getType()) == 'atom') {
+            return;
+        }
+        $this->_appendNamespaces();
+        $this->_setFeedLinks($this->_dom, $this->_base);
+        $this->_setHubs($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:atom',
+            'http://www.w3.org/2005/Atom');  
+    }
+
+    protected function _setFeedLinks(DOMDocument $dom, DOMElement $root)
+    {
+        $flinks = $this->getDataContainer()->getFeedLinks();
+        if(!$flinks || empty($flinks)) {
+            return;
+        }
+        foreach ($flinks as $type => $href) {
+            $mime = 'application/' . strtolower($type) . '+xml';
+            $flink = $dom->createElement('atom:link');
+            $root->appendChild($flink);
+            $flink->setAttribute('rel', 'self');
+            $flink->setAttribute('type', $mime);
+            $flink->setAttribute('href', $href);
+        }
+    }
+    
+    protected function _setHubs(DOMDocument $dom, DOMElement $root)
+    {
+        $hubs = $this->getDataContainer()->getHubs();
+        if (!$hubs || empty($hubs)) {
+            return;
+        }
+        foreach ($hubs as $hubUrl) {
+            $hub = $dom->createElement('atom:link');
+            $hub->setAttribute('rel', 'hub');
+            $hub->setAttribute('href', $hubUrl);
+            $root->appendChild($hub);
+        }
+    }
+
+}

+ 67 - 0
library/Zend/Feed/Writer/Extension/Content/Renderer/Entry.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_Content_Renderer_Entry
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        if (strtolower($this->getType()) == 'atom') {
+            return;
+        }
+        $this->_appendNamespaces();
+        $this->_setContent($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:content',
+            'http://purl.org/rss/1.0/modules/content/');  
+    }
+
+    protected function _setContent(DOMDocument $dom, DOMElement $root)
+    {
+        $content = $this->getDataContainer()->getContent();
+        if (!$content) {
+            return;
+        }
+        $element = $dom->createElement('content:encoded');
+        $root->appendChild($element);
+        $element->nodeValue = htmlentities(
+            $this->getDataContainer()->getContent(),
+            ENT_QUOTES,
+            $this->getDataContainer()->getEncoding()
+        );
+    }
+
+}

+ 67 - 0
library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Entry.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_DublinCore_Renderer_Entry
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        if (strtolower($this->getType()) == 'atom') {
+            return;
+        }
+        $this->_appendNamespaces();
+        $this->_setAuthors($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:dc',
+            'http://purl.org/dc/elements/1.1/');  
+    }
+
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->getDataContainer()->getAuthors();
+        if (!$authors || empty($authors)) {
+            return;
+        }
+        foreach ($authors as $data) {
+            $author = $this->_dom->createElement('dc:creator');
+            if (array_key_exists('name', $data)) {
+                $author->nodeValue = $data['name'];
+                $root->appendChild($author);   
+            }
+        }
+    }
+
+}

+ 67 - 0
library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Feed.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_DublinCore_Renderer_Feed
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        if (strtolower($this->getType()) == 'atom') {
+            return;
+        }
+        $this->_appendNamespaces();
+        $this->_setAuthors($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:dc',
+            'http://purl.org/dc/elements/1.1/');  
+    }
+
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->getDataContainer()->getAuthors();
+        if (!$authors || empty($authors)) {
+            return;
+        }
+        foreach ($authors as $data) {
+            $author = $this->_dom->createElement('dc:creator');
+            if (array_key_exists('name', $data)) {
+                $author->nodeValue = $data['name'];
+                $root->appendChild($author);   
+            }
+        }
+    }
+
+}

+ 171 - 0
library/Zend/Feed/Writer/Extension/ITunes/Entry.php

@@ -0,0 +1,171 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_ITunes_Entry
+{
+
+    /**
+     * Array of Feed data for rendering by Extension's renderers
+     *
+     * @var array
+     */
+    protected $_data = array();
+    
+    /**
+     * Encoding of all text values
+     *
+     * @var string
+     */
+    protected $_encoding = 'UTF-8';
+    
+    public function setEncoding($enc)
+    {
+        $this->_encoding = $enc;
+    }
+    
+    public function getEncoding()
+    {
+        return $this->_encoding;
+    }
+    
+    /**
+     * Set a block value of "yes" or "no". You may also set an empty string.
+     *
+     * @param string
+     */
+    public function setItunesBlock($value)
+    {
+        if (!ctype_alpha($value) && strlen($value) > 0) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "block" may only'
+            . ' contain alphabetic characters');
+        }
+        if (iconv_strlen($value, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "block" may only'
+            . ' contain a maximum of 255 characters');
+        }
+        $this->_data['block'] = $value;
+    }
+    
+    public function addItunesAuthors(array $values)
+    {
+        foreach ($values as $value) {
+            $this->addItunesAuthor($value);
+        }
+    }
+    
+    public function addItunesAuthor($value)
+    {
+        if (iconv_strlen($value, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: any "author" may only'
+            . ' contain a maximum of 255 characters each');
+        }
+        if (!isset($this->_data['authors'])) {
+            $this->_data['authors'] = array();
+        }
+        $this->_data['authors'][] = $value;   
+    }
+    
+    public function setItunesDuration($value)
+    {
+        $value = (string) $value;
+        if (!ctype_digit($value)
+        && !preg_match("/^\d+:[0-5]{1}[0-9]{1}$/", $value)
+        && !preg_match("/^\d+:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}$/", $value)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "duration" may only'
+            . ' be of a specified [[HH:]MM:]SS format');
+        }
+        $this->_data['duration'] = $value;
+    }
+    
+    public function setItunesExplicit($value)
+    {
+        if (!in_array($value, array('yes','no','clean'))) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "explicit" may only'
+            . ' be one of "yes", "no" or "clean"');
+        }
+        $this->_data['explicit'] = $value;
+    }
+    
+    public function setItunesKeywords(array $value)
+    {
+        if (count($value) > 12) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "keywords" may only'
+            . ' contain a maximum of 12 terms');
+        }
+        $concat = implode(',', $value);
+        if (iconv_strlen($concat, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "keywords" may only'
+            . ' have a concatenated length of 255 chars where terms are delimited'
+            . ' by a comma');
+        }
+        $this->_data['keywords'] = $value;
+    }
+    
+    public function setItunesSubtitle($value)
+    {
+        if (iconv_strlen($value, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "subtitle" may only'
+            . ' contain a maximum of 255 characters');
+        }
+        $this->_data['subtitle'] = $value;
+    }
+    
+    public function setItunesSummary($value)
+    {
+        if (iconv_strlen($value, $this->getEncoding()) > 4000) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "summary" may only'
+            . ' contain a maximum of 4000 characters');
+        }
+        $this->_data['summary'] = $value;
+    }
+    
+    public function __call($method, array $params)
+    {
+        $point = lcfirst(substr($method, 9));
+        if (!method_exists($this, 'setItunes' . ucfirst($point))
+        && !method_exists($this, 'addItunes' . ucfirst($point))) {
+            require_once 'Zend/Feed/Writer/Exception/InvalidMethodException.php';
+            throw new Zend_Feed_Writer_Exception_InvalidMethodException(
+                'invalid method: ' . $method
+            );
+        }
+        if (!array_key_exists($point, $this->_data) || empty($this->_data[$point])) {
+            return null;
+        }
+        return $this->_data[$point];
+    }
+
+}

+ 255 - 0
library/Zend/Feed/Writer/Extension/ITunes/Feed.php

@@ -0,0 +1,255 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_ITunes_Feed
+{
+
+    /**
+     * Array of Feed data for rendering by Extension's renderers
+     *
+     * @var array
+     */
+    protected $_data = array();
+    
+    /**
+     * Encoding of all text values
+     *
+     * @var string
+     */
+    protected $_encoding = 'UTF-8';
+    
+    public function setEncoding($enc)
+    {
+        $this->_encoding = $enc;
+    }
+    
+    public function getEncoding()
+    {
+        return $this->_encoding;
+    }
+    
+    /**
+     * Set a block value of "yes" or "no". You may also set an empty string.
+     *
+     * @param string
+     */
+    public function setItunesBlock($value)
+    {
+        if (!ctype_alpha($value) && strlen($value) > 0) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "block" may only'
+            . ' contain alphabetic characters');
+        }
+        if (iconv_strlen($value, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "block" may only'
+            . ' contain a maximum of 255 characters');
+        }
+        $this->_data['block'] = $value;
+    }
+    
+    public function addItunesAuthors(array $values)
+    {
+        foreach ($values as $value) {
+            $this->addItunesAuthor($value);
+        }
+    }
+    
+    public function addItunesAuthor($value)
+    {
+        if (iconv_strlen($value, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: any "author" may only'
+            . ' contain a maximum of 255 characters each');
+        }
+        if (!isset($this->_data['authors'])) {
+            $this->_data['authors'] = array();
+        }
+        $this->_data['authors'][] = $value;   
+    }
+    
+    public function setItunesCategories(array $values)
+    {
+        if (!isset($this->_data['categories'])) {
+            $this->_data['categories'] = array();
+        }
+        foreach ($values as $key=>$value) {
+            if (!is_array($value)) {
+                if (iconv_strlen($value, $this->getEncoding()) > 255) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('invalid parameter: any "category" may only'
+                    . ' contain a maximum of 255 characters each');
+                }
+                $this->_data['categories'][] = $value;
+            } else {
+                if (iconv_strlen($key, $this->getEncoding()) > 255) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('invalid parameter: any "category" may only'
+                    . ' contain a maximum of 255 characters each');
+                }
+                $this->_data['categories'][$key] = array();
+                foreach ($value as $val) {
+                    if (iconv_strlen($val, $this->getEncoding()) > 255) {
+                        require_once 'Zend/Feed/Exception.php';
+                        throw new Zend_Feed_Exception('invalid parameter: any "category" may only'
+                        . ' contain a maximum of 255 characters each');
+                    }
+                    $this->_data['categories'][$key][] = $val;
+                } 
+            }
+        }
+    }
+    
+    public function setItunesImage($value)
+    {
+        if (!Zend_Uri::check($value)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "image" may only'
+            . ' be a valid URI/IRI');
+        }
+        if (!in_array(substr($value, -3), array('jpg','png'))) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "image" may only'
+            . ' use file extension "jpg" or "png" which must be the last three'
+            . ' characters of the URI (i.e. no query string or fragment)');
+        }
+        $this->_data['image'] = $value;
+    }
+    
+    public function setItunesDuration($value)
+    {
+        $value = (string) $value;
+        if (!ctype_digit($value)
+        && !preg_match("/^\d+:[0-5]{1}[0-9]{1}$/", $value)
+        && !preg_match("/^\d+:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}$/", $value)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "duration" may only'
+            . ' be of a specified [[HH:]MM:]SS format');
+        }
+        $this->_data['duration'] = $value;
+    }
+    
+    public function setItunesExplicit($value)
+    {
+        if (!in_array($value, array('yes','no','clean'))) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "explicit" may only'
+            . ' be one of "yes", "no" or "clean"');
+        }
+        $this->_data['explicit'] = $value;
+    }
+    
+    public function setItunesKeywords(array $value)
+    {
+        if (count($value) > 12) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "keywords" may only'
+            . ' contain a maximum of 12 terms');
+        }
+        $concat = implode(',', $value);
+        if (iconv_strlen($concat, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "keywords" may only'
+            . ' have a concatenated length of 255 chars where terms are delimited'
+            . ' by a comma');
+        }
+        $this->_data['keywords'] = $value;
+    }
+    
+    public function setItunesNewFeedUrl($value)
+    {
+        if (!Zend_Uri::check($value)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "newFeedUrl" may only'
+            . ' be a valid URI/IRI');
+        }
+        $this->_data['newFeedUrl'] = $value;
+    }
+    
+    public function addItunesOwners(array $values)
+    {
+        foreach ($values as $value) {
+            $this->addItunesOwner($value); 
+        }
+    }
+    
+    public function addItunesOwner(array $value)
+    {
+        if (!isset($value['name']) || !isset($value['email'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: any "owner" must'
+            . ' be an array containing keys "name" and "email"');
+        }
+        if (iconv_strlen($value['name'], $this->getEncoding()) > 255
+        || iconv_strlen($value['email'], $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: any "owner" may only'
+            . ' contain a maximum of 255 characters each for "name" and "email"');
+        }
+        if (!isset($this->_data['owners'])) {
+            $this->_data['owners'] = array();
+        }
+        $this->_data['owners'][] = $value;
+    }
+    
+    public function setItunesSubtitle($value)
+    {
+        if (iconv_strlen($value, $this->getEncoding()) > 255) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "subtitle" may only'
+            . ' contain a maximum of 255 characters');
+        }
+        $this->_data['subtitle'] = $value;
+    }
+    
+    public function setItunesSummary($value)
+    {
+        if (iconv_strlen($value, $this->getEncoding()) > 4000) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('invalid parameter: "summary" may only'
+            . ' contain a maximum of 4000 characters');
+        }
+        $this->_data['summary'] = $value;
+    }
+    
+    public function __call($method, array $params)
+    {
+        $point = lcfirst(substr($method, 9));
+        if (!method_exists($this, 'setItunes' . ucfirst($point))
+        && !method_exists($this, 'addItunes' . ucfirst($point))) {
+            require_once 'Zend/Feed/Writer/Exception/InvalidMethodException.php';
+            throw new Zend_Feed_Writer_Exception_InvalidMethodException(
+                'invalid method: ' . $method
+            );
+        }
+        if (!array_key_exists($point, $this->_data) || empty($this->_data[$point])) {
+            return null;
+        }
+        return $this->_data[$point];
+    }
+
+}

+ 142 - 0
library/Zend/Feed/Writer/Extension/ITunes/Renderer/Entry.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_ITunes_Renderer_Entry
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        $this->_appendNamespaces();
+        $this->_setAuthors($this->_dom, $this->_base);
+        $this->_setBlock($this->_dom, $this->_base);
+        $this->_setDuration($this->_dom, $this->_base);
+        $this->_setExplicit($this->_dom, $this->_base);
+        $this->_setKeywords($this->_dom, $this->_base);
+        $this->_setSubtitle($this->_dom, $this->_base);
+        $this->_setSummary($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:itunes',
+            'http://www.itunes.com/dtds/podcast-1.0.dtd');  
+    }
+
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->getDataContainer()->getItunesAuthors();
+        if (!$authors || empty($authors)) {
+            return;
+        }
+        foreach ($authors as $author) {
+            $el = $dom->createElement('itunes:author');
+            $el->nodeValue = Zend_Feed_Writer::xmlentities(
+                $author, $this->getEncoding()
+            );
+            $root->appendChild($el);
+        }
+    }
+    
+    protected function _setBlock(DOMDocument $dom, DOMElement $root)
+    {
+        $block = $this->getDataContainer()->getItunesBlock();
+        if (is_null($block)) {
+            return;
+        }
+        $el = $dom->createElement('itunes:block');
+        $el->nodeValue = $block;
+        $root->appendChild($el);
+    }
+    
+    protected function _setDuration(DOMDocument $dom, DOMElement $root)
+    {
+        $duration = $this->getDataContainer()->getItunesDuration();
+        if (!$duration) {
+            return;
+        }
+        $el = $dom->createElement('itunes:duration');
+        $el->nodeValue = $duration;
+        $root->appendChild($el);
+    }
+    
+    protected function _setExplicit(DOMDocument $dom, DOMElement $root)
+    {
+        $explicit = $this->getDataContainer()->getItunesExplicit();
+        if (is_null($explicit)) {
+            return;
+        }
+        $el = $dom->createElement('itunes:explicit');
+        $el->nodeValue = $explicit;
+        $root->appendChild($el);
+    }
+    
+    protected function _setKeywords(DOMDocument $dom, DOMElement $root)
+    {
+        $keywords = $this->getDataContainer()->getItunesKeywords();
+        if (!$keywords || empty($keywords)) {
+            return;
+        }
+        $el = $dom->createElement('itunes:keywords');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            implode(',', $keywords), $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+    
+    protected function _setSubtitle(DOMDocument $dom, DOMElement $root)
+    {
+        $subtitle = $this->getDataContainer()->getItunesSubtitle();
+        if (!$subtitle) {
+            return;
+        }
+        $el = $dom->createElement('itunes:subtitle');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            $subtitle, $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+    
+    protected function _setSummary(DOMDocument $dom, DOMElement $root)
+    {
+        $summary = $this->getDataContainer()->getItunesSummary();
+        if (!$summary) {
+            return;
+        }
+        $el = $dom->createElement('itunes:summary');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            $summary, $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+
+}

+ 222 - 0
library/Zend/Feed/Writer/Extension/ITunes/Renderer/Feed.php

@@ -0,0 +1,222 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_ITunes_Renderer_Feed
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        $this->_appendNamespaces();
+        $this->_setAuthors($this->_dom, $this->_base);
+        $this->_setBlock($this->_dom, $this->_base);
+        $this->_setCategories($this->_dom, $this->_base);
+        $this->_setImage($this->_dom, $this->_base);
+        $this->_setDuration($this->_dom, $this->_base);
+        $this->_setExplicit($this->_dom, $this->_base);
+        $this->_setKeywords($this->_dom, $this->_base);
+        $this->_setNewFeedUrl($this->_dom, $this->_base);
+        $this->_setOwners($this->_dom, $this->_base);
+        $this->_setSubtitle($this->_dom, $this->_base);
+        $this->_setSummary($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:itunes',
+            'http://www.itunes.com/dtds/podcast-1.0.dtd');  
+    }
+
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->getDataContainer()->getItunesAuthors();
+        if (!$authors || empty($authors)) {
+            return;
+        }
+        foreach ($authors as $author) {
+            $el = $dom->createElement('itunes:author');
+            $el->nodeValue = Zend_Feed_Writer::xmlentities(
+                $author, $this->getEncoding()
+            );
+            $root->appendChild($el);
+        }
+    }
+    
+    protected function _setBlock(DOMDocument $dom, DOMElement $root)
+    {
+        $block = $this->getDataContainer()->getItunesBlock();
+        if (is_null($block)) {
+            return;
+        }
+        $el = $dom->createElement('itunes:block');
+        $el->nodeValue = $block;
+        $root->appendChild($el);
+    }
+    
+    protected function _setCategories(DOMDocument $dom, DOMElement $root)
+    {
+        $cats = $this->getDataContainer()->getItunesCategories();
+        if (!$cats || empty($cats)) {
+            return;
+        }
+        foreach ($cats as $key=>$cat) {
+            if (!is_array($cat)) {
+                $el = $dom->createElement('itunes:category');
+                $el->setAttribute('text', Zend_Feed_Writer::xmlentities(
+                    $cat, $this->getEncoding()
+                ));
+                $root->appendChild($el);
+            } else {
+                $el = $dom->createElement('itunes:category');
+                $el->setAttribute('text', Zend_Feed_Writer::xmlentities(
+                    $key, $this->getEncoding()
+                ));
+                $root->appendChild($el);
+                foreach ($cat as $subcat) {
+                    $el2 = $dom->createElement('itunes:category');
+                    $el2->setAttribute('text', Zend_Feed_Writer::xmlentities(
+                        $subcat, $this->getEncoding()
+                    ));
+                    $el->appendChild($el2);
+                }
+            }
+        }
+    }
+    
+    protected function _setImage(DOMDocument $dom, DOMElement $root)
+    {
+        $image = $this->getDataContainer()->getItunesImage();
+        if (!$image) {
+            return;
+        }
+        $el = $dom->createElement('itunes:image');
+        $el->setAttribute('href', $image);
+        $root->appendChild($el);
+    }
+    
+    protected function _setDuration(DOMDocument $dom, DOMElement $root)
+    {
+        $duration = $this->getDataContainer()->getItunesDuration();
+        if (!$duration) {
+            return;
+        }
+        $el = $dom->createElement('itunes:duration');
+        $el->nodeValue = $duration;
+        $root->appendChild($el);
+    }
+    
+    protected function _setExplicit(DOMDocument $dom, DOMElement $root)
+    {
+        $explicit = $this->getDataContainer()->getItunesExplicit();
+        if (is_null($explicit)) {
+            return;
+        }
+        $el = $dom->createElement('itunes:explicit');
+        $el->nodeValue = $explicit;
+        $root->appendChild($el);
+    }
+    
+    protected function _setKeywords(DOMDocument $dom, DOMElement $root)
+    {
+        $keywords = $this->getDataContainer()->getItunesKeywords();
+        if (!$keywords || empty($keywords)) {
+            return;
+        }
+        $el = $dom->createElement('itunes:keywords');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            implode(',', $keywords), $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+    
+    protected function _setNewFeedUrl(DOMDocument $dom, DOMElement $root)
+    {
+        $url = $this->getDataContainer()->getItunesNewFeedUrl();
+        if (!$url) {
+            return;
+        }
+        $el = $dom->createElement('itunes:new-feed-url');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            $url, $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+    
+    protected function _setOwners(DOMDocument $dom, DOMElement $root)
+    {
+        $owners = $this->getDataContainer()->getItunesOwners();
+        if (!$owners || empty($owners)) {
+            return;
+        }
+        foreach ($owners as $owner) {
+            $el = $dom->createElement('itunes:owner');
+            $name = $dom->createElement('itunes:name');
+            $name->nodeValue = Zend_Feed_Writer::xmlentities(
+                $owner['name'], $this->getEncoding()
+            );
+            $email = $dom->createElement('itunes:email');
+            $email->nodeValue = Zend_Feed_Writer::xmlentities(
+                $owner['email'], $this->getEncoding()
+            );
+            $root->appendChild($el);
+            $el->appendChild($name);
+            $el->appendChild($email);
+        }
+    }
+    
+    protected function _setSubtitle(DOMDocument $dom, DOMElement $root)
+    {
+        $subtitle = $this->getDataContainer()->getItunesSubtitle();
+        if (!$subtitle) {
+            return;
+        }
+        $el = $dom->createElement('itunes:subtitle');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            $subtitle, $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+    
+    protected function _setSummary(DOMDocument $dom, DOMElement $root)
+    {
+        $summary = $this->getDataContainer()->getItunesSummary();
+        if (!$summary) {
+            return;
+        }
+        $el = $dom->createElement('itunes:summary');
+        $el->nodeValue = Zend_Feed_Writer::xmlentities(
+            $summary, $this->getEncoding()
+        );
+        $root->appendChild($el);
+    }
+
+}

+ 103 - 0
library/Zend/Feed/Writer/Extension/RendererAbstract.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to padraic dot brady at yahoo dot com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer_Entry_Rss
+ * @copyright  Copyright (c) 2009 Padraic Brady
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererInterface
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererInterface.php';
+ 
+ /**
+ * @category   Zend
+ * @package    Zend_Feed_Writer_Entry_Rss
+ * @copyright  Copyright (c) 2009 Padraic Brady
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Feed_Writer_Extension_RendererAbstract
+implements Zend_Feed_Writer_Extension_RendererInterface
+{
+
+    protected $_dom = null;
+    
+    protected $_entry = null;
+    
+    protected $_base = null;
+    
+    protected $_container = null;
+    
+    protected $_type = null;
+    
+    protected $_rootElement = null;
+    
+    /**
+     * Encoding of all text values
+     *
+     * @var string
+     */
+    protected $_encoding = 'UTF-8';
+
+    public function __construct($container)
+    {
+        $this->_container = $container;
+    }
+    
+    public function setEncoding($enc)
+    {
+        $this->_encoding = $enc;
+    }
+    
+    public function getEncoding()
+    {
+        return $this->_encoding;
+    }
+    
+    public function setDomDocument(DOMDocument $dom, DOMElement $base)
+    {
+        $this->_dom = $dom;
+        $this->_base = $base;
+    }
+    
+    public function getDataContainer()
+    {
+        return $this->_container;
+    }
+    
+    public function setType($type)
+    {
+        $this->_type = $type;
+    }
+    
+    public function getType()
+    {
+        return $this->_type;
+    }
+    
+    public function setRootElement(DOMElement $root)
+    {
+        $this->_rootElement = $root;
+    }
+    
+    public function getRootElement()
+    {
+        return $this->_rootElement;
+    }
+    
+    abstract protected function _appendNamespaces();
+
+}

+ 38 - 0
library/Zend/Feed/Writer/Extension/RendererInterface.php

@@ -0,0 +1,38 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to padraic dot brady at yahoo dot com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2009 Padraic Brady
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2009 Padraic Brady
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Feed_Writer_Extension_RendererInterface
+{
+
+    public function __construct($container);
+    
+    public function setDomDocument(DOMDocument $dom, DOMElement $base);
+    
+    public function render();
+    
+    public function getDataContainer();
+
+}

+ 63 - 0
library/Zend/Feed/Writer/Extension/Slash/Renderer/Entry.php

@@ -0,0 +1,63 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_Slash_Renderer_Entry
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        if (strtolower($this->getType()) == 'atom') {
+            return; // RSS 2.0 only
+        }
+        $this->_appendNamespaces();
+        $this->_setCommentCount($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:slash',
+            'http://purl.org/rss/1.0/modules/slash/');  
+    }
+
+    protected function _setCommentCount(DOMDocument $dom, DOMElement $root)
+    {
+        $count = $this->getDataContainer()->getCommentCount();
+        if (!$count) {
+            $count = 0;
+        }
+        $tcount = $this->_dom->createElement('slash:comments');
+        $tcount->nodeValue = $count;
+        $root->appendChild($tcount);
+    }
+
+}

+ 103 - 0
library/Zend/Feed/Writer/Extension/Threading/Renderer/Entry.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_Threading_Renderer_Entry
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        if (strtolower($this->getType()) == 'rss') {
+            return; // Atom 1.0 only
+        }
+        $this->_appendNamespaces();
+        $this->_setCommentLink($this->_dom, $this->_base);
+        $this->_setCommentFeedLinks($this->_dom, $this->_base);
+        $this->_setCommentCount($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:thr',
+            'http://purl.org/syndication/thread/1.0');  
+    }
+    
+    protected function _setCommentLink(DOMDocument $dom, DOMElement $root)
+    {
+        $link = $this->getDataContainer()->getCommentLink();
+        if (!$link) {
+            return;
+        }
+        $clink = $this->_dom->createElement('link');
+        $clink->setAttribute('rel', 'replies');
+        $clink->setAttribute('type', 'text/html');
+        $clink->setAttribute('href', $link);
+        $count = $this->getDataContainer()->getCommentCount();
+        if (!$count) {
+            $count = 0;
+        }
+        $clink->setAttribute('thr:count', $count);
+        $root->appendChild($clink);
+    }
+    
+    protected function _setCommentFeedLinks(DOMDocument $dom, DOMElement $root)
+    {
+        $links = $this->getDataContainer()->getCommentFeedLinks();
+        if (!$links || empty($links)) {
+            return;
+        }
+        foreach ($links as $link) {
+            $flink = $this->_dom->createElement('link');
+            $flink->setAttribute('rel', 'replies');
+            $flink->setAttribute('type', 'application/'. $link['type'] .'+xml');
+            $flink->setAttribute('href', $link['uri']);
+            $count = $this->getDataContainer()->getCommentCount();
+            if (!$count) {
+                $count = 0;
+            }
+            $flink->setAttribute('thr:count', $count);
+            $root->appendChild($flink);
+        }
+    }
+
+    protected function _setCommentCount(DOMDocument $dom, DOMElement $root)
+    {
+        $count = $this->getDataContainer()->getCommentCount();
+        if (!$count) {
+            $count = 0;
+        }
+        $tcount = $this->_dom->createElement('thr:total');
+        $tcount->nodeValue = $count;
+        $root->appendChild($tcount);
+    }
+
+}

+ 67 - 0
library/Zend/Feed/Writer/Extension/WellFormedWeb/Renderer/Entry.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+/**
+ * @see Zend_Feed_Writer_Extension_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Extension/RendererAbstract.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Extension_WellFormedWeb_Renderer_Entry
+extends Zend_Feed_Writer_Extension_RendererAbstract
+{
+
+    public function render()
+    {
+        if (strtolower($this->getType()) == 'atom') {
+            return; // RSS 2.0 only
+        }
+        $this->_appendNamespaces();
+        $this->_setCommentFeedLinks($this->_dom, $this->_base);
+    }
+    
+    protected function _appendNamespaces()
+    {
+        $this->getRootElement()->setAttribute('xmlns:wfw',
+            'http://wellformedweb.org/CommentAPI/');  
+    }
+    
+    protected function _setCommentFeedLinks(DOMDocument $dom, DOMElement $root)
+    {
+        $links = $this->getDataContainer()->getCommentFeedLinks();
+        if (!$links || empty($links)) {
+            return;
+        }
+        foreach ($links as $link) {
+            if ($link['type'] == 'rss') {
+                $flink = $this->_dom->createElement('wfw:commentRss');
+                $flink->nodeValue = $link['uri'];
+                $root->appendChild($flink);
+            }
+        }
+    }
+
+}

+ 900 - 0
library/Zend/Feed/Writer/Feed.php

@@ -0,0 +1,900 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Date
+ */
+require_once 'Zend/Date.php';
+
+/**
+ * @see Zend_Date
+ */
+require_once 'Zend/Uri.php';
+
+/**
+ * @see Zend_Feed_Writer
+ */
+require_once 'Zend/Feed/Writer.php';
+
+/**
+ * @see Zend_Feed_Writer_Entry
+ */
+require_once 'Zend/Feed/Writer/Entry.php';
+
+/**
+ * @see Zend_Feed_Writer_Renderer_Feed_Atom
+ */
+require_once 'Zend/Feed/Writer/Renderer/Feed/Atom.php';
+
+/**
+ * @see Zend_Feed_Writer_Renderer_Feed_Rss
+ */
+require_once 'Zend/Feed/Writer/Renderer/Feed/Rss.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Feed implements Iterator, Countable
+{
+
+    /**
+     * Contains all Feed level date to append in feed output
+     *
+     * @var array
+     */
+    protected $_data = array();
+
+    /**
+     * Contains all entry objects
+     *
+     * @var array
+     */
+    protected $_entries = array();
+
+    /**
+     * A pointer for the iterator to keep track of the entries array
+     *
+     * @var int
+     */
+    protected $_entriesKey = 0;
+    
+    /**
+     * Holds the value "atom" or "rss" depending on the feed type set when
+     * when last exported.
+     *
+     * @var string
+     */
+    protected $_type = null;
+    
+    /**
+     * Constructor: Primarily triggers the registration of core extensions and
+     * loads those appropriate to this data container.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        Zend_Feed_Writer::registerCoreExtensions();
+        $this->_loadExtensions();
+    }
+
+    /**
+     * Set a single author
+     *
+     * @param  int $index
+     * @return string|null
+     */
+    public function addAuthor($name, $email = null, $uri = null)
+    {
+        $author = array();
+        if (is_array($name)) {
+            if (!array_key_exists('name', $name) || empty($name['name']) || !is_string($name['name'])) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('Invalid parameter: author array must include a "name" key with a non-empty string value');
+            }
+            $author['name'] = $name['name'];
+            if (isset($name['email'])) {
+                if (empty($name['email']) || !is_string($name['email'])) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "email" array value must be a non-empty string');
+                }
+                $author['email'] = $name['email'];
+            }
+            if (isset($name['uri'])) {
+                if (empty($name['uri']) || !is_string($name['uri']) || !Zend_Uri::check($name['uri'])) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "uri" array value must be a non-empty string and valid URI/IRI');
+                }
+                $author['uri'] = $name['uri'];
+            }
+        } else {
+            if (empty($name['name']) || !is_string($name['name'])) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('Invalid parameter: "name" must be a non-empty string value');
+            }
+            $author['name'] = $name;
+            if (isset($email)) {
+                if (empty($email) || !is_string($email)) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "email" value must be a non-empty string');
+                }
+                $author['email'] = $email;
+            }
+            if (isset($uri)) {
+                if (empty($uri) || !is_string($uri) || !Zend_Uri::check($uri)) {
+                    require_once 'Zend/Feed/Exception.php';
+                    throw new Zend_Feed_Exception('Invalid parameter: "uri" value must be a non-empty string and valid URI/IRI');
+                }
+                $author['uri'] = $uri;
+            }
+        }
+        $this->_data['authors'][] = $author;
+    }
+
+    /**
+     * Set an array with feed authors
+     *
+     * @return array
+     */
+    public function addAuthors(array $authors)
+    {
+        foreach($authors as $author) {
+            $this->addAuthor($author);
+        }
+    }
+
+    /**
+     * Set the copyright entry
+     *
+     * @return string|null
+     */
+    public function setCopyright($copyright)
+    {
+        if (empty($copyright) || !is_string($copyright)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['copyright'] = $copyright;
+    }
+
+    /**
+     * Set the feed creation date
+     *
+     * @param null|integer|Zend_Date
+     */
+    public function setDateCreated($date = null)
+    {
+        $zdate = null;
+        if (is_null($date)) {
+            $zdate = new Zend_Date;
+        } elseif (ctype_digit($date) && strlen($date) == 10) {
+            $zdate = new Zend_Date($date, Zend_Date::TIMESTAMP);
+        } elseif ($date instanceof Zend_Date) {
+            $zdate = $date;
+        } else {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid Zend_Date object or UNIX Timestamp passed as parameter');
+        }
+        $this->_data['dateCreated'] = $zdate;
+    }
+
+    /**
+     * Set the feed modification date
+     *
+     * @param null|integer|Zend_Date
+     */
+    public function setDateModified($date = null)
+    {
+        $zdate = null;
+        if (is_null($date)) {
+            $zdate = new Zend_Date;
+        } elseif (ctype_digit($date) && strlen($date) == 10) {
+            $zdate = new Zend_Date($date, Zend_Date::TIMESTAMP);
+        } elseif ($date instanceof Zend_Date) {
+            $zdate = $date;
+        } else {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid Zend_Date object or UNIX Timestamp passed as parameter');
+        }
+        $this->_data['dateModified'] = $zdate;
+    }
+
+    /**
+     * Set the feed description
+     *
+     * @return string|null
+     */
+    public function setDescription($description)
+    {
+        if (empty($description) || !is_string($description)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['description'] = $description;
+    }
+
+    /**
+     * Set the feed generator entry
+     *
+     * @return string|null
+     */
+    public function setGenerator($name, $version = null, $uri = null)
+    {
+        if (empty($name) || !is_string($name)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "name" must be a non-empty string');
+        }
+        $generator = array('name' => $name);
+        if (isset($version)) {
+            if (empty($version) || !is_string($version)) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('Invalid parameter: "version" must be a non-empty string');
+            }
+            $generator['version'] = $version;
+        }
+        if (isset($uri)) {
+            if (empty($uri) || !is_string($uri) || !Zend_Uri::check($uri)) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('Invalid parameter: "uri" must be a non-empty string and a valid URI/IRI');
+            }
+            $generator['uri'] = $uri;
+        }
+        $this->_data['generator'] = $generator;
+    }
+
+    /**
+     * Set the feed ID - URI or URN (via PCRE pattern) supported
+     *
+     * @return string|null
+     */
+    public function setId($id)
+    {
+        if ((empty($id) || !is_string($id) || !Zend_Uri::check($id)) &&
+        !preg_match("#^urn:[a-zA-Z0-9][a-zA-Z0-9\-]{1,31}:([a-zA-Z0-9\(\)\+\,\.\:\=\@\;\$\_\!\*\-]|%[0-9a-fA-F]{2})*#", $id)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string and valid URI/IRI');
+        }
+        $this->_data['id'] = $id;
+    }
+
+    /**
+     * Set the feed language
+     *
+     * @return string|null
+     */
+    public function setLanguage($language)
+    {
+        if (empty($language) || !is_string($language)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['language'] = $language;
+    }
+
+    /**
+     * Set a link to the HTML source
+     *
+     * @return string|null
+     */
+    public function setLink($link)
+    {
+        if (empty($link) || !is_string($link) || !Zend_Uri::check($link)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string and valid URI/IRI');
+        }
+        $this->_data['link'] = $link;
+    }
+
+    /**
+     * Set a link to an XML feed for any feed type/version
+     *
+     * @return string|null
+     */
+    public function setFeedLink($link, $type)
+    {
+        if (empty($link) || !is_string($link) || !Zend_Uri::check($link)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "link"" must be a non-empty string and valid URI/IRI');
+        }
+        if (!in_array(strtolower($type), array('rss', 'rdf', 'atom'))) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "type"; You must declare the type of feed the link points to, i.e. RSS, RDF or Atom');
+        }
+        $this->_data['feedLinks'][strtolower($type)] = $link;
+    }
+
+    /**
+     * Set the feed title
+     *
+     * @return string|null
+     */
+    public function setTitle($title)
+    {
+        if (empty($title) || !is_string($title)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['title'] = $title;
+    }
+
+    /**
+     * Set the feed character encoding
+     *
+     * @param string $encoding
+     */
+    public function setEncoding($encoding)
+    {
+        if (empty($encoding) || !is_string($encoding)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: parameter must be a non-empty string');
+        }
+        $this->_data['encoding'] = $encoding;
+    }
+    
+    /**
+     * Set the feed's base URL
+     *
+     * @param string $url
+     */
+    public function setBaseUrl($url)
+    {
+        if (empty($url) || !is_string($url) || !Zend_Uri::check($url)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "url" array value'
+            . ' must be a non-empty string and valid URI/IRI');
+        }
+        $this->_data['baseUrl'] = $url;
+    }
+    
+    /**
+     * Add a Pubsubhubbub hub endpoint URL
+     *
+     * @param string $url
+     */
+    public function addHub($url)
+    {
+        if (empty($url) || !is_string($url) || !Zend_Uri::check($url)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: "url" array value'
+            . ' must be a non-empty string and valid URI/IRI');
+        }
+        if (!isset($this->_data['hubs'])) {
+            $this->_data['hubs'] = array();
+        }
+        $this->_data['hubs'][] = $url;
+    }
+    
+    /**
+     * Add Pubsubhubbub hub endpoint URLs
+     *
+     * @param array $urls
+     */
+    public function addHubs(array $urls)
+    {
+        foreach ($urls as $url) {
+            $this->addHub($url);
+        }
+    }
+    
+    /**
+     * Add a feed category
+     *
+     * @param string $category
+     */ 
+    public function addCategory(array $category)
+    {
+        if (!isset($category['term'])) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Each category must be an array and '
+            . 'contain at least a "term" element containing the machine '
+            . ' readable category name');
+        }
+        if (isset($category['scheme'])) {
+            if (empty($category['scheme']) || !is_string($category['scheme'])
+            || !Zend_Uri::check($category['scheme'])) {
+                require_once 'Zend/Feed/Exception.php';
+                throw new Zend_Feed_Exception('The Atom scheme or RSS domain of'
+                . ' a category must be a valid URI');
+            }
+        }
+        if (!isset($this->_data['categories'])) {
+            $this->_data['categories'] = array();
+        }
+        $this->_data['categories'][] = $category;
+    }
+    
+    /**
+     * Set an array of feed categories
+     *
+     * @param array $categories
+     */
+    public function addCategories(array $categories)
+    {
+        foreach ($categories as $category) {
+            $this->addCategory($category);
+        }
+    }
+
+    /**
+     * Get a single author
+     *
+     * @param  int $index
+     * @return string|null
+     */
+    public function getAuthor($index = 0)
+    {
+        if (isset($this->_data['authors'][$index])) {
+            return $this->_data['authors'][$index];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get an array with feed authors
+     *
+     * @return array
+     */
+    public function getAuthors()
+    {
+        if (!array_key_exists('authors', $this->_data)) {
+            return null;
+        }
+        return $this->_data['authors'];
+    }
+
+    /**
+     * Get the copyright entry
+     *
+     * @return string|null
+     */
+    public function getCopyright()
+    {
+        if (!array_key_exists('copyright', $this->_data)) {
+            return null;
+        }
+        return $this->_data['copyright'];
+    }
+
+    /**
+     * Get the feed creation date
+     *
+     * @return string|null
+     */
+    public function getDateCreated()
+    {
+        if (!array_key_exists('dateCreated', $this->_data)) {
+            return null;
+        }
+        return $this->_data['dateCreated'];
+    }
+
+    /**
+     * Get the feed modification date
+     *
+     * @return string|null
+     */
+    public function getDateModified()
+    {
+        if (!array_key_exists('dateModified', $this->_data)) {
+            return null;
+        }
+        return $this->_data['dateModified'];
+    }
+
+    /**
+     * Get the feed description
+     *
+     * @return string|null
+     */
+    public function getDescription()
+    {
+        if (!array_key_exists('description', $this->_data)) {
+            return null;
+        }
+        return $this->_data['description'];
+    }
+
+    /**
+     * Get the feed generator entry
+     *
+     * @return string|null
+     */
+    public function getGenerator()
+    {
+        if (!array_key_exists('generator', $this->_data)) {
+            return null;
+        }
+        return $this->_data['generator'];
+    }
+
+    /**
+     * Get the feed ID
+     *
+     * @return string|null
+     */
+    public function getId()
+    {
+        if (!array_key_exists('id', $this->_data)) {
+            return null;
+        }
+        return $this->_data['id'];
+    }
+
+    /**
+     * Get the feed language
+     *
+     * @return string|null
+     */
+    public function getLanguage()
+    {
+        if (!array_key_exists('language', $this->_data)) {
+            return null;
+        }
+        return $this->_data['language'];
+    }
+
+    /**
+     * Get a link to the HTML source
+     *
+     * @return string|null
+     */
+    public function getLink()
+    {
+        if (!array_key_exists('link', $this->_data)) {
+            return null;
+        }
+        return $this->_data['link'];
+    }
+
+    /**
+     * Get a link to the XML feed
+     *
+     * @return string|null
+     */
+    public function getFeedLinks()
+    {
+        if (!array_key_exists('feedLinks', $this->_data)) {
+            return null;
+        }
+        return $this->_data['feedLinks'];
+    }
+
+    /**
+     * Get the feed title
+     *
+     * @return string|null
+     */
+    public function getTitle()
+    {
+        if (!array_key_exists('title', $this->_data)) {
+            return null;
+        }
+        return $this->_data['title'];
+    }
+
+    /**
+     * Get the feed character encoding
+     *
+     * @return string|null
+     */
+    public function getEncoding()
+    {
+        if (!array_key_exists('encoding', $this->_data)) {
+            return 'UTF-8';
+        }
+        return $this->_data['encoding'];
+    }
+    
+    /**
+     * Get the feed's base url
+     *
+     * @return string|null
+     */
+    public function getBaseUrl()
+    {
+        if (!array_key_exists('baseUrl', $this->_data)) {
+            return null;
+        }
+        return $this->_data['baseUrl'];
+    }
+    
+    /**
+     * Get the URLs used as Pubsubhubbub hubs endpoints
+     *
+     * @return string|null
+     */
+    public function getHubs()
+    {
+        if (!array_key_exists('hubs', $this->_data)) {
+            return null;
+        }
+        return $this->_data['hubs'];
+    }
+    
+    /**
+     * Get the feed categories
+     *
+     * @return string|null
+     */
+    public function getCategories()
+    {
+        if (!array_key_exists('categories', $this->_data)) {
+            return null;
+        }
+        return $this->_data['categories'];
+    }
+
+    /**
+     * Resets the instance and deletes all data
+     *
+     * @return void
+     */
+    public function reset()
+    {
+        $this->_data = array();
+    }
+
+    /**
+     * Creates a new Zend_Feed_Writer_Entry data container for use. This is NOT
+     * added to the current feed automatically, but is necessary to create a
+     * container with some initial values preset based on the current feed data.
+     *
+     * @return Zend_Feed_Writer_Entry
+     */
+    public function createEntry()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        if ($this->getEncoding()) {
+            $entry->setEncoding($this->getEncoding());
+        }
+        $entry->setType($this->getType());
+        return $entry;
+    }
+
+    /**
+     * Appends a Zend_Feed_Writer_Entry object representing a new entry/item
+     * the feed data container's internal group of entries.
+     *
+     * @param Zend_Feed_Writer_Entry $entry
+     */
+    public function addEntry(Zend_Feed_Writer_Entry $entry)
+    {
+        $this->_entries[] = $entry;
+    }
+
+    /**
+     * Removes a specific indexed entry from the internal queue. Entries must be
+     * added to a feed container in order to be indexed.
+     *
+     * @param int $index
+     */
+    public function removeEntry($index)
+    {
+        if (isset($this->_entries[$index])) {
+            unset($this->_entries[$index]);
+        }
+        require_once 'Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Undefined index: ' . $index . '. Entry does not exist.');
+    }
+
+    /**
+     * Retrieve a specific indexed entry from the internal queue. Entries must be
+     * added to a feed container in order to be indexed.
+     *
+     * @param int $index
+     */
+    public function getEntry($index = 0)
+    {
+        if (isset($this->_entries[$index])) {
+            return $this->_entries[$index];
+        }
+        require_once 'Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Undefined index: ' . $index . '. Entry does not exist.');
+    }
+
+    /**
+     * Orders all indexed entries by date, thus offering date ordered readable
+     * content where a parser (or Homo Sapien) ignores the generic rule that
+     * XML element order is irrelevant and has no intrinsic meaning.
+     *
+     * Using this method will alter the original indexation.
+     *
+     * @return void
+     */
+    public function orderByDate()
+    {
+        /**
+         * Could do with some improvement for performance perhaps
+         */
+        $timestamp = time();
+        $entries = array();
+        foreach ($this->_entries as $entry) {
+            if ($entry->getDateModified()) {
+                $timestamp = (int) $entry->getDateModified()->get(Zend_Date::TIMESTAMP);
+            } elseif ($entry->getDateCreated()) {
+                $timestamp = (int) $entry->getDateCreated()->get(Zend_Date::TIMESTAMP);
+            }
+            $entries[$timestamp] = $entry;
+        }
+        krsort($entries, SORT_NUMERIC);
+        $this->_entries = array_values($entries);
+    }
+
+    /**
+     * Get the number of feed entries.
+     * Required by the Iterator interface.
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_entries);
+    }
+
+	/**
+     * Return the current entry
+     *
+     * @return Zend_Feed_Reader_Entry_Interface
+     */
+    public function current()
+    {
+        return $this->_entries[$this->key()];
+    }
+
+    /**
+     * Return the current feed key
+     *
+     * @return unknown
+     */
+    public function key()
+    {
+        return $this->_entriesKey;
+    }
+
+	/**
+     * Move the feed pointer forward
+     *
+     * @return void
+     */
+    public function next()
+    {
+        ++$this->_entriesKey;
+    }
+
+    /**
+     * Reset the pointer in the feed object
+     *
+     * @return void
+     */
+    public function rewind()
+    {
+        $this->_entriesKey = 0;
+    }
+
+    /**
+     * Check to see if the iterator is still valid
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return 0 <= $this->_entriesKey && $this->_entriesKey < $this->count();
+    }
+    
+    /**
+     * Set the current feed type being exported to "rss" or "atom". This allows
+     * other objects to gracefully choose whether to execute or not, depending
+     * on their appropriateness for the current type, e.g. renderers.
+     *
+     * @param string $type
+     */
+    public function setType($type)
+    {
+        $this->_type = $type;
+    }
+    
+    /**
+     * Retrieve the current or last feed type exported.
+     *
+     * @return string Value will be "rss" or "atom"
+     */
+    public function getType()
+    {
+        return $this->_type;
+    }
+
+    /**
+     * Attempt to build and return the feed resulting from the data set
+     *
+     * @param $type The feed type "rss" or "atom" to export as
+     * @return string
+     */
+    public function export($type, $ignoreExceptions = false)
+    {
+        $this->setType(strtolower($type));
+        $type = ucfirst($this->getType());
+        if ($type !== 'Rss' && $type !== 'Atom') {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid feed type specified: ' . $type . '.'
+            . ' Should be one of "rss" or "atom".');
+        }
+        $renderClass = 'Zend_Feed_Writer_Renderer_Feed_' . $type;
+        $renderer = new $renderClass($this);
+        if ($ignoreExceptions) {
+            $renderer->ignoreExceptions();
+        }
+        return $renderer->render()->saveXml();
+    }
+    
+    /**
+     * Unset a specific data point
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (isset($this->_data[$name])) {
+            unset($this->_data[$name]);
+        }
+    }
+    
+    /**
+     * Method overloading: call given method on first extension implementing it
+     *
+     * @param  string $method
+     * @param  array $args
+     * @return mixed
+     * @throws Zend_Feed_Exception if no extensions implements the method
+     */
+    public function __call($method, $args)
+    {
+        foreach ($this->_extensions as $extension) {
+            try {
+                return call_user_func_array(array($extension, $method), $args);
+            } catch (Zend_Feed_Writer_Exception_InvalidMethodException $e) {
+            }
+        }
+        require_once 'Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Method: ' . $method
+            . ' does not exist and could not be located on a registered Extension');
+    }
+
+    /**
+     * Load extensions from Zend_Feed_Writer
+     *
+     * @return void
+     */
+    protected function _loadExtensions()
+    {
+        $all = Zend_Feed_Writer::getExtensions();
+        $exts = $all['feed'];
+        foreach ($exts as $ext) {
+            $className = Zend_Feed_Writer::getPluginLoader()->getClassName($ext);
+            $this->_extensions[$ext] = new $className();
+            $this->_extensions[$ext]->setEncoding($this->getEncoding());
+        }
+    }
+
+}

+ 248 - 0
library/Zend/Feed/Writer/Renderer/Entry/Atom.php

@@ -0,0 +1,248 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Feed_Writer_Renderer_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Renderer/RendererAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Renderer_Entry_Atom
+extends Zend_Feed_Writer_Renderer_RendererAbstract
+implements Zend_Feed_Writer_Renderer_RendererInterface
+{
+
+    public function __construct (Zend_Feed_Writer_Entry $container)
+    {
+        parent::__construct($container);
+    }
+
+    public function render()
+    {
+        $this->_dom = new DOMDocument('1.0', $this->_container->getEncoding());
+        $this->_dom->formatOutput = true;
+        $entry = $this->_dom->createElementNS(Zend_Feed_Writer::NAMESPACE_ATOM_10, 'entry');
+        $this->_dom->appendChild($entry);
+        
+        $this->_setTitle($this->_dom, $entry);
+        $this->_setDescription($this->_dom, $entry);
+        $this->_setDateCreated($this->_dom, $entry);
+        $this->_setDateModified($this->_dom, $entry);
+        $this->_setLink($this->_dom, $entry);
+        $this->_setId($this->_dom, $entry);
+        $this->_setAuthors($this->_dom, $entry);
+        $this->_setEnclosure($this->_dom, $entry);
+        $this->_setContent($this->_dom, $entry);
+
+        foreach ($this->_extensions as $ext) {
+            $ext->setType($this->getType());
+            $ext->setRootElement($this->getRootElement());
+            $ext->setDomDocument($this->getDomDocument(), $entry);
+            $ext->render();
+        }
+        
+        return $this;
+    }
+    
+    protected function _setTitle(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getTitle()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 entry elements MUST contain exactly one'
+            . ' atom:title element but a title has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+        $title = $dom->createElement('title');
+        $root->appendChild($title);
+        $title->setAttribute('type', 'html');
+        $cdata = $dom->createCDATASection($this->getDataContainer()->getTitle());
+        $title->appendChild($cdata);
+    }
+    
+    protected function _setDescription(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDescription()) {
+            return; // unless src content or base64
+        }
+        $subtitle = $dom->createElement('summary');
+        $root->appendChild($subtitle);
+        $subtitle->setAttribute('type', 'html');
+        $cdata = $dom->createCDATASection(
+            $this->getDataContainer()->getDescription()
+        );
+        $subtitle->appendChild($cdata);
+    }
+    
+    protected function _setDateModified(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDateModified()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 entry elements MUST contain exactly one'
+            . ' atom:updated element but a modification date has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        $updated = $dom->createElement('updated');
+        $root->appendChild($updated);
+        $updated->nodeValue = $this->getDataContainer()->getDateModified()
+            ->get(Zend_Date::ISO_8601);
+    }
+    
+    protected function _setDateCreated(DOMDocument $dom, DOMElement $root)
+    {
+        if (!$this->getDataContainer()->getDateCreated()) {
+            return;
+        }
+        $updated = $dom->createElement('published');
+        $root->appendChild($updated);
+        $updated->nodeValue = $this->getDataContainer()->getDateCreated()
+            ->get(Zend_Date::ISO_8601);
+    }
+    
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->_container->getAuthors();
+        if ((!$authors || empty($authors))) {
+            /**
+             * This will actually trigger an Exception at the feed level if
+             * a feed level author is not set.
+             */
+            return;
+        }
+        foreach ($authors as $data) {
+            $author = $this->_dom->createElement('author');
+            $name = $this->_dom->createElement('name');
+            $author->appendChild($name);
+            $root->appendChild($author);
+            $name->nodeValue = $data['name'];
+            if (array_key_exists('email', $data)) {
+                $email = $this->_dom->createElement('email');
+                $author->appendChild($email);
+                $email->nodeValue = $data['email'];
+            }
+            if (array_key_exists('uri', $data)) {
+                $uri = $this->_dom->createElement('uri');
+                $author->appendChild($uri);
+                $uri->nodeValue = $data['uri'];
+            }
+        }
+    }
+    
+    protected function _setEnclosure(DOMDocument $dom, DOMElement $root)
+    {
+        $data = $this->_container->getEnclosure();
+        if ((!$data || empty($data))) {
+            return;
+        }
+        $enclosure = $this->_dom->createElement('link');
+        $enclosure->setAttribute('rel', 'enclosure');
+        $enclosure->setAttribute('type', $data['type']);
+        $enclosure->setAttribute('length', $data['length']);
+        $enclosure->setAttribute('href', $data['uri']);
+        $root->appendChild($enclosure);
+    }
+    
+    protected function _setLink(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getLink()) {
+            return;
+        }
+        $link = $dom->createElement('link');
+        $root->appendChild($link);
+        $link->setAttribute('rel', 'alternate');
+        $link->setAttribute('type', 'text/html');
+        $link->setAttribute('href', $this->getDataContainer()->getLink());
+    }
+    
+    protected function _setId(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getId()
+        && !$this->getDataContainer()->getLink()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 entry elements MUST contain exactly one '
+            . 'atom:id element, or as an alternative, we can use the same '
+            . 'value as atom:link however neither a suitable link nor an '
+            . 'id have been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        if (!$this->getDataContainer()->getId()) {
+            $this->getDataContainer()->setId(
+                $this->getDataContainer()->getLink());
+        }
+        if (!Zend_Uri::check($this->getDataContainer()->getId()) &&
+        !preg_match("#^urn:[a-zA-Z0-9][a-zA-Z0-9\-]{1,31}:([a-zA-Z0-9\(\)\+\,\.\:\=\@\;\$\_\!\*\-]|%[0-9a-fA-F]{2})*#", $this->getDataContainer()->getId())) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Atom 1.0 IDs must be a valid URI/IRI');
+        }
+        $id = $dom->createElement('id');
+        $root->appendChild($id);
+        $id->nodeValue = $this->getDataContainer()->getId();
+    }
+    
+    protected function _setContent(DOMDocument $dom, DOMElement $root)
+    {
+        $content = $this->getDataContainer()->getContent();
+        if (!$content && !$this->getDataContainer()->getLink()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 entry elements MUST contain exactly one '
+            . 'atom:content element, or as an alternative, at least one link '
+            . 'with a rel attribute of "alternate" to indicate an alternate '
+            . 'method to consume the content.';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+        $element = $dom->createElement('content');
+        $element->setAttribute('type', 'html');
+        $cdata = $dom->createCDATASection($content);
+        $element->appendChild($cdata);
+        $root->appendChild($element);
+    }
+
+}

+ 214 - 0
library/Zend/Feed/Writer/Renderer/Entry/Rss.php

@@ -0,0 +1,214 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Feed_Writer_Renderer_RendererAbstract
+ */
+require_once 'Zend/Feed/Writer/Renderer/RendererAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Renderer_Entry_Rss
+extends Zend_Feed_Writer_Renderer_RendererAbstract
+implements Zend_Feed_Writer_Renderer_RendererInterface
+{
+
+    public function __construct (Zend_Feed_Writer_Entry $container)
+    {
+        parent::__construct($container);
+    }
+    
+    public function render()
+    {
+        $this->_dom = new DOMDocument('1.0', $this->_container->getEncoding());
+        $this->_dom->formatOutput = true;
+        $entry = $this->_dom->createElement('item');
+        $this->_dom->appendChild($entry);
+        
+        $this->_setTitle($this->_dom, $entry);
+        $this->_setDescription($this->_dom, $entry);
+        $this->_setDateCreated($this->_dom, $entry);
+        $this->_setDateModified($this->_dom, $entry);
+        $this->_setLink($this->_dom, $entry);
+        $this->_setId($this->_dom, $entry);
+        $this->_setAuthors($this->_dom, $entry);
+        $this->_setEnclosure($this->_dom, $entry);
+        $this->_setCommentLink($this->_dom, $entry);
+        foreach ($this->_extensions as $ext) {
+            $ext->setType($this->getType());
+            $ext->setRootElement($this->getRootElement());
+            $ext->setDomDocument($this->getDomDocument(), $entry);
+            $ext->render();
+        }
+
+        return $this;
+    }
+    
+    protected function _setTitle(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDescription()
+        && !$this->getDataContainer()->getTitle()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'RSS 2.0 entry elements SHOULD contain exactly one'
+            . ' title element but a title has not been set. In addition, there'
+            . ' is no description as required in the absence of a title.';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+        $title = $dom->createElement('title');
+        $root->appendChild($title);
+        $title->nodeValue = htmlentities(
+            $this->getDataContainer()->getTitle(),
+            ENT_QUOTES,
+            $this->getDataContainer()->getEncoding()
+        );
+    }
+    
+    protected function _setDescription(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDescription()
+        && !$this->getDataContainer()->getTitle()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'RSS 2.0 entry elements SHOULD contain exactly one'
+            . ' description element but a description has not been set. In'
+            . ' addition, there is no title element as required in the absence'
+            . ' of a description.';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+        $subtitle = $dom->createElement('description');
+        $root->appendChild($subtitle);
+        $subtitle->nodeValue = htmlentities(
+            $this->getDataContainer()->getDescription(),
+            ENT_QUOTES,
+            $this->getDataContainer()->getEncoding()
+        );
+    }
+    
+    protected function _setDateModified(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDateModified()) {
+            return;
+        }
+
+        $updated = $dom->createElement('pubDate');
+        $root->appendChild($updated);
+        $updated->nodeValue = $this->getDataContainer()->getDateModified()
+            ->get(Zend_Date::RSS);
+    }
+    
+    protected function _setDateCreated(DOMDocument $dom, DOMElement $root)
+    {
+        if (!$this->getDataContainer()->getDateCreated()) {
+            return;
+        }
+        if (!$this->getDataContainer()->getDateModified()) {
+            $this->getDataContainer()->setDateModified(
+                $this->getDataContainer()->getDateCreated()
+            );
+        }
+    }
+    
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->_container->getAuthors();
+        if ((!$authors || empty($authors))) {
+            return;
+        }
+        foreach ($authors as $data) {
+            $author = $this->_dom->createElement('author');
+            $name = $data['name'];
+            if (array_key_exists('email', $data)) {
+                $name = $data['email'] . ' (' . $data['name'] . ')';
+            }
+            $author->nodeValue = $name;
+            $root->appendChild($author);
+        }
+    }
+    
+    protected function _setEnclosure(DOMDocument $dom, DOMElement $root)
+    {
+        $data = $this->_container->getEnclosure();
+        if ((!$data || empty($data))) {
+            return;
+        }
+        $enclosure = $this->_dom->createElement('enclosure');
+        $enclosure->setAttribute('type', $data['type']);
+        $enclosure->setAttribute('length', $data['length']);
+        $enclosure->setAttribute('url', $data['uri']);
+        $root->appendChild($enclosure);
+    }
+    
+    protected function _setLink(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getLink()) {
+            return;
+        }
+        $link = $dom->createElement('link');
+        $root->appendChild($link);
+        $link->nodeValue = $this->getDataContainer()->getLink();
+    }
+    
+    protected function _setId(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getId()
+        && !$this->getDataContainer()->getLink()) {
+            return;
+        }
+
+        $id = $dom->createElement('guid');
+        $root->appendChild($id);
+        if (!$this->getDataContainer()->getId()) {
+            $this->getDataContainer()->setId(
+                $this->getDataContainer()->getLink());
+        }
+        $id->nodeValue = $this->getDataContainer()->getId();
+        if (!Zend_Uri::check($this->getDataContainer()->getId())) {
+            $id->setAttribute('isPermaLink', 'false');
+        }
+    }
+    
+    protected function _setCommentLink(DOMDocument $dom, DOMElement $root)
+    {
+        $link = $this->getDataContainer()->getCommentLink();
+        if (!$link) {
+            return;
+        }
+        $clink = $this->_dom->createElement('comments');
+        $clink->nodeValue = $link;
+        $root->appendChild($clink);
+    }
+
+}

+ 348 - 0
library/Zend/Feed/Writer/Renderer/Feed/Atom.php

@@ -0,0 +1,348 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+require_once 'Zend/Feed/Writer/Feed.php';
+
+require_once 'Zend/Version.php';
+
+require_once 'Zend/Feed/Writer/Renderer/RendererInterface.php';
+
+require_once 'Zend/Feed/Writer/Renderer/Entry/Atom.php';
+
+require_once 'Zend/Feed/Writer/Renderer/RendererAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Renderer_Feed_Atom
+extends Zend_Feed_Writer_Renderer_RendererAbstract
+implements Zend_Feed_Writer_Renderer_RendererInterface
+{
+
+    public function __construct (Zend_Feed_Writer_Feed $container)
+    {
+        parent::__construct($container);
+    }
+
+    public function render()
+    {
+        if (!$this->_container->getEncoding()) {
+            $this->_container->setEncoding('UTF-8');
+        }
+        $this->_dom = new DOMDocument('1.0', $this->_container->getEncoding());
+        $this->_dom->formatOutput = true;
+        $root = $this->_dom->createElementNS(
+            Zend_Feed_Writer::NAMESPACE_ATOM_10, 'feed'
+        );
+        $this->setRootElement($root);
+        $this->_dom->appendChild($root);
+        $this->_setLanguage($this->_dom, $root);
+        $this->_setBaseUrl($this->_dom, $root);
+        $this->_setTitle($this->_dom, $root);
+        $this->_setDescription($this->_dom, $root);
+        $this->_setDateCreated($this->_dom, $root);
+        $this->_setDateModified($this->_dom, $root);
+        $this->_setGenerator($this->_dom, $root);
+        $this->_setLink($this->_dom, $root);
+        $this->_setFeedLinks($this->_dom, $root);
+        $this->_setId($this->_dom, $root);
+        $this->_setAuthors($this->_dom, $root);
+        $this->_setCopyright($this->_dom, $root);
+        $this->_setCategories($this->_dom, $root);
+        $this->_setHubs($this->_dom, $root);
+        
+        foreach ($this->_extensions as $ext) {
+            $ext->setType($this->getType());
+            $ext->setRootElement($this->getRootElement());
+            $ext->setDomDocument($this->getDomDocument(), $root);
+            $ext->render();
+        }
+        
+        foreach ($this->_container as $entry) {
+            if ($this->getDataContainer()->getEncoding()) {
+                $entry->setEncoding($this->getDataContainer()->getEncoding());
+            }
+            $renderer = new Zend_Feed_Writer_Renderer_Entry_Atom($entry);
+            if ($this->_ignoreExceptions === true) {
+                $renderer->ignoreExceptions();
+            }
+            $renderer->setType($this->getType());
+            $renderer->setRootElement($this->_dom->documentElement);
+            $renderer->render();
+            $element = $renderer->getElement();
+            $imported = $this->_dom->importNode($element, true);
+            $root->appendChild($imported);
+        }
+        return $this;
+    }
+
+    protected function _setLanguage(DOMDocument $dom, DOMElement $root)
+    {
+        if ($this->getDataContainer()->getLanguage()) {
+            $root->setAttribute('xml:lang', $this->getDataContainer()
+                ->getLanguage());
+        }
+    }
+
+    protected function _setTitle(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getTitle()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 feed elements MUST contain exactly one'
+            . ' atom:title element but a title has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        $title = $dom->createElement('title');
+        $root->appendChild($title);
+        $title->setAttribute('type', 'text');
+        $title->nodeValue = $this->getDataContainer()->getTitle();
+    }
+
+    protected function _setDescription(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDescription()) {
+            return;
+        }
+        $subtitle = $dom->createElement('subtitle');
+        $root->appendChild($subtitle);
+        $subtitle->setAttribute('type', 'text');
+        $subtitle->nodeValue = $this->getDataContainer()->getDescription();
+    }
+
+    protected function _setDateModified(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDateModified()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 feed elements MUST contain exactly one'
+            . ' atom:updated element but a modification date has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        $updated = $dom->createElement('updated');
+        $root->appendChild($updated);
+        $updated->nodeValue = $this->getDataContainer()->getDateModified()
+            ->get(Zend_Date::ISO_8601);
+    }
+
+    protected function _setGenerator(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getGenerator()) {
+            $this->getDataContainer()->setGenerator('Zend_Feed_Writer',
+                Zend_Version::VERSION, 'http://framework.zend.com');
+        }
+
+        $gdata = $this->getDataContainer()->getGenerator();
+        $generator = $dom->createElement('generator');
+        $root->appendChild($generator);
+        $generator->nodeValue = $gdata['name'];
+        if (array_key_exists('uri', $gdata)) {
+            $generator->setAttribute('uri', $gdata['uri']);
+        }
+        if (array_key_exists('version', $gdata)) {
+            $generator->setAttribute('version', $gdata['version']);
+        }
+    }
+
+    protected function _setLink(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getLink()) {
+            return;
+        }
+        $link = $dom->createElement('link');
+        $root->appendChild($link);
+        $link->setAttribute('rel', 'alternate');
+        $link->setAttribute('type', 'text/html');
+        $link->setAttribute('href', $this->getDataContainer()->getLink());
+    }
+
+    protected function _setFeedLinks(DOMDocument $dom, DOMElement $root)
+    {
+        $flinks = $this->getDataContainer()->getFeedLinks();
+        if(!$flinks || !array_key_exists('atom', $flinks)) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 feed elements SHOULD contain one atom:link '
+            . 'element with a rel attribute value of "self".  This is the '
+            . 'preferred URI for retrieving Atom Feed Documents representing '
+            . 'this Atom feed but a feed link has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        foreach ($flinks as $type => $href) {
+            $mime = 'application/' . strtolower($type) . '+xml';
+            $flink = $dom->createElement('link');
+            $root->appendChild($flink);
+            $flink->setAttribute('rel', 'self');
+            $flink->setAttribute('type', $mime);
+            $flink->setAttribute('href', $href);
+        }
+    }
+    
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->_container->getAuthors();
+        if (!$authors || empty($authors)) {
+            /**
+             * Technically we should defer an exception until we can check
+             * that all entries contain an author. If any entry is missing
+             * an author, then a missing feed author element is invalid
+             */
+            return;
+        }
+        foreach ($authors as $data) {
+            $author = $this->_dom->createElement('author');
+            $name = $this->_dom->createElement('name');
+            $author->appendChild($name);
+            $root->appendChild($author);
+            $name->nodeValue = $data['name'];
+            if (array_key_exists('email', $data)) {
+                $email = $this->_dom->createElement('email');
+                $author->appendChild($email);
+                $email->nodeValue = $data['email'];
+            }
+            if (array_key_exists('uri', $data)) {
+                $uri = $this->_dom->createElement('uri');
+                $author->appendChild($uri);
+                $uri->nodeValue = $data['uri'];
+            }
+        }
+    }
+
+    protected function _setId(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getId()
+        && !$this->getDataContainer()->getLink()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'Atom 1.0 feed elements MUST contain exactly one '
+            . 'atom:id element, or as an alternative, we can use the same '
+            . 'value as atom:link however neither a suitable link nor an '
+            . 'id have been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        if (!$this->getDataContainer()->getId()) {
+            $this->getDataContainer()->setId(
+                $this->getDataContainer()->getLink());
+        }
+        $id = $dom->createElement('id');
+        $root->appendChild($id);
+        $id->nodeValue = $this->getDataContainer()->getId();
+    }
+    
+    protected function _setCopyright(DOMDocument $dom, DOMElement $root)
+    {
+        $copyright = $this->getDataContainer()->getCopyright();
+        if (!$copyright) {
+            return;
+        }
+        $copy = $dom->createElement('rights');
+        $root->appendChild($copy);
+        $copy->nodeValue = $copyright;
+    }
+    
+    protected function _setDateCreated(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDateCreated()) {
+            return;
+        }
+        if(!$this->getDataContainer()->getDateModified()) {
+            $this->getDataContainer()->setDateModified(
+                $this->getDataContainer()->getDateCreated()
+            );
+        }
+    }
+    
+    protected function _setBaseUrl(DOMDocument $dom, DOMElement $root)
+    {
+        $baseUrl = $this->getDataContainer()->getBaseUrl();
+        if (!$baseUrl) {
+            return;
+        }
+        $root->setAttribute('xml:base', $baseUrl);
+    }
+    
+    protected function _setHubs(DOMDocument $dom, DOMElement $root)
+    {
+        $hubs = $this->getDataContainer()->getHubs();
+        if (!$hubs) {
+            return;
+        }
+        foreach ($hubs as $hubUrl) {
+            $hub = $dom->createElement('link');
+            $hub->setAttribute('rel', 'hub');
+            $hub->setAttribute('href', $hubUrl);
+            $root->appendChild($hub);
+        }
+    }
+    
+    protected function _setCategories(DOMDocument $dom, DOMElement $root)
+    {
+        $categories = $this->getDataContainer()->getCategories();
+        if (!$categories) {
+            return;
+        }
+        foreach ($categories as $cat) {
+            $category = $dom->createElement('category');
+            $category->setAttribute('term', $cat['term']);
+            if (isset($cat['label'])) {
+                $category->setAttribute('label',
+                    htmlentities($cat['label'], ENT_QUOTES, $this->getDataContainer()->getEncoding())
+                );
+            } else {
+                $category->setAttribute('label',
+                    htmlentities($cat['term'], ENT_QUOTES, $this->getDataContainer()->getEncoding())
+                );
+            }
+            if (isset($cat['scheme'])) {
+                $category->setAttribute('scheme', $cat['scheme']);
+            }
+            $root->appendChild($category);
+        }
+    }
+
+}

+ 277 - 0
library/Zend/Feed/Writer/Renderer/Feed/Rss.php

@@ -0,0 +1,277 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+require_once 'Zend/Feed/Writer/Feed.php';
+
+require_once 'Zend/Version.php';
+
+require_once 'Zend/Feed/Writer/Renderer/RendererInterface.php';
+
+require_once 'Zend/Feed/Writer/Renderer/Entry/Rss.php';
+
+require_once 'Zend/Feed/Writer/Renderer/RendererAbstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Renderer_Feed_Rss
+extends Zend_Feed_Writer_Renderer_RendererAbstract
+implements Zend_Feed_Writer_Renderer_RendererInterface
+{
+
+    public function __construct (Zend_Feed_Writer_Feed $container)
+    {
+        parent::__construct($container);
+    }
+
+    public function render()
+    {
+        if (!$this->_container->getEncoding()) {
+            $this->_container->setEncoding('UTF-8');
+        }
+        $this->_dom = new DOMDocument('1.0', $this->_container->getEncoding());
+        $this->_dom->formatOutput = true;
+        $rss = $this->_dom->createElement('rss');
+        $this->setRootElement($rss);
+        $rss->setAttribute('version', '2.0');
+        
+        $channel = $this->_dom->createElement('channel');
+        $rss->appendChild($channel);
+        $this->_dom->appendChild($rss);
+        $this->_setLanguage($this->_dom, $channel);
+        $this->_setBaseUrl($this->_dom, $channel);
+        $this->_setTitle($this->_dom, $channel);
+        $this->_setDescription($this->_dom, $channel);
+        $this->_setDateCreated($this->_dom, $channel);
+        $this->_setDateModified($this->_dom, $channel);
+        $this->_setGenerator($this->_dom, $channel);
+        $this->_setLink($this->_dom, $channel);
+        $this->_setAuthors($this->_dom, $channel);
+        $this->_setCopyright($this->_dom, $channel);
+        $this->_setCategories($this->_dom, $channel);
+        
+        foreach ($this->_extensions as $ext) {
+            $ext->setType($this->getType());
+            $ext->setRootElement($this->getRootElement());
+            $ext->setDomDocument($this->getDomDocument(), $channel);
+            $ext->render();
+        }
+        
+        foreach ($this->_container as $entry) {
+            if ($this->getDataContainer()->getEncoding()) {
+                $entry->setEncoding($this->getDataContainer()->getEncoding());
+            }
+            $renderer = new Zend_Feed_Writer_Renderer_Entry_Rss($entry);
+            if ($this->_ignoreExceptions === true) {
+                $renderer->ignoreExceptions();
+            }
+            $renderer->setType($this->getType());
+            $renderer->setRootElement($this->_dom->documentElement);
+            $renderer->render();
+            $element = $renderer->getElement();
+            $imported = $this->_dom->importNode($element, true);
+            $channel->appendChild($imported);
+        }
+        return $this;
+    }
+
+    protected function _setLanguage(DOMDocument $dom, DOMElement $root)
+    {
+        $lang = $this->getDataContainer()->getLanguage();
+        if (!$lang) {
+            return;
+        }
+        $language = $dom->createElement('language');
+        $root->appendChild($language);
+        $language->nodeValue = $lang;
+    }
+
+    protected function _setTitle(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getTitle()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'RSS 2.0 feed elements MUST contain exactly one'
+            . ' title element but a title has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+
+        $title = $dom->createElement('title');
+        $root->appendChild($title);
+        $title->nodeValue = htmlentities(
+            $this->getDataContainer()->getTitle(),
+            ENT_QUOTES,
+            $this->getDataContainer()->getEncoding()
+        );
+    }
+
+    protected function _setDescription(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDescription()) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'RSS 2.0 feed elements MUST contain exactly one'
+            . ' description element but one has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+        $subtitle = $dom->createElement('description');
+        $root->appendChild($subtitle);
+        $subtitle->nodeValue = htmlentities(
+            $this->getDataContainer()->getDescription(),
+            ENT_QUOTES,
+            $this->getDataContainer()->getEncoding()
+        );
+    }
+
+    protected function _setDateModified(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDateModified()) {
+            return;
+        }
+
+        $updated = $dom->createElement('pubDate');
+        $root->appendChild($updated);
+        $updated->nodeValue = $this->getDataContainer()->getDateModified()
+            ->get(Zend_Date::RSS);
+    }
+
+    protected function _setGenerator(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getGenerator()) {
+            $this->getDataContainer()->setGenerator('Zend_Feed_Writer',
+                Zend_Version::VERSION, 'http://framework.zend.com');
+        }
+
+        $gdata = $this->getDataContainer()->getGenerator();
+        $generator = $dom->createElement('generator');
+        $root->appendChild($generator);
+        $name = $gdata['name'];
+        if (array_key_exists('version', $gdata)) {
+            $name .= ' ' . $gdata['version'];
+        }
+        if (array_key_exists('uri', $gdata)) {
+            $name .= ' (' . $gdata['uri'] . ')';
+        }
+        $generator->nodeValue = $name;
+    }
+
+    protected function _setLink(DOMDocument $dom, DOMElement $root)
+    {
+        $value = $this->getDataContainer()->getLink();
+        if(!$value) {
+            require_once 'Zend/Feed/Exception.php';
+            $message = 'RSS 2.0 feed elements MUST contain exactly one'
+            . ' link element but one has not been set';
+            $exception = new Zend_Feed_Exception($message);
+            if (!$this->_ignoreExceptions) {
+                throw $exception;
+            } else {
+                $this->_exceptions[] = $exception;
+                return;
+            }
+        }
+        $link = $dom->createElement('link');
+        $root->appendChild($link);
+        $link->nodeValue = $value;
+        if (!Zend_Uri::check($value)) {
+            $link->setAttribute('isPermaLink', 'false');
+        }
+    }
+    
+    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
+    {
+        $authors = $this->getDataContainer()->getAuthors();
+        if (!$authors || empty($authors)) {
+            return;
+        }
+        foreach ($authors as $data) {
+            $author = $this->_dom->createElement('author');
+            $name = $data['name'];
+            if (array_key_exists('email', $data)) {
+                $name = $data['email'] . ' (' . $data['name'] . ')';
+            }
+            $author->nodeValue = $name;
+            $root->appendChild($author);
+        }
+    }
+    
+    protected function _setCopyright(DOMDocument $dom, DOMElement $root)
+    {
+        $copyright = $this->getDataContainer()->getCopyright();
+        if (!$copyright) {
+            return;
+        }
+        $copy = $dom->createElement('copyright');
+        $root->appendChild($copy);
+        $copy->nodeValue = $copyright;
+    }
+    
+    protected function _setDateCreated(DOMDocument $dom, DOMElement $root)
+    {
+        if(!$this->getDataContainer()->getDateCreated()) {
+            return;
+        }
+        if(!$this->getDataContainer()->getDateModified()) {
+            $this->getDataContainer()->setDateModified(
+                $this->getDataContainer()->getDateCreated()
+            );
+        }
+    }
+    
+    protected function _setBaseUrl(DOMDocument $dom, DOMElement $root)
+    {
+        $baseUrl = $this->getDataContainer()->getBaseUrl();
+        if (!$baseUrl) {
+            return;
+        }
+        $root->setAttribute('xml:base', $baseUrl);
+    }
+    
+    protected function _setCategories(DOMDocument $dom, DOMElement $root)
+    {
+        $categories = $this->getDataContainer()->getCategories();
+        if (!$categories) {
+            return;
+        }
+        foreach ($categories as $cat) {
+            $category = $dom->createElement('category');
+            if (isset($cat['scheme'])) {
+                $category->setAttribute('domain', $cat['scheme']);
+            }
+            $category->nodeValue = $cat['term'];
+            $root->appendChild($category);
+        }
+    }
+
+}

+ 181 - 0
library/Zend/Feed/Writer/Renderer/RendererAbstract.php

@@ -0,0 +1,181 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+ 
+require_once 'Zend/Feed/Writer.php';
+
+require_once 'Zend/Version.php';
+ 
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Writer_Renderer_RendererAbstract
+{
+
+    protected $_extensions = array();
+    
+    protected $_container = null;
+
+    protected $_dom = null;
+
+    protected $_ignoreExceptions = false;
+
+    protected $_exceptions = array();
+    
+    /**
+     * Encoding of all text values
+     *
+     * @var string
+     */
+    protected $_encoding = 'UTF-8';
+    
+    /**
+     * Holds the value "atom" or "rss" depending on the feed type set when
+     * when last exported.
+     *
+     * @var string
+     */
+    protected $_type = null;
+    
+    protected $_rootElement = null;
+
+    public function __construct($container)
+    {
+        $this->_container = $container;
+        $this->setType($container->getType());
+        $this->_loadExtensions();
+    }
+    
+    public function saveXml()
+    {
+        return $this->getDomDocument()->saveXml();
+    }
+
+    public function getDomDocument()
+    {
+        return $this->_dom;
+    }
+
+    public function getElement()
+    {
+        return $this->getDomDocument()->documentElement;
+    }
+
+    public function getDataContainer()
+    {
+        return $this->_container;
+    }
+    
+    public function setEncoding($enc)
+    {
+        $this->_encoding = $enc;
+    }
+    
+    public function getEncoding()
+    {
+        return $this->_encoding;
+    }
+
+    public function ignoreExceptions($bool = true)
+    {
+        if (!is_bool($bool)) {
+            require_once 'Zend/Feed/Exception.php';
+            throw new Zend_Feed_Exception('Invalid parameter: $bool. Should be TRUE or FALSE (defaults to TRUE if null)');
+        }
+        $this->_ignoreExceptions = $bool;
+    }
+
+    public function getExceptions()
+    {
+        return $this->_exceptions;
+    }
+    
+    /**
+     * Set the current feed type being exported to "rss" or "atom". This allows
+     * other objects to gracefully choose whether to execute or not, depending
+     * on their appropriateness for the current type, e.g. renderers.
+     *
+     * @param string $type
+     */
+    public function setType($type)
+    {
+        $this->_type = $type;
+    }
+    
+    /**
+     * Retrieve the current or last feed type exported.
+     *
+     * @return string Value will be "rss" or "atom"
+     */
+    public function getType()
+    {
+        return $this->_type;
+    }
+    
+    /**
+     * Sets the absolute root element for the XML feed being generated. This
+     * helps simplify the appending of namespace declarations, but also ensures
+     * namespaces are added to the root element - not scattered across the entire
+     * XML file - may assist namespace unsafe parsers and looks pretty ;).
+     *
+     * @param DOMElement $root
+     */
+    public function setRootElement(DOMElement $root)
+    {
+        $this->_rootElement = $root;
+    }
+    
+    /**
+     * Retrieve the absolute root element for the XML feed being generated.
+     *
+     * @return DOMElement
+     */
+    public function getRootElement()
+    {
+        return $this->_rootElement;
+    }
+    
+    /**
+     * Load extensions from Zend_Feed_Writer
+     *
+     * @return void
+     */
+    protected function _loadExtensions()
+    {
+        Zend_Feed_Writer::registerCoreExtensions();
+        $all = Zend_Feed_Writer::getExtensions();
+        if (stripos(get_class($this), 'entry')) {
+            $exts = $all['entryRenderer'];
+        } else {
+            $exts = $all['feedRenderer'];
+        }
+        foreach ($exts as $extension) {
+            $className = Zend_Feed_Writer::getPluginLoader()->getClassName($extension);
+            $this->_extensions[$extension] = new $className(
+                $this->getDataContainer()
+            );
+            $this->_extensions[$extension]->setEncoding($this->getEncoding());
+        }
+    }
+
+}

+ 78 - 0
library/Zend/Feed/Writer/Renderer/RendererInterface.php

@@ -0,0 +1,78 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Feed_Writer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Feed_Writer_Renderer_RendererInterface
+{
+
+    public function render();
+
+    public function saveXml();
+
+    public function getDomDocument();
+
+    public function getElement();
+
+    public function getDataContainer();
+
+    public function ignoreExceptions();
+    
+    public function getExceptions();
+    
+    /**
+     * Set the current feed type being exported to "rss" or "atom". This allows
+     * other objects to gracefully choose whether to execute or not, depending
+     * on their appropriateness for the current type, e.g. renderers.
+     *
+     * @param string $type
+     */
+    public function setType($type);
+    
+    /**
+     * Retrieve the current or last feed type exported.
+     *
+     * @return string Value will be "rss" or "atom"
+     */
+    public function getType();
+    
+    /**
+     * Sets the absolute root element for the XML feed being generated. This
+     * helps simplify the appending of namespace declarations, but also ensures
+     * namespaces are added to the root element - not scattered across the entire
+     * XML file - may assist namespace unsafe parsers and looks pretty ;).
+     *
+     * @param DOMElement $root
+     */
+    public function setRootElement(DOMElement $root);
+    
+    /**
+     * Retrieve the absolute root element for the XML feed being generated.
+     *
+     * @return DOMElement
+     */
+    public function getRootElement();
+
+}

+ 20 - 0
tests/Zend/Feed/AllTests.php

@@ -51,6 +51,16 @@ require_once 'Zend/Feed/Reader/Integration/WordpressAtom10Test.php';
 require_once 'Zend/Feed/Reader/Integration/LautDeRdfTest.php';
 require_once 'Zend/Feed/Reader/Integration/H-OnlineComAtom10Test.php';
 
+require_once 'Zend/Feed/Writer/FeedTest.php';
+require_once 'Zend/Feed/Writer/EntryTest.php';
+require_once 'Zend/Feed/Writer/Renderer/Feed/AtomTest.php';
+require_once 'Zend/Feed/Writer/Renderer/Feed/RssTest.php';
+require_once 'Zend/Feed/Writer/Renderer/Entry/AtomTest.php';
+require_once 'Zend/Feed/Writer/Renderer/Entry/RssTest.php';
+
+require_once 'Zend/Feed/Writer/Extension/ITunes/EntryTest.php';
+require_once 'Zend/Feed/Writer/Extension/ITunes/FeedTest.php';
+
 /**
  * @category   Zend
  * @package    Zend_Feed
@@ -105,6 +115,16 @@ class Zend_Feed_AllTests
         $suite->addTestSuite('Zend_Feed_Reader_Integration_WordpressAtom10Test');
         $suite->addTestSuite('Zend_Feed_Reader_Integration_LautDeRdfTest');
         $suite->addTestSuite('Zend_Feed_Reader_Integration_HOnlineComAtom10Test');
+        
+        $suite->addTestSuite('Zend_Feed_Writer_FeedTest');
+        $suite->addTestSuite('Zend_Feed_Writer_EntryTest');
+        $suite->addTestSuite('Zend_Feed_Writer_Renderer_Feed_AtomTest');
+        $suite->addTestSuite('Zend_Feed_Writer_Renderer_Feed_RssTest');
+        $suite->addTestSuite('Zend_Feed_Writer_Renderer_Entry_AtomTest');
+        $suite->addTestSuite('Zend_Feed_Writer_Renderer_Entry_RssTest');
+        
+        $suite->addTestSuite('Zend_Feed_Writer_Extension_ITunes_EntryTest');
+        $suite->addTestSuite('Zend_Feed_Writer_Extension_ITunes_FeedTest');
 
         return $suite;
     }

+ 571 - 0
tests/Zend/Feed/Writer/EntryTest.php

@@ -0,0 +1,571 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Entry.php';
+
+class Zend_Feed_Writer_EntryTest extends PHPUnit_Framework_TestCase
+{
+
+    protected $_feedSamplePath = null;
+
+    public function setup()
+    {
+        $this->_feedSamplePath = dirname(__FILE__) . '/_files';
+    }
+
+    public function testAddsAuthorName()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthor('Joe');
+        $this->assertEquals(array(array('name'=>'Joe')), $entry->getAuthors());
+    }
+
+    public function testAddsAuthorEmail()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthor('Joe', 'joe@example.com');
+        $this->assertEquals(array(array('name'=>'Joe', 'email' => 'joe@example.com')), $entry->getAuthors());
+    }
+
+    public function testAddsAuthorUri()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthor('Joe', null, 'http://www.example.com');
+        $this->assertEquals(array(array('name'=>'Joe', 'uri' => 'http://www.example.com')), $entry->getAuthors());
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidName()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidEmail()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor('Joe', '');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidUri()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor('Joe', null, 'notauri');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddsAuthorNameFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthor(array('name'=>'Joe'));
+        $this->assertEquals(array(array('name'=>'Joe')), $entry->getAuthors());
+    }
+
+    public function testAddsAuthorEmailFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthor(array('name'=>'Joe','email'=>'joe@example.com'));
+        $this->assertEquals(array(array('name'=>'Joe', 'email' => 'joe@example.com')), $entry->getAuthors());
+    }
+
+    public function testAddsAuthorUriFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthor(array('name'=>'Joe','uri'=>'http://www.example.com'));
+        $this->assertEquals(array(array('name'=>'Joe', 'uri' => 'http://www.example.com')), $entry->getAuthors());
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidNameFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor(array('name'=>''));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidEmailFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor(array('name'=>'Joe','email'=>''));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidUriFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor(array('name'=>'Joe','uri'=>'notauri'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionIfNameOmittedFromArray()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->addAuthor(array('uri'=>'notauri'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddsAuthorsFromArrayOfAuthors()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addAuthors(array(
+            array('name'=>'Joe','uri'=>'http://www.example.com'),
+            array('name'=>'Jane','uri'=>'http://www.example.com')
+        ));
+        $expected = array(
+            array('name'=>'Joe','uri'=>'http://www.example.com'),
+            array('name'=>'Jane','uri'=>'http://www.example.com')
+        );
+        $this->assertEquals($expected, $entry->getAuthors());
+    }
+    
+    public function testAddsEnclosure()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'uri' => 'http://example.com/audio.mp3',
+            'length' => '1337'
+        ));
+        $expected = array(
+            'type' => 'audio/mpeg',
+            'uri' => 'http://example.com/audio.mp3',
+            'length' => '1337'
+        );
+        $this->assertEquals($expected, $entry->getEnclosure());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddsEnclosureThrowsExceptionOnMissingType()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'uri' => 'http://example.com/audio.mp3',
+            'length' => '1337'
+        ));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddsEnclosureThrowsExceptionOnMissingUri()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'length' => '1337'
+        ));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddsEnclosureThrowsExceptionOnMissingLength()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'uri' => 'http://example.com/audio.mp3'
+        ));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddsEnclosureThrowsExceptionOnNonNumericLength()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'uri' => 'http://example.com/audio.mp3',
+            'length' => 'abc'
+        ));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddsEnclosureThrowsExceptionOnNegativeLength()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'uri' => 'http://example.com/audio.mp3',
+            'length' => -23
+        ));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddsEnclosureThrowsExceptionWhenUriIsInvalid()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'uri' => 'http://',
+            'length' => '1337'
+        ));
+    }
+
+    public function testSetsCopyright()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setCopyright('Copyright (c) 2009 Paddy Brady');
+        $this->assertEquals('Copyright (c) 2009 Paddy Brady', $entry->getCopyright());
+    }
+
+    public function testSetCopyrightThrowsExceptionOnInvalidParam()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCopyright('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetsContent()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setContent('I\'m content.');
+        $this->assertEquals("I'm content.", $entry->getContent());
+    }
+
+    public function testSetContentThrowsExceptionOnInvalidParam()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setContent('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetDateCreatedDefaultsToCurrentTime()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDateCreated();
+        $dateNow = new Zend_Date;
+        $this->assertTrue($dateNow->isLater($entry->getDateCreated()) || $dateNow->equals($entry->getDateCreated()));
+    }
+
+    public function testSetDateCreatedUsesGivenUnixTimestamp()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDateCreated(1234567890);
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($entry->getDateCreated()));
+    }
+
+    public function testSetDateCreatedUsesZendDateObject()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDateCreated(new Zend_Date('1234567890', Zend_Date::TIMESTAMP));
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($entry->getDateCreated()));
+    }
+
+    public function testSetDateModifiedDefaultsToCurrentTime()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDateModified();
+        $dateNow = new Zend_Date;
+        $this->assertTrue($dateNow->isLater($entry->getDateModified()) || $dateNow->equals($entry->getDateModified()));
+    }
+
+    public function testSetDateModifiedUsesGivenUnixTimestamp()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDateModified(1234567890);
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($entry->getDateModified()));
+    }
+
+    public function testSetDateModifiedUsesZendDateObject()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDateModified(new Zend_Date('1234567890', Zend_Date::TIMESTAMP));
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($entry->getDateModified()));
+    }
+
+    public function testSetDateCreatedThrowsExceptionOnInvalidParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setDateCreated('abc');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetDateModifiedThrowsExceptionOnInvalidParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setDateModified('abc');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetDateCreatedReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getDateCreated()));
+    }
+
+    public function testGetDateModifiedReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getDateModified()));
+    }
+
+    public function testGetCopyrightReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getCopyright()));
+    }
+
+    public function testGetContentReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getContent()));
+    }
+
+    public function testSetsDescription()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setDescription('abc');
+        $this->assertEquals('abc', $entry->getDescription());
+    }
+
+    public function testSetDescriptionThrowsExceptionOnInvalidParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setDescription('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetDescriptionReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getDescription()));
+    }
+
+    public function testSetsId()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setId('http://www.example.com/id');
+        $this->assertEquals('http://www.example.com/id', $entry->getId());
+    }
+
+    public function testSetIdThrowsExceptionOnInvalidParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setId('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetIdReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getId()));
+    }
+
+    public function testSetsLink()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setLink('http://www.example.com/id');
+        $this->assertEquals('http://www.example.com/id', $entry->getLink());
+    }
+
+    public function testSetLinkThrowsExceptionOnEmptyString()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setLink('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetLinkThrowsExceptionOnInvalidUri()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setLink('http://');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetLinkReturnsNullIfNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getLink()));
+    }
+
+    public function testSetsCommentLink()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setCommentLink('http://www.example.com/id/comments');
+        $this->assertEquals('http://www.example.com/id/comments', $entry->getCommentLink());
+    }
+
+    public function testSetCommentLinkThrowsExceptionOnEmptyString()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentLink('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetCommentLinkThrowsExceptionOnInvalidUri()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentLink('http://');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetCommentLinkReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getCommentLink()));
+    }
+
+    public function testSetsCommentFeedLink()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        
+        $entry->setCommentFeedLink(array('uri'=>'http://www.example.com/id/comments', 'type'=>'rdf'));
+        $this->assertEquals(array(array('uri'=>'http://www.example.com/id/comments', 'type'=>'rdf')), $entry->getCommentFeedLinks());
+    }
+
+    public function testSetCommentFeedLinkThrowsExceptionOnEmptyString()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentFeedLink(array('uri'=>'', 'type'=>'rdf'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetCommentFeedLinkThrowsExceptionOnInvalidUri()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentFeedLink(array('uri'=>'http://', 'type'=>'rdf'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+    
+    public function testSetCommentFeedLinkThrowsExceptionOnInvalidType()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentFeedLink(array('uri'=>'http://www.example.com/id/comments', 'type'=>'foo'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetCommentFeedLinkReturnsNullIfNoneSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getCommentFeedLinks()));
+    }
+
+    public function testSetsTitle()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setTitle('abc');
+        $this->assertEquals('abc', $entry->getTitle());
+    }
+
+    public function testSetTitleThrowsExceptionOnInvalidParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setTitle('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetTitleReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getTitle()));
+    }
+
+    public function testSetsCommentCount()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setCommentCount('10');
+        $this->assertEquals(10, $entry->getCommentCount());
+    }
+
+    public function testSetCommentCountThrowsExceptionOnInvalidEmptyParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentCount('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetCommentCountThrowsExceptionOnInvalidNonIntegerParameter()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        try {
+            $entry->setCommentCount('a');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetCommentCountReturnsNullIfDateNotSet()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $this->assertTrue(is_null($entry->getCommentCount()));
+    }
+
+}

+ 201 - 0
tests/Zend/Feed/Writer/Extension/ITunes/EntryTest.php

@@ -0,0 +1,201 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Entry.php';
+
+class Zend_Feed_Writer_Extension_ITunes_EntryTest extends PHPUnit_Framework_TestCase
+{
+
+    public function testSetBlock()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesBlock('yes');
+        $this->assertEquals('yes', $entry->getItunesBlock());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetBlockThrowsExceptionOnNonAlphaValue()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesBlock('123');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetBlockThrowsExceptionIfValueGreaterThan255CharsLength()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesBlock(str_repeat('a', 256));
+    }
+    
+    public function testAddAuthors()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addItunesAuthors(array('joe', 'jane'));
+        $this->assertEquals(array('joe', 'jane'), $entry->getItunesAuthors());
+    }
+    
+    public function testAddAuthor()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addItunesAuthor('joe');
+        $this->assertEquals(array('joe'), $entry->getItunesAuthors());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddAuthorThrowsExceptionIfValueGreaterThan255CharsLength()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->addItunesAuthor(str_repeat('a', 256));
+    }
+    
+    public function testSetDurationAsSeconds()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesDuration(23);
+        $this->assertEquals(23, $entry->getItunesDuration());
+    }
+    
+    public function testSetDurationAsMinutesAndSeconds()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesDuration('23:23');
+        $this->assertEquals('23:23', $entry->getItunesDuration());
+    }
+    
+    public function testSetDurationAsHoursMinutesAndSeconds()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesDuration('23:23:23');
+        $this->assertEquals('23:23:23', $entry->getItunesDuration());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetDurationThrowsExceptionOnUnknownFormat()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesDuration('abc');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetDurationThrowsExceptionOnInvalidSeconds()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesDuration('23:456');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetDurationThrowsExceptionOnInvalidMinutes()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesDuration('23:234:45');
+    }
+    
+    public function testSetExplicitToYes()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesExplicit('yes');
+        $this->assertEquals('yes', $entry->getItunesExplicit());
+    }
+    
+    public function testSetExplicitToNo()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesExplicit('no');
+        $this->assertEquals('no', $entry->getItunesExplicit());
+    }
+    
+    public function testSetExplicitToClean()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesExplicit('clean');
+        $this->assertEquals('clean', $entry->getItunesExplicit());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetExplicitThrowsExceptionOnUnknownTerm()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesExplicit('abc');
+    }
+    
+    public function testSetKeywords()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $words = array(
+            'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10', 'a11', 'a12'
+        );
+        $entry->setItunesKeywords($words);
+        $this->assertEquals($words, $entry->getItunesKeywords());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetKeywordsThrowsExceptionIfMaxKeywordsExceeded()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $words = array(
+            'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10', 'a11', 'a12', 'a13'
+        );
+        $entry->setItunesKeywords($words);
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetKeywordsThrowsExceptionIfFormattedKeywordsExceeds255CharLength()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $words = array(
+            str_repeat('a', 253), str_repeat('b', 2)
+        );
+        $entry->setItunesKeywords($words);
+    }
+    
+    public function testSetSubtitle()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesSubtitle('abc');
+        $this->assertEquals('abc', $entry->getItunesSubtitle());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetSubtitleThrowsExceptionWhenValueExceeds255Chars()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesSubtitle(str_repeat('a', 256));
+    }
+    
+    public function testSetSummary()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesSummary('abc');
+        $this->assertEquals('abc', $entry->getItunesSummary());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetSummaryThrowsExceptionWhenValueExceeds255Chars()
+    {
+        $entry = new Zend_Feed_Writer_Entry;
+        $entry->setItunesSummary(str_repeat('a', 4001));
+    }
+
+}

+ 288 - 0
tests/Zend/Feed/Writer/Extension/ITunes/FeedTest.php

@@ -0,0 +1,288 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Feed.php';
+
+class Zend_Feed_Writer_Extension_ITunes_FeedTest extends PHPUnit_Framework_TestCase
+{
+
+    public function testSetBlock()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesBlock('yes');
+        $this->assertEquals('yes', $feed->getItunesBlock());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetBlockThrowsExceptionOnNonAlphaValue()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesBlock('123');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetBlockThrowsExceptionIfValueGreaterThan255CharsLength()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesBlock(str_repeat('a', 256));
+    }
+    
+    public function testAddAuthors()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->addItunesAuthors(array('joe', 'jane'));
+        $this->assertEquals(array('joe', 'jane'), $feed->getItunesAuthors());
+    }
+    
+    public function testAddAuthor()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->addItunesAuthor('joe');
+        $this->assertEquals(array('joe'), $feed->getItunesAuthors());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testAddAuthorThrowsExceptionIfValueGreaterThan255CharsLength()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->addItunesAuthor(str_repeat('a', 256));
+    }
+    
+    public function testSetCategories()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $cats = array(
+            'cat1',
+            'cat2' => array('cat2-1', 'cat2-a&b')
+        );
+        $feed->setItunesCategories($cats);
+        $this->assertEquals($cats, $feed->getItunesCategories());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetCategoriesThrowsExceptionIfAnyCatNameGreaterThan255CharsLength()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $cats = array(
+            'cat1',
+            'cat2' => array('cat2-1', str_repeat('a', 256))
+        );
+        $feed->setItunesCategories($cats);
+        $this->assertEquals($cats, $feed->getItunesAuthors());
+    }
+    
+    public function testSetImageAsPngFile()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesImage('http://www.example.com/image.png');
+        $this->assertEquals('http://www.example.com/image.png', $feed->getItunesImage());
+    }
+    
+    public function testSetImageAsJpgFile()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesImage('http://www.example.com/image.jpg');
+        $this->assertEquals('http://www.example.com/image.jpg', $feed->getItunesImage());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetImageThrowsExceptionOnInvalidUri()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesImage('http://');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetImageThrowsExceptionOnInvalidImageExtension()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesImage('http://www.example.com/image.gif');
+    }
+    
+    public function testSetDurationAsSeconds()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesDuration(23);
+        $this->assertEquals(23, $feed->getItunesDuration());
+    }
+    
+    public function testSetDurationAsMinutesAndSeconds()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesDuration('23:23');
+        $this->assertEquals('23:23', $feed->getItunesDuration());
+    }
+    
+    public function testSetDurationAsHoursMinutesAndSeconds()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesDuration('23:23:23');
+        $this->assertEquals('23:23:23', $feed->getItunesDuration());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetDurationThrowsExceptionOnUnknownFormat()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesDuration('abc');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetDurationThrowsExceptionOnInvalidSeconds()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesDuration('23:456');
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetDurationThrowsExceptionOnInvalidMinutes()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesDuration('23:234:45');
+    }
+    
+    public function testSetExplicitToYes()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesExplicit('yes');
+        $this->assertEquals('yes', $feed->getItunesExplicit());
+    }
+    
+    public function testSetExplicitToNo()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesExplicit('no');
+        $this->assertEquals('no', $feed->getItunesExplicit());
+    }
+    
+    public function testSetExplicitToClean()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesExplicit('clean');
+        $this->assertEquals('clean', $feed->getItunesExplicit());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetExplicitThrowsExceptionOnUnknownTerm()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesExplicit('abc');
+    }
+    
+    public function testSetKeywords()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $words = array(
+            'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10', 'a11', 'a12'
+        );
+        $feed->setItunesKeywords($words);
+        $this->assertEquals($words, $feed->getItunesKeywords());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetKeywordsThrowsExceptionIfMaxKeywordsExceeded()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $words = array(
+            'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10', 'a11', 'a12', 'a13'
+        );
+        $feed->setItunesKeywords($words);
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetKeywordsThrowsExceptionIfFormattedKeywordsExceeds255CharLength()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $words = array(
+            str_repeat('a', 253), str_repeat('b', 2)
+        );
+        $feed->setItunesKeywords($words);
+    }
+    
+    public function testSetNewFeedUrl()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesNewFeedUrl('http://example.com/feed');
+        $this->assertEquals('http://example.com/feed', $feed->getItunesNewFeedUrl());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetNewFeedUrlThrowsExceptionOnInvalidUri()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesNewFeedUrl('http://');
+    }
+    
+    public function testAddOwner()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->addItunesOwner(array('name'=>'joe','email'=>'joe@example.com'));
+        $this->assertEquals(array(array('name'=>'joe','email'=>'joe@example.com')), $feed->getItunesOwners());
+    }
+    
+    public function testAddOwners()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->addItunesOwners(array(array('name'=>'joe','email'=>'joe@example.com')));
+        $this->assertEquals(array(array('name'=>'joe','email'=>'joe@example.com')), $feed->getItunesOwners());
+    }
+    
+    public function testSetSubtitle()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesSubtitle('abc');
+        $this->assertEquals('abc', $feed->getItunesSubtitle());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetSubtitleThrowsExceptionWhenValueExceeds255Chars()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesSubtitle(str_repeat('a', 256));
+    }
+    
+    public function testSetSummary()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesSummary('abc');
+        $this->assertEquals('abc', $feed->getItunesSummary());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testSetSummaryThrowsExceptionWhenValueExceeds4000Chars()
+    {
+        $feed = new Zend_Feed_Writer_Feed;
+        $feed->setItunesSummary(str_repeat('a',4001));
+    }
+
+}

+ 609 - 0
tests/Zend/Feed/Writer/FeedTest.php

@@ -0,0 +1,609 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Feed.php';
+
+class Zend_Feed_Writer_FeedTest extends PHPUnit_Framework_TestCase
+{
+
+    protected $_feedSamplePath = null;
+
+    public function setup()
+    {
+        $this->_feedSamplePath = dirname(__FILE__) . '/Writer/_files';
+    }
+
+    public function testAddsAuthorName()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthor('Joe');
+        $this->assertEquals(array('name'=>'Joe'), $writer->getAuthor());
+    }
+
+    public function testAddsAuthorEmail()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthor('Joe', 'joe@example.com');
+        $this->assertEquals(array('name'=>'Joe', 'email' => 'joe@example.com'), $writer->getAuthor());
+    }
+
+    public function testAddsAuthorUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthor('Joe', null, 'http://www.example.com');
+        $this->assertEquals(array('name'=>'Joe', 'uri' => 'http://www.example.com'), $writer->getAuthor());
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidName()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidEmail()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor('Joe', '');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor('Joe', null, 'notauri');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddsAuthorNameFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthor(array('name'=>'Joe'));
+        $this->assertEquals(array('name'=>'Joe'), $writer->getAuthor());
+    }
+
+    public function testAddsAuthorEmailFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthor(array('name'=>'Joe','email'=>'joe@example.com'));
+        $this->assertEquals(array('name'=>'Joe', 'email' => 'joe@example.com'), $writer->getAuthor());
+    }
+
+    public function testAddsAuthorUriFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthor(array('name'=>'Joe','uri'=>'http://www.example.com'));
+        $this->assertEquals(array('name'=>'Joe', 'uri' => 'http://www.example.com'), $writer->getAuthor());
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidNameFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor(array('name'=>''));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidEmailFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor(array('name'=>'Joe','email'=>''));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionOnInvalidUriFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor(array('name'=>'Joe','uri'=>'notauri'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddAuthorThrowsExceptionIfNameOmittedFromArray()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor(array('uri'=>'notauri'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddsAuthorsFromArrayOfAuthors()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addAuthors(array(
+            array('name'=>'Joe','uri'=>'http://www.example.com'),
+            array('name'=>'Jane','uri'=>'http://www.example.com')
+        ));
+        $this->assertEquals(array('name'=>'Jane', 'uri' => 'http://www.example.com'), $writer->getAuthor(1));
+    }
+
+    public function testSetsCopyright()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setCopyright('Copyright (c) 2009 Paddy Brady');
+        $this->assertEquals('Copyright (c) 2009 Paddy Brady', $writer->getCopyright());
+    }
+
+    public function testSetCopyrightThrowsExceptionOnInvalidParam()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setCopyright('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetDateCreatedDefaultsToCurrentTime()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDateCreated();
+        $dateNow = new Zend_Date;
+        $this->assertTrue($dateNow->isLater($writer->getDateCreated()) || $dateNow->equals($writer->getDateCreated()));
+    }
+
+    public function testSetDateCreatedUsesGivenUnixTimestamp()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDateCreated(1234567890);
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($writer->getDateCreated()));
+    }
+
+    public function testSetDateCreatedUsesZendDateObject()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDateCreated(new Zend_Date('1234567890', Zend_Date::TIMESTAMP));
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($writer->getDateCreated()));
+    }
+
+    public function testSetDateModifiedDefaultsToCurrentTime()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDateModified();
+        $dateNow = new Zend_Date;
+        $this->assertTrue($dateNow->isLater($writer->getDateModified()) || $dateNow->equals($writer->getDateModified()));
+    }
+
+    public function testSetDateModifiedUsesGivenUnixTimestamp()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDateModified(1234567890);
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($writer->getDateModified()));
+    }
+
+    public function testSetDateModifiedUsesZendDateObject()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDateModified(new Zend_Date('1234567890', Zend_Date::TIMESTAMP));
+        $myDate = new Zend_Date('1234567890', Zend_Date::TIMESTAMP);
+        $this->assertTrue($myDate->equals($writer->getDateModified()));
+    }
+
+    public function testSetDateCreatedThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setDateCreated('abc');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetDateModifiedThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setDateModified('abc');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetDateCreatedReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getDateCreated()));
+    }
+
+    public function testGetDateModifiedReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getDateModified()));
+    }
+
+    public function testGetCopyrightReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getCopyright()));
+    }
+
+    public function testSetsDescription()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setDescription('abc');
+        $this->assertEquals('abc', $writer->getDescription());
+    }
+
+    public function testSetDescriptionThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setDescription('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetDescriptionReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getDescription()));
+    }
+
+    public function testSetsId()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setId('http://www.example.com/id');
+        $this->assertEquals('http://www.example.com/id', $writer->getId());
+    }
+
+    public function testSetsIdAcceptsUrns()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setId('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6');
+        $this->assertEquals('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6', $writer->getId());
+    }
+
+    public function testSetIdThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setId('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetIdThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setId('http://');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetIdReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getId()));
+    }
+
+    public function testSetsLanguage()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setLanguage('abc');
+        $this->assertEquals('abc', $writer->getLanguage());
+    }
+
+    public function testSetLanguageThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setLanguage('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetLanguageReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getLanguage()));
+    }
+
+    public function testSetsLink()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setLink('http://www.example.com/id');
+        $this->assertEquals('http://www.example.com/id', $writer->getLink());
+    }
+
+    public function testSetLinkThrowsExceptionOnEmptyString()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setLink('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetLinkThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setLink('http://');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetLinkReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getLink()));
+    }
+
+    public function testSetsEncoding()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setEncoding('utf-16');
+        $this->assertEquals('utf-16', $writer->getEncoding());
+    }
+
+    public function testSetEncodingThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setEncoding('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetEncodingReturnsUtf8IfNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertEquals('UTF-8', $writer->getEncoding());
+    }
+
+    public function testSetsTitle()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setTitle('abc');
+        $this->assertEquals('abc', $writer->getTitle());
+    }
+
+    public function testSetTitleThrowsExceptionOnInvalidParameter()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setTitle('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetTitleReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getTitle()));
+    }
+
+    public function testSetsGeneratorName()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setGenerator('ZFW');
+        $this->assertEquals(array('name'=>'ZFW'), $writer->getGenerator());
+    }
+
+    public function testSetsGeneratorVersion()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setGenerator('ZFW', '1.0');
+        $this->assertEquals(array('name'=>'ZFW', 'version' => '1.0'), $writer->getGenerator());
+    }
+
+    public function testSetsGeneratorUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setGenerator('ZFW', null, 'http://www.example.com');
+        $this->assertEquals(array('name'=>'ZFW', 'uri' => 'http://www.example.com'), $writer->getGenerator());
+    }
+
+    public function testSetsGeneratorThrowsExceptionOnInvalidName()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setGenerator('');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetsGeneratorThrowsExceptionOnInvalidVersion()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addAuthor('ZFW', '');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetsGeneratorThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setGenerator('ZFW', null, 'notauri');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetGeneratorReturnsNullIfDateNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getGenerator()));
+    }
+
+    public function testSetsFeedLink()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setFeedLink('http://www.example.com/rss', 'RSS');
+        $this->assertEquals(array('rss'=>'http://www.example.com/rss'), $writer->getFeedLinks());
+    }
+
+    public function testSetsFeedLinkThrowsExceptionOnInvalidType()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setFeedLink('http://www.example.com/rss', 'abc');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testSetsFeedLinkThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setFeedLink('http://', 'rss');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetFeedLinksReturnsNullIfNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getFeedLinks()));
+    }
+    
+    public function testSetsBaseUrl()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->setBaseUrl('http://www.example.com');
+        $this->assertEquals('http://www.example.com', $writer->getBaseUrl());
+    }
+
+    public function testSetsBaseUrlThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->setBaseUrl('http://');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetBaseUrlReturnsNullIfNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getBaseUrl()));
+    }
+    
+    public function testAddsHubUrl()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addHub('http://www.example.com/hub');
+        $this->assertEquals(array('http://www.example.com/hub'), $writer->getHubs());
+    }
+    
+    public function testAddsManyHubUrls()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addHubs(array('http://www.example.com/hub', 'http://www.example.com/hub2'));
+        $this->assertEquals(array('http://www.example.com/hub', 'http://www.example.com/hub2'), $writer->getHubs());
+    }
+
+    public function testAddingHubUrlThrowsExceptionOnInvalidUri()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addHub('http://');
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testAddingHubUrlReturnsNullIfNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getHubs()));
+    }
+
+    public function testCreatesNewEntryDataContainer()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $entry = $writer->createEntry();
+        $this->assertTrue($entry instanceof Zend_Feed_Writer_Entry);
+    }
+    
+    public function testAddsCategory()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addCategory(array('term'=>'cat_dog'));
+        $this->assertEquals(array(array('term'=>'cat_dog')), $writer->getCategories());
+    }
+    
+    public function testAddsManyCategories()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $writer->addCategories(array(array('term'=>'cat_dog'),array('term'=>'cat_mouse')));
+        $this->assertEquals(array(array('term'=>'cat_dog'),array('term'=>'cat_mouse')), $writer->getCategories());
+    }
+
+    public function testAddingCategoryWithoutTermThrowsException()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addCategory(array('label' => 'Cats & Dogs', 'scheme' => 'http://www.example.com/schema1'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+    
+    public function testAddingCategoryWithInvalidUriAsSchemeThrowsException()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        try {
+            $writer->addCategory(array('term' => 'cat_dog', 'scheme' => 'http://'));
+            $this->fail();
+        } catch (Zend_Feed_Exception $e) {
+        }
+    }
+
+    public function testGetCategoriesReturnsNullIfNotSet()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $this->assertTrue(is_null($writer->getCategories()));
+    }
+
+    public function testAddsAndOrdersEntriesByDateIfRequested()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $entry = $writer->createEntry();
+        $entry->setDateCreated(1234567890);
+        $entry2 = $writer->createEntry();
+        $entry2->setDateCreated(1230000000);
+        $writer->addEntry($entry);
+        $writer->addEntry($entry2);
+        $writer->orderByDate();
+        $this->assertEquals(1230000000, $writer->getEntry(1)->getDateCreated()->get(Zend_Date::TIMESTAMP));
+    }
+
+}

+ 247 - 0
tests/Zend/Feed/Writer/Renderer/Entry/AtomTest.php

@@ -0,0 +1,247 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Renderer/Feed/Atom.php';
+
+require_once 'Zend/Feed/Reader.php';
+require_once 'Zend/Version.php';
+
+class Zend_Feed_Writer_Renderer_Entry_AtomTest extends PHPUnit_Framework_TestCase
+{
+
+    protected $_validWriter = null;
+    protected $_validEntry = null;
+
+    public function setUp()
+    {
+        $this->_validWriter = new Zend_Feed_Writer_Feed;
+        
+        $this->_validWriter->setType('atom');
+        
+        $this->_validWriter->setTitle('This is a test feed.');
+        $this->_validWriter->setDescription('This is a test description.');
+        $this->_validWriter->setDateModified(1234567890);
+        $this->_validWriter->setLink('http://www.example.com');
+        $this->_validWriter->setFeedLink('http://www.example.com/atom', 'atom');
+        $this->_validWriter->addAuthor('Joe', 'joe@example.com', 'http://www.example.com/joe');
+        $this->_validEntry = $this->_validWriter->createEntry();
+        $this->_validEntry->setTitle('This is a test entry.');
+        $this->_validEntry->setDescription('This is a test entry description.');
+        $this->_validEntry->setDateModified(1234567890);
+        $this->_validEntry->setDateCreated(1234567000);
+        $this->_validEntry->setLink('http://www.example.com/1');
+        $this->_validEntry->addAuthor('Jane', 'jane@example.com', 'http://www.example.com/jane');
+        $this->_validEntry->setContent('This is test entry content.');
+        $this->_validWriter->addEntry($this->_validEntry);
+    }
+
+    public function tearDown()
+    {
+        $this->_validWriter = null;
+        $this->_validEntry = null;
+    }
+
+    public function testRenderMethodRunsMinimalWriterContainerProperlyBeforeICheckAtomCompliance()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        try {
+            $renderer->render();
+        } catch (Zend_Feed_Exception $e) {
+            $this->fail('Valid Writer object caused an exception when building which should never happen');
+        }
+    }
+
+    public function testEntryEncodingHasBeenSet()
+    {
+        $this->_validWriter->setEncoding('iso-8859-1');
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('iso-8859-1', $entry->getEncoding());
+    }
+
+    public function testEntryEncodingDefaultIsUsedIfEncodingNotSetByHand()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('UTF-8', $entry->getEncoding());
+    }
+
+    public function testEntryTitleHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('This is a test entry.', $entry->getTitle());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedTitleIfMissingThrowsException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->remove('title');
+        $atomFeed->render();
+    }
+
+    public function testEntrySummaryDescriptionHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('This is a test entry description.', $entry->getDescription());
+    }
+
+    public function testEntryContentHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('This is test entry content.', $entry->getContent());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedContentIfMissingThrowsExceptionIfThereIsNoLink()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->remove('content');
+        $this->_validEntry->remove('link');
+        $atomFeed->render();
+    }
+
+    public function testEntryUpdatedDateHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals(1234567890, $entry->getDateModified()->get(Zend_Date::TIMESTAMP));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedUpdatedDateIfMissingThrowsException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->remove('dateModified');
+        $atomFeed->render();
+    }
+
+    public function testEntryPublishedDateHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals(1234567000, $entry->getDateCreated()->get(Zend_Date::TIMESTAMP));
+    }
+
+    public function testEntryIncludesLinkToHtmlVersionOfFeed()
+    {
+        $renderer= new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('http://www.example.com/1', $entry->getLink());
+    }
+
+    public function testEntryHoldsAnyAuthorAdded()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $author = $entry->getAuthor();
+        $this->assertEquals('jane@example.com (Jane)', $entry->getAuthor());
+    }
+    
+    public function testEntryHoldsAnyEnclosureAdded()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'length' => '1337',
+            'uri' => 'http://example.com/audio.mp3'
+        ));
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $enc = $entry->getEnclosure();
+        $this->assertEquals('audio/mpeg', $enc->type);
+        $this->assertEquals('1337', $enc->length);
+        $this->assertEquals('http://example.com/audio.mp3', $enc->url);
+    }
+
+    public function testEntryIdHasBeenSet()
+    {
+        $this->_validEntry->setId('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6');
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6', $entry->getId());
+    }
+
+    public function testFeedIdDefaultIsUsedIfNotSetByHand()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals($entry->getLink(), $entry->getId());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedIdIfMissingThrowsException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->remove('id');
+        $this->_validEntry->remove('link');
+        $atomFeed->render();
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedIdThrowsExceptionIfNotUri()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->remove('id');
+        $this->_validEntry->remove('link');
+        $this->_validEntry->setId('not-a-uri');
+        $atomFeed->render();
+    }
+    
+    public function testCommentLinkRendered()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->setCommentLink('http://www.example.com/id/1');
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('http://www.example.com/id/1', $entry->getCommentLink());
+    }
+    
+    public function testCommentCountRendered()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->setCommentCount(22);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals(22, $entry->getCommentCount());
+    }
+    
+    public function testCommentFeedLinksRendered()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validEntry->setCommentFeedLinks(array(
+            array('uri'=>'http://www.example.com/atom/id/1','type'=>'atom'),
+            array('uri'=>'http://www.example.com/rss/id/1','type'=>'rss'),
+        ));
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        // Skipped over due to ZFR bug (detects Atom in error when RSS requested)
+        //$this->assertEquals('http://www.example.com/rss/id/1', $entry->getCommentFeedLink('rss'));
+        $this->assertEquals('http://www.example.com/atom/id/1', $entry->getCommentFeedLink('atom'));
+    }
+    
+}

+ 217 - 0
tests/Zend/Feed/Writer/Renderer/Entry/RssTest.php

@@ -0,0 +1,217 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Renderer/Feed/Rss.php';
+
+require_once 'Zend/Feed/Reader.php';
+require_once 'Zend/Version.php';
+
+class Zend_Feed_Writer_Renderer_Entry_RssTest extends PHPUnit_Framework_TestCase
+{
+
+    protected $_validWriter = null;
+    protected $_validEntry = null;
+
+    public function setUp()
+    {
+        $this->_validWriter = new Zend_Feed_Writer_Feed;
+        
+        $this->_validWriter->setType('rss');
+        
+        $this->_validWriter->setTitle('This is a test feed.');
+        $this->_validWriter->setDescription('This is a test description.');
+        $this->_validWriter->setLink('http://www.example.com');
+        $this->_validEntry = $this->_validWriter->createEntry();
+        $this->_validEntry->setTitle('This is a test entry.');
+        $this->_validEntry->setDescription('This is a test entry description.');
+        $this->_validEntry->setLink('http://www.example.com/1');;
+        $this->_validWriter->addEntry($this->_validEntry);
+    }
+
+    public function tearDown()
+    {
+        $this->_validWriter = null;
+        $this->_validEntry = null;
+    }
+
+    public function testRenderMethodRunsMinimalWriterContainerProperlyBeforeICheckAtomCompliance()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        try {
+            $renderer->render();
+        } catch (Zend_Feed_Exception $e) {
+            $this->fail('Valid Writer object caused an exception when building which should never happen');
+        }
+    }
+
+    public function testEntryEncodingHasBeenSet()
+    {
+        $this->_validWriter->setEncoding('iso-8859-1');
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('iso-8859-1', $entry->getEncoding());
+    }
+
+    public function testEntryEncodingDefaultIsUsedIfEncodingNotSetByHand()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('UTF-8', $entry->getEncoding());
+    }
+
+    public function testEntryTitleHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('This is a test entry.', $entry->getTitle());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedTitleIfMissingThrowsExceptionIfDescriptionAlsoMissing()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validEntry->remove('title');
+        $this->_validEntry->remove('description');
+        $atomFeed->render();
+    }
+
+    public function testEntrySummaryDescriptionHasBeenSet()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('This is a test entry description.', $entry->getDescription());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedDescriptionIfMissingThrowsExceptionIfAlsoNoTitle()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validEntry->remove('description');
+        $this->_validEntry->remove('title');
+        $atomFeed->render();
+    }
+    
+    public function testEntryContentHasBeenSet()
+    {
+        $this->_validEntry->setContent('This is test entry content.');
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('This is test entry content.', $entry->getContent());
+    }
+
+    public function testEntryUpdatedDateHasBeenSet()
+    {
+        $this->_validEntry->setDateModified(1234567890);
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals(1234567890, $entry->getDateModified()->get(Zend_Date::TIMESTAMP));
+    }
+
+    public function testEntryPublishedDateHasBeenSet()
+    {
+        $this->_validEntry->setDateCreated(1234567000);
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals(1234567000, $entry->getDateCreated()->get(Zend_Date::TIMESTAMP));
+    }
+
+    public function testEntryIncludesLinkToHtmlVersionOfFeed()
+    {
+        $renderer= new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('http://www.example.com/1', $entry->getLink());
+    }
+
+    public function testEntryHoldsAnyAuthorAdded()
+    {
+        $this->_validEntry->addAuthor('Jane', 'jane@example.com', 'http://www.example.com/jane');
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $author = $entry->getAuthor();
+        $this->assertEquals('Jane', $entry->getAuthor());
+    }
+    
+    public function testEntryHoldsAnyEnclosureAdded()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validEntry->setEnclosure(array(
+            'type' => 'audio/mpeg',
+            'length' => '1337',
+            'uri' => 'http://example.com/audio.mp3'
+        ));
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $enc = $entry->getEnclosure();
+        $this->assertEquals('audio/mpeg', $enc->type);
+        $this->assertEquals('1337', $enc->length);
+        $this->assertEquals('http://example.com/audio.mp3', $enc->url);
+    }
+
+    public function testEntryIdHasBeenSet()
+    {
+        $this->_validEntry->setId('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6');
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6', $entry->getId());
+    }
+    
+    public function testEntryIdHasBeenSetWithPermaLinkAsFalseWhenNotUri()
+    {
+        $this->markTestIncomplete('Untest due to ZFR potential bug');
+    }
+
+    public function testFeedIdDefaultIsUsedIfNotSetByHand()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals($entry->getLink(), $entry->getId());
+    }
+    
+    public function testCommentLinkRendered()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validEntry->setCommentLink('http://www.example.com/id/1');
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals('http://www.example.com/id/1', $entry->getCommentLink());
+    }
+    
+    public function testCommentCountRendered()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validEntry->setCommentCount(22);
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        $this->assertEquals(22, $entry->getCommentCount());
+    }
+    
+    public function testCommentFeedLinksRendered()
+    {
+        $renderer = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validEntry->setCommentFeedLinks(array(
+            array('uri'=>'http://www.example.com/atom/id/1','type'=>'atom'),
+            array('uri'=>'http://www.example.com/rss/id/1','type'=>'rss'),
+        ));
+        $feed = Zend_Feed_Reader::importString($renderer->render()->saveXml());
+        $entry = $feed->current();
+        // Skipped assertion is because RSS has no facility to show Atom feeds without an extension
+        $this->assertEquals('http://www.example.com/rss/id/1', $entry->getCommentFeedLink('rss'));
+        //$this->assertEquals('http://www.example.com/atom/id/1', $entry->getCommentFeedLink('atom'));
+    }
+
+}

+ 296 - 0
tests/Zend/Feed/Writer/Renderer/Feed/AtomTest.php

@@ -0,0 +1,296 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Renderer/Feed/Atom.php';
+
+require_once 'Zend/Feed/Reader.php';
+require_once 'Zend/Version.php';
+
+class Zend_Feed_Writer_Renderer_Feed_AtomTest extends PHPUnit_Framework_TestCase
+{
+
+    protected $_validWriter = null;
+
+    public function setUp()
+    {
+        $this->_validWriter = new Zend_Feed_Writer_Feed;
+        $this->_validWriter->setTitle('This is a test feed.');
+        $this->_validWriter->setDescription('This is a test description.');
+        $this->_validWriter->setDateModified(1234567890);
+        $this->_validWriter->setLink('http://www.example.com');
+        $this->_validWriter->setFeedLink('http://www.example.com/atom', 'atom');
+        $this->_validWriter->addAuthor('Joe', 'joe@example.com', 'http://www.example.com/joe');
+        
+        $this->_validWriter->setType('atom');
+    }
+
+    public function tearDown()
+    {
+        $this->_validWriter = null;
+    }
+
+    public function testSetsWriterInConstructor()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $feed = new Zend_Feed_Writer_Renderer_Feed_Atom($writer);
+        $this->assertTrue($feed->getDataContainer() instanceof Zend_Feed_Writer_Feed);
+    }
+
+    public function testBuildMethodRunsMinimalWriterContainerProperlyBeforeICheckAtomCompliance()
+    {
+        $feed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        try {
+            $feed->render();
+        } catch (Zend_Feed_Exception $e) {
+            $this->fail('Valid Writer object caused an exception when building which should never happen');
+        }
+    }
+
+    public function testFeedEncodingHasBeenSet()
+    {
+        $this->_validWriter->setEncoding('iso-8859-1');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('iso-8859-1', $feed->getEncoding());
+    }
+
+    public function testFeedEncodingDefaultIsUsedIfEncodingNotSetByHand()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('UTF-8', $feed->getEncoding());
+    }
+
+    public function testFeedTitleHasBeenSet()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('This is a test feed.', $feed->getTitle());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedTitleIfMissingThrowsException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('title');
+        $atomFeed->render();
+    }
+
+    public function testFeedSubtitleHasBeenSet()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('This is a test description.', $feed->getDescription());
+    }
+    
+    public function testFeedSubtitleThrowsNoExceptionIfMissing()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('description');
+        $atomFeed->render();
+    }
+
+    public function testFeedUpdatedDateHasBeenSet()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals(1234567890, $feed->getDateModified()->get(Zend_Date::TIMESTAMP));
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedUpdatedDateIfMissingThrowsException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('dateModified');
+        $atomFeed->render();
+    }
+
+    public function testFeedGeneratorHasBeenSet()
+    {
+        $this->_validWriter->setGenerator('FooFeedBuilder', '1.00', 'http://www.example.com');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('FooFeedBuilder', $feed->getGenerator());
+    }
+    
+    public function testFeedGeneratorIfMissingThrowsNoException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('generator');
+        $atomFeed->render();
+    }
+
+    public function testFeedGeneratorDefaultIsUsedIfGeneratorNotSetByHand()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('Zend_Feed_Writer', $feed->getGenerator());
+    }
+
+    public function testFeedLanguageHasBeenSet()
+    {
+        $this->_validWriter->setLanguage('fr');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('fr', $feed->getLanguage());
+    }
+    
+    public function testFeedLanguageIfMissingThrowsNoException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('language');
+        $atomFeed->render();
+    }
+
+    public function testFeedLanguageDefaultIsUsedIfGeneratorNotSetByHand()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals(null, $feed->getLanguage());
+    }
+
+    public function testFeedIncludesLinkToHtmlVersionOfFeed()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('http://www.example.com', $feed->getLink());
+    }
+    
+    public function testFeedLinkToHtmlVersionOfFeedIfMissingThrowsNoExceptionIfIdSet()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->setId('http://www.example.com');
+        $this->_validWriter->remove('link');
+        $atomFeed->render();
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedLinkToHtmlVersionOfFeedIfMissingThrowsExceptionIfIdMissing()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('link');
+        $atomFeed->render();
+    }
+
+    public function testFeedIncludesLinkToXmlAtomWhereTheFeedWillBeAvailable()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('http://www.example.com/atom', $feed->getFeedLink());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedLinkToXmlAtomWhereTheFeedWillBeAvailableIfMissingThrowsException()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $this->_validWriter->remove('feedLinks');
+        $atomFeed->render();
+    }
+
+    public function testFeedHoldsAnyAuthorAdded()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $author = $feed->getAuthor();
+        $this->assertEquals('joe@example.com (Joe)', $feed->getAuthor());
+    }
+    
+    public function testFeedAuthorIfNotSetThrowsExceptionIfAnyEntriesAlsoAreMissingAuthors()
+    {
+        $this->markTestIncomplete('Not yet implemented...');
+    }
+    
+    public function testFeedAuthorIfNotSetThrowsNoExceptionIfAllEntriesIncludeAtLeastOneAuthor()
+    {
+        $this->markTestIncomplete('Not yet implemented...');
+    }
+    
+    public function testFeedIdHasBeenSet()
+    {
+        $this->_validWriter->setId('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6', $feed->getId());
+    }
+
+    public function testFeedIdDefaultOfHtmlLinkIsUsedIfNotSetByHand()
+    {
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals($feed->getLink(), $feed->getId());
+    }
+    
+    public function testBaseUrlCanBeSet()
+    {
+        $this->_validWriter->setBaseUrl('http://www.example.com/base');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('http://www.example.com/base', $feed->getBaseUrl());
+    }
+    
+    public function testCopyrightCanBeSet()
+    {
+        $this->_validWriter->setCopyright('Copyright © 2009 Paddy');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $this->assertEquals('Copyright © 2009 Paddy', $feed->getCopyright());
+    }
+    
+    public function testCategoriesCanBeSet()
+    {
+        $this->_validWriter->addCategories(array(
+            array('term'=>'cat_dog', 'label' => 'Cats & Dogs', 'scheme' => 'http://example.com/schema1'),
+            array('term'=>'cat_dog2')
+        ));
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $expected = array(
+            array('term'=>'cat_dog', 'label' => 'Cats & Dogs', 'scheme' => 'http://example.com/schema1'),
+            array('term'=>'cat_dog2', 'label' => 'cat_dog2', 'scheme' => null)
+        );
+        $this->assertEquals($expected, (array) $feed->getCategories());
+    }
+    
+    /**
+     * @group ZFW030
+     */
+    public function testHubsCanBeSet()
+    {
+        $this->_validWriter->addHubs(
+            array('http://www.example.com/hub', 'http://www.example.com/hub2')
+        );
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Atom($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $expected = array(
+            'http://www.example.com/hub', 'http://www.example.com/hub2'
+        );
+        $this->assertEquals($expected, (array) $feed->getHubs());
+    }
+
+}

+ 258 - 0
tests/Zend/Feed/Writer/Renderer/Feed/RssTest.php

@@ -0,0 +1,258 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+require_once 'Zend/Feed/Writer/Renderer/Feed/Rss.php';
+
+require_once 'Zend/Feed/Reader.php';
+require_once 'Zend/Version.php';
+
+class Zend_Feed_Writer_Renderer_Feed_RssTest extends PHPUnit_Framework_TestCase
+{
+
+    protected $_validWriter = null;
+
+    public function setUp()
+    {
+        $this->_validWriter = new Zend_Feed_Writer_Feed;
+        $this->_validWriter->setTitle('This is a test feed.');
+        $this->_validWriter->setDescription('This is a test description.');
+        $this->_validWriter->setLink('http://www.example.com');
+        
+        $this->_validWriter->setType('rss');
+    }
+
+    public function tearDown()
+    {
+        $this->_validWriter = null;
+    }
+
+    public function testSetsWriterInConstructor()
+    {
+        $writer = new Zend_Feed_Writer_Feed;
+        $feed = new Zend_Feed_Writer_Renderer_Feed_Rss($writer);
+        $this->assertTrue($feed->getDataContainer() instanceof Zend_Feed_Writer_Feed);
+    }
+
+    public function testBuildMethodRunsMinimalWriterContainerProperlyBeforeICheckRssCompliance()
+    {
+        $feed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        try {
+            $feed->render();
+        } catch (Zend_Feed_Exception $e) {
+            $this->fail('Valid Writer object caused an exception when building which should never happen');
+        }
+    }
+
+    public function testFeedEncodingHasBeenSet()
+    {
+        $this->_validWriter->setEncoding('iso-8859-1');
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('iso-8859-1', $feed->getEncoding());
+    }
+
+    public function testFeedEncodingDefaultIsUsedIfEncodingNotSetByHand()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('UTF-8', $feed->getEncoding());
+    }
+
+    public function testFeedTitleHasBeenSet()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('This is a test feed.', $feed->getTitle());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedTitleIfMissingThrowsException()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('title');
+        $rssFeed->render();
+    }
+
+    public function testFeedDescriptionHasBeenSet()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('This is a test description.', $feed->getDescription());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedDescriptionThrowsExceptionIfMissing()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('description');
+        $rssFeed->render();
+    }
+
+    public function testFeedUpdatedDateHasBeenSet()
+    {
+        $this->_validWriter->setDateModified(1234567890);
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals(1234567890, $feed->getDateModified()->get(Zend_Date::TIMESTAMP));
+    }
+    
+    public function testFeedUpdatedDateIfMissingThrowsNoException()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('dateModified');
+        $rssFeed->render();
+    }
+
+    public function testFeedGeneratorHasBeenSet()
+    {
+        $this->_validWriter->setGenerator('FooFeedBuilder', '1.00', 'http://www.example.com');
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('FooFeedBuilder 1.00 (http://www.example.com)', $feed->getGenerator());
+    }
+    
+    public function testFeedGeneratorIfMissingThrowsNoException()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('generator');
+        $rssFeed->render();
+    }
+
+    public function testFeedGeneratorDefaultIsUsedIfGeneratorNotSetByHand()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('Zend_Feed_Writer ' . Zend_Version::VERSION . ' (http://framework.zend.com)', $feed->getGenerator());
+    }
+
+    public function testFeedLanguageHasBeenSet()
+    {
+        $this->_validWriter->setLanguage('fr');
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('fr', $feed->getLanguage());
+    }
+    
+    public function testFeedLanguageIfMissingThrowsNoException()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('language');
+        $rssFeed->render();
+    }
+
+    public function testFeedLanguageDefaultIsUsedIfGeneratorNotSetByHand()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals(null, $feed->getLanguage());
+    }
+
+    public function testFeedIncludesLinkToHtmlVersionOfFeed()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('http://www.example.com', $feed->getLink());
+    }
+    
+    /**
+     * @expectedException Zend_Feed_Exception
+     */
+    public function testFeedLinkToHtmlVersionOfFeedIfMissingThrowsException()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('link');
+        $rssFeed->render();
+    }
+
+    public function testFeedIncludesLinkToXmlRssWhereTheFeedWillBeAvailable()
+    {
+        $this->_validWriter->setFeedLink('http://www.example.com/rss', 'rss');
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('http://www.example.com/rss', $feed->getFeedLink());
+    }
+    
+    public function testFeedLinkToXmlRssWhereTheFeedWillBeAvailableIfMissingThrowsNoException()
+    {
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $this->_validWriter->remove('feedLinks');
+        $rssFeed->render();
+    }
+    
+    public function testBaseUrlCanBeSet()
+    {
+        $this->_validWriter->setBaseUrl('http://www.example.com/base');
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('http://www.example.com/base', $feed->getBaseUrl());
+    }
+    
+    /**
+     * @group ZFW003
+     */
+    public function testFeedHoldsAnyAuthorAdded()
+    {
+        $this->_validWriter->addAuthor('Joe', 'joe@example.com', 'http://www.example.com/joe');
+        $atomFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $atomFeed->render();
+        $feed = Zend_Feed_Reader::importString($atomFeed->saveXml());
+        $author = $feed->getAuthor();
+        $this->assertEquals('Joe', $feed->getAuthor());
+    }
+    
+    public function testCopyrightCanBeSet()
+    {
+        $this->_validWriter->setCopyright('Copyright © 2009 Paddy');
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $this->assertEquals('Copyright © 2009 Paddy', $feed->getCopyright());
+    }
+    
+    public function testCategoriesCanBeSet()
+    {
+        $this->_validWriter->addCategories(array(
+            array('term'=>'cat_dog', 'label' => 'Cats & Dogs', 'scheme' => 'http://example.com/schema1'),
+            array('term'=>'cat_dog2')
+        ));
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $expected = array(
+            array('term'=>'cat_dog', 'label' => 'cat_dog', 'scheme' => 'http://example.com/schema1'),
+            array('term'=>'cat_dog2', 'label' => 'cat_dog2', 'scheme' => null)
+        );
+        $this->assertEquals($expected, (array) $feed->getCategories());
+    }
+    
+    public function testHubsCanBeSet()
+    {
+        $this->_validWriter->addHubs(
+            array('http://www.example.com/hub', 'http://www.example.com/hub2')
+        );
+        $rssFeed = new Zend_Feed_Writer_Renderer_Feed_Rss($this->_validWriter);
+        $rssFeed->render();
+        $feed = Zend_Feed_Reader::importString($rssFeed->saveXml());
+        $expected = array(
+            'http://www.example.com/hub', 'http://www.example.com/hub2'
+        );
+        $this->assertEquals($expected, (array) $feed->getHubs());
+    }
+
+}