xref: /dokuwiki/vendor/simplepie/simplepie/src/SimplePie.php (revision 8e88a29b81301f78509349ab1152bb09c229123e)
1<?php
2
3// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
4// SPDX-License-Identifier: BSD-3-Clause
5
6declare(strict_types=1);
7
8namespace SimplePie;
9
10use InvalidArgumentException;
11use Psr\Http\Client\ClientInterface;
12use Psr\Http\Message\RequestFactoryInterface;
13use Psr\Http\Message\UriFactoryInterface;
14use Psr\SimpleCache\CacheInterface;
15use SimplePie\Cache\Base;
16use SimplePie\Cache\BaseDataCache;
17use SimplePie\Cache\CallableNameFilter;
18use SimplePie\Cache\DataCache;
19use SimplePie\Cache\NameFilter;
20use SimplePie\Cache\Psr16;
21use SimplePie\Content\Type\Sniffer;
22use SimplePie\Exception as SimplePieException;
23use SimplePie\HTTP\Client;
24use SimplePie\HTTP\ClientException;
25use SimplePie\HTTP\FileClient;
26use SimplePie\HTTP\Psr18Client;
27use SimplePie\HTTP\Response;
28
29/**
30 * SimplePie
31 */
32class 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();
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
3465class_alias('SimplePie\SimplePie', 'SimplePie');
3466