euromark 12 years ago
parent
commit
4fec391afd
4 changed files with 700 additions and 0 deletions
  1. 3 0
      Test/Case/AllToolsTest.php
  2. 18 0
      Test/Case/AllViewTestsTest.php
  3. 320 0
      Test/Case/View/RssViewTest.php
  4. 359 0
      View/RssView.php

+ 3 - 0
Test/Case/AllToolsTest.php

@@ -22,6 +22,9 @@ class AllToolsTest extends PHPUnit_Framework_TestSuite {
 		$Suite->addTestDirectory($path . DS . 'Lib' . DS . 'Misc');
 		$Suite->addTestDirectory($path . DS . 'Lib' . DS . 'Misc');
 
 
 		$path = dirname(__FILE__);
 		$path = dirname(__FILE__);
+		$Suite->addTestDirectory($path . DS . 'View');
+
+		$path = dirname(__FILE__);
 		$Suite->addTestDirectory($path . DS . 'View' . DS . 'Helper');
 		$Suite->addTestDirectory($path . DS . 'View' . DS . 'Helper');
 
 
 		$path = dirname(__FILE__);
 		$path = dirname(__FILE__);

+ 18 - 0
Test/Case/AllViewTestsTest.php

@@ -0,0 +1,18 @@
+<?php
+/**
+ * Group test - Tools
+ */
+class AllViewTestsTest extends PHPUnit_Framework_TestSuite {
+
+	/**
+	 * Suite method, defines tests for this suite.
+	 *
+	 * @return void
+	 */
+	public static function suite() {
+		$Suite = new CakeTestSuite('All View tests');
+		$path = dirname(__FILE__);
+		$Suite->addTestDirectory($path . DS . 'View');
+		return $Suite;
+	}
+}

+ 320 - 0
Test/Case/View/RssViewTest.php

@@ -0,0 +1,320 @@
+<?php
+/**
+ * PHP 5
+ *
+ * CakePHP(tm) Tests <http://book.cakephp.org/2.0/en/development/testing.html>
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://book.cakephp.org/2.0/en/development/testing.html CakePHP(tm) Tests
+ * @package       Cake.Test.Case.View
+ * @since         CakePHP(tm) v 2.5.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+App::uses('Controller', 'Controller');
+App::uses('CakeRequest', 'Network');
+App::uses('CakeResponse', 'Network');
+App::uses('RssView', 'Tools.View');
+
+/**
+ * RssViewTest
+ *
+ */
+class RssViewTest extends CakeTestCase {
+
+	public $Rss;
+
+	public function setUp() {
+		parent::setUp();
+		//Configure::write('debug', 0);
+
+		$this->Rss = new RssView();
+
+		$this->baseUrl = HTTP_BASE;
+	}
+
+	/**
+	 * testTime method
+	 *
+	 * @return void
+	 */
+	public function testTime() {
+		$now = time();
+		$time = $this->Rss->time($now);
+		$this->assertEquals(date('r', $now), $time);
+	}
+
+	/**
+	 * RssViewTest::testSerialize()
+	 *
+	 * @return void
+	 */
+	public function testSerialize() {
+		$Request = new CakeRequest();
+		$Response = new CakeResponse();
+		$Controller = new Controller($Request, $Response);
+		$data = array(
+			'channel' => array(
+				'title' => 'Channel title',
+				'link' => 'http://channel.example.org',
+				'description' => 'Channel description'
+			),
+			'items' => array(
+				array('title' => 'Title One', 'link' => 'http://example.org/one', 'author' => 'one@example.org', 'description' => 'Content one'),
+				array('title' => 'Title Two', 'link' => 'http://example.org/two', 'author' => 'two@example.org', 'description' => 'Content two'),
+			));
+		$Controller->set(array('channel' => $data, '_serialize' => 'channel'));
+		$View = new RssView($Controller);
+		$result = $View->render(false);
+
+		$expected = <<<RSS
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+  <channel>
+    <title>Channel title</title>
+    <link>http://channel.example.org</link>
+    <description>Channel description</description>
+    <item>
+      <title>Title One</title>
+      <link>http://example.org/one</link>
+      <author>one@example.org</author>
+      <description>Content one</description>
+    </item>
+    <item>
+      <title>Title Two</title>
+      <link>http://example.org/two</link>
+      <author>two@example.org</author>
+      <description>Content two</description>
+    </item>
+  </channel>
+</rss>
+
+RSS;
+		//debug($output); ob_flush();
+		$this->assertSame('application/rss+xml', $Response->type());
+		$this->assertTextEquals($expected, $result);
+	}
+
+	/**
+	 * RssViewTest::testSerialize()
+	 *
+	 * @return void
+	 */
+	public function testSerializeWithPrefixes() {
+		$Request = new CakeRequest();
+		$Response = new CakeResponse();
+		$Controller = new Controller($Request, $Response);
+
+		$time = time();
+		$data = array(
+			'channel' => array(
+				'title' => 'Channel title',
+				'link' => 'http://channel.example.org',
+				'description' => 'Channel description',
+				'sy:updatePeriod' => 'hourly',
+				'sy:updateFrequency' => 1
+			),
+			'items' => array(
+				array('title' => 'Title One', 'link' => 'http://example.org/one', 'dc:creator' => 'Author One', 'pubDate' => $time),
+				array('title' => 'Title Two', 'link' => 'http://example.org/two', 'dc:creator' => 'Author Two', 'pubDate' => $time),
+			)
+		);
+		$Controller->set(array('channel' => $data, '_serialize' => 'channel'));
+		$View = new RssView($Controller);
+		$result = $View->render(false);
+
+		$time = date('r', $time);
+		$expected = <<<RSS
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+  <channel>
+    <title>Channel title</title>
+    <link>http://channel.example.org</link>
+    <description>Channel description</description>
+    <sy:updatePeriod>hourly</sy:updatePeriod>
+    <sy:updateFrequency>1</sy:updateFrequency>
+    <item>
+      <title>Title One</title>
+      <link>http://example.org/one</link>
+      <dc:creator>Author One</dc:creator>
+      <pubDate>$time</pubDate>
+    </item>
+    <item>
+      <title>Title Two</title>
+      <link>http://example.org/two</link>
+      <dc:creator>Author Two</dc:creator>
+      <pubDate>$time</pubDate>
+    </item>
+  </channel>
+</rss>
+
+RSS;
+		//debug($result); ob_flush();
+		$this->assertSame('application/rss+xml', $Response->type());
+		$this->assertTextEquals($expected, $result);
+	}
+
+	/**
+	 * RssViewTest::testSerializeWithArrayLinks()
+	 *
+	 * `'atom:link' => array('@href' => array(...)` becomes
+	 * '@rel' => 'self', '@type' => 'application/rss+xml' automatically set for atom:link
+	 *
+	 * @return void
+	 */
+	public function testSerializeWithArrayLinks() {
+		$Request = new CakeRequest();
+		$Response = new CakeResponse();
+		$Controller = new Controller($Request, $Response);
+		$data = array(
+			'channel' => array(
+				'title' => 'Channel title',
+				'link' => 'http://channel.example.org',
+				'atom:link' => array('@href' => array('controller' => 'foo', 'action' => 'bar')),
+				'description' => 'Channel description',
+			),
+			'items' => array(
+				array('title' => 'Title One', 'link' => array('controller' => 'foo', 'action' => 'bar'), 'description' => 'Content one'),
+				array('title' => 'Title Two', 'link' => array('controller' => 'foo', 'action' => 'bar'), 'description' => 'Content two'),
+			)
+		);
+		$Controller->set(array('channel' => $data, '_serialize' => 'channel'));
+		$View = new RssView($Controller);
+		$result = $View->render(false);
+
+$expected = <<<RSS
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+  <channel>
+    <title>Channel title</title>
+    <link>http://channel.example.org</link>
+    <atom:link href="$this->baseUrl/foo/bar" rel="self" type="application/rss+xml"/>
+    <description>Channel description</description>
+    <item>
+      <title>Title One</title>
+      <link>$this->baseUrl/foo/bar</link>
+      <description>Content one</description>
+    </item>
+    <item>
+      <title>Title Two</title>
+      <link>$this->baseUrl/foo/bar</link>
+      <description>Content two</description>
+    </item>
+  </channel>
+</rss>
+
+RSS;
+		//debug($result); ob_flush();
+		$this->assertSame('application/rss+xml', $Response->type());
+		$this->assertTextEquals($expected, $result);
+	}
+
+	/**
+	 * RssViewTest::testSerializeWithContent()
+	 *
+	 * @return void
+	 */
+	public function testSerializeWithContent() {
+		$Request = new CakeRequest();
+		$Response = new CakeResponse();
+		$Controller = new Controller($Request, $Response);
+		$data = array(
+			'channel' => array(
+				'title' => 'Channel title',
+				'link' => 'http://channel.example.org',
+				'guid' => array('url' => 'http://channel.example.org', '@isPermaLink' => 'true'),
+				'atom:link' => array('@href' => array('controller' => 'foo', 'action' => 'bar')),
+			),
+			'items' => array(
+				array('title' => 'Title One', 'link' => array('controller' => 'foo', 'action' => 'bar'), 'description' => 'Content one',
+					'content:encoded' => 'HTML <img src="http://domain.com/some/link/to/image.jpg"/> <b>content</b> one'),
+				array('title' => 'Title Two', 'link' => array('controller' => 'foo', 'action' => 'bar'), 'description' => 'Content two',
+					'content:encoded' => 'HTML <img src="http://domain.com/some/link/to/image.jpg"/> <b>content</b> two'),
+			)
+		);
+		$Controller->set(array('channel' => $data, '_serialize' => 'channel'));
+		$View = new RssView($Controller);
+		$result = $View->render(false);
+
+		$expected = <<<RSS
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
+  <channel>
+    <title>Channel title</title>
+    <link>http://channel.example.org</link>
+    <guid isPermaLink="true">http://channel.example.org</guid>
+    <atom:link href="$this->baseUrl/foo/bar" rel="self" type="application/rss+xml"/>
+    <description/>
+    <item>
+      <title>Title One</title>
+      <link>$this->baseUrl/foo/bar</link>
+      <description>Content one</description>
+      <content:encoded><![CDATA[HTML <img src="http://domain.com/some/link/to/image.jpg"/> <b>content</b> one]]></content:encoded>
+    </item>
+    <item>
+      <title>Title Two</title>
+      <link>$this->baseUrl/foo/bar</link>
+      <description>Content two</description>
+      <content:encoded><![CDATA[HTML <img src="http://domain.com/some/link/to/image.jpg"/> <b>content</b> two]]></content:encoded>
+    </item>
+  </channel>
+</rss>
+
+RSS;
+		//debug($output); ob_flush();
+		$this->assertTextEquals($expected, $result);
+	}
+
+	/**
+	 * RssViewTest::testSerializeWithCustomNamespace()
+	 *
+	 * @return void
+	 */
+	public function testSerializeWithCustomNamespace() {
+		$Request = new CakeRequest();
+		$Response = new CakeResponse();
+		$Controller = new Controller($Request, $Response);
+		$data = array(
+			'document' => array(
+				'namespace' => array(
+					'admin' => 'http://webns.net/mvcb/',
+					'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+				)
+			),
+			'channel' => array(
+				'title' => 'Channel title',
+				'admin:errorReportsTo' => array('@rdf:resource' => 'mailto:me@example.com')
+			),
+			'items' => array(
+				array('title' => 'Title One', 'link' => array('controller' => 'foo', 'action' => 'bar')),
+			)
+		);
+		$Controller->set(array('channel' => $data, '_serialize' => 'channel'));
+		$View = new RssView($Controller);
+		$result = $View->render(false);
+
+		$expected = <<<RSS
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:admin="http://webns.net/mvcb/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" version="2.0">
+  <channel>
+    <title>Channel title</title>
+    <admin:errorReportsTo rdf:resource="mailto:me@example.com"/>
+    <description/>
+    <item>
+      <title>Title One</title>
+      <link>$this->baseUrl/foo/bar</link>
+    </item>
+  </channel>
+</rss>
+
+RSS;
+		//debug($result); ob_flush();
+		$this->assertTextEquals($expected, $result);
+	}
+
+}

+ 359 - 0
View/RssView.php

@@ -0,0 +1,359 @@
+<?php
+/**
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author     Mark Scherer
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+App::uses('View', 'View');
+App::uses('Xml', 'Utility');
+App::uses('CakeTime', 'Utility');
+App::uses('Routing', 'Router');
+
+/**
+ * A view class that is used for creating RSS feeds.
+ *
+ * By setting the '_serialize' key in your controller, you can specify a view variable
+ * that should be serialized to XML and used as the response for the request.
+ * This allows you to omit views + layouts, if your just need to emit a single view
+ * variable as the XML response.
+ *
+ * In your controller, you could do the following:
+ *
+ * `$this->set(array('posts' => $posts, '_serialize' => 'posts'));`
+ *
+ * When the view is rendered, the `$posts` view variable will be serialized
+ * into XML.
+ *
+ * **Note** The view variable you specify must be compatible with Xml::fromArray().
+ *
+ * You can also define `'_serialize'` as an array. This will create an additional
+ * top level element named `<response>` containing all the named view variables:
+ *
+ * {{{
+ * $this->set(compact('posts', 'users', 'stuff'));
+ * $this->set('_serialize', array('posts', 'users'));
+ * }}}
+ *
+ * The above would generate a XML object that looks like:
+ *
+ * `<response><posts>...</posts><users>...</users></response>`
+ *
+ * If you don't use the `_serialize` key, you will need a view. You can use extended
+ * views to provide layout like functionality.
+ */
+class RssView extends View {
+
+/**
+ * Default spec version of generated RSS.
+ *
+ * @var string
+ */
+	public $version = '2.0';
+
+/**
+ * The subdirectory. RSS views are always in rss. Currently not in use.
+ *
+ * @var string
+ */
+	public $subDir = 'rss';
+
+/**
+ * Holds usable namespaces.
+ *
+ * @var array
+ * @link http://validator.w3.org/feed/docs/howto/declare_namespaces.html
+ */
+	protected $_namespaces = array(
+		'atom' => 'http://www.w3.org/2005/Atom',
+		'content' => 'http://purl.org/rss/1.0/modules/content/',
+		'dc' => 'http://purl.org/dc/elements/1.1/',
+		'sy' => 'http://purl.org/rss/1.0/modules/syndication/'
+	);
+
+/**
+ * Holds the namespace keys in use.
+ *
+ * @var array
+ */
+	protected $_usedNamespaces = array();
+
+/**
+ * Constructor
+ *
+ * @param Controller $controller
+ */
+	public function __construct(Controller $controller = null) {
+		parent::__construct($controller);
+
+		if (isset($controller->response) && $controller->response instanceof CakeResponse) {
+			$controller->response->type('rss');
+		}
+	}
+
+	/**
+	 * If you are using namespaces that are not yet known to the class, you need to globablly
+	 * add them with this method. Namespaces will only be added for actually used prefixes.
+	 *
+	 * @param string $prefix
+	 * @param string $url
+	 * @return void
+	 */
+	public function setNamespace($prefix, $url) {
+		$this->_namespaces[$prefix] = $url;
+	}
+
+/**
+ * Converts an array into an `<item />` element and its contents
+ *
+ * @param array $att The attributes of the `<item />` element
+ * @param array $elements The list of elements contained in this `<item />`
+ * @return string An RSS `<item />` element
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/rss.html#RssHelper::item
+ */
+	public function channel($channel) {
+		if (!isset($channel['title']) && !empty($this->pageTitle)) {
+			$channel['title'] = $this->pageTitle;
+		}
+		if (!isset($channel['description'])) {
+			$channel['description'] = '';
+		}
+		//$channel['link'] = Router::url($elements['link'], true);
+
+		$channel = $this->_prepareOutput($channel);
+		return $channel;
+	}
+
+/**
+ * Converts a time in any format to an RSS time
+ *
+ * @param integer|string|DateTime $time
+ * @return string An RSS-formatted timestamp
+ * @see CakeTime::toRSS
+ */
+	public function time($time) {
+		return CakeTime::toRSS($time);
+	}
+
+/**
+ * Skip loading helpers if this is a _serialize based view.
+ *
+ * @return void
+ */
+	public function loadHelpers() {
+		if (isset($this->viewVars['_serialize'])) {
+			return;
+		}
+		parent::loadHelpers();
+	}
+
+/**
+ * Render a RSS view.
+ *
+ * Uses the special '_serialize' parameter to convert a set of
+ * view variables into a XML response. Makes generating simple
+ * XML responses very easy. You can omit the '_serialize' parameter,
+ * and use a normal view + layout as well.
+ *
+ * @param string $view The view being rendered.
+ * @param string $layout The layout being rendered.
+ * @return string The rendered view.
+ */
+	public function render($view = null, $layout = null) {
+		if (isset($this->viewVars['_serialize'])) {
+			return $this->_serialize($this->viewVars['_serialize']);
+		}
+		if ($view !== false && $this->_getViewFileName($view)) {
+			return parent::render($view, false);
+		}
+	}
+
+/**
+ * Serialize view vars.
+ *
+ * @param array $serialize The viewVars that need to be serialized.
+ * @return string The serialized data
+ */
+	protected function _serialize($serialize) {
+		$rootNode = isset($this->viewVars['_rootNode']) ? $this->viewVars['_rootNode'] : 'channel';
+
+		if (is_array($serialize)) {
+			$data = array($rootNode => array());
+			foreach ($serialize as $alias => $key) {
+				if (is_numeric($alias)) {
+					$alias = $key;
+				}
+				$data[$rootNode][$alias] = $this->viewVars[$key];
+			}
+		} else {
+			$data = isset($this->viewVars[$serialize]) ? $this->viewVars[$serialize] : null;
+			if (is_array($data) && Set::numeric(array_keys($data))) {
+				$data = array($rootNode => array($serialize => $data));
+			}
+		}
+
+		$defaults = array('document' => array(), 'channel' => array(), 'items' => array());
+		$data += $defaults;
+		if (!empty($data['document']['namespace'])) {
+			foreach ($data['document']['namespace'] as $prefix => $url) {
+				$this->setNamespace($prefix, $url);
+			}
+		}
+
+		$channel = $this->channel($data['channel']);
+
+		foreach ($data['items'] as $item) {
+	 		$channel['item'][] = $this->_prepareOutput($item);
+		}
+
+		$array = array(
+			'rss' => array(
+				'@version' => $this->version,
+				'channel' => $channel,
+			)
+		);
+		$namespaces = array();
+		foreach ($this->_usedNamespaces as $usedNamespacePrefix) {
+			$namespaces['xmlns:' . $usedNamespacePrefix] = $this->_namespaces[$usedNamespacePrefix];
+		}
+		$array['rss'] += $namespaces;
+
+		$options = array();
+		if (Configure::read('debug')) {
+			$options['pretty'] = true;
+		}
+
+		$output = Xml::fromArray($array, $options)->asXML();
+		//$output = preg_replace('/version="([0-9\.]+)" encoding="([a-z-]+)"/i', 'version="\1"', $output);
+		//$output = str_replace(' encoding="UTF-8"', '', $output);
+
+		$output = $this->_replaceCdata($output);
+
+		return $output;
+	}
+
+	/**
+	 * RssView::_prepareOutput()
+	 *
+	 * @param aray $item
+	 * @return void
+	 */
+	protected function _prepareOutput($item) {
+		foreach ($item as $key => $val) {
+			// Detect namespaces
+			$prefix = null;
+			$bareKey = $key;
+			if (strpos($key, ':') !== false) {
+				list($prefix, $bareKey) = explode(':', $key, 2);
+				if (strpos($prefix, '@') !== false) {
+					$prefix = substr($prefix, 1);
+				}
+				if (!in_array($prefix, $this->_usedNamespaces)) {
+					$this->_usedNamespaces[] = $prefix;
+				}
+			}
+			if (is_array($val)) {
+				$val = $this->_prepareOutput($val);
+			}
+
+			$attrib = null;
+			switch ($bareKey) {
+				case 'encoded':
+					$val = $this->_newCdata($val);
+					break;
+
+				case 'pubDate':
+					$val = $this->time($val);
+					break;
+				/*
+				case 'category' :
+					if (is_array($val) && !empty($val[0])) {
+						foreach ($val as $category) {
+							$attrib = array();
+							if (is_array($category) && isset($category['domain'])) {
+								$attrib['domain'] = $category['domain'];
+								unset($category['domain']);
+							}
+							$categories[] = $this->elem($key, $attrib, $category);
+						}
+						$elements[$key] = implode('', $categories);
+						continue 2;
+					} elseif (is_array($val) && isset($val['domain'])) {
+						$attrib['domain'] = $val['domain'];
+					}
+					break;
+				*/
+				case 'link':
+				case 'guid':
+				case 'comments':
+					if (is_array($val) && isset($val['@href'])) {
+						$attrib = $val;
+						$attrib['@href'] = Router::url($val['@href'], true);
+						if ($prefix === 'atom') {
+							$attrib['@rel'] = 'self';
+							$attrib['@type'] = 'application/rss+xml';
+						}
+						$val = $attrib;
+
+					} elseif (is_array($val) && isset($val['url'])) {
+						$val['url'] = Router::url($val['url'], true);
+						if ($bareKey === 'guid') {
+							$val['@'] = $val['url'];
+							unset($val['url']);
+						}
+					} else {
+						$val = Router::url($val, true);
+					}
+					break;
+				case 'source':
+					if (is_array($val) && isset($val['url'])) {
+						$attrib['url'] = Router::url($val['url'], true);
+						$val = $val['title'];
+					} elseif (is_array($val)) {
+						$attrib['url'] = Router::url($val[0], true);
+						$val = $val[1];
+					}
+					break;
+				case 'enclosure':
+					if (is_string($val['url']) && is_file(WWW_ROOT . $val['url']) && file_exists(WWW_ROOT . $val['url'])) {
+						if (!isset($val['length']) && strpos($val['url'], '://') === false) {
+							$val['length'] = sprintf("%u", filesize(WWW_ROOT . $val['url']));
+						}
+						if (!isset($val['type']) && function_exists('mime_content_type')) {
+							$val['type'] = mime_content_type(WWW_ROOT . $val['url']);
+						}
+					}
+					$val['url'] = Router::url($val['url'], true);
+					$attrib = $val;
+					$val = null;
+					break;
+				default:
+					//$attrib = $att;
+			}
+
+			$item[$key] = $val;
+		}
+
+		return $item;
+	}
+
+	protected $_cdata = array();
+
+	protected function _newCdata($content) {
+		$i = count($this->_cdata);
+		$this->_cdata[$i] = $content;
+		return '###CDATA-' . $i . '###';
+	}
+
+	protected function _replaceCdata($content) {
+		foreach ($this->_cdata as $n => $data) {
+			$data = '<![CDATA[' . $data . ']]>';
+			$content = str_replace('###CDATA-' . $n . '###', $data, $content);
+		}
+		return $content;
+	}
+
+}