RssView.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <?php
  2. /**
  3. * Licensed under The MIT License
  4. * For full copyright and license information, please see the LICENSE.txt
  5. * Redistributions of files must retain the above copyright notice.
  6. *
  7. * @author Mark Scherer
  8. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  9. * @link http://www.dereuromark.de/2013/10/03/rss-feeds-in-cakephp
  10. */
  11. App::uses('View', 'View');
  12. App::uses('Xml', 'Utility');
  13. App::uses('CakeTime', 'Utility');
  14. App::uses('Routing', 'Router');
  15. /**
  16. * A view class that is used for creating RSS feeds.
  17. *
  18. * By setting the '_serialize' key in your controller, you can specify a view variable
  19. * that should be serialized to XML and used as the response for the request.
  20. * This allows you to omit views + layouts, if your just need to emit a single view
  21. * variable as the XML response.
  22. *
  23. * In your controller, you could do the following:
  24. *
  25. * `$this->set(array('posts' => $posts, '_serialize' => 'posts'));`
  26. *
  27. * When the view is rendered, the `$posts` view variable will be serialized
  28. * into the RSS XML.
  29. *
  30. * **Note** The view variable you specify must be compatible with Xml::fromArray().
  31. *
  32. * If you don't use the `_serialize` key, you will need a view. You can use extended
  33. * views to provide layout like functionality. This is currently not yet tested/supported.
  34. */
  35. class RssView extends View {
  36. /**
  37. * Default spec version of generated RSS.
  38. *
  39. * @var string
  40. */
  41. public $version = '2.0';
  42. /**
  43. * The subdirectory. RSS views are always in rss. Currently not in use.
  44. *
  45. * @var string
  46. */
  47. public $subDir = 'rss';
  48. /**
  49. * Holds usable namespaces.
  50. *
  51. * @var array
  52. * @link http://validator.w3.org/feed/docs/howto/declare_namespaces.html
  53. */
  54. protected $_namespaces = array(
  55. 'atom' => 'http://www.w3.org/2005/Atom',
  56. 'content' => 'http://purl.org/rss/1.0/modules/content/',
  57. 'dc' => 'http://purl.org/dc/elements/1.1/',
  58. 'sy' => 'http://purl.org/rss/1.0/modules/syndication/'
  59. );
  60. /**
  61. * Holds the namespace keys in use.
  62. *
  63. * @var array
  64. */
  65. protected $_usedNamespaces = array();
  66. /**
  67. * Holds CDATA placeholders.
  68. *
  69. * @var array
  70. */
  71. protected $_cdata = array();
  72. /**
  73. * Constructor
  74. *
  75. * @param Controller $controller
  76. */
  77. public function __construct(Controller $controller = null) {
  78. parent::__construct($controller);
  79. if (isset($controller->response) && $controller->response instanceof CakeResponse) {
  80. $controller->response->type('rss');
  81. }
  82. }
  83. /**
  84. * If you are using namespaces that are not yet known to the class, you need to globablly
  85. * add them with this method. Namespaces will only be added for actually used prefixes.
  86. *
  87. * @param string $prefix
  88. * @param string $url
  89. * @return void
  90. */
  91. public function setNamespace($prefix, $url) {
  92. $this->_namespaces[$prefix] = $url;
  93. }
  94. /**
  95. * Prepares the channel and sets default values.
  96. *
  97. * @param array $channel
  98. * @return array Channel
  99. */
  100. public function channel($channel) {
  101. if (!isset($channel['description'])) {
  102. $channel['description'] = '';
  103. }
  104. $channel = $this->_prepareOutput($channel);
  105. return $channel;
  106. }
  107. /**
  108. * Converts a time in any format to an RSS time
  109. *
  110. * @param integer|string|DateTime $time
  111. * @return string An RSS-formatted timestamp
  112. * @see CakeTime::toRSS
  113. */
  114. public function time($time) {
  115. return CakeTime::toRSS($time);
  116. }
  117. /**
  118. * Skip loading helpers if this is a _serialize based view.
  119. *
  120. * @return void
  121. */
  122. public function loadHelpers() {
  123. if (isset($this->viewVars['_serialize'])) {
  124. return;
  125. }
  126. parent::loadHelpers();
  127. }
  128. /**
  129. * Render a RSS view.
  130. *
  131. * Uses the special '_serialize' parameter to convert a set of
  132. * view variables into a XML response. Makes generating simple
  133. * XML responses very easy. You can omit the '_serialize' parameter,
  134. * and use a normal view + layout as well.
  135. *
  136. * @param string $view The view being rendered.
  137. * @param string $layout The layout being rendered.
  138. * @return string The rendered view.
  139. */
  140. public function render($view = null, $layout = null) {
  141. if (isset($this->viewVars['_serialize'])) {
  142. return $this->_serialize($this->viewVars['_serialize']);
  143. }
  144. if ($view !== false && $this->_getViewFileName($view)) {
  145. return parent::render($view, false);
  146. }
  147. }
  148. /**
  149. * Serialize view vars.
  150. *
  151. * @param array $serialize The viewVars that need to be serialized.
  152. * @return string The serialized data
  153. */
  154. protected function _serialize($serialize) {
  155. $rootNode = isset($this->viewVars['_rootNode']) ? $this->viewVars['_rootNode'] : 'channel';
  156. if (is_array($serialize)) {
  157. $data = array($rootNode => array());
  158. foreach ($serialize as $alias => $key) {
  159. if (is_numeric($alias)) {
  160. $alias = $key;
  161. }
  162. $data[$rootNode][$alias] = $this->viewVars[$key];
  163. }
  164. } else {
  165. $data = isset($this->viewVars[$serialize]) ? $this->viewVars[$serialize] : null;
  166. if (is_array($data) && Set::numeric(array_keys($data))) {
  167. $data = array($rootNode => array($serialize => $data));
  168. }
  169. }
  170. $defaults = array('document' => array(), 'channel' => array(), 'items' => array());
  171. $data += $defaults;
  172. if (!empty($data['document']['namespace'])) {
  173. foreach ($data['document']['namespace'] as $prefix => $url) {
  174. $this->setNamespace($prefix, $url);
  175. }
  176. }
  177. $channel = $this->channel($data['channel']);
  178. foreach ($data['items'] as $item) {
  179. $channel['item'][] = $this->_prepareOutput($item);
  180. }
  181. $array = array(
  182. 'rss' => array(
  183. '@version' => $this->version,
  184. 'channel' => $channel,
  185. )
  186. );
  187. $namespaces = array();
  188. foreach ($this->_usedNamespaces as $usedNamespacePrefix) {
  189. if (!isset($this->_namespaces[$usedNamespacePrefix])) {
  190. throw new RuntimeException(__('The prefix %s is not specified.', $usedNamespacePrefix));
  191. }
  192. $namespaces['xmlns:' . $usedNamespacePrefix] = $this->_namespaces[$usedNamespacePrefix];
  193. }
  194. $array['rss'] += $namespaces;
  195. $options = array();
  196. if (Configure::read('debug')) {
  197. $options['pretty'] = true;
  198. }
  199. $output = Xml::fromArray($array, $options)->asXML();
  200. $output = $this->_replaceCdata($output);
  201. return $output;
  202. }
  203. /**
  204. * RssView::_prepareOutput()
  205. *
  206. * @param aray $item
  207. * @return void
  208. */
  209. protected function _prepareOutput($item) {
  210. foreach ($item as $key => $val) {
  211. // Detect namespaces
  212. $prefix = null;
  213. $bareKey = $key;
  214. if (strpos($key, ':') !== false) {
  215. list($prefix, $bareKey) = explode(':', $key, 2);
  216. if (strpos($prefix, '@') !== false) {
  217. $prefix = substr($prefix, 1);
  218. }
  219. if (!in_array($prefix, $this->_usedNamespaces)) {
  220. $this->_usedNamespaces[] = $prefix;
  221. }
  222. }
  223. if (is_array($val)) {
  224. $val = $this->_prepareOutput($val);
  225. }
  226. $attrib = null;
  227. switch ($bareKey) {
  228. case 'encoded':
  229. $val = $this->_newCdata($val);
  230. break;
  231. case 'pubDate':
  232. $val = $this->time($val);
  233. break;
  234. /*
  235. case 'category' :
  236. if (is_array($val) && !empty($val[0])) {
  237. foreach ($val as $category) {
  238. $attrib = array();
  239. if (is_array($category) && isset($category['domain'])) {
  240. $attrib['domain'] = $category['domain'];
  241. unset($category['domain']);
  242. }
  243. $categories[] = $this->elem($key, $attrib, $category);
  244. }
  245. $elements[$key] = implode('', $categories);
  246. continue 2;
  247. } elseif (is_array($val) && isset($val['domain'])) {
  248. $attrib['domain'] = $val['domain'];
  249. }
  250. break;
  251. */
  252. case 'link':
  253. case 'guid':
  254. case 'comments':
  255. if (is_array($val) && isset($val['@href'])) {
  256. $attrib = $val;
  257. $attrib['@href'] = Router::url($val['@href'], true);
  258. if ($prefix === 'atom') {
  259. $attrib['@rel'] = 'self';
  260. $attrib['@type'] = 'application/rss+xml';
  261. }
  262. $val = $attrib;
  263. } elseif (is_array($val) && isset($val['url'])) {
  264. $val['url'] = Router::url($val['url'], true);
  265. if ($bareKey === 'guid') {
  266. $val['@'] = $val['url'];
  267. unset($val['url']);
  268. }
  269. } else {
  270. $val = Router::url($val, true);
  271. }
  272. break;
  273. case 'source':
  274. if (is_array($val) && isset($val['url'])) {
  275. $attrib['url'] = Router::url($val['url'], true);
  276. $val = $val['title'];
  277. } elseif (is_array($val)) {
  278. $attrib['url'] = Router::url($val[0], true);
  279. $val = $val[1];
  280. }
  281. break;
  282. case 'enclosure':
  283. if (is_string($val['url']) && is_file(WWW_ROOT . $val['url']) && file_exists(WWW_ROOT . $val['url'])) {
  284. if (!isset($val['length']) && strpos($val['url'], '://') === false) {
  285. $val['length'] = sprintf("%u", filesize(WWW_ROOT . $val['url']));
  286. }
  287. if (!isset($val['type']) && function_exists('mime_content_type')) {
  288. $val['type'] = mime_content_type(WWW_ROOT . $val['url']);
  289. }
  290. }
  291. $val['url'] = Router::url($val['url'], true);
  292. $attrib = $val;
  293. $val = null;
  294. break;
  295. default:
  296. //$attrib = $att;
  297. }
  298. $item[$key] = $val;
  299. }
  300. return $item;
  301. }
  302. /**
  303. * RssView::_newCdata()
  304. *
  305. * @param string $content
  306. * @return string
  307. */
  308. protected function _newCdata($content) {
  309. $i = count($this->_cdata);
  310. $this->_cdata[$i] = $content;
  311. return '###CDATA-' . $i . '###';
  312. }
  313. /**
  314. * RssView::_replaceCdata()
  315. *
  316. * @param string $content
  317. * @return string
  318. */
  319. protected function _replaceCdata($content) {
  320. foreach ($this->_cdata as $n => $data) {
  321. $data = '<![CDATA[' . $data . ']]>';
  322. $content = str_replace('###CDATA-' . $n . '###', $data, $content);
  323. }
  324. return $content;
  325. }
  326. }