[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/SimplePie/src/ -> SimplePie.php (source)

   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>&amp;</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                      '&amp;',
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                      '&amp;',
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');


Generated : Fri Oct 10 08:20:03 2025 Cross-referenced by PHPXref