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