1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-feed for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-feed/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-feed/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Feed\Writer\Renderer\Feed;
10
11use DateTime;
12use DOMDocument;
13use DOMElement;
14use Laminas\Feed\Uri;
15use Laminas\Feed\Writer;
16use Laminas\Feed\Writer\Renderer;
17use Laminas\Feed\Writer\Version;
18
19class Rss extends Renderer\AbstractRenderer implements Renderer\RendererInterface
20{
21    public function __construct(Writer\Feed $container)
22    {
23        parent::__construct($container);
24    }
25
26    /**
27     * Render RSS feed
28     *
29     * @return $this
30     */
31    public function render()
32    {
33        $this->dom                     = new DOMDocument('1.0', $this->container->getEncoding());
34        $this->dom->formatOutput       = true;
35        $this->dom->substituteEntities = false;
36        $rss                           = $this->dom->createElement('rss');
37        $this->setRootElement($rss);
38        $rss->setAttribute('version', '2.0');
39
40        $channel = $this->dom->createElement('channel');
41        $rss->appendChild($channel);
42        $this->dom->appendChild($rss);
43        $this->_setLanguage($this->dom, $channel);
44        $this->_setBaseUrl($this->dom, $channel);
45        $this->_setTitle($this->dom, $channel);
46        $this->_setDescription($this->dom, $channel);
47        $this->_setImage($this->dom, $channel);
48        $this->_setDateCreated($this->dom, $channel);
49        $this->_setDateModified($this->dom, $channel);
50        $this->_setLastBuildDate($this->dom, $channel);
51        $this->_setGenerator($this->dom, $channel);
52        $this->_setLink($this->dom, $channel);
53        $this->_setAuthors($this->dom, $channel);
54        $this->_setCopyright($this->dom, $channel);
55        $this->_setCategories($this->dom, $channel);
56
57        foreach ($this->extensions as $ext) {
58            $ext->setType($this->getType());
59            $ext->setRootElement($this->getRootElement());
60            $ext->setDomDocument($this->getDomDocument(), $channel);
61            $ext->render();
62        }
63
64        foreach ($this->container as $entry) {
65            if ($this->getDataContainer()->getEncoding()) {
66                $entry->setEncoding($this->getDataContainer()->getEncoding());
67            }
68            if ($entry instanceof Writer\Entry) {
69                $renderer = new Renderer\Entry\Rss($entry);
70            } else {
71                continue;
72            }
73            if ($this->ignoreExceptions === true) {
74                $renderer->ignoreExceptions();
75            }
76            $renderer->setType($this->getType());
77            $renderer->setRootElement($this->dom->documentElement);
78            $renderer->render();
79            $element  = $renderer->getElement();
80            $deep     = version_compare(PHP_VERSION, '7', 'ge') ? 1 : true;
81            $imported = $this->dom->importNode($element, $deep);
82            $channel->appendChild($imported);
83        }
84        return $this;
85    }
86
87    /**
88     * Set feed language
89     *
90     * @param DOMDocument $dom
91     * @param DOMElement $root
92     * @return void
93     */
94    // @codingStandardsIgnoreStart
95    protected function _setLanguage(DOMDocument $dom, DOMElement $root)
96    {
97        // @codingStandardsIgnoreEnd
98        $lang = $this->getDataContainer()->getLanguage();
99        if (! $lang) {
100            return;
101        }
102        $language = $dom->createElement('language');
103        $root->appendChild($language);
104        $language->nodeValue = $lang;
105    }
106
107    /**
108     * Set feed title
109     *
110     * @param  DOMDocument $dom
111     * @param  DOMElement $root
112     * @return void
113     * @throws Writer\Exception\InvalidArgumentException
114     */
115    // @codingStandardsIgnoreStart
116    protected function _setTitle(DOMDocument $dom, DOMElement $root)
117    {
118        // @codingStandardsIgnoreEnd
119        if (! $this->getDataContainer()->getTitle()) {
120            $message   = 'RSS 2.0 feed elements MUST contain exactly one'
121                . ' title element but a title has not been set';
122            $exception = new Writer\Exception\InvalidArgumentException($message);
123            if (! $this->ignoreExceptions) {
124                throw $exception;
125            } else {
126                $this->exceptions[] = $exception;
127                return;
128            }
129        }
130
131        $title = $dom->createElement('title');
132        $root->appendChild($title);
133        $text = $dom->createTextNode($this->getDataContainer()->getTitle());
134        $title->appendChild($text);
135    }
136
137    /**
138     * Set feed description
139     *
140     * @param  DOMDocument $dom
141     * @param  DOMElement $root
142     * @return void
143     * @throws Writer\Exception\InvalidArgumentException
144     */
145    // @codingStandardsIgnoreStart
146    protected function _setDescription(DOMDocument $dom, DOMElement $root)
147    {
148        // @codingStandardsIgnoreEnd
149        if (! $this->getDataContainer()->getDescription()) {
150            $message   = 'RSS 2.0 feed elements MUST contain exactly one'
151                . ' description element but one has not been set';
152            $exception = new Writer\Exception\InvalidArgumentException($message);
153            if (! $this->ignoreExceptions) {
154                throw $exception;
155            } else {
156                $this->exceptions[] = $exception;
157                return;
158            }
159        }
160        $subtitle = $dom->createElement('description');
161        $root->appendChild($subtitle);
162        $text = $dom->createTextNode($this->getDataContainer()->getDescription());
163        $subtitle->appendChild($text);
164    }
165
166    /**
167     * Set date feed was last modified
168     *
169     * @param  DOMDocument $dom
170     * @param  DOMElement $root
171     * @return void
172     */
173    // @codingStandardsIgnoreStart
174    protected function _setDateModified(DOMDocument $dom, DOMElement $root)
175    {
176        // @codingStandardsIgnoreEnd
177        if (! $this->getDataContainer()->getDateModified()) {
178            return;
179        }
180
181        $updated = $dom->createElement('pubDate');
182        $root->appendChild($updated);
183        $text = $dom->createTextNode(
184            $this->getDataContainer()->getDateModified()->format(DateTime::RSS)
185        );
186        $updated->appendChild($text);
187    }
188
189    /**
190     * Set feed generator string
191     *
192     * @param  DOMDocument $dom
193     * @param  DOMElement $root
194     * @return void
195     */
196    // @codingStandardsIgnoreStart
197    protected function _setGenerator(DOMDocument $dom, DOMElement $root)
198    {
199        // @codingStandardsIgnoreEnd
200        if (! $this->getDataContainer()->getGenerator()) {
201            $this->getDataContainer()->setGenerator(
202                'Laminas_Feed_Writer',
203                Version::VERSION,
204                'https://getlaminas.org'
205            );
206        }
207
208        $gdata     = $this->getDataContainer()->getGenerator();
209        $generator = $dom->createElement('generator');
210        $root->appendChild($generator);
211        $name = $gdata['name'];
212        if (array_key_exists('version', $gdata)) {
213            $name .= ' ' . $gdata['version'];
214        }
215        if (array_key_exists('uri', $gdata)) {
216            $name .= ' (' . $gdata['uri'] . ')';
217        }
218        $text = $dom->createTextNode($name);
219        $generator->appendChild($text);
220    }
221
222    /**
223     * Set link to feed
224     *
225     * @param  DOMDocument $dom
226     * @param  DOMElement $root
227     * @return void
228     * @throws Writer\Exception\InvalidArgumentException
229     */
230    // @codingStandardsIgnoreStart
231    protected function _setLink(DOMDocument $dom, DOMElement $root)
232    {
233        // @codingStandardsIgnoreEnd
234        $value = $this->getDataContainer()->getLink();
235        if (! $value) {
236            $message   = 'RSS 2.0 feed elements MUST contain exactly one'
237                . ' link element but one has not been set';
238            $exception = new Writer\Exception\InvalidArgumentException($message);
239            if (! $this->ignoreExceptions) {
240                throw $exception;
241            } else {
242                $this->exceptions[] = $exception;
243                return;
244            }
245        }
246        $link = $dom->createElement('link');
247        $root->appendChild($link);
248        $text = $dom->createTextNode($value);
249        $link->appendChild($text);
250        if (! Uri::factory($value)->isValid()) {
251            $link->setAttribute('isPermaLink', 'false');
252        }
253    }
254
255    /**
256     * Set feed authors
257     *
258     * @param  DOMDocument $dom
259     * @param  DOMElement $root
260     * @return void
261     */
262    // @codingStandardsIgnoreStart
263    protected function _setAuthors(DOMDocument $dom, DOMElement $root)
264    {
265        // @codingStandardsIgnoreEnd
266        $authors = $this->getDataContainer()->getAuthors();
267        if (! $authors || empty($authors)) {
268            return;
269        }
270        foreach ($authors as $data) {
271            $author = $this->dom->createElement('author');
272            $name   = $data['name'];
273            if (array_key_exists('email', $data)) {
274                $name = $data['email'] . ' (' . $data['name'] . ')';
275            }
276            $text = $dom->createTextNode($name);
277            $author->appendChild($text);
278            $root->appendChild($author);
279        }
280    }
281
282    /**
283     * Set feed copyright
284     *
285     * @param  DOMDocument $dom
286     * @param  DOMElement $root
287     * @return void
288     */
289    // @codingStandardsIgnoreStart
290    protected function _setCopyright(DOMDocument $dom, DOMElement $root)
291    {
292        // @codingStandardsIgnoreEnd
293        $copyright = $this->getDataContainer()->getCopyright();
294        if (! $copyright) {
295            return;
296        }
297        $copy = $dom->createElement('copyright');
298        $root->appendChild($copy);
299        $text = $dom->createTextNode($copyright);
300        $copy->appendChild($text);
301    }
302
303    /**
304     * Set feed channel image
305     *
306     * @param  DOMDocument $dom
307     * @param  DOMElement $root
308     * @return void
309     * @throws Writer\Exception\InvalidArgumentException
310     */
311    // @codingStandardsIgnoreStart
312    protected function _setImage(DOMDocument $dom, DOMElement $root)
313    {
314        // @codingStandardsIgnoreEnd
315        $image = $this->getDataContainer()->getImage();
316        if (! $image) {
317            return;
318        }
319
320        if (! isset($image['title']) || empty($image['title'])
321            || ! is_string($image['title'])
322        ) {
323            $message   = 'RSS 2.0 feed images must include a title';
324            $exception = new Writer\Exception\InvalidArgumentException($message);
325            if (! $this->ignoreExceptions) {
326                throw $exception;
327            } else {
328                $this->exceptions[] = $exception;
329                return;
330            }
331        }
332
333        if (empty($image['link']) || ! is_string($image['link'])
334            || ! Uri::factory($image['link'])->isValid()
335        ) {
336            $message   = 'Invalid parameter: parameter \'link\''
337                . ' must be a non-empty string and valid URI/IRI';
338            $exception = new Writer\Exception\InvalidArgumentException($message);
339            if (! $this->ignoreExceptions) {
340                throw $exception;
341            } else {
342                $this->exceptions[] = $exception;
343                return;
344            }
345        }
346
347        $img = $dom->createElement('image');
348        $root->appendChild($img);
349
350        $url  = $dom->createElement('url');
351        $text = $dom->createTextNode($image['uri']);
352        $url->appendChild($text);
353
354        $title = $dom->createElement('title');
355        $text  = $dom->createTextNode($image['title']);
356        $title->appendChild($text);
357
358        $link = $dom->createElement('link');
359        $text = $dom->createTextNode($image['link']);
360        $link->appendChild($text);
361
362        $img->appendChild($url);
363        $img->appendChild($title);
364        $img->appendChild($link);
365
366        if (isset($image['height'])) {
367            if (! ctype_digit((string) $image['height']) || $image['height'] > 400) {
368                $message   = 'Invalid parameter: parameter \'height\''
369                    . ' must be an integer not exceeding 400';
370                $exception = new Writer\Exception\InvalidArgumentException($message);
371                if (! $this->ignoreExceptions) {
372                    throw $exception;
373                } else {
374                    $this->exceptions[] = $exception;
375                    return;
376                }
377            }
378            $height = $dom->createElement('height');
379            $text   = $dom->createTextNode($image['height']);
380            $height->appendChild($text);
381            $img->appendChild($height);
382        }
383        if (isset($image['width'])) {
384            if (! ctype_digit((string) $image['width']) || $image['width'] > 144) {
385                $message   = 'Invalid parameter: parameter \'width\''
386                    . ' must be an integer not exceeding 144';
387                $exception = new Writer\Exception\InvalidArgumentException($message);
388                if (! $this->ignoreExceptions) {
389                    throw $exception;
390                } else {
391                    $this->exceptions[] = $exception;
392                    return;
393                }
394            }
395            $width = $dom->createElement('width');
396            $text  = $dom->createTextNode($image['width']);
397            $width->appendChild($text);
398            $img->appendChild($width);
399        }
400        if (isset($image['description'])) {
401            if (empty($image['description']) || ! is_string($image['description'])) {
402                $message   = 'Invalid parameter: parameter \'description\''
403                    . ' must be a non-empty string';
404                $exception = new Writer\Exception\InvalidArgumentException($message);
405                if (! $this->ignoreExceptions) {
406                    throw $exception;
407                } else {
408                    $this->exceptions[] = $exception;
409                    return;
410                }
411            }
412            $desc = $dom->createElement('description');
413            $text = $dom->createTextNode($image['description']);
414            $desc->appendChild($text);
415            $img->appendChild($desc);
416        }
417    }
418
419    /**
420     * Set date feed was created
421     *
422     * @param  DOMDocument $dom
423     * @param  DOMElement $root
424     * @return void
425     */
426    // @codingStandardsIgnoreStart
427    protected function _setDateCreated(DOMDocument $dom, DOMElement $root)
428    {
429        // @codingStandardsIgnoreEnd
430        if (! $this->getDataContainer()->getDateCreated()) {
431            return;
432        }
433        if (! $this->getDataContainer()->getDateModified()) {
434            $this->getDataContainer()->setDateModified(
435                $this->getDataContainer()->getDateCreated()
436            );
437        }
438    }
439
440    /**
441     * Set date feed last build date
442     *
443     * @param  DOMDocument $dom
444     * @param  DOMElement $root
445     * @return void
446     */
447    // @codingStandardsIgnoreStart
448    protected function _setLastBuildDate(DOMDocument $dom, DOMElement $root)
449    {
450        // @codingStandardsIgnoreEnd
451        if (! $this->getDataContainer()->getLastBuildDate()) {
452            return;
453        }
454
455        $lastBuildDate = $dom->createElement('lastBuildDate');
456        $root->appendChild($lastBuildDate);
457        $text = $dom->createTextNode(
458            $this->getDataContainer()->getLastBuildDate()->format(DateTime::RSS)
459        );
460        $lastBuildDate->appendChild($text);
461    }
462
463    /**
464     * Set base URL to feed links
465     *
466     * @param  DOMDocument $dom
467     * @param  DOMElement $root
468     * @return void
469     */
470    // @codingStandardsIgnoreStart
471    protected function _setBaseUrl(DOMDocument $dom, DOMElement $root)
472    {
473        // @codingStandardsIgnoreEnd
474        $baseUrl = $this->getDataContainer()->getBaseUrl();
475        if (! $baseUrl) {
476            return;
477        }
478        $root->setAttribute('xml:base', $baseUrl);
479    }
480
481    /**
482     * Set feed categories
483     *
484     * @param  DOMDocument $dom
485     * @param  DOMElement $root
486     * @return void
487     */
488    // @codingStandardsIgnoreStart
489    protected function _setCategories(DOMDocument $dom, DOMElement $root)
490    {
491        // @codingStandardsIgnoreEnd
492        $categories = $this->getDataContainer()->getCategories();
493        if (! $categories) {
494            return;
495        }
496        foreach ($categories as $cat) {
497            $category = $dom->createElement('category');
498            if (isset($cat['scheme'])) {
499                $category->setAttribute('domain', $cat['scheme']);
500            }
501            $text = $dom->createTextNode($cat['term']);
502            $category->appendChild($text);
503            $root->appendChild($category);
504        }
505    }
506}
507