[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 3 // SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue 4 // SPDX-License-Identifier: BSD-3-Clause 5 6 declare(strict_types=1); 7 8 namespace SimplePie; 9 10 use InvalidArgumentException; 11 use Psr\Http\Client\ClientInterface; 12 use Psr\Http\Message\RequestFactoryInterface; 13 use Psr\Http\Message\UriFactoryInterface; 14 use Psr\SimpleCache\CacheInterface; 15 use SimplePie\Cache\Base; 16 use SimplePie\Cache\BaseDataCache; 17 use SimplePie\Cache\CallableNameFilter; 18 use SimplePie\Cache\DataCache; 19 use SimplePie\Cache\NameFilter; 20 use SimplePie\Cache\Psr16; 21 use SimplePie\Content\Type\Sniffer; 22 use SimplePie\Exception as SimplePieException; 23 use SimplePie\HTTP\Client; 24 use SimplePie\HTTP\ClientException; 25 use SimplePie\HTTP\FileClient; 26 use SimplePie\HTTP\Psr18Client; 27 use SimplePie\HTTP\Response; 28 29 /** 30 * SimplePie 31 */ 32 class SimplePie 33 { 34 /** 35 * SimplePie Name 36 */ 37 public const NAME = 'SimplePie'; 38 39 /** 40 * SimplePie Version 41 */ 42 public const VERSION = '1.9.0'; 43 44 /** 45 * SimplePie Website URL 46 */ 47 public const URL = 'http://simplepie.org'; 48 49 /** 50 * SimplePie Linkback 51 */ 52 public const LINKBACK = '<a href="' . self::URL . '" title="' . self::NAME . ' ' . self::VERSION . '">' . self::NAME . '</a>'; 53 54 /** 55 * No Autodiscovery 56 * @see SimplePie::set_autodiscovery_level() 57 */ 58 public const LOCATOR_NONE = 0; 59 60 /** 61 * Feed Link Element Autodiscovery 62 * @see SimplePie::set_autodiscovery_level() 63 */ 64 public const LOCATOR_AUTODISCOVERY = 1; 65 66 /** 67 * Local Feed Extension Autodiscovery 68 * @see SimplePie::set_autodiscovery_level() 69 */ 70 public const LOCATOR_LOCAL_EXTENSION = 2; 71 72 /** 73 * Local Feed Body Autodiscovery 74 * @see SimplePie::set_autodiscovery_level() 75 */ 76 public const LOCATOR_LOCAL_BODY = 4; 77 78 /** 79 * Remote Feed Extension Autodiscovery 80 * @see SimplePie::set_autodiscovery_level() 81 */ 82 public const LOCATOR_REMOTE_EXTENSION = 8; 83 84 /** 85 * Remote Feed Body Autodiscovery 86 * @see SimplePie::set_autodiscovery_level() 87 */ 88 public const LOCATOR_REMOTE_BODY = 16; 89 90 /** 91 * All Feed Autodiscovery 92 * @see SimplePie::set_autodiscovery_level() 93 */ 94 public const LOCATOR_ALL = 31; 95 96 /** 97 * No known feed type 98 */ 99 public const TYPE_NONE = 0; 100 101 /** 102 * RSS 0.90 103 */ 104 public const TYPE_RSS_090 = 1; 105 106 /** 107 * RSS 0.91 (Netscape) 108 */ 109 public const TYPE_RSS_091_NETSCAPE = 2; 110 111 /** 112 * RSS 0.91 (Userland) 113 */ 114 public const TYPE_RSS_091_USERLAND = 4; 115 116 /** 117 * RSS 0.91 (both Netscape and Userland) 118 */ 119 public const TYPE_RSS_091 = 6; 120 121 /** 122 * RSS 0.92 123 */ 124 public const TYPE_RSS_092 = 8; 125 126 /** 127 * RSS 0.93 128 */ 129 public const TYPE_RSS_093 = 16; 130 131 /** 132 * RSS 0.94 133 */ 134 public const TYPE_RSS_094 = 32; 135 136 /** 137 * RSS 1.0 138 */ 139 public const TYPE_RSS_10 = 64; 140 141 /** 142 * RSS 2.0 143 */ 144 public const TYPE_RSS_20 = 128; 145 146 /** 147 * RDF-based RSS 148 */ 149 public const TYPE_RSS_RDF = 65; 150 151 /** 152 * Non-RDF-based RSS (truly intended as syndication format) 153 */ 154 public const TYPE_RSS_SYNDICATION = 190; 155 156 /** 157 * All RSS 158 */ 159 public const TYPE_RSS_ALL = 255; 160 161 /** 162 * Atom 0.3 163 */ 164 public const TYPE_ATOM_03 = 256; 165 166 /** 167 * Atom 1.0 168 */ 169 public const TYPE_ATOM_10 = 512; 170 171 /** 172 * All Atom 173 */ 174 public const TYPE_ATOM_ALL = 768; 175 176 /** 177 * All feed types 178 */ 179 public const TYPE_ALL = 1023; 180 181 /** 182 * No construct 183 */ 184 public const CONSTRUCT_NONE = 0; 185 186 /** 187 * Text construct 188 */ 189 public const CONSTRUCT_TEXT = 1; 190 191 /** 192 * HTML construct 193 */ 194 public const CONSTRUCT_HTML = 2; 195 196 /** 197 * XHTML construct 198 */ 199 public const CONSTRUCT_XHTML = 4; 200 201 /** 202 * base64-encoded construct 203 */ 204 public const CONSTRUCT_BASE64 = 8; 205 206 /** 207 * IRI construct 208 */ 209 public const CONSTRUCT_IRI = 16; 210 211 /** 212 * A construct that might be HTML 213 */ 214 public const CONSTRUCT_MAYBE_HTML = 32; 215 216 /** 217 * All constructs 218 */ 219 public const CONSTRUCT_ALL = 63; 220 221 /** 222 * Don't change case 223 */ 224 public const SAME_CASE = 1; 225 226 /** 227 * Change to lowercase 228 */ 229 public const LOWERCASE = 2; 230 231 /** 232 * Change to uppercase 233 */ 234 public const UPPERCASE = 4; 235 236 /** 237 * PCRE for HTML attributes 238 */ 239 public const PCRE_HTML_ATTRIBUTE = '((?:[\x09\x0A\x0B\x0C\x0D\x20]+[^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"(?:[^"]*)"|\'(?:[^\']*)\'|(?:[^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?)*)[\x09\x0A\x0B\x0C\x0D\x20]*'; 240 241 /** 242 * PCRE for XML attributes 243 */ 244 public const PCRE_XML_ATTRIBUTE = '((?:\s+(?:(?:[^\s:]+:)?[^\s:]+)\s*=\s*(?:"(?:[^"]*)"|\'(?:[^\']*)\'))*)\s*'; 245 246 /** 247 * XML Namespace 248 */ 249 public const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace'; 250 251 /** 252 * Atom 1.0 Namespace 253 */ 254 public const NAMESPACE_ATOM_10 = 'http://www.w3.org/2005/Atom'; 255 256 /** 257 * Atom 0.3 Namespace 258 */ 259 public const NAMESPACE_ATOM_03 = 'http://purl.org/atom/ns#'; 260 261 /** 262 * RDF Namespace 263 */ 264 public const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; 265 266 /** 267 * RSS 0.90 Namespace 268 */ 269 public const NAMESPACE_RSS_090 = 'http://my.netscape.com/rdf/simple/0.9/'; 270 271 /** 272 * RSS 1.0 Namespace 273 */ 274 public const NAMESPACE_RSS_10 = 'http://purl.org/rss/1.0/'; 275 276 /** 277 * RSS 1.0 Content Module Namespace 278 */ 279 public const NAMESPACE_RSS_10_MODULES_CONTENT = 'http://purl.org/rss/1.0/modules/content/'; 280 281 /** 282 * RSS 2.0 Namespace 283 * (Stupid, I know, but I'm certain it will confuse people less with support.) 284 */ 285 public const NAMESPACE_RSS_20 = ''; 286 287 /** 288 * DC 1.0 Namespace 289 */ 290 public const NAMESPACE_DC_10 = 'http://purl.org/dc/elements/1.0/'; 291 292 /** 293 * DC 1.1 Namespace 294 */ 295 public const NAMESPACE_DC_11 = 'http://purl.org/dc/elements/1.1/'; 296 297 /** 298 * W3C Basic Geo (WGS84 lat/long) Vocabulary Namespace 299 */ 300 public const NAMESPACE_W3C_BASIC_GEO = 'http://www.w3.org/2003/01/geo/wgs84_pos#'; 301 302 /** 303 * GeoRSS Namespace 304 */ 305 public const NAMESPACE_GEORSS = 'http://www.georss.org/georss'; 306 307 /** 308 * Media RSS Namespace 309 */ 310 public const NAMESPACE_MEDIARSS = 'http://search.yahoo.com/mrss/'; 311 312 /** 313 * Wrong Media RSS Namespace. Caused by a long-standing typo in the spec. 314 */ 315 public const NAMESPACE_MEDIARSS_WRONG = 'http://search.yahoo.com/mrss'; 316 317 /** 318 * Wrong Media RSS Namespace #2. New namespace introduced in Media RSS 1.5. 319 */ 320 public const NAMESPACE_MEDIARSS_WRONG2 = 'http://video.search.yahoo.com/mrss'; 321 322 /** 323 * Wrong Media RSS Namespace #3. A possible typo of the Media RSS 1.5 namespace. 324 */ 325 public const NAMESPACE_MEDIARSS_WRONG3 = 'http://video.search.yahoo.com/mrss/'; 326 327 /** 328 * Wrong Media RSS Namespace #4. New spec location after the RSS Advisory Board takes it over, but not a valid namespace. 329 */ 330 public const NAMESPACE_MEDIARSS_WRONG4 = 'http://www.rssboard.org/media-rss'; 331 332 /** 333 * Wrong Media RSS Namespace #5. A possible typo of the RSS Advisory Board URL. 334 */ 335 public const NAMESPACE_MEDIARSS_WRONG5 = 'http://www.rssboard.org/media-rss/'; 336 337 /** 338 * iTunes RSS Namespace 339 */ 340 public const NAMESPACE_ITUNES = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; 341 342 /** 343 * XHTML Namespace 344 */ 345 public const NAMESPACE_XHTML = 'http://www.w3.org/1999/xhtml'; 346 347 /** 348 * IANA Link Relations Registry 349 */ 350 public const IANA_LINK_RELATIONS_REGISTRY = 'http://www.iana.org/assignments/relation/'; 351 352 /** 353 * No file source 354 */ 355 public const FILE_SOURCE_NONE = 0; 356 357 /** 358 * Remote file source 359 */ 360 public const FILE_SOURCE_REMOTE = 1; 361 362 /** 363 * Local file source 364 */ 365 public const FILE_SOURCE_LOCAL = 2; 366 367 /** 368 * fsockopen() file source 369 */ 370 public const FILE_SOURCE_FSOCKOPEN = 4; 371 372 /** 373 * cURL file source 374 */ 375 public const FILE_SOURCE_CURL = 8; 376 377 /** 378 * file_get_contents() file source 379 */ 380 public const FILE_SOURCE_FILE_GET_CONTENTS = 16; 381 382 /** 383 * @internal Default value of the HTTP Accept header when fetching/locating feeds 384 */ 385 public const DEFAULT_HTTP_ACCEPT_HEADER = 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1'; 386 387 /** 388 * @var array<string, mixed> Raw data 389 * @access private 390 */ 391 public $data = []; 392 393 /** 394 * @var string|string[]|null Error string (or array when multiple feeds are initialized) 395 * @access private 396 */ 397 public $error = null; 398 399 /** 400 * @var int HTTP status code 401 * @see SimplePie::status_code() 402 * @access private 403 */ 404 public $status_code = 0; 405 406 /** 407 * @var Sanitize instance of Sanitize class 408 * @see SimplePie::set_sanitize_class() 409 * @access private 410 */ 411 public $sanitize; 412 413 /** 414 * @var string SimplePie Useragent 415 * @see SimplePie::set_useragent() 416 * @access private 417 */ 418 public $useragent = ''; 419 420 /** 421 * @var string Feed URL 422 * @see SimplePie::set_feed_url() 423 * @access private 424 */ 425 public $feed_url; 426 427 /** 428 * @var ?string Original feed URL, or new feed URL iff HTTP 301 Moved Permanently 429 * @see SimplePie::subscribe_url() 430 * @access private 431 */ 432 public $permanent_url = null; 433 434 /** 435 * @var File Instance of File class to use as a feed 436 * @see SimplePie::set_file() 437 */ 438 private $file; 439 440 /** 441 * @var string|false Raw feed data 442 * @see SimplePie::set_raw_data() 443 * @access private 444 */ 445 public $raw_data; 446 447 /** 448 * @var int Timeout for fetching remote files 449 * @see SimplePie::set_timeout() 450 * @access private 451 */ 452 public $timeout = 10; 453 454 /** 455 * @var array<int, mixed> Custom curl options 456 * @see SimplePie::set_curl_options() 457 * @access private 458 */ 459 public $curl_options = []; 460 461 /** 462 * @var bool Forces fsockopen() to be used for remote files instead 463 * of cURL, even if a new enough version is installed 464 * @see SimplePie::force_fsockopen() 465 * @access private 466 */ 467 public $force_fsockopen = false; 468 469 /** 470 * @var bool Force the given data/URL to be treated as a feed no matter what 471 * it appears like 472 * @see SimplePie::force_feed() 473 * @access private 474 */ 475 public $force_feed = false; 476 477 /** 478 * @var bool Enable/Disable Caching 479 * @see SimplePie::enable_cache() 480 * @access private 481 */ 482 private $enable_cache = true; 483 484 /** 485 * @var DataCache|null 486 * @see SimplePie::set_cache() 487 */ 488 private $cache = null; 489 490 /** 491 * @var NameFilter 492 * @see SimplePie::set_cache_namefilter() 493 */ 494 private $cache_namefilter; 495 496 /** 497 * @var bool Force SimplePie to fallback to expired cache, if enabled, 498 * when feed is unavailable. 499 * @see SimplePie::force_cache_fallback() 500 * @access private 501 */ 502 public $force_cache_fallback = false; 503 504 /** 505 * @var int Cache duration (in seconds) 506 * @see SimplePie::set_cache_duration() 507 * @access private 508 */ 509 public $cache_duration = 3600; 510 511 /** 512 * @var int Auto-discovery cache duration (in seconds) 513 * @see SimplePie::set_autodiscovery_cache_duration() 514 * @access private 515 */ 516 public $autodiscovery_cache_duration = 604800; // 7 Days. 517 518 /** 519 * @var string Cache location (relative to executing script) 520 * @see SimplePie::set_cache_location() 521 * @access private 522 */ 523 public $cache_location = './cache'; 524 525 /** 526 * @var string&(callable(string): string) Function that creates the cache filename 527 * @see SimplePie::set_cache_name_function() 528 * @access private 529 */ 530 public $cache_name_function = 'md5'; 531 532 /** 533 * @var bool Reorder feed by date descending 534 * @see SimplePie::enable_order_by_date() 535 * @access private 536 */ 537 public $order_by_date = true; 538 539 /** 540 * @var mixed Force input encoding to be set to the follow value 541 * (false, or anything type-cast to false, disables this feature) 542 * @see SimplePie::set_input_encoding() 543 * @access private 544 */ 545 public $input_encoding = false; 546 547 /** 548 * @var self::LOCATOR_* Feed Autodiscovery Level 549 * @see SimplePie::set_autodiscovery_level() 550 * @access private 551 */ 552 public $autodiscovery = self::LOCATOR_ALL; 553 554 /** 555 * Class registry object 556 * 557 * @var Registry 558 */ 559 public $registry; 560 561 /** 562 * @var int Maximum number of feeds to check with autodiscovery 563 * @see SimplePie::set_max_checked_feeds() 564 * @access private 565 */ 566 public $max_checked_feeds = 10; 567 568 /** 569 * @var array<Response>|null All the feeds found during the autodiscovery process 570 * @see SimplePie::get_all_discovered_feeds() 571 * @access private 572 */ 573 public $all_discovered_feeds = []; 574 575 /** 576 * @var string Web-accessible path to the handler_image.php file. 577 * @see SimplePie::set_image_handler() 578 * @access private 579 */ 580 public $image_handler = ''; 581 582 /** 583 * @var array<string> Stores the URLs when multiple feeds are being initialized. 584 * @see SimplePie::set_feed_url() 585 * @access private 586 */ 587 public $multifeed_url = []; 588 589 /** 590 * @var array<int, static> Stores SimplePie objects when multiple feeds initialized. 591 * @access private 592 */ 593 public $multifeed_objects = []; 594 595 /** 596 * @var array<mixed> Stores the get_object_vars() array for use with multifeeds. 597 * @see SimplePie::set_feed_url() 598 * @access private 599 */ 600 public $config_settings = null; 601 602 /** 603 * @var int Stores the number of items to return per-feed with multifeeds. 604 * @see SimplePie::set_item_limit() 605 * @access private 606 */ 607 public $item_limit = 0; 608 609 /** 610 * @var bool Stores if last-modified and/or etag headers were sent with the 611 * request when checking a feed. 612 */ 613 public $check_modified = false; 614 615 /** 616 * @var array<string> Stores the default attributes to be stripped by strip_attributes(). 617 * @see SimplePie::strip_attributes() 618 * @access private 619 */ 620 public $strip_attributes = ['bgsound', 'class', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc']; 621 622 /** 623 * @var array<string, array<string, string>> Stores the default attributes to add to different tags by add_attributes(). 624 * @see SimplePie::add_attributes() 625 * @access private 626 */ 627 public $add_attributes = ['audio' => ['preload' => 'none'], 'iframe' => ['sandbox' => 'allow-scripts allow-same-origin'], 'video' => ['preload' => 'none']]; 628 629 /** 630 * @var array<string> Stores the default tags to be stripped by strip_htmltags(). 631 * @see SimplePie::strip_htmltags() 632 * @access private 633 */ 634 public $strip_htmltags = ['base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style']; 635 636 /** 637 * @var string[]|string Stores the default attributes to be renamed by rename_attributes(). 638 * @see SimplePie::rename_attributes() 639 * @access private 640 */ 641 public $rename_attributes = []; 642 643 /** 644 * @var bool Should we throw exceptions, or use the old-style error property? 645 * @access private 646 */ 647 public $enable_exceptions = false; 648 649 /** 650 * @var Client|null 651 */ 652 private $http_client = null; 653 654 /** @var bool Whether HTTP client has been injected */ 655 private $http_client_injected = false; 656 657 /** 658 * The SimplePie class contains feed level data and options 659 * 660 * To use SimplePie, create the SimplePie object with no parameters. You can 661 * then set configuration options using the provided methods. After setting 662 * them, you must initialise the feed using $feed->init(). At that point the 663 * object's methods and properties will be available to you. 664 * 665 * Previously, it was possible to pass in the feed URL along with cache 666 * options directly into the constructor. This has been removed as of 1.3 as 667 * it caused a lot of confusion. 668 * 669 * @since 1.0 Preview Release 670 */ 671 public function __construct() 672 { 673 if (version_compare(PHP_VERSION, '7.2', '<')) { 674 exit('Please upgrade to PHP 7.2 or newer.'); 675 } 676 677 $this->set_useragent(); 678 679 $this->set_cache_namefilter(new CallableNameFilter($this->cache_name_function)); 680 681 // Other objects, instances created here so we can set options on them 682 $this->sanitize = new Sanitize(); 683 $this->registry = new Registry(); 684 685 if (func_num_args() > 0) { 686 trigger_error('Passing parameters to the constructor is no longer supported. Please use set_feed_url(), set_cache_location(), and set_cache_duration() directly.', \E_USER_DEPRECATED); 687 688 $args = func_get_args(); 689 switch (count($args)) { 690 case 3: 691 $this->set_cache_duration($args[2]); 692 // no break 693 case 2: 694 $this->set_cache_location($args[1]); 695 // no break 696 case 1: 697 $this->set_feed_url($args[0]); 698 $this->init(); 699 } 700 } 701 } 702 703 /** 704 * Used for converting object to a string 705 * @return string 706 */ 707 public function __toString() 708 { 709 return md5(serialize($this->data)); 710 } 711 712 /** 713 * Remove items that link back to this before destroying this object 714 * @return void 715 */ 716 public function __destruct() 717 { 718 if (!gc_enabled()) { 719 if (!empty($this->data['items'])) { 720 foreach ($this->data['items'] as $item) { 721 $item->__destruct(); 722 } 723 unset($item, $this->data['items']); 724 } 725 if (!empty($this->data['ordered_items'])) { 726 foreach ($this->data['ordered_items'] as $item) { 727 $item->__destruct(); 728 } 729 unset($item, $this->data['ordered_items']); 730 } 731 } 732 } 733 734 /** 735 * Force the given data/URL to be treated as a feed 736 * 737 * This tells SimplePie to ignore the content-type provided by the server. 738 * Be careful when using this option, as it will also disable autodiscovery. 739 * 740 * @since 1.1 741 * @param bool $enable Force the given data/URL to be treated as a feed 742 * @return void 743 */ 744 public function force_feed(bool $enable = false) 745 { 746 $this->force_feed = $enable; 747 } 748 749 /** 750 * Set the URL of the feed you want to parse 751 * 752 * This allows you to enter the URL of the feed you want to parse, or the 753 * website you want to try to use auto-discovery on. This takes priority 754 * over any set raw data. 755 * 756 * Deprecated since 1.9.0: You can set multiple feeds to mash together by passing an array instead 757 * of a string for the $url. Remember that with each additional feed comes 758 * additional processing and resources. 759 * 760 * @since 1.0 Preview Release 761 * @see set_raw_data() 762 * @param string|string[] $url This is the URL (or (deprecated) array of URLs) that you want to parse. 763 * @return void 764 */ 765 public function set_feed_url($url) 766 { 767 $this->multifeed_url = []; 768 if (is_array($url)) { 769 trigger_error('Fetching multiple feeds with single SimplePie instance is deprecated since SimplePie 1.9.0, create one SimplePie instance per feed and use SimplePie::merge_items to get a single list of items.', \E_USER_DEPRECATED); 770 foreach ($url as $value) { 771 $this->multifeed_url[] = $this->registry->call(Misc::class, 'fix_protocol', [$value, 1]); 772 } 773 } else { 774 $this->feed_url = $this->registry->call(Misc::class, 'fix_protocol', [$url, 1]); 775 $this->permanent_url = $this->feed_url; 776 } 777 } 778 779 /** 780 * Set an instance of {@see File} to use as a feed 781 * 782 * @deprecated since SimplePie 1.9.0, use \SimplePie\SimplePie::set_http_client() or \SimplePie\SimplePie::set_raw_data() instead. 783 * 784 * @param File &$file 785 * @return bool True on success, false on failure 786 */ 787 public function set_file(File &$file) 788 { 789 // trigger_error(sprintf('SimplePie\SimplePie::set_file() is deprecated since SimplePie 1.9.0, please use "SimplePie\SimplePie::set_http_client()" or "SimplePie\SimplePie::set_raw_data()" instead.'), \E_USER_DEPRECATED); 790 791 $this->feed_url = $file->get_final_requested_uri(); 792 $this->permanent_url = $this->feed_url; 793 $this->file = &$file; 794 795 return true; 796 } 797 798 /** 799 * Set the raw XML data to parse 800 * 801 * Allows you to use a string of RSS/Atom data instead of a remote feed. 802 * 803 * If you have a feed available as a string in PHP, you can tell SimplePie 804 * to parse that data string instead of a remote feed. Any set feed URL 805 * takes precedence. 806 * 807 * @since 1.0 Beta 3 808 * @param string $data RSS or Atom data as a string. 809 * @see set_feed_url() 810 * @return void 811 */ 812 public function set_raw_data(string $data) 813 { 814 $this->raw_data = $data; 815 } 816 817 /** 818 * Set a PSR-18 client and PSR-17 factories 819 * 820 * Allows you to use your own HTTP client implementations. 821 * This will become required with SimplePie 2.0.0. 822 */ 823 final public function set_http_client( 824 ClientInterface $http_client, 825 RequestFactoryInterface $request_factory, 826 UriFactoryInterface $uri_factory 827 ): void { 828 $this->http_client = new Psr18Client($http_client, $request_factory, $uri_factory); 829 } 830 831 /** 832 * Set the default timeout for fetching remote feeds 833 * 834 * This allows you to change the maximum time the feed's server to respond 835 * and send the feed back. 836 * 837 * @since 1.0 Beta 3 838 * @param int $timeout The maximum number of seconds to spend waiting to retrieve a feed. 839 * @return void 840 */ 841 public function set_timeout(int $timeout = 10) 842 { 843 if ($this->http_client_injected) { 844 throw new SimplePieException(sprintf( 845 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure timeout in your HTTP client instead.', 846 __METHOD__, 847 self::class 848 )); 849 } 850 851 $this->timeout = (int) $timeout; 852 853 // Reset a possible existing FileClient, 854 // so a new client with the changed value will be created 855 if (is_object($this->http_client) && $this->http_client instanceof FileClient) { 856 $this->http_client = null; 857 } elseif (is_object($this->http_client)) { 858 // Trigger notice if a PSR-18 client was set 859 trigger_error(sprintf( 860 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure the timeout in your HTTP client instead.', 861 __METHOD__, 862 get_class($this) 863 ), \E_USER_NOTICE); 864 } 865 } 866 867 /** 868 * Set custom curl options 869 * 870 * This allows you to change default curl options 871 * 872 * @since 1.0 Beta 3 873 * @param array<int, mixed> $curl_options Curl options to add to default settings 874 * @return void 875 */ 876 public function set_curl_options(array $curl_options = []) 877 { 878 if ($this->http_client_injected) { 879 throw new SimplePieException(sprintf( 880 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure custom curl options in your HTTP client instead.', 881 __METHOD__, 882 self::class 883 )); 884 } 885 886 $this->curl_options = $curl_options; 887 888 // Reset a possible existing FileClient, 889 // so a new client with the changed value will be created 890 if (is_object($this->http_client) && $this->http_client instanceof FileClient) { 891 $this->http_client = null; 892 } elseif (is_object($this->http_client)) { 893 // Trigger notice if a PSR-18 client was set 894 trigger_error(sprintf( 895 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure the curl options in your HTTP client instead.', 896 __METHOD__, 897 get_class($this) 898 ), \E_USER_NOTICE); 899 } 900 } 901 902 /** 903 * Force SimplePie to use fsockopen() instead of cURL 904 * 905 * @since 1.0 Beta 3 906 * @param bool $enable Force fsockopen() to be used 907 * @return void 908 */ 909 public function force_fsockopen(bool $enable = false) 910 { 911 if ($this->http_client_injected) { 912 throw new SimplePieException(sprintf( 913 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure fsockopen in your HTTP client instead.', 914 __METHOD__, 915 self::class 916 )); 917 } 918 919 $this->force_fsockopen = $enable; 920 921 // Reset a possible existing FileClient, 922 // so a new client with the changed value will be created 923 if (is_object($this->http_client) && $this->http_client instanceof FileClient) { 924 $this->http_client = null; 925 } elseif (is_object($this->http_client)) { 926 // Trigger notice if a PSR-18 client was set 927 trigger_error(sprintf( 928 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure fsockopen in your HTTP client instead.', 929 __METHOD__, 930 get_class($this) 931 ), \E_USER_NOTICE); 932 } 933 } 934 935 /** 936 * Enable/disable caching in SimplePie. 937 * 938 * This option allows you to disable caching all-together in SimplePie. 939 * However, disabling the cache can lead to longer load times. 940 * 941 * @since 1.0 Preview Release 942 * @param bool $enable Enable caching 943 * @return void 944 */ 945 public function enable_cache(bool $enable = true) 946 { 947 $this->enable_cache = $enable; 948 } 949 950 /** 951 * Set a PSR-16 implementation as cache 952 * 953 * @param CacheInterface $cache The PSR-16 cache implementation 954 * 955 * @return void 956 */ 957 public function set_cache(CacheInterface $cache) 958 { 959 $this->cache = new Psr16($cache); 960 } 961 962 /** 963 * SimplePie to continue to fall back to expired cache, if enabled, when 964 * feed is unavailable. 965 * 966 * This tells SimplePie to ignore any file errors and fall back to cache 967 * instead. This only works if caching is enabled and cached content 968 * still exists. 969 * 970 * @deprecated since SimplePie 1.8.0, expired cache will not be used anymore. 971 * 972 * @param bool $enable Force use of cache on fail. 973 * @return void 974 */ 975 public function force_cache_fallback(bool $enable = false) 976 { 977 // @trigger_error(sprintf('SimplePie\SimplePie::force_cache_fallback() is deprecated since SimplePie 1.8.0, expired cache will not be used anymore.'), \E_USER_DEPRECATED); 978 $this->force_cache_fallback = $enable; 979 } 980 981 /** 982 * Set the length of time (in seconds) that the contents of a feed will be 983 * cached 984 * 985 * @param int $seconds The feed content cache duration 986 * @return void 987 */ 988 public function set_cache_duration(int $seconds = 3600) 989 { 990 $this->cache_duration = $seconds; 991 } 992 993 /** 994 * Set the length of time (in seconds) that the autodiscovered feed URL will 995 * be cached 996 * 997 * @param int $seconds The autodiscovered feed URL cache duration. 998 * @return void 999 */ 1000 public function set_autodiscovery_cache_duration(int $seconds = 604800) 1001 { 1002 $this->autodiscovery_cache_duration = $seconds; 1003 } 1004 1005 /** 1006 * Set the file system location where the cached files should be stored 1007 * 1008 * @deprecated since SimplePie 1.8.0, use SimplePie::set_cache() instead. 1009 * 1010 * @param string $location The file system location. 1011 * @return void 1012 */ 1013 public function set_cache_location(string $location = './cache') 1014 { 1015 // @trigger_error(sprintf('SimplePie\SimplePie::set_cache_location() is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache()" instead.'), \E_USER_DEPRECATED); 1016 $this->cache_location = $location; 1017 } 1018 1019 /** 1020 * Return the filename (i.e. hash, without path and without extension) of the file to cache a given URL. 1021 * 1022 * @param string $url The URL of the feed to be cached. 1023 * @return string A filename (i.e. hash, without path and without extension). 1024 */ 1025 public function get_cache_filename(string $url) 1026 { 1027 // Append custom parameters to the URL to avoid cache pollution in case of multiple calls with different parameters. 1028 $url .= $this->force_feed ? '#force_feed' : ''; 1029 $options = []; 1030 if ($this->timeout != 10) { 1031 $options[CURLOPT_TIMEOUT] = $this->timeout; 1032 } 1033 if ($this->useragent !== Misc::get_default_useragent()) { 1034 $options[CURLOPT_USERAGENT] = $this->useragent; 1035 } 1036 if (!empty($this->curl_options)) { 1037 foreach ($this->curl_options as $k => $v) { 1038 $options[$k] = $v; 1039 } 1040 } 1041 if (!empty($options)) { 1042 ksort($options); 1043 $url .= '#' . urlencode(var_export($options, true)); 1044 } 1045 1046 return $this->cache_namefilter->filter($url); 1047 } 1048 1049 /** 1050 * Set whether feed items should be sorted into reverse chronological order 1051 * 1052 * @param bool $enable Sort as reverse chronological order. 1053 * @return void 1054 */ 1055 public function enable_order_by_date(bool $enable = true) 1056 { 1057 $this->order_by_date = $enable; 1058 } 1059 1060 /** 1061 * Set the character encoding used to parse the feed 1062 * 1063 * This overrides the encoding reported by the feed, however it will fall 1064 * back to the normal encoding detection if the override fails 1065 * 1066 * @param string|false $encoding Character encoding 1067 * @return void 1068 */ 1069 public function set_input_encoding($encoding = false) 1070 { 1071 if ($encoding) { 1072 $this->input_encoding = (string) $encoding; 1073 } else { 1074 $this->input_encoding = false; 1075 } 1076 } 1077 1078 /** 1079 * Set how much feed autodiscovery to do 1080 * 1081 * @see self::LOCATOR_NONE 1082 * @see self::LOCATOR_AUTODISCOVERY 1083 * @see self::LOCATOR_LOCAL_EXTENSION 1084 * @see self::LOCATOR_LOCAL_BODY 1085 * @see self::LOCATOR_REMOTE_EXTENSION 1086 * @see self::LOCATOR_REMOTE_BODY 1087 * @see self::LOCATOR_ALL 1088 * @param self::LOCATOR_* $level Feed Autodiscovery Level (level can be a combination of the above constants, see bitwise OR operator) 1089 * @return void 1090 */ 1091 public function set_autodiscovery_level(int $level = self::LOCATOR_ALL) 1092 { 1093 $this->autodiscovery = $level; 1094 } 1095 1096 /** 1097 * Get the class registry 1098 * 1099 * Use this to override SimplePie's default classes 1100 * 1101 * @return Registry 1102 */ 1103 public function &get_registry() 1104 { 1105 return $this->registry; 1106 } 1107 1108 /** 1109 * Set which class SimplePie uses for caching 1110 * 1111 * @deprecated since SimplePie 1.3, use {@see set_cache()} instead 1112 * 1113 * @param class-string<Cache> $class Name of custom class 1114 * 1115 * @return bool True on success, false otherwise 1116 */ 1117 public function set_cache_class(string $class = Cache::class) 1118 { 1119 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::set_cache()" instead.', __METHOD__), \E_USER_DEPRECATED); 1120 1121 return $this->registry->register(Cache::class, $class, true); 1122 } 1123 1124 /** 1125 * Set which class SimplePie uses for auto-discovery 1126 * 1127 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1128 * 1129 * @param class-string<Locator> $class Name of custom class 1130 * 1131 * @return bool True on success, false otherwise 1132 */ 1133 public function set_locator_class(string $class = Locator::class) 1134 { 1135 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1136 1137 return $this->registry->register(Locator::class, $class, true); 1138 } 1139 1140 /** 1141 * Set which class SimplePie uses for XML parsing 1142 * 1143 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1144 * 1145 * @param class-string<Parser> $class Name of custom class 1146 * 1147 * @return bool True on success, false otherwise 1148 */ 1149 public function set_parser_class(string $class = Parser::class) 1150 { 1151 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1152 1153 return $this->registry->register(Parser::class, $class, true); 1154 } 1155 1156 /** 1157 * Set which class SimplePie uses for remote file fetching 1158 * 1159 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1160 * 1161 * @param class-string<File> $class Name of custom class 1162 * 1163 * @return bool True on success, false otherwise 1164 */ 1165 public function set_file_class(string $class = File::class) 1166 { 1167 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1168 1169 return $this->registry->register(File::class, $class, true); 1170 } 1171 1172 /** 1173 * Set which class SimplePie uses for data sanitization 1174 * 1175 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1176 * 1177 * @param class-string<Sanitize> $class Name of custom class 1178 * 1179 * @return bool True on success, false otherwise 1180 */ 1181 public function set_sanitize_class(string $class = Sanitize::class) 1182 { 1183 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1184 1185 return $this->registry->register(Sanitize::class, $class, true); 1186 } 1187 1188 /** 1189 * Set which class SimplePie uses for handling feed items 1190 * 1191 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1192 * 1193 * @param class-string<Item> $class Name of custom class 1194 * 1195 * @return bool True on success, false otherwise 1196 */ 1197 public function set_item_class(string $class = Item::class) 1198 { 1199 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1200 1201 return $this->registry->register(Item::class, $class, true); 1202 } 1203 1204 /** 1205 * Set which class SimplePie uses for handling author data 1206 * 1207 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1208 * 1209 * @param class-string<Author> $class Name of custom class 1210 * 1211 * @return bool True on success, false otherwise 1212 */ 1213 public function set_author_class(string $class = Author::class) 1214 { 1215 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1216 1217 return $this->registry->register(Author::class, $class, true); 1218 } 1219 1220 /** 1221 * Set which class SimplePie uses for handling category data 1222 * 1223 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1224 * 1225 * @param class-string<Category> $class Name of custom class 1226 * 1227 * @return bool True on success, false otherwise 1228 */ 1229 public function set_category_class(string $class = Category::class) 1230 { 1231 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1232 1233 return $this->registry->register(Category::class, $class, true); 1234 } 1235 1236 /** 1237 * Set which class SimplePie uses for feed enclosures 1238 * 1239 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1240 * 1241 * @param class-string<Enclosure> $class Name of custom class 1242 * 1243 * @return bool True on success, false otherwise 1244 */ 1245 public function set_enclosure_class(string $class = Enclosure::class) 1246 { 1247 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1248 1249 return $this->registry->register(Enclosure::class, $class, true); 1250 } 1251 1252 /** 1253 * Set which class SimplePie uses for `<media:text>` captions 1254 * 1255 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1256 * 1257 * @param class-string<Caption> $class Name of custom class 1258 * 1259 * @return bool True on success, false otherwise 1260 */ 1261 public function set_caption_class(string $class = Caption::class) 1262 { 1263 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1264 1265 return $this->registry->register(Caption::class, $class, true); 1266 } 1267 1268 /** 1269 * Set which class SimplePie uses for `<media:copyright>` 1270 * 1271 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1272 * 1273 * @param class-string<Copyright> $class Name of custom class 1274 * 1275 * @return bool True on success, false otherwise 1276 */ 1277 public function set_copyright_class(string $class = Copyright::class) 1278 { 1279 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1280 1281 return $this->registry->register(Copyright::class, $class, true); 1282 } 1283 1284 /** 1285 * Set which class SimplePie uses for `<media:credit>` 1286 * 1287 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1288 * 1289 * @param class-string<Credit> $class Name of custom class 1290 * 1291 * @return bool True on success, false otherwise 1292 */ 1293 public function set_credit_class(string $class = Credit::class) 1294 { 1295 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1296 1297 return $this->registry->register(Credit::class, $class, true); 1298 } 1299 1300 /** 1301 * Set which class SimplePie uses for `<media:rating>` 1302 * 1303 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1304 * 1305 * @param class-string<Rating> $class Name of custom class 1306 * 1307 * @return bool True on success, false otherwise 1308 */ 1309 public function set_rating_class(string $class = Rating::class) 1310 { 1311 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1312 1313 return $this->registry->register(Rating::class, $class, true); 1314 } 1315 1316 /** 1317 * Set which class SimplePie uses for `<media:restriction>` 1318 * 1319 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1320 * 1321 * @param class-string<Restriction> $class Name of custom class 1322 * 1323 * @return bool True on success, false otherwise 1324 */ 1325 public function set_restriction_class(string $class = Restriction::class) 1326 { 1327 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1328 1329 return $this->registry->register(Restriction::class, $class, true); 1330 } 1331 1332 /** 1333 * Set which class SimplePie uses for content-type sniffing 1334 * 1335 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1336 * 1337 * @param class-string<Sniffer> $class Name of custom class 1338 * 1339 * @return bool True on success, false otherwise 1340 */ 1341 public function set_content_type_sniffer_class(string $class = Sniffer::class) 1342 { 1343 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1344 1345 return $this->registry->register(Sniffer::class, $class, true); 1346 } 1347 1348 /** 1349 * Set which class SimplePie uses item sources 1350 * 1351 * @deprecated since SimplePie 1.3, use {@see get_registry()} instead 1352 * 1353 * @param class-string<Source> $class Name of custom class 1354 * 1355 * @return bool True on success, false otherwise 1356 */ 1357 public function set_source_class(string $class = Source::class) 1358 { 1359 trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.3, please use "SimplePie\SimplePie::get_registry()" instead.', __METHOD__), \E_USER_DEPRECATED); 1360 1361 return $this->registry->register(Source::class, $class, true); 1362 } 1363 1364 /** 1365 * Set the user agent string 1366 * 1367 * @param string $ua New user agent string. 1368 * @return void 1369 */ 1370 public function set_useragent(?string $ua = null) 1371 { 1372 if ($this->http_client_injected) { 1373 throw new SimplePieException(sprintf( 1374 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure user agent string in your HTTP client instead.', 1375 __METHOD__, 1376 self::class 1377 )); 1378 } 1379 1380 if ($ua === null) { 1381 $ua = Misc::get_default_useragent(); 1382 } 1383 1384 $this->useragent = (string) $ua; 1385 1386 // Reset a possible existing FileClient, 1387 // so a new client with the changed value will be created 1388 if (is_object($this->http_client) && $this->http_client instanceof FileClient) { 1389 $this->http_client = null; 1390 } elseif (is_object($this->http_client)) { 1391 // Trigger notice if a PSR-18 client was set 1392 trigger_error(sprintf( 1393 'Using "%s()" has no effect, because you already provided a HTTP client with "%s::set_http_client()". Configure the useragent in your HTTP client instead.', 1394 __METHOD__, 1395 get_class($this) 1396 ), \E_USER_NOTICE); 1397 } 1398 } 1399 1400 /** 1401 * Set a namefilter to modify the cache filename with 1402 * 1403 * @param NameFilter $filter 1404 * 1405 * @return void 1406 */ 1407 public function set_cache_namefilter(NameFilter $filter): void 1408 { 1409 $this->cache_namefilter = $filter; 1410 } 1411 1412 /** 1413 * Set callback function to create cache filename with 1414 * 1415 * @deprecated since SimplePie 1.8.0, use {@see set_cache_namefilter()} instead 1416 * 1417 * @param (string&(callable(string): string))|null $function Callback function 1418 * @return void 1419 */ 1420 public function set_cache_name_function(?string $function = null) 1421 { 1422 // trigger_error(sprintf('"%s()" is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache_namefilter()" instead.', __METHOD__), \E_USER_DEPRECATED); 1423 1424 if ($function === null) { 1425 $function = 'md5'; 1426 } 1427 1428 $this->cache_name_function = $function; 1429 1430 $this->set_cache_namefilter(new CallableNameFilter($this->cache_name_function)); 1431 } 1432 1433 /** 1434 * Set options to make SP as fast as possible 1435 * 1436 * Forgoes a substantial amount of data sanitization in favor of speed. This 1437 * turns SimplePie into a dumb parser of feeds. 1438 * 1439 * @param bool $set Whether to set them or not 1440 * @return void 1441 */ 1442 public function set_stupidly_fast(bool $set = false) 1443 { 1444 if ($set) { 1445 $this->enable_order_by_date(false); 1446 $this->remove_div(false); 1447 $this->strip_comments(false); 1448 $this->strip_htmltags([]); 1449 $this->strip_attributes([]); 1450 $this->add_attributes([]); 1451 $this->set_image_handler(false); 1452 $this->set_https_domains([]); 1453 } 1454 } 1455 1456 /** 1457 * Set maximum number of feeds to check with autodiscovery 1458 * 1459 * @param int $max Maximum number of feeds to check 1460 * @return void 1461 */ 1462 public function set_max_checked_feeds(int $max = 10) 1463 { 1464 $this->max_checked_feeds = $max; 1465 } 1466 1467 /** 1468 * @return void 1469 */ 1470 public function remove_div(bool $enable = true) 1471 { 1472 $this->sanitize->remove_div($enable); 1473 } 1474 1475 /** 1476 * @param string[]|string|false $tags Set a list of tags to strip, or set empty string to use default tags, or false to strip nothing. 1477 * @return void 1478 */ 1479 public function strip_htmltags($tags = '', ?bool $encode = null) 1480 { 1481 if ($tags === '') { 1482 $tags = $this->strip_htmltags; 1483 } 1484 $this->sanitize->strip_htmltags($tags); 1485 if ($encode !== null) { 1486 $this->sanitize->encode_instead_of_strip($encode); 1487 } 1488 } 1489 1490 /** 1491 * @return void 1492 */ 1493 public function encode_instead_of_strip(bool $enable = true) 1494 { 1495 $this->sanitize->encode_instead_of_strip($enable); 1496 } 1497 1498 /** 1499 * @param string[]|string $attribs 1500 * @return void 1501 */ 1502 public function rename_attributes($attribs = '') 1503 { 1504 if ($attribs === '') { 1505 $attribs = $this->rename_attributes; 1506 } 1507 $this->sanitize->rename_attributes($attribs); 1508 } 1509 1510 /** 1511 * @param string[]|string $attribs 1512 * @return void 1513 */ 1514 public function strip_attributes($attribs = '') 1515 { 1516 if ($attribs === '') { 1517 $attribs = $this->strip_attributes; 1518 } 1519 $this->sanitize->strip_attributes($attribs); 1520 } 1521 1522 /** 1523 * @param array<string, array<string, string>>|'' $attribs 1524 * @return void 1525 */ 1526 public function add_attributes($attribs = '') 1527 { 1528 if ($attribs === '') { 1529 $attribs = $this->add_attributes; 1530 } 1531 $this->sanitize->add_attributes($attribs); 1532 } 1533 1534 /** 1535 * Set the output encoding 1536 * 1537 * Allows you to override SimplePie's output to match that of your webpage. 1538 * This is useful for times when your webpages are not being served as 1539 * UTF-8. This setting will be obeyed by {@see handle_content_type()}, and 1540 * is similar to {@see set_input_encoding()}. 1541 * 1542 * It should be noted, however, that not all character encodings can support 1543 * all characters. If your page is being served as ISO-8859-1 and you try 1544 * to display a Japanese feed, you'll likely see garbled characters. 1545 * Because of this, it is highly recommended to ensure that your webpages 1546 * are served as UTF-8. 1547 * 1548 * The number of supported character encodings depends on whether your web 1549 * host supports {@link http://php.net/mbstring mbstring}, 1550 * {@link http://php.net/iconv iconv}, or both. See 1551 * {@link http://simplepie.org/wiki/faq/Supported_Character_Encodings} for 1552 * more information. 1553 * 1554 * @param string $encoding 1555 * @return void 1556 */ 1557 public function set_output_encoding(string $encoding = 'UTF-8') 1558 { 1559 $this->sanitize->set_output_encoding($encoding); 1560 } 1561 1562 /** 1563 * @return void 1564 */ 1565 public function strip_comments(bool $strip = false) 1566 { 1567 $this->sanitize->strip_comments($strip); 1568 } 1569 1570 /** 1571 * Set element/attribute key/value pairs of HTML attributes 1572 * containing URLs that need to be resolved relative to the feed 1573 * 1574 * Defaults to |a|@href, |area|@href, |blockquote|@cite, |del|@cite, 1575 * |form|@action, |img|@longdesc, |img|@src, |input|@src, |ins|@cite, 1576 * |q|@cite 1577 * 1578 * @since 1.0 1579 * @param array<string, string|string[]>|null $element_attribute Element/attribute key/value pairs, null for default 1580 * @return void 1581 */ 1582 public function set_url_replacements(?array $element_attribute = null) 1583 { 1584 $this->sanitize->set_url_replacements($element_attribute); 1585 } 1586 1587 /** 1588 * Set the list of domains for which to force HTTPS. 1589 * @see Sanitize::set_https_domains() 1590 * @param array<string> $domains List of HTTPS domains. Example array('biz', 'example.com', 'example.org', 'www.example.net'). 1591 * @return void 1592 */ 1593 public function set_https_domains(array $domains = []) 1594 { 1595 $this->sanitize->set_https_domains($domains); 1596 } 1597 1598 /** 1599 * Set the handler to enable the display of cached images. 1600 * 1601 * @param string|false $page Web-accessible path to the handler_image.php file. 1602 * @param string $qs The query string that the value should be passed to. 1603 * @return void 1604 */ 1605 public function set_image_handler($page = false, string $qs = 'i') 1606 { 1607 if ($page !== false) { 1608 $this->sanitize->set_image_handler($page . '?' . $qs . '='); 1609 } else { 1610 $this->image_handler = ''; 1611 } 1612 } 1613 1614 /** 1615 * Set the limit for items returned per-feed with multifeeds 1616 * 1617 * @param int $limit The maximum number of items to return. 1618 * @return void 1619 */ 1620 public function set_item_limit(int $limit = 0) 1621 { 1622 $this->item_limit = $limit; 1623 } 1624 1625 /** 1626 * Enable throwing exceptions 1627 * 1628 * @param bool $enable Should we throw exceptions, or use the old-style error property? 1629 * @return void 1630 */ 1631 public function enable_exceptions(bool $enable = true) 1632 { 1633 $this->enable_exceptions = $enable; 1634 } 1635 1636 /** 1637 * Initialize the feed object 1638 * 1639 * This is what makes everything happen. Period. This is where all of the 1640 * configuration options get processed, feeds are fetched, cached, and 1641 * parsed, and all of that other good stuff. 1642 * 1643 * @return bool True if successful, false otherwise 1644 */ 1645 public function init() 1646 { 1647 // Check absolute bare minimum requirements. 1648 if (!extension_loaded('xml') || !extension_loaded('pcre')) { 1649 $this->error = 'XML or PCRE extensions not loaded!'; 1650 return false; 1651 } 1652 // Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader. 1653 elseif (!extension_loaded('xmlreader')) { 1654 static $xml_is_sane = null; 1655 if ($xml_is_sane === null) { 1656 $parser_check = xml_parser_create(); 1657 xml_parse_into_struct($parser_check, '<foo>&</foo>', $values); 1658 if (\PHP_VERSION_ID < 80000) { 1659 xml_parser_free($parser_check); 1660 } 1661 $xml_is_sane = isset($values[0]['value']); 1662 } 1663 if (!$xml_is_sane) { 1664 return false; 1665 } 1666 } 1667 1668 // The default sanitize class gets set in the constructor, check if it has 1669 // changed. 1670 if ($this->registry->get_class(Sanitize::class) !== Sanitize::class) { 1671 $this->sanitize = $this->registry->create(Sanitize::class); 1672 } 1673 if (method_exists($this->sanitize, 'set_registry')) { 1674 $this->sanitize->set_registry($this->registry); 1675 } 1676 1677 // Pass whatever was set with config options over to the sanitizer. 1678 // Pass the classes in for legacy support; new classes should use the registry instead 1679 $cache = $this->registry->get_class(Cache::class); 1680 \assert($cache !== null, 'Cache must be defined'); 1681 $this->sanitize->pass_cache_data( 1682 $this->enable_cache, 1683 $this->cache_location, 1684 $this->cache_namefilter, 1685 $cache, 1686 $this->cache 1687 ); 1688 1689 $http_client = $this->get_http_client(); 1690 1691 if ($http_client instanceof Psr18Client) { 1692 $this->sanitize->set_http_client( 1693 $http_client->getHttpClient(), 1694 $http_client->getRequestFactory(), 1695 $http_client->getUriFactory() 1696 ); 1697 } 1698 1699 if (!empty($this->multifeed_url)) { 1700 $i = 0; 1701 $success = 0; 1702 $this->multifeed_objects = []; 1703 $this->error = []; 1704 foreach ($this->multifeed_url as $url) { 1705 $this->multifeed_objects[$i] = clone $this; 1706 $this->multifeed_objects[$i]->set_feed_url($url); 1707 $single_success = $this->multifeed_objects[$i]->init(); 1708 $success |= $single_success; 1709 if (!$single_success) { 1710 $this->error[$i] = $this->multifeed_objects[$i]->error(); 1711 } 1712 $i++; 1713 } 1714 return (bool) $success; 1715 } elseif ($this->feed_url === null && $this->raw_data === null) { 1716 return false; 1717 } 1718 1719 $this->error = null; 1720 $this->data = []; 1721 $this->check_modified = false; 1722 $this->multifeed_objects = []; 1723 $cache = false; 1724 1725 if ($this->feed_url !== null) { 1726 $parsed_feed_url = $this->registry->call(Misc::class, 'parse_url', [$this->feed_url]); 1727 1728 // Decide whether to enable caching 1729 if ($this->enable_cache && $parsed_feed_url['scheme'] !== '') { 1730 $cache = $this->get_cache($this->feed_url); 1731 } 1732 1733 // Fetch the data into $this->raw_data 1734 if (($fetched = $this->fetch_data($cache)) === true) { 1735 return true; 1736 } elseif ($fetched === false) { 1737 return false; 1738 } 1739 1740 [$headers, $sniffed] = $fetched; 1741 } 1742 1743 // Empty response check 1744 if (empty($this->raw_data)) { 1745 $this->error = "A feed could not be found at `$this->feed_url`. Empty body."; 1746 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); 1747 return false; 1748 } 1749 1750 // Set up array of possible encodings 1751 $encodings = []; 1752 1753 // First check to see if input has been overridden. 1754 if ($this->input_encoding !== false) { 1755 $encodings[] = strtoupper($this->input_encoding); 1756 } 1757 1758 $application_types = ['application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity']; 1759 $text_types = ['text/xml', 'text/xml-external-parsed-entity']; 1760 1761 // RFC 3023 (only applies to sniffed content) 1762 if (isset($sniffed)) { 1763 if (in_array($sniffed, $application_types) || substr($sniffed, 0, 12) === 'application/' && substr($sniffed, -4) === '+xml') { 1764 if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) { 1765 $encodings[] = strtoupper($charset[1]); 1766 } 1767 $encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry])); 1768 $encodings[] = 'UTF-8'; 1769 } elseif (in_array($sniffed, $text_types) || substr($sniffed, 0, 5) === 'text/' && substr($sniffed, -4) === '+xml') { 1770 if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) { 1771 $encodings[] = strtoupper($charset[1]); 1772 } 1773 $encodings[] = 'US-ASCII'; 1774 } 1775 // Text MIME-type default 1776 elseif (substr($sniffed, 0, 5) === 'text/') { 1777 $encodings[] = 'UTF-8'; 1778 } 1779 } 1780 1781 // Fallback to XML 1.0 Appendix F.1/UTF-8/ISO-8859-1 1782 $encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry])); 1783 $encodings[] = 'UTF-8'; 1784 $encodings[] = 'ISO-8859-1'; 1785 1786 // There's no point in trying an encoding twice 1787 $encodings = array_unique($encodings); 1788 1789 // Loop through each possible encoding, till we return something, or run out of possibilities 1790 foreach ($encodings as $encoding) { 1791 // Change the encoding to UTF-8 (as we always use UTF-8 internally) 1792 if ($utf8_data = $this->registry->call(Misc::class, 'change_encoding', [$this->raw_data, $encoding, 'UTF-8'])) { 1793 // Create new parser 1794 $parser = $this->registry->create(Parser::class); 1795 1796 // If it's parsed fine 1797 if ($parser->parse($utf8_data, 'UTF-8', $this->permanent_url ?? '')) { 1798 $this->data = $parser->get_data(); 1799 if (!($this->get_type() & ~self::TYPE_NONE)) { 1800 $this->error = "A feed could not be found at `$this->feed_url`. This does not appear to be a valid RSS or Atom feed."; 1801 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); 1802 return false; 1803 } 1804 1805 if (isset($headers)) { 1806 $this->data['headers'] = $headers; 1807 } 1808 $this->data['build'] = Misc::get_build(); 1809 1810 // Cache the file if caching is enabled 1811 $this->data['cache_expiration_time'] = $this->cache_duration + time(); 1812 1813 if ($cache && !$cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->cache_duration)) { 1814 trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); 1815 } 1816 return true; 1817 } 1818 } 1819 } 1820 1821 if (isset($parser)) { 1822 // We have an error, just set Misc::error to it and quit 1823 $this->error = $this->feed_url; 1824 $this->error .= sprintf(' is invalid XML, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column()); 1825 } else { 1826 $this->error = 'The data could not be converted to UTF-8.'; 1827 if (!extension_loaded('mbstring') && !extension_loaded('iconv') && !class_exists('\UConverter')) { 1828 $this->error .= ' You MUST have either the iconv, mbstring or intl (PHP 5.5+) extension installed and enabled.'; 1829 } else { 1830 $missingExtensions = []; 1831 if (!extension_loaded('iconv')) { 1832 $missingExtensions[] = 'iconv'; 1833 } 1834 if (!extension_loaded('mbstring')) { 1835 $missingExtensions[] = 'mbstring'; 1836 } 1837 if (!class_exists('\UConverter')) { 1838 $missingExtensions[] = 'intl (PHP 5.5+)'; 1839 } 1840 $this->error .= ' Try installing/enabling the ' . implode(' or ', $missingExtensions) . ' extension.'; 1841 } 1842 } 1843 1844 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); 1845 1846 return false; 1847 } 1848 1849 /** 1850 * Fetch the data 1851 * 1852 * If the data is already cached, attempt to fetch it from there instead 1853 * 1854 * @param Base|DataCache|false $cache Cache handler, or false to not load from the cache 1855 * @return array{array<string, string>, string}|bool Returns true if the data was loaded from the cache, or an array of HTTP headers and sniffed type 1856 */ 1857 protected function fetch_data(&$cache) 1858 { 1859 if ($cache instanceof Base) { 1860 // @trigger_error(sprintf('Providing $cache as "\SimplePie\Cache\Base" in %s() is deprecated since SimplePie 1.8.0, please provide "\SimplePie\Cache\DataCache" implementation instead.', __METHOD__), \E_USER_DEPRECATED); 1861 $cache = new BaseDataCache($cache); 1862 } 1863 1864 // @phpstan-ignore-next-line Enforce PHPDoc type. 1865 if ($cache !== false && !$cache instanceof DataCache) { 1866 throw new InvalidArgumentException(sprintf( 1867 '%s(): Argument #1 ($cache) must be of type %s|false', 1868 __METHOD__, 1869 DataCache::class 1870 ), 1); 1871 } 1872 1873 $cacheKey = $this->get_cache_filename($this->feed_url); 1874 1875 // If it's enabled, use the cache 1876 if ($cache) { 1877 // Load the Cache 1878 $this->data = $cache->get_data($cacheKey, []); 1879 1880 if (!empty($this->data)) { 1881 // If the cache is for an outdated build of SimplePie 1882 if (!isset($this->data['build']) || $this->data['build'] !== Misc::get_build()) { 1883 $cache->delete_data($cacheKey); 1884 $this->data = []; 1885 } 1886 // If we've hit a collision just rerun it with caching disabled 1887 elseif (isset($this->data['url']) && $this->data['url'] !== $this->feed_url) { 1888 $cache = false; 1889 $this->data = []; 1890 } 1891 // If we've got a non feed_url stored (if the page isn't actually a feed, or is a redirect) use that URL. 1892 elseif (isset($this->data['feed_url'])) { 1893 // Do not need to do feed autodiscovery yet. 1894 if ($this->data['feed_url'] !== $this->data['url']) { 1895 $this->set_feed_url($this->data['feed_url']); 1896 $this->data['url'] = $this->data['feed_url']; 1897 1898 $cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->autodiscovery_cache_duration); 1899 1900 return $this->init(); 1901 } 1902 1903 $cache->delete_data($this->get_cache_filename($this->feed_url)); 1904 $this->data = []; 1905 } 1906 // Check if the cache has been updated 1907 elseif (!isset($this->data['cache_expiration_time']) || $this->data['cache_expiration_time'] < time()) { 1908 // Want to know if we tried to send last-modified and/or etag headers 1909 // when requesting this file. (Note that it's up to the file to 1910 // support this, but we don't always send the headers either.) 1911 $this->check_modified = true; 1912 if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) { 1913 $headers = [ 1914 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, 1915 ]; 1916 if (isset($this->data['headers']['last-modified'])) { 1917 $headers['if-modified-since'] = $this->data['headers']['last-modified']; 1918 } 1919 if (isset($this->data['headers']['etag'])) { 1920 $headers['if-none-match'] = $this->data['headers']['etag']; 1921 } 1922 1923 try { 1924 $file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers); 1925 $this->status_code = $file->get_status_code(); 1926 } catch (ClientException $th) { 1927 $this->check_modified = false; 1928 $this->status_code = 0; 1929 1930 if ($this->force_cache_fallback) { 1931 $this->data['cache_expiration_time'] = $this->cache_duration + time(); 1932 $cache->set_data($cacheKey, $this->data, $this->cache_duration); 1933 1934 return true; 1935 } 1936 1937 $failedFileReason = $th->getMessage(); 1938 } 1939 1940 if ($this->status_code === 304) { 1941 // Set raw_data to false here too, to signify that the cache 1942 // is still valid. 1943 $this->raw_data = false; 1944 $this->data['cache_expiration_time'] = $this->cache_duration + time(); 1945 $cache->set_data($cacheKey, $this->data, $this->cache_duration); 1946 1947 return true; 1948 } 1949 } 1950 } 1951 // If the cache is still valid, just return true 1952 else { 1953 $this->raw_data = false; 1954 return true; 1955 } 1956 } 1957 // If the cache is empty 1958 else { 1959 $this->data = []; 1960 } 1961 } 1962 1963 // If we don't already have the file (it'll only exist if we've opened it to check if the cache has been modified), open it. 1964 if (!isset($file)) { 1965 if ($this->file instanceof File && $this->file->get_final_requested_uri() === $this->feed_url) { 1966 $file = &$this->file; 1967 } elseif (isset($failedFileReason)) { 1968 // Do not try to fetch again if we already failed once. 1969 // If the file connection had an error, set SimplePie::error to that and quit 1970 $this->error = $failedFileReason; 1971 1972 return !empty($this->data); 1973 } else { 1974 $headers = [ 1975 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, 1976 ]; 1977 try { 1978 $file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers); 1979 } catch (ClientException $th) { 1980 // If the file connection has an error, set SimplePie::error to that and quit 1981 $this->error = $th->getMessage(); 1982 1983 return !empty($this->data); 1984 } 1985 } 1986 } 1987 $this->status_code = $file->get_status_code(); 1988 1989 // If the file connection has an error, set SimplePie::error to that and quit 1990 if (!(!Misc::is_remote_uri($file->get_final_requested_uri()) || ($file->get_status_code() === 200 || $file->get_status_code() > 206 && $file->get_status_code() < 300))) { 1991 $this->error = 'Retrieved unsupported status code "' . $this->status_code . '"'; 1992 return !empty($this->data); 1993 } 1994 1995 if (!$this->force_feed) { 1996 // Check if the supplied URL is a feed, if it isn't, look for it. 1997 $locate = $this->registry->create(Locator::class, [ 1998 (!$file instanceof File) ? File::fromResponse($file) : $file, 1999 $this->timeout, 2000 $this->useragent, 2001 $this->max_checked_feeds, 2002 $this->force_fsockopen, 2003 $this->curl_options 2004 ]); 2005 2006 $http_client = $this->get_http_client(); 2007 2008 if ($http_client instanceof Psr18Client) { 2009 $locate->set_http_client( 2010 $http_client->getHttpClient(), 2011 $http_client->getRequestFactory(), 2012 $http_client->getUriFactory() 2013 ); 2014 } 2015 2016 if (!$locate->is_feed($file)) { 2017 $copyStatusCode = $file->get_status_code(); 2018 $copyContentType = $file->get_header_line('content-type'); 2019 try { 2020 $microformats = false; 2021 if (class_exists('DOMXpath') && function_exists('Mf2\parse')) { 2022 $doc = new \DOMDocument(); 2023 @$doc->loadHTML($file->get_body_content()); 2024 $xpath = new \DOMXpath($doc); 2025 // Check for both h-feed and h-entry, as both a feed with no entries 2026 // and a list of entries without an h-feed wrapper are both valid. 2027 $query = '//*[contains(concat(" ", @class, " "), " h-feed ") or '. 2028 'contains(concat(" ", @class, " "), " h-entry ")]'; 2029 2030 /** @var \DOMNodeList<\DOMElement> $result */ 2031 $result = $xpath->query($query); 2032 $microformats = $result->length !== 0; 2033 } 2034 // Now also do feed discovery, but if microformats were found don't 2035 // overwrite the current value of file. 2036 $discovered = $locate->find( 2037 $this->autodiscovery, 2038 $this->all_discovered_feeds 2039 ); 2040 if ($microformats) { 2041 $hub = $locate->get_rel_link('hub'); 2042 $self = $locate->get_rel_link('self'); 2043 if ($hub || $self) { 2044 $file = $this->store_links($file, $hub, $self); 2045 } 2046 // Push the current file onto all_discovered feeds so the user can 2047 // be shown this as one of the options. 2048 if ($this->all_discovered_feeds !== null) { 2049 $this->all_discovered_feeds[] = $file; 2050 } 2051 } else { 2052 if ($discovered) { 2053 $file = $discovered; 2054 } else { 2055 // We need to unset this so that if SimplePie::set_file() has 2056 // been called that object is untouched 2057 unset($file); 2058 $this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`"; 2059 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]); 2060 return false; 2061 } 2062 } 2063 } catch (SimplePieException $e) { 2064 // We need to unset this so that if SimplePie::set_file() has been called that object is untouched 2065 unset($file); 2066 // This is usually because DOMDocument doesn't exist 2067 $this->error = $e->getMessage(); 2068 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, $e->getFile(), $e->getLine()]); 2069 return false; 2070 } 2071 2072 if ($cache) { 2073 $this->data = [ 2074 'url' => $this->feed_url, 2075 'feed_url' => $file->get_final_requested_uri(), 2076 'build' => Misc::get_build(), 2077 'cache_expiration_time' => $this->cache_duration + time(), 2078 ]; 2079 2080 if (!$cache->set_data($cacheKey, $this->data, $this->cache_duration)) { 2081 trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); 2082 } 2083 } 2084 } 2085 $this->feed_url = $file->get_final_requested_uri(); 2086 $locate = null; 2087 } 2088 2089 $this->raw_data = $file->get_body_content(); 2090 $this->permanent_url = $file->get_permanent_uri(); 2091 2092 $headers = []; 2093 foreach ($file->get_headers() as $key => $values) { 2094 $headers[$key] = implode(', ', $values); 2095 } 2096 2097 $sniffer = $this->registry->create(Sniffer::class, [&$file]); 2098 $sniffed = $sniffer->get_type(); 2099 2100 return [$headers, $sniffed]; 2101 } 2102 2103 /** 2104 * Get the error message for the occurred error 2105 * 2106 * @return string|string[]|null Error message, or array of messages for multifeeds 2107 */ 2108 public function error() 2109 { 2110 return $this->error; 2111 } 2112 2113 /** 2114 * Get the last HTTP status code 2115 * 2116 * @return int Status code 2117 */ 2118 public function status_code() 2119 { 2120 return $this->status_code; 2121 } 2122 2123 /** 2124 * Get the raw XML 2125 * 2126 * This is the same as the old `$feed->enable_xml_dump(true)`, but returns 2127 * the data instead of printing it. 2128 * 2129 * @return string|false Raw XML data, false if the cache is used 2130 */ 2131 public function get_raw_data() 2132 { 2133 return $this->raw_data; 2134 } 2135 2136 /** 2137 * Get the character encoding used for output 2138 * 2139 * @since Preview Release 2140 * @return string 2141 */ 2142 public function get_encoding() 2143 { 2144 return $this->sanitize->output_encoding; 2145 } 2146 2147 /** 2148 * Send the content-type header with correct encoding 2149 * 2150 * This method ensures that the SimplePie-enabled page is being served with 2151 * the correct {@link http://www.iana.org/assignments/media-types/ mime-type} 2152 * and character encoding HTTP headers (character encoding determined by the 2153 * {@see set_output_encoding} config option). 2154 * 2155 * This won't work properly if any content or whitespace has already been 2156 * sent to the browser, because it relies on PHP's 2157 * {@link http://php.net/header header()} function, and these are the 2158 * circumstances under which the function works. 2159 * 2160 * Because it's setting these settings for the entire page (as is the nature 2161 * of HTTP headers), this should only be used once per page (again, at the 2162 * top). 2163 * 2164 * @param string $mime MIME type to serve the page as 2165 * @return void 2166 */ 2167 public function handle_content_type(string $mime = 'text/html') 2168 { 2169 if (!headers_sent()) { 2170 $header = "Content-type: $mime;"; 2171 if ($this->get_encoding()) { 2172 $header .= ' charset=' . $this->get_encoding(); 2173 } else { 2174 $header .= ' charset=UTF-8'; 2175 } 2176 header($header); 2177 } 2178 } 2179 2180 /** 2181 * Get the type of the feed 2182 * 2183 * This returns a self::TYPE_* constant, which can be tested against 2184 * using {@link http://php.net/language.operators.bitwise bitwise operators} 2185 * 2186 * @since 0.8 (usage changed to using constants in 1.0) 2187 * @see self::TYPE_NONE Unknown. 2188 * @see self::TYPE_RSS_090 RSS 0.90. 2189 * @see self::TYPE_RSS_091_NETSCAPE RSS 0.91 (Netscape). 2190 * @see self::TYPE_RSS_091_USERLAND RSS 0.91 (Userland). 2191 * @see self::TYPE_RSS_091 RSS 0.91. 2192 * @see self::TYPE_RSS_092 RSS 0.92. 2193 * @see self::TYPE_RSS_093 RSS 0.93. 2194 * @see self::TYPE_RSS_094 RSS 0.94. 2195 * @see self::TYPE_RSS_10 RSS 1.0. 2196 * @see self::TYPE_RSS_20 RSS 2.0.x. 2197 * @see self::TYPE_RSS_RDF RDF-based RSS. 2198 * @see self::TYPE_RSS_SYNDICATION Non-RDF-based RSS (truly intended as syndication format). 2199 * @see self::TYPE_RSS_ALL Any version of RSS. 2200 * @see self::TYPE_ATOM_03 Atom 0.3. 2201 * @see self::TYPE_ATOM_10 Atom 1.0. 2202 * @see self::TYPE_ATOM_ALL Any version of Atom. 2203 * @see self::TYPE_ALL Any known/supported feed type. 2204 * @return int-mask-of<self::TYPE_*> constant 2205 */ 2206 public function get_type() 2207 { 2208 if (!isset($this->data['type'])) { 2209 $this->data['type'] = self::TYPE_ALL; 2210 if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'])) { 2211 $this->data['type'] &= self::TYPE_ATOM_10; 2212 } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'])) { 2213 $this->data['type'] &= self::TYPE_ATOM_03; 2214 } elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'])) { 2215 if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['channel']) 2216 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['image']) 2217 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['item']) 2218 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['textinput'])) { 2219 $this->data['type'] &= self::TYPE_RSS_10; 2220 } 2221 if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['channel']) 2222 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['image']) 2223 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['item']) 2224 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['textinput'])) { 2225 $this->data['type'] &= self::TYPE_RSS_090; 2226 } 2227 } elseif (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'])) { 2228 $this->data['type'] &= self::TYPE_RSS_ALL; 2229 if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) { 2230 switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) { 2231 case '0.91': 2232 $this->data['type'] &= self::TYPE_RSS_091; 2233 if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) { 2234 switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) { 2235 case '0': 2236 $this->data['type'] &= self::TYPE_RSS_091_NETSCAPE; 2237 break; 2238 2239 case '24': 2240 $this->data['type'] &= self::TYPE_RSS_091_USERLAND; 2241 break; 2242 } 2243 } 2244 break; 2245 2246 case '0.92': 2247 $this->data['type'] &= self::TYPE_RSS_092; 2248 break; 2249 2250 case '0.93': 2251 $this->data['type'] &= self::TYPE_RSS_093; 2252 break; 2253 2254 case '0.94': 2255 $this->data['type'] &= self::TYPE_RSS_094; 2256 break; 2257 2258 case '2.0': 2259 $this->data['type'] &= self::TYPE_RSS_20; 2260 break; 2261 } 2262 } 2263 } else { 2264 $this->data['type'] = self::TYPE_NONE; 2265 } 2266 } 2267 return $this->data['type']; 2268 } 2269 2270 /** 2271 * Get the URL for the feed 2272 * 2273 * When the 'permanent' mode is enabled, returns the original feed URL, 2274 * except in the case of an `HTTP 301 Moved Permanently` status response, 2275 * in which case the location of the first redirection is returned. 2276 * 2277 * When the 'permanent' mode is disabled (default), 2278 * may or may not be different from the URL passed to {@see set_feed_url()}, 2279 * depending on whether auto-discovery was used, and whether there were 2280 * any redirects along the way. 2281 * 2282 * @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.) 2283 * @todo Support <itunes:new-feed-url> 2284 * @todo Also, |atom:link|@rel=self 2285 * @param bool $permanent Permanent mode to return only the original URL or the first redirection 2286 * iff it is a 301 redirection 2287 * @return string|null 2288 */ 2289 public function subscribe_url(bool $permanent = false) 2290 { 2291 if ($permanent) { 2292 if ($this->permanent_url !== null) { 2293 // sanitize encodes ampersands which are required when used in a url. 2294 return str_replace( 2295 '&', 2296 '&', 2297 $this->sanitize( 2298 $this->permanent_url, 2299 self::CONSTRUCT_IRI 2300 ) 2301 ); 2302 } 2303 } else { 2304 if ($this->feed_url !== null) { 2305 return str_replace( 2306 '&', 2307 '&', 2308 $this->sanitize( 2309 $this->feed_url, 2310 self::CONSTRUCT_IRI 2311 ) 2312 ); 2313 } 2314 } 2315 return null; 2316 } 2317 2318 /** 2319 * Get data for an feed-level element 2320 * 2321 * This method allows you to get access to ANY element/attribute that is a 2322 * sub-element of the opening feed tag. 2323 * 2324 * The return value is an indexed array of elements matching the given 2325 * namespace and tag name. Each element has `attribs`, `data` and `child` 2326 * subkeys. For `attribs` and `child`, these contain namespace subkeys. 2327 * `attribs` then has one level of associative name => value data (where 2328 * `value` is a string) after the namespace. `child` has tag-indexed keys 2329 * after the namespace, each member of which is an indexed array matching 2330 * this same format. 2331 * 2332 * For example: 2333 * <pre> 2334 * // This is probably a bad example because we already support 2335 * // <media:content> natively, but it shows you how to parse through 2336 * // the nodes. 2337 * $group = $item->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'group'); 2338 * $content = $group[0]['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content']; 2339 * $file = $content[0]['attribs']['']['url']; 2340 * echo $file; 2341 * </pre> 2342 * 2343 * @since 1.0 2344 * @see http://simplepie.org/wiki/faq/supported_xml_namespaces 2345 * @param string $namespace The URL of the XML namespace of the elements you're trying to access 2346 * @param string $tag Tag name 2347 * @return array<array<string, mixed>>|null 2348 */ 2349 public function get_feed_tags(string $namespace, string $tag) 2350 { 2351 $type = $this->get_type(); 2352 if ($type & self::TYPE_ATOM_10) { 2353 if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag])) { 2354 return $this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag]; 2355 } 2356 } 2357 if ($type & self::TYPE_ATOM_03) { 2358 if (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag])) { 2359 return $this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag]; 2360 } 2361 } 2362 if ($type & self::TYPE_RSS_RDF) { 2363 if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag])) { 2364 return $this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag]; 2365 } 2366 } 2367 if ($type & self::TYPE_RSS_SYNDICATION) { 2368 if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag])) { 2369 return $this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag]; 2370 } 2371 } 2372 return null; 2373 } 2374 2375 /** 2376 * Get data for an channel-level element 2377 * 2378 * This method allows you to get access to ANY element/attribute in the 2379 * channel/header section of the feed. 2380 * 2381 * See {@see SimplePie::get_feed_tags()} for a description of the return value 2382 * 2383 * @since 1.0 2384 * @see http://simplepie.org/wiki/faq/supported_xml_namespaces 2385 * @param string $namespace The URL of the XML namespace of the elements you're trying to access 2386 * @param string $tag Tag name 2387 * @return array<array<string, mixed>>|null 2388 */ 2389 public function get_channel_tags(string $namespace, string $tag) 2390 { 2391 $type = $this->get_type(); 2392 if ($type & self::TYPE_ATOM_ALL) { 2393 if ($return = $this->get_feed_tags($namespace, $tag)) { 2394 return $return; 2395 } 2396 } 2397 if ($type & self::TYPE_RSS_10) { 2398 if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'channel')) { 2399 if (isset($channel[0]['child'][$namespace][$tag])) { 2400 return $channel[0]['child'][$namespace][$tag]; 2401 } 2402 } 2403 } 2404 if ($type & self::TYPE_RSS_090) { 2405 if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'channel')) { 2406 if (isset($channel[0]['child'][$namespace][$tag])) { 2407 return $channel[0]['child'][$namespace][$tag]; 2408 } 2409 } 2410 } 2411 if ($type & self::TYPE_RSS_SYNDICATION) { 2412 if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_20, 'channel')) { 2413 if (isset($channel[0]['child'][$namespace][$tag])) { 2414 return $channel[0]['child'][$namespace][$tag]; 2415 } 2416 } 2417 } 2418 return null; 2419 } 2420 2421 /** 2422 * Get data for an channel-level element 2423 * 2424 * This method allows you to get access to ANY element/attribute in the 2425 * image/logo section of the feed. 2426 * 2427 * See {@see SimplePie::get_feed_tags()} for a description of the return value 2428 * 2429 * @since 1.0 2430 * @see http://simplepie.org/wiki/faq/supported_xml_namespaces 2431 * @param string $namespace The URL of the XML namespace of the elements you're trying to access 2432 * @param string $tag Tag name 2433 * @return array<array<string, mixed>>|null 2434 */ 2435 public function get_image_tags(string $namespace, string $tag) 2436 { 2437 $type = $this->get_type(); 2438 if ($type & self::TYPE_RSS_10) { 2439 if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'image')) { 2440 if (isset($image[0]['child'][$namespace][$tag])) { 2441 return $image[0]['child'][$namespace][$tag]; 2442 } 2443 } 2444 } 2445 if ($type & self::TYPE_RSS_090) { 2446 if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'image')) { 2447 if (isset($image[0]['child'][$namespace][$tag])) { 2448 return $image[0]['child'][$namespace][$tag]; 2449 } 2450 } 2451 } 2452 if ($type & self::TYPE_RSS_SYNDICATION) { 2453 if ($image = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'image')) { 2454 if (isset($image[0]['child'][$namespace][$tag])) { 2455 return $image[0]['child'][$namespace][$tag]; 2456 } 2457 } 2458 } 2459 return null; 2460 } 2461 2462 /** 2463 * Get the base URL value from the feed 2464 * 2465 * Uses `<xml:base>` if available, 2466 * otherwise uses the first 'self' link or the first 'alternate' link of the feed, 2467 * or failing that, the URL of the feed itself. 2468 * 2469 * @see get_link 2470 * @see subscribe_url 2471 * 2472 * @param array<string, mixed> $element 2473 * @return string 2474 */ 2475 public function get_base(array $element = []) 2476 { 2477 if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) { 2478 return $element['xml_base']; 2479 } 2480 if (($link = $this->get_link(0, 'alternate')) !== null) { 2481 return $link; 2482 } 2483 if (($link = $this->get_link(0, 'self')) !== null) { 2484 return $link; 2485 } 2486 2487 return $this->subscribe_url() ?? ''; 2488 } 2489 2490 /** 2491 * Sanitize feed data 2492 * 2493 * @access private 2494 * @see Sanitize::sanitize() 2495 * @param string $data Data to sanitize 2496 * @param int-mask-of<SimplePie::CONSTRUCT_*> $type 2497 * @param string $base Base URL to resolve URLs against 2498 * @return string Sanitized data 2499 */ 2500 public function sanitize(string $data, int $type, string $base = '') 2501 { 2502 try { 2503 // This really returns string|false but changing encoding is uncommon and we are going to deprecate it, so let’s just lie to PHPStan in the interest of cleaner annotations. 2504 return $this->sanitize->sanitize($data, $type, $base); 2505 } catch (SimplePieException $e) { 2506 if (!$this->enable_exceptions) { 2507 $this->error = $e->getMessage(); 2508 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_WARNING, $e->getFile(), $e->getLine()]); 2509 return ''; 2510 } 2511 2512 throw $e; 2513 } 2514 } 2515 2516 /** 2517 * Get the title of the feed 2518 * 2519 * Uses `<atom:title>`, `<title>` or `<dc:title>` 2520 * 2521 * @since 1.0 (previously called `get_feed_title` since 0.8) 2522 * @return string|null 2523 */ 2524 public function get_title() 2525 { 2526 if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'title')) { 2527 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); 2528 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'title')) { 2529 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); 2530 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'title')) { 2531 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); 2532 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'title')) { 2533 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); 2534 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'title')) { 2535 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); 2536 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'title')) { 2537 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2538 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'title')) { 2539 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2540 } 2541 2542 return null; 2543 } 2544 2545 /** 2546 * Get a category for the feed 2547 * 2548 * @since Unknown 2549 * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 2550 * @return Category|null 2551 */ 2552 public function get_category(int $key = 0) 2553 { 2554 $categories = $this->get_categories(); 2555 if (isset($categories[$key])) { 2556 return $categories[$key]; 2557 } 2558 2559 return null; 2560 } 2561 2562 /** 2563 * Get all categories for the feed 2564 * 2565 * Uses `<atom:category>`, `<category>` or `<dc:subject>` 2566 * 2567 * @since Unknown 2568 * @return array<Category>|null List of {@see Category} objects 2569 */ 2570 public function get_categories() 2571 { 2572 $categories = []; 2573 2574 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'category') as $category) { 2575 $term = null; 2576 $scheme = null; 2577 $label = null; 2578 if (isset($category['attribs']['']['term'])) { 2579 $term = $this->sanitize($category['attribs']['']['term'], self::CONSTRUCT_TEXT); 2580 } 2581 if (isset($category['attribs']['']['scheme'])) { 2582 $scheme = $this->sanitize($category['attribs']['']['scheme'], self::CONSTRUCT_TEXT); 2583 } 2584 if (isset($category['attribs']['']['label'])) { 2585 $label = $this->sanitize($category['attribs']['']['label'], self::CONSTRUCT_TEXT); 2586 } 2587 $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]); 2588 } 2589 foreach ((array) $this->get_channel_tags(self::NAMESPACE_RSS_20, 'category') as $category) { 2590 // This is really the label, but keep this as the term also for BC. 2591 // Label will also work on retrieving because that falls back to term. 2592 $term = $this->sanitize($category['data'], self::CONSTRUCT_TEXT); 2593 if (isset($category['attribs']['']['domain'])) { 2594 $scheme = $this->sanitize($category['attribs']['']['domain'], self::CONSTRUCT_TEXT); 2595 } else { 2596 $scheme = null; 2597 } 2598 $categories[] = $this->registry->create(Category::class, [$term, $scheme, null]); 2599 } 2600 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'subject') as $category) { 2601 $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]); 2602 } 2603 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'subject') as $category) { 2604 $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]); 2605 } 2606 2607 if (!empty($categories)) { 2608 return array_unique($categories); 2609 } 2610 2611 return null; 2612 } 2613 2614 /** 2615 * Get an author for the feed 2616 * 2617 * @since 1.1 2618 * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 2619 * @return Author|null 2620 */ 2621 public function get_author(int $key = 0) 2622 { 2623 $authors = $this->get_authors(); 2624 if (isset($authors[$key])) { 2625 return $authors[$key]; 2626 } 2627 2628 return null; 2629 } 2630 2631 /** 2632 * Get all authors for the feed 2633 * 2634 * Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>` 2635 * 2636 * @since 1.1 2637 * @return array<Author>|null List of {@see Author} objects 2638 */ 2639 public function get_authors() 2640 { 2641 $authors = []; 2642 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'author') as $author) { 2643 $name = null; 2644 $uri = null; 2645 $email = null; 2646 if (isset($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) { 2647 $name = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT); 2648 } 2649 if (isset($author['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) { 2650 $uri = $author['child'][self::NAMESPACE_ATOM_10]['uri'][0]; 2651 $uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri)); 2652 } 2653 if (isset($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) { 2654 $email = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT); 2655 } 2656 if ($name !== null || $email !== null || $uri !== null) { 2657 $authors[] = $this->registry->create(Author::class, [$name, $uri, $email]); 2658 } 2659 } 2660 if ($author = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'author')) { 2661 $name = null; 2662 $url = null; 2663 $email = null; 2664 if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) { 2665 $name = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT); 2666 } 2667 if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) { 2668 $url = $author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0]; 2669 $url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url)); 2670 } 2671 if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) { 2672 $email = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT); 2673 } 2674 if ($name !== null || $email !== null || $url !== null) { 2675 $authors[] = $this->registry->create(Author::class, [$name, $url, $email]); 2676 } 2677 } 2678 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'creator') as $author) { 2679 $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]); 2680 } 2681 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'creator') as $author) { 2682 $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]); 2683 } 2684 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ITUNES, 'author') as $author) { 2685 $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]); 2686 } 2687 2688 if (!empty($authors)) { 2689 return array_unique($authors); 2690 } 2691 2692 return null; 2693 } 2694 2695 /** 2696 * Get a contributor for the feed 2697 * 2698 * @since 1.1 2699 * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 2700 * @return Author|null 2701 */ 2702 public function get_contributor(int $key = 0) 2703 { 2704 $contributors = $this->get_contributors(); 2705 if (isset($contributors[$key])) { 2706 return $contributors[$key]; 2707 } 2708 2709 return null; 2710 } 2711 2712 /** 2713 * Get all contributors for the feed 2714 * 2715 * Uses `<atom:contributor>` 2716 * 2717 * @since 1.1 2718 * @return array<Author>|null List of {@see Author} objects 2719 */ 2720 public function get_contributors() 2721 { 2722 $contributors = []; 2723 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'contributor') as $contributor) { 2724 $name = null; 2725 $uri = null; 2726 $email = null; 2727 if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) { 2728 $name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT); 2729 } 2730 if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) { 2731 $uri = $contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0]; 2732 $uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri)); 2733 } 2734 if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) { 2735 $email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT); 2736 } 2737 if ($name !== null || $email !== null || $uri !== null) { 2738 $contributors[] = $this->registry->create(Author::class, [$name, $uri, $email]); 2739 } 2740 } 2741 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'contributor') as $contributor) { 2742 $name = null; 2743 $url = null; 2744 $email = null; 2745 if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) { 2746 $name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT); 2747 } 2748 if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) { 2749 $url = $contributor['child'][self::NAMESPACE_ATOM_03]['url'][0]; 2750 $url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url)); 2751 } 2752 if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) { 2753 $email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT); 2754 } 2755 if ($name !== null || $email !== null || $url !== null) { 2756 $contributors[] = $this->registry->create(Author::class, [$name, $url, $email]); 2757 } 2758 } 2759 2760 if (!empty($contributors)) { 2761 return array_unique($contributors); 2762 } 2763 2764 return null; 2765 } 2766 2767 /** 2768 * Get a single link for the feed 2769 * 2770 * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) 2771 * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 2772 * @param string $rel The relationship of the link to return 2773 * @return string|null Link URL 2774 */ 2775 public function get_link(int $key = 0, string $rel = 'alternate') 2776 { 2777 $links = $this->get_links($rel); 2778 if (isset($links[$key])) { 2779 return $links[$key]; 2780 } 2781 2782 return null; 2783 } 2784 2785 /** 2786 * Get the permalink for the item 2787 * 2788 * Returns the first link available with a relationship of "alternate". 2789 * Identical to {@see get_link()} with key 0 2790 * 2791 * @see get_link 2792 * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) 2793 * @internal Added for parity between the parent-level and the item/entry-level. 2794 * @return string|null Link URL 2795 */ 2796 public function get_permalink() 2797 { 2798 return $this->get_link(0); 2799 } 2800 2801 /** 2802 * Get all links for the feed 2803 * 2804 * Uses `<atom:link>` or `<link>` 2805 * 2806 * @since Beta 2 2807 * @param string $rel The relationship of links to return 2808 * @return array<string>|null Links found for the feed (strings) 2809 */ 2810 public function get_links(string $rel = 'alternate') 2811 { 2812 if (!isset($this->data['links'])) { 2813 $this->data['links'] = []; 2814 if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'link')) { 2815 foreach ($links as $link) { 2816 if (isset($link['attribs']['']['href'])) { 2817 $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; 2818 $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link)); 2819 } 2820 } 2821 } 2822 if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'link')) { 2823 foreach ($links as $link) { 2824 if (isset($link['attribs']['']['href'])) { 2825 $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; 2826 $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link)); 2827 } 2828 } 2829 } 2830 if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'link')) { 2831 $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0])); 2832 } 2833 if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'link')) { 2834 $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0])); 2835 } 2836 if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'link')) { 2837 $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0])); 2838 } 2839 2840 $keys = array_keys($this->data['links']); 2841 foreach ($keys as $key) { 2842 if ($this->registry->call(Misc::class, 'is_isegment_nz_nc', [$key])) { 2843 if (isset($this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key])) { 2844 $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key]); 2845 $this->data['links'][$key] = &$this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key]; 2846 } else { 2847 $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key]; 2848 } 2849 } elseif (substr($key, 0, 41) === self::IANA_LINK_RELATIONS_REGISTRY) { 2850 $this->data['links'][substr($key, 41)] = &$this->data['links'][$key]; 2851 } 2852 $this->data['links'][$key] = array_unique($this->data['links'][$key]); 2853 } 2854 } 2855 2856 if (isset($this->data['headers']['link'])) { 2857 $link_headers = $this->data['headers']['link']; 2858 if (is_array($link_headers)) { 2859 $link_headers = implode(',', $link_headers); 2860 } 2861 // https://datatracker.ietf.org/doc/html/rfc8288 2862 if (is_string($link_headers) && 2863 preg_match_all('/<(?P<uri>[^>]+)>\s*;\s*rel\s*=\s*(?P<quote>"?)' . preg_quote($rel) . '(?P=quote)\s*(?=,|$)/i', $link_headers, $matches)) { 2864 return $matches['uri']; 2865 } 2866 } 2867 2868 if (isset($this->data['links'][$rel])) { 2869 return $this->data['links'][$rel]; 2870 } 2871 2872 return null; 2873 } 2874 2875 /** 2876 * @return ?array<Response> 2877 */ 2878 public function get_all_discovered_feeds() 2879 { 2880 return $this->all_discovered_feeds; 2881 } 2882 2883 /** 2884 * Get the content for the item 2885 * 2886 * Uses `<atom:subtitle>`, `<atom:tagline>`, `<description>`, 2887 * `<dc:description>`, `<itunes:summary>` or `<itunes:subtitle>` 2888 * 2889 * @since 1.0 (previously called `get_feed_description()` since 0.8) 2890 * @return string|null 2891 */ 2892 public function get_description() 2893 { 2894 if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'subtitle')) { 2895 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); 2896 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'tagline')) { 2897 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); 2898 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'description')) { 2899 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); 2900 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'description')) { 2901 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); 2902 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'description')) { 2903 return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0])); 2904 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'description')) { 2905 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2906 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'description')) { 2907 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2908 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'summary')) { 2909 return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0])); 2910 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'subtitle')) { 2911 return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0])); 2912 } 2913 2914 return null; 2915 } 2916 2917 /** 2918 * Get the copyright info for the feed 2919 * 2920 * Uses `<atom:rights>`, `<atom:copyright>` or `<dc:rights>` 2921 * 2922 * @since 1.0 (previously called `get_feed_copyright()` since 0.8) 2923 * @return string|null 2924 */ 2925 public function get_copyright() 2926 { 2927 if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'rights')) { 2928 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); 2929 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'copyright')) { 2930 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0])); 2931 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'copyright')) { 2932 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2933 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'rights')) { 2934 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2935 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'rights')) { 2936 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2937 } 2938 2939 return null; 2940 } 2941 2942 /** 2943 * Get the language for the feed 2944 * 2945 * Uses `<language>`, `<dc:language>`, or @xml_lang 2946 * 2947 * @since 1.0 (previously called `get_feed_language()` since 0.8) 2948 * @return string|null 2949 */ 2950 public function get_language() 2951 { 2952 if ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'language')) { 2953 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2954 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'language')) { 2955 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2956 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'language')) { 2957 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 2958 } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'])) { 2959 return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT); 2960 } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'])) { 2961 return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT); 2962 } elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'])) { 2963 return $this->sanitize($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'], self::CONSTRUCT_TEXT); 2964 } elseif (isset($this->data['headers']['content-language'])) { 2965 return $this->sanitize($this->data['headers']['content-language'], self::CONSTRUCT_TEXT); 2966 } 2967 2968 return null; 2969 } 2970 2971 /** 2972 * Get the latitude coordinates for the item 2973 * 2974 * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications 2975 * 2976 * Uses `<geo:lat>` or `<georss:point>` 2977 * 2978 * @since 1.0 2979 * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo 2980 * @link http://www.georss.org/ GeoRSS 2981 * @return float|null 2982 */ 2983 public function get_latitude() 2984 { 2985 if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lat')) { 2986 return (float) $return[0]['data']; 2987 } elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { 2988 return (float) $match[1]; 2989 } 2990 2991 return null; 2992 } 2993 2994 /** 2995 * Get the longitude coordinates for the feed 2996 * 2997 * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications 2998 * 2999 * Uses `<geo:long>`, `<geo:lon>` or `<georss:point>` 3000 * 3001 * @since 1.0 3002 * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo 3003 * @link http://www.georss.org/ GeoRSS 3004 * @return float|null 3005 */ 3006 public function get_longitude() 3007 { 3008 if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'long')) { 3009 return (float) $return[0]['data']; 3010 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lon')) { 3011 return (float) $return[0]['data']; 3012 } elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) { 3013 return (float) $match[2]; 3014 } 3015 3016 return null; 3017 } 3018 3019 /** 3020 * Get the feed logo's title 3021 * 3022 * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" title. 3023 * 3024 * Uses `<image><title>` or `<image><dc:title>` 3025 * 3026 * @return string|null 3027 */ 3028 public function get_image_title() 3029 { 3030 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'title')) { 3031 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 3032 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'title')) { 3033 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 3034 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'title')) { 3035 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 3036 } elseif ($return = $this->get_image_tags(self::NAMESPACE_DC_11, 'title')) { 3037 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 3038 } elseif ($return = $this->get_image_tags(self::NAMESPACE_DC_10, 'title')) { 3039 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT); 3040 } 3041 3042 return null; 3043 } 3044 3045 /** 3046 * Get the feed logo's URL 3047 * 3048 * RSS 0.9.0, 2.0, Atom 1.0, and feeds with iTunes RSS tags are allowed to 3049 * have a "feed logo" URL. This points directly to the image itself. 3050 * 3051 * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`, 3052 * `<image><title>` or `<image><dc:title>` 3053 * 3054 * @return string|null 3055 */ 3056 public function get_image_url() 3057 { 3058 if ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'image')) { 3059 return $this->sanitize($return[0]['attribs']['']['href'], self::CONSTRUCT_IRI); 3060 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'logo')) { 3061 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3062 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'icon')) { 3063 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3064 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'url')) { 3065 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3066 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'url')) { 3067 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3068 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) { 3069 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3070 } 3071 3072 return null; 3073 } 3074 3075 3076 /** 3077 * Get the feed logo's link 3078 * 3079 * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" link. This 3080 * points to a human-readable page that the image should link to. 3081 * 3082 * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`, 3083 * `<image><title>` or `<image><dc:title>` 3084 * 3085 * @return string|null 3086 */ 3087 public function get_image_link() 3088 { 3089 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'link')) { 3090 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3091 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'link')) { 3092 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3093 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'link')) { 3094 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0])); 3095 } 3096 3097 return null; 3098 } 3099 3100 /** 3101 * Get the feed logo's link 3102 * 3103 * RSS 2.0 feeds are allowed to have a "feed logo" width. 3104 * 3105 * Uses `<image><width>` or defaults to 88 if no width is specified and 3106 * the feed is an RSS 2.0 feed. 3107 * 3108 * @return int|null 3109 */ 3110 public function get_image_width() 3111 { 3112 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'width')) { 3113 return intval($return[0]['data']); 3114 } elseif ($this->get_type() & self::TYPE_RSS_SYNDICATION && $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) { 3115 return 88; 3116 } 3117 3118 return null; 3119 } 3120 3121 /** 3122 * Get the feed logo's height 3123 * 3124 * RSS 2.0 feeds are allowed to have a "feed logo" height. 3125 * 3126 * Uses `<image><height>` or defaults to 31 if no height is specified and 3127 * the feed is an RSS 2.0 feed. 3128 * 3129 * @return int|null 3130 */ 3131 public function get_image_height() 3132 { 3133 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'height')) { 3134 return intval($return[0]['data']); 3135 } elseif ($this->get_type() & self::TYPE_RSS_SYNDICATION && $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) { 3136 return 31; 3137 } 3138 3139 return null; 3140 } 3141 3142 /** 3143 * Get the number of items in the feed 3144 * 3145 * This is well-suited for {@link http://php.net/for for()} loops with 3146 * {@see get_item()} 3147 * 3148 * @param int $max Maximum value to return. 0 for no limit 3149 * @return int Number of items in the feed 3150 */ 3151 public function get_item_quantity(int $max = 0) 3152 { 3153 $qty = count($this->get_items()); 3154 if ($max === 0) { 3155 return $qty; 3156 } 3157 3158 return min($qty, $max); 3159 } 3160 3161 /** 3162 * Get a single item from the feed 3163 * 3164 * This is better suited for {@link http://php.net/for for()} loops, whereas 3165 * {@see get_items()} is better suited for 3166 * {@link http://php.net/foreach foreach()} loops. 3167 * 3168 * @see get_item_quantity() 3169 * @since Beta 2 3170 * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1 3171 * @return Item|null 3172 */ 3173 public function get_item(int $key = 0) 3174 { 3175 $items = $this->get_items(); 3176 if (isset($items[$key])) { 3177 return $items[$key]; 3178 } 3179 3180 return null; 3181 } 3182 3183 /** 3184 * Get all items from the feed 3185 * 3186 * This is better suited for {@link http://php.net/for for()} loops, whereas 3187 * {@see get_items()} is better suited for 3188 * {@link http://php.net/foreach foreach()} loops. 3189 * 3190 * @see get_item_quantity 3191 * @since Beta 2 3192 * @param int $start Index to start at 3193 * @param int $end Number of items to return. 0 for all items after `$start` 3194 * @return Item[] List of {@see Item} objects 3195 */ 3196 public function get_items(int $start = 0, int $end = 0) 3197 { 3198 if (!isset($this->data['items'])) { 3199 if (!empty($this->multifeed_objects)) { 3200 $this->data['items'] = SimplePie::merge_items($this->multifeed_objects, $start, $end, $this->item_limit); 3201 if (empty($this->data['items'])) { 3202 return []; 3203 } 3204 return $this->data['items']; 3205 } 3206 $this->data['items'] = []; 3207 if ($items = $this->get_feed_tags(self::NAMESPACE_ATOM_10, 'entry')) { 3208 $keys = array_keys($items); 3209 foreach ($keys as $key) { 3210 $this->data['items'][] = $this->make_item($items[$key]); 3211 } 3212 } 3213 if ($items = $this->get_feed_tags(self::NAMESPACE_ATOM_03, 'entry')) { 3214 $keys = array_keys($items); 3215 foreach ($keys as $key) { 3216 $this->data['items'][] = $this->make_item($items[$key]); 3217 } 3218 } 3219 if ($items = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'item')) { 3220 $keys = array_keys($items); 3221 foreach ($keys as $key) { 3222 $this->data['items'][] = $this->make_item($items[$key]); 3223 } 3224 } 3225 if ($items = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'item')) { 3226 $keys = array_keys($items); 3227 foreach ($keys as $key) { 3228 $this->data['items'][] = $this->make_item($items[$key]); 3229 } 3230 } 3231 if ($items = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'item')) { 3232 $keys = array_keys($items); 3233 foreach ($keys as $key) { 3234 $this->data['items'][] = $this->make_item($items[$key]); 3235 } 3236 } 3237 } 3238 3239 if (empty($this->data['items'])) { 3240 return []; 3241 } 3242 3243 if ($this->order_by_date) { 3244 if (!isset($this->data['ordered_items'])) { 3245 $this->data['ordered_items'] = $this->data['items']; 3246 usort($this->data['ordered_items'], [get_class($this), 'sort_items']); 3247 } 3248 $items = $this->data['ordered_items']; 3249 } else { 3250 $items = $this->data['items']; 3251 } 3252 // Slice the data as desired 3253 if ($end === 0) { 3254 return array_slice($items, $start); 3255 } 3256 3257 return array_slice($items, $start, $end); 3258 } 3259 3260 /** 3261 * Set the favicon handler 3262 * 3263 * @deprecated Use your own favicon handling instead 3264 * @param string|false $page 3265 * @return bool 3266 */ 3267 public function set_favicon_handler($page = false, string $qs = 'i') 3268 { 3269 trigger_error('Favicon handling has been removed since SimplePie 1.3, please use your own handling', \E_USER_DEPRECATED); 3270 return false; 3271 } 3272 3273 /** 3274 * Get the favicon for the current feed 3275 * 3276 * @deprecated Use your own favicon handling instead 3277 * @return string|bool 3278 */ 3279 public function get_favicon() 3280 { 3281 trigger_error('Favicon handling has been removed since SimplePie 1.3, please use your own handling', \E_USER_DEPRECATED); 3282 3283 if (($url = $this->get_link()) !== null) { 3284 return 'https://www.google.com/s2/favicons?domain=' . urlencode($url); 3285 } 3286 3287 return false; 3288 } 3289 3290 /** 3291 * Magic method handler 3292 * 3293 * @param string $method Method name 3294 * @param array<mixed> $args Arguments to the method 3295 * @return mixed 3296 */ 3297 public function __call(string $method, array $args) 3298 { 3299 if (strpos($method, 'subscribe_') === 0) { 3300 trigger_error('subscribe_*() has been deprecated since SimplePie 1.3, implement the callback yourself', \E_USER_DEPRECATED); 3301 return ''; 3302 } 3303 if ($method === 'enable_xml_dump') { 3304 trigger_error('enable_xml_dump() has been deprecated since SimplePie 1.3, use get_raw_data() instead', \E_USER_DEPRECATED); 3305 return false; 3306 } 3307 3308 $class = get_class($this); 3309 $trace = debug_backtrace(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection 3310 $file = $trace[0]['file'] ?? ''; 3311 $line = $trace[0]['line'] ?? ''; 3312 throw new SimplePieException("Call to undefined method $class::$method() in $file on line $line"); 3313 } 3314 3315 /** 3316 * Item factory 3317 * 3318 * @param array<string, mixed> $data 3319 */ 3320 private function make_item(array $data): Item 3321 { 3322 $item = $this->registry->create(Item::class, [$this, $data]); 3323 $item->set_sanitize($this->sanitize); 3324 3325 return $item; 3326 } 3327 3328 /** 3329 * Sorting callback for items 3330 * 3331 * @access private 3332 * @param Item $a 3333 * @param Item $b 3334 * @return -1|0|1 3335 */ 3336 public static function sort_items(Item $a, Item $b) 3337 { 3338 $a_date = $a->get_date('U'); 3339 $b_date = $b->get_date('U'); 3340 if ($a_date && $b_date) { 3341 return $a_date > $b_date ? -1 : 1; 3342 } 3343 // Sort items without dates to the top. 3344 if ($a_date) { 3345 return 1; 3346 } 3347 if ($b_date) { 3348 return -1; 3349 } 3350 return 0; 3351 } 3352 3353 /** 3354 * Merge items from several feeds into one 3355 * 3356 * If you're merging multiple feeds together, they need to all have dates 3357 * for the items or else SimplePie will refuse to sort them. 3358 * 3359 * @link http://simplepie.org/wiki/tutorial/sort_multiple_feeds_by_time_and_date#if_feeds_require_separate_per-feed_settings 3360 * @param array<SimplePie> $urls List of SimplePie feed objects to merge 3361 * @param int $start Starting item 3362 * @param int $end Number of items to return 3363 * @param int $limit Maximum number of items per feed 3364 * @return array<Item> 3365 */ 3366 public static function merge_items(array $urls, int $start = 0, int $end = 0, int $limit = 0) 3367 { 3368 if (count($urls) > 0) { 3369 $items = []; 3370 foreach ($urls as $arg) { 3371 if ($arg instanceof SimplePie) { 3372 $items = array_merge($items, $arg->get_items(0, $limit)); 3373 3374 // @phpstan-ignore-next-line Enforce PHPDoc type. 3375 } else { 3376 trigger_error('Arguments must be SimplePie objects', E_USER_WARNING); 3377 } 3378 } 3379 3380 usort($items, [get_class($urls[0]), 'sort_items']); 3381 3382 if ($end === 0) { 3383 return array_slice($items, $start); 3384 } 3385 3386 return array_slice($items, $start, $end); 3387 } 3388 3389 trigger_error('Cannot merge zero SimplePie objects', E_USER_WARNING); 3390 return []; 3391 } 3392 3393 /** 3394 * Store PubSubHubbub links as headers 3395 * 3396 * There is no way to find PuSH links in the body of a microformats feed, 3397 * so they are added to the headers when found, to be used later by get_links. 3398 */ 3399 private function store_links(Response $file, ?string $hub, ?string $self): Response 3400 { 3401 $linkHeaderLine = $file->get_header_line('link'); 3402 $linkHeader = $file->get_header('link'); 3403 3404 if ($hub && !preg_match('/rel=hub/', $linkHeaderLine)) { 3405 $linkHeader[] = '<'.$hub.'>; rel=hub'; 3406 } 3407 3408 if ($self && !preg_match('/rel=self/', $linkHeaderLine)) { 3409 $linkHeader[] = '<'.$self.'>; rel=self'; 3410 } 3411 3412 if (count($linkHeader) > 0) { 3413 $file = $file->with_header('link', $linkHeader); 3414 } 3415 3416 return $file; 3417 } 3418 3419 /** 3420 * Get a DataCache 3421 * 3422 * @param string $feed_url Only needed for BC, can be removed in SimplePie 2.0.0 3423 * 3424 * @return DataCache 3425 */ 3426 private function get_cache(string $feed_url = ''): DataCache 3427 { 3428 if ($this->cache === null) { 3429 // @trigger_error(sprintf('Not providing as PSR-16 cache implementation is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache()".'), \E_USER_DEPRECATED); 3430 $cache = $this->registry->call(Cache::class, 'get_handler', [ 3431 $this->cache_location, 3432 $this->get_cache_filename($feed_url), 3433 Base::TYPE_FEED 3434 ]); 3435 3436 return new BaseDataCache($cache); 3437 } 3438 3439 return $this->cache; 3440 } 3441 3442 /** 3443 * Get a HTTP client 3444 */ 3445 private function get_http_client(): Client 3446 { 3447 if ($this->http_client === null) { 3448 $this->http_client = new FileClient( 3449 $this->get_registry(), 3450 [ 3451 'timeout' => $this->timeout, 3452 'redirects' => 5, 3453 'useragent' => $this->useragent, 3454 'force_fsockopen' => $this->force_fsockopen, 3455 'curl_options' => $this->curl_options, 3456 ] 3457 ); 3458 $this->http_client_injected = true; 3459 } 3460 3461 return $this->http_client; 3462 } 3463 } 3464 3465 class_alias('SimplePie\SimplePie', 'SimplePie');
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Fri Oct 10 08:20:03 2025 | Cross-referenced by PHPXref |