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>&</foo>', $values);
1658 if (\PHP_VERSION_ID < 80000) {
1659 xml_parser_free($parser_check);
1660 }
1661 $xml_is_sane = isset($values[0]['value']);
1662 }
1663 if (!$xml_is_sane) {
1664 return false;
1665 }
1666 }
1667
1668 // The default sanitize class gets set in the constructor, check if it has
1669 // changed.
1670 if ($this->registry->get_class(Sanitize::class) !== Sanitize::class) {
1671 $this->sanitize = $this->registry->create(Sanitize::class);
1672 }
1673 if (method_exists($this->sanitize, 'set_registry')) {
1674 $this->sanitize->set_registry($this->registry);
1675 }
1676
1677 // Pass whatever was set with config options over to the sanitizer.
1678 // Pass the classes in for legacy support; new classes should use the registry instead
1679 $cache = $this->registry->get_class(Cache::class);
1680 \assert($cache !== null, 'Cache must be defined');
1681 $this->sanitize->pass_cache_data(
1682 $this->enable_cache,
1683 $this->cache_location,
1684 $this->cache_namefilter,
1685 $cache,
1686 $this->cache
1687 );
1688
1689 $http_client = $this->get_http_client();
1690
1691 if ($http_client instanceof Psr18Client) {
1692 $this->sanitize->set_http_client(
1693 $http_client->getHttpClient(),
1694 $http_client->getRequestFactory(),
1695 $http_client->getUriFactory()
1696 );
1697 }
1698
1699 if (!empty($this->multifeed_url)) {
1700 $i = 0;
1701 $success = 0;
1702 $this->multifeed_objects = [];
1703 $this->error = [];
1704 foreach ($this->multifeed_url as $url) {
1705 $this->multifeed_objects[$i] = clone $this;
1706 $this->multifeed_objects[$i]->set_feed_url($url);
1707 $single_success = $this->multifeed_objects[$i]->init();
1708 $success |= $single_success;
1709 if (!$single_success) {
1710 $this->error[$i] = $this->multifeed_objects[$i]->error();
1711 }
1712 $i++;
1713 }
1714 return (bool) $success;
1715 } elseif ($this->feed_url === null && $this->raw_data === null) {
1716 return false;
1717 }
1718
1719 $this->error = null;
1720 $this->data = [];
1721 $this->check_modified = false;
1722 $this->multifeed_objects = [];
1723 $cache = false;
1724
1725 if ($this->feed_url !== null) {
1726 $parsed_feed_url = $this->registry->call(Misc::class, 'parse_url', [$this->feed_url]);
1727
1728 // Decide whether to enable caching
1729 if ($this->enable_cache && $parsed_feed_url['scheme'] !== '') {
1730 $cache = $this->get_cache($this->feed_url);
1731 }
1732
1733 // Fetch the data into $this->raw_data
1734 if (($fetched = $this->fetch_data($cache)) === true) {
1735 return true;
1736 } elseif ($fetched === false) {
1737 return false;
1738 }
1739
1740 [$headers, $sniffed] = $fetched;
1741 }
1742
1743 // Empty response check
1744 if (empty($this->raw_data)) {
1745 $this->error = "A feed could not be found at `$this->feed_url`. Empty body.";
1746 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
1747 return false;
1748 }
1749
1750 // Set up array of possible encodings
1751 $encodings = [];
1752
1753 // First check to see if input has been overridden.
1754 if ($this->input_encoding !== false) {
1755 $encodings[] = strtoupper($this->input_encoding);
1756 }
1757
1758 $application_types = ['application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity'];
1759 $text_types = ['text/xml', 'text/xml-external-parsed-entity'];
1760
1761 // RFC 3023 (only applies to sniffed content)
1762 if (isset($sniffed)) {
1763 if (in_array($sniffed, $application_types) || substr($sniffed, 0, 12) === 'application/' && substr($sniffed, -4) === '+xml') {
1764 if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) {
1765 $encodings[] = strtoupper($charset[1]);
1766 }
1767 $encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry]));
1768 $encodings[] = 'UTF-8';
1769 } elseif (in_array($sniffed, $text_types) || substr($sniffed, 0, 5) === 'text/' && substr($sniffed, -4) === '+xml') {
1770 if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) {
1771 $encodings[] = strtoupper($charset[1]);
1772 }
1773 $encodings[] = 'US-ASCII';
1774 }
1775 // Text MIME-type default
1776 elseif (substr($sniffed, 0, 5) === 'text/') {
1777 $encodings[] = 'UTF-8';
1778 }
1779 }
1780
1781 // Fallback to XML 1.0 Appendix F.1/UTF-8/ISO-8859-1
1782 $encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry]));
1783 $encodings[] = 'UTF-8';
1784 $encodings[] = 'ISO-8859-1';
1785
1786 // There's no point in trying an encoding twice
1787 $encodings = array_unique($encodings);
1788
1789 // Loop through each possible encoding, till we return something, or run out of possibilities
1790 foreach ($encodings as $encoding) {
1791 // Change the encoding to UTF-8 (as we always use UTF-8 internally)
1792 if ($utf8_data = $this->registry->call(Misc::class, 'change_encoding', [$this->raw_data, $encoding, 'UTF-8'])) {
1793 // Create new parser
1794 $parser = $this->registry->create(Parser::class);
1795
1796 // If it's parsed fine
1797 if ($parser->parse($utf8_data, 'UTF-8', $this->permanent_url ?? '')) {
1798 $this->data = $parser->get_data();
1799 if (!($this->get_type() & ~self::TYPE_NONE)) {
1800 $this->error = "A feed could not be found at `$this->feed_url`. This does not appear to be a valid RSS or Atom feed.";
1801 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
1802 return false;
1803 }
1804
1805 if (isset($headers)) {
1806 $this->data['headers'] = $headers;
1807 }
1808 $this->data['build'] = Misc::get_build();
1809
1810 // Cache the file if caching is enabled
1811 $this->data['cache_expiration_time'] = $this->cache_duration + time();
1812
1813 if ($cache && !$cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->cache_duration)) {
1814 trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
1815 }
1816 return true;
1817 }
1818 }
1819 }
1820
1821 if (isset($parser)) {
1822 // We have an error, just set Misc::error to it and quit
1823 $this->error = $this->feed_url;
1824 $this->error .= sprintf(' is invalid XML, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column());
1825 } else {
1826 $this->error = 'The data could not be converted to UTF-8.';
1827 if (!extension_loaded('mbstring') && !extension_loaded('iconv') && !class_exists('\UConverter')) {
1828 $this->error .= ' You MUST have either the iconv, mbstring or intl (PHP 5.5+) extension installed and enabled.';
1829 } else {
1830 $missingExtensions = [];
1831 if (!extension_loaded('iconv')) {
1832 $missingExtensions[] = 'iconv';
1833 }
1834 if (!extension_loaded('mbstring')) {
1835 $missingExtensions[] = 'mbstring';
1836 }
1837 if (!class_exists('\UConverter')) {
1838 $missingExtensions[] = 'intl (PHP 5.5+)';
1839 }
1840 $this->error .= ' Try installing/enabling the ' . implode(' or ', $missingExtensions) . ' extension.';
1841 }
1842 }
1843
1844 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
1845
1846 return false;
1847 }
1848
1849 /**
1850 * Fetch the data
1851 *
1852 * If the data is already cached, attempt to fetch it from there instead
1853 *
1854 * @param Base|DataCache|false $cache Cache handler, or false to not load from the cache
1855 * @return array{array<string, string>, string}|bool Returns true if the data was loaded from the cache, or an array of HTTP headers and sniffed type
1856 */
1857 protected function fetch_data(&$cache)
1858 {
1859 if ($cache instanceof Base) {
1860 // @trigger_error(sprintf('Providing $cache as "\SimplePie\Cache\Base" in %s() is deprecated since SimplePie 1.8.0, please provide "\SimplePie\Cache\DataCache" implementation instead.', __METHOD__), \E_USER_DEPRECATED);
1861 $cache = new BaseDataCache($cache);
1862 }
1863
1864 // @phpstan-ignore-next-line Enforce PHPDoc type.
1865 if ($cache !== false && !$cache instanceof DataCache) {
1866 throw new InvalidArgumentException(sprintf(
1867 '%s(): Argument #1 ($cache) must be of type %s|false',
1868 __METHOD__,
1869 DataCache::class
1870 ), 1);
1871 }
1872
1873 $cacheKey = $this->get_cache_filename($this->feed_url);
1874
1875 // If it's enabled, use the cache
1876 if ($cache) {
1877 // Load the Cache
1878 $this->data = $cache->get_data($cacheKey, []);
1879
1880 if (!empty($this->data)) {
1881 // If the cache is for an outdated build of SimplePie
1882 if (!isset($this->data['build']) || $this->data['build'] !== Misc::get_build()) {
1883 $cache->delete_data($cacheKey);
1884 $this->data = [];
1885 }
1886 // If we've hit a collision just rerun it with caching disabled
1887 elseif (isset($this->data['url']) && $this->data['url'] !== $this->feed_url) {
1888 $cache = false;
1889 $this->data = [];
1890 }
1891 // If we've got a non feed_url stored (if the page isn't actually a feed, or is a redirect) use that URL.
1892 elseif (isset($this->data['feed_url'])) {
1893 // Do not need to do feed autodiscovery yet.
1894 if ($this->data['feed_url'] !== $this->data['url']) {
1895 $this->set_feed_url($this->data['feed_url']);
1896 $this->data['url'] = $this->data['feed_url'];
1897
1898 $cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->autodiscovery_cache_duration);
1899
1900 return $this->init();
1901 }
1902
1903 $cache->delete_data($this->get_cache_filename($this->feed_url));
1904 $this->data = [];
1905 }
1906 // Check if the cache has been updated
1907 elseif (!isset($this->data['cache_expiration_time']) || $this->data['cache_expiration_time'] < time()) {
1908 // Want to know if we tried to send last-modified and/or etag headers
1909 // when requesting this file. (Note that it's up to the file to
1910 // support this, but we don't always send the headers either.)
1911 $this->check_modified = true;
1912 if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) {
1913 $headers = [
1914 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
1915 ];
1916 if (isset($this->data['headers']['last-modified'])) {
1917 $headers['if-modified-since'] = $this->data['headers']['last-modified'];
1918 }
1919 if (isset($this->data['headers']['etag'])) {
1920 $headers['if-none-match'] = $this->data['headers']['etag'];
1921 }
1922
1923 try {
1924 $file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers);
1925 $this->status_code = $file->get_status_code();
1926 } catch (ClientException $th) {
1927 $this->check_modified = false;
1928 $this->status_code = 0;
1929
1930 if ($this->force_cache_fallback) {
1931 $this->data['cache_expiration_time'] = $this->cache_duration + time();
1932 $cache->set_data($cacheKey, $this->data, $this->cache_duration);
1933
1934 return true;
1935 }
1936
1937 $failedFileReason = $th->getMessage();
1938 }
1939
1940 if ($this->status_code === 304) {
1941 // Set raw_data to false here too, to signify that the cache
1942 // is still valid.
1943 $this->raw_data = false;
1944 $this->data['cache_expiration_time'] = $this->cache_duration + time();
1945 $cache->set_data($cacheKey, $this->data, $this->cache_duration);
1946
1947 return true;
1948 }
1949 }
1950 }
1951 // If the cache is still valid, just return true
1952 else {
1953 $this->raw_data = false;
1954 return true;
1955 }
1956 }
1957 // If the cache is empty
1958 else {
1959 $this->data = [];
1960 }
1961 }
1962
1963 // If we don't already have the file (it'll only exist if we've opened it to check if the cache has been modified), open it.
1964 if (!isset($file)) {
1965 if ($this->file instanceof File && $this->file->get_final_requested_uri() === $this->feed_url) {
1966 $file = &$this->file;
1967 } elseif (isset($failedFileReason)) {
1968 // Do not try to fetch again if we already failed once.
1969 // If the file connection had an error, set SimplePie::error to that and quit
1970 $this->error = $failedFileReason;
1971
1972 return !empty($this->data);
1973 } else {
1974 $headers = [
1975 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
1976 ];
1977 try {
1978 $file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers);
1979 } catch (ClientException $th) {
1980 // If the file connection has an error, set SimplePie::error to that and quit
1981 $this->error = $th->getMessage();
1982
1983 return !empty($this->data);
1984 }
1985 }
1986 }
1987 $this->status_code = $file->get_status_code();
1988
1989 // If the file connection has an error, set SimplePie::error to that and quit
1990 if (!(!Misc::is_remote_uri($file->get_final_requested_uri()) || ($file->get_status_code() === 200 || $file->get_status_code() > 206 && $file->get_status_code() < 300))) {
1991 $this->error = 'Retrieved unsupported status code "' . $this->status_code . '"';
1992 return !empty($this->data);
1993 }
1994
1995 if (!$this->force_feed) {
1996 // Check if the supplied URL is a feed, if it isn't, look for it.
1997 $locate = $this->registry->create(Locator::class, [
1998 (!$file instanceof File) ? File::fromResponse($file) : $file,
1999 $this->timeout,
2000 $this->useragent,
2001 $this->max_checked_feeds,
2002 $this->force_fsockopen,
2003 $this->curl_options
2004 ]);
2005
2006 $http_client = $this->get_http_client();
2007
2008 if ($http_client instanceof Psr18Client) {
2009 $locate->set_http_client(
2010 $http_client->getHttpClient(),
2011 $http_client->getRequestFactory(),
2012 $http_client->getUriFactory()
2013 );
2014 }
2015
2016 if (!$locate->is_feed($file)) {
2017 $copyStatusCode = $file->get_status_code();
2018 $copyContentType = $file->get_header_line('content-type');
2019 try {
2020 $microformats = false;
2021 if (class_exists('DOMXpath') && function_exists('Mf2\parse')) {
2022 $doc = new \DOMDocument();
2023 @$doc->loadHTML($file->get_body_content());
2024 $xpath = new \DOMXpath($doc);
2025 // Check for both h-feed and h-entry, as both a feed with no entries
2026 // and a list of entries without an h-feed wrapper are both valid.
2027 $query = '//*[contains(concat(" ", @class, " "), " h-feed ") or '.
2028 'contains(concat(" ", @class, " "), " h-entry ")]';
2029
2030 /** @var \DOMNodeList<\DOMElement> $result */
2031 $result = $xpath->query($query);
2032 $microformats = $result->length !== 0;
2033 }
2034 // Now also do feed discovery, but if microformats were found don't
2035 // overwrite the current value of file.
2036 $discovered = $locate->find(
2037 $this->autodiscovery,
2038 $this->all_discovered_feeds
2039 );
2040 if ($microformats) {
2041 $hub = $locate->get_rel_link('hub');
2042 $self = $locate->get_rel_link('self');
2043 if ($hub || $self) {
2044 $file = $this->store_links($file, $hub, $self);
2045 }
2046 // Push the current file onto all_discovered feeds so the user can
2047 // be shown this as one of the options.
2048 if ($this->all_discovered_feeds !== null) {
2049 $this->all_discovered_feeds[] = $file;
2050 }
2051 } else {
2052 if ($discovered) {
2053 $file = $discovered;
2054 } else {
2055 // We need to unset this so that if SimplePie::set_file() has
2056 // been called that object is untouched
2057 unset($file);
2058 $this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`";
2059 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
2060 return false;
2061 }
2062 }
2063 } catch (SimplePieException $e) {
2064 // We need to unset this so that if SimplePie::set_file() has been called that object is untouched
2065 unset($file);
2066 // This is usually because DOMDocument doesn't exist
2067 $this->error = $e->getMessage();
2068 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, $e->getFile(), $e->getLine()]);
2069 return false;
2070 }
2071
2072 if ($cache) {
2073 $this->data = [
2074 'url' => $this->feed_url,
2075 'feed_url' => $file->get_final_requested_uri(),
2076 'build' => Misc::get_build(),
2077 'cache_expiration_time' => $this->cache_duration + time(),
2078 ];
2079
2080 if (!$cache->set_data($cacheKey, $this->data, $this->cache_duration)) {
2081 trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
2082 }
2083 }
2084 }
2085 $this->feed_url = $file->get_final_requested_uri();
2086 $locate = null;
2087 }
2088
2089 $this->raw_data = $file->get_body_content();
2090 $this->permanent_url = $file->get_permanent_uri();
2091
2092 $headers = [];
2093 foreach ($file->get_headers() as $key => $values) {
2094 $headers[$key] = implode(', ', $values);
2095 }
2096
2097 $sniffer = $this->registry->create(Sniffer::class, [&$file]);
2098 $sniffed = $sniffer->get_type();
2099
2100 return [$headers, $sniffed];
2101 }
2102
2103 /**
2104 * Get the error message for the occurred error
2105 *
2106 * @return string|string[]|null Error message, or array of messages for multifeeds
2107 */
2108 public function error()
2109 {
2110 return $this->error;
2111 }
2112
2113 /**
2114 * Get the last HTTP status code
2115 *
2116 * @return int Status code
2117 */
2118 public function status_code()
2119 {
2120 return $this->status_code;
2121 }
2122
2123 /**
2124 * Get the raw XML
2125 *
2126 * This is the same as the old `$feed->enable_xml_dump(true)`, but returns
2127 * the data instead of printing it.
2128 *
2129 * @return string|false Raw XML data, false if the cache is used
2130 */
2131 public function get_raw_data()
2132 {
2133 return $this->raw_data;
2134 }
2135
2136 /**
2137 * Get the character encoding used for output
2138 *
2139 * @since Preview Release
2140 * @return string
2141 */
2142 public function get_encoding()
2143 {
2144 return $this->sanitize->output_encoding;
2145 }
2146
2147 /**
2148 * Send the content-type header with correct encoding
2149 *
2150 * This method ensures that the SimplePie-enabled page is being served with
2151 * the correct {@link http://www.iana.org/assignments/media-types/ mime-type}
2152 * and character encoding HTTP headers (character encoding determined by the
2153 * {@see set_output_encoding} config option).
2154 *
2155 * This won't work properly if any content or whitespace has already been
2156 * sent to the browser, because it relies on PHP's
2157 * {@link http://php.net/header header()} function, and these are the
2158 * circumstances under which the function works.
2159 *
2160 * Because it's setting these settings for the entire page (as is the nature
2161 * of HTTP headers), this should only be used once per page (again, at the
2162 * top).
2163 *
2164 * @param string $mime MIME type to serve the page as
2165 * @return void
2166 */
2167 public function handle_content_type(string $mime = 'text/html')
2168 {
2169 if (!headers_sent()) {
2170 $header = "Content-type: $mime;";
2171 if ($this->get_encoding()) {
2172 $header .= ' charset=' . $this->get_encoding();
2173 } else {
2174 $header .= ' charset=UTF-8';
2175 }
2176 header($header);
2177 }
2178 }
2179
2180 /**
2181 * Get the type of the feed
2182 *
2183 * This returns a self::TYPE_* constant, which can be tested against
2184 * using {@link http://php.net/language.operators.bitwise bitwise operators}
2185 *
2186 * @since 0.8 (usage changed to using constants in 1.0)
2187 * @see self::TYPE_NONE Unknown.
2188 * @see self::TYPE_RSS_090 RSS 0.90.
2189 * @see self::TYPE_RSS_091_NETSCAPE RSS 0.91 (Netscape).
2190 * @see self::TYPE_RSS_091_USERLAND RSS 0.91 (Userland).
2191 * @see self::TYPE_RSS_091 RSS 0.91.
2192 * @see self::TYPE_RSS_092 RSS 0.92.
2193 * @see self::TYPE_RSS_093 RSS 0.93.
2194 * @see self::TYPE_RSS_094 RSS 0.94.
2195 * @see self::TYPE_RSS_10 RSS 1.0.
2196 * @see self::TYPE_RSS_20 RSS 2.0.x.
2197 * @see self::TYPE_RSS_RDF RDF-based RSS.
2198 * @see self::TYPE_RSS_SYNDICATION Non-RDF-based RSS (truly intended as syndication format).
2199 * @see self::TYPE_RSS_ALL Any version of RSS.
2200 * @see self::TYPE_ATOM_03 Atom 0.3.
2201 * @see self::TYPE_ATOM_10 Atom 1.0.
2202 * @see self::TYPE_ATOM_ALL Any version of Atom.
2203 * @see self::TYPE_ALL Any known/supported feed type.
2204 * @return int-mask-of<self::TYPE_*> constant
2205 */
2206 public function get_type()
2207 {
2208 if (!isset($this->data['type'])) {
2209 $this->data['type'] = self::TYPE_ALL;
2210 if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'])) {
2211 $this->data['type'] &= self::TYPE_ATOM_10;
2212 } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'])) {
2213 $this->data['type'] &= self::TYPE_ATOM_03;
2214 } elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'])) {
2215 if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['channel'])
2216 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['image'])
2217 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['item'])
2218 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['textinput'])) {
2219 $this->data['type'] &= self::TYPE_RSS_10;
2220 }
2221 if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['channel'])
2222 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['image'])
2223 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['item'])
2224 || isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['textinput'])) {
2225 $this->data['type'] &= self::TYPE_RSS_090;
2226 }
2227 } elseif (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'])) {
2228 $this->data['type'] &= self::TYPE_RSS_ALL;
2229 if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) {
2230 switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) {
2231 case '0.91':
2232 $this->data['type'] &= self::TYPE_RSS_091;
2233 if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) {
2234 switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) {
2235 case '0':
2236 $this->data['type'] &= self::TYPE_RSS_091_NETSCAPE;
2237 break;
2238
2239 case '24':
2240 $this->data['type'] &= self::TYPE_RSS_091_USERLAND;
2241 break;
2242 }
2243 }
2244 break;
2245
2246 case '0.92':
2247 $this->data['type'] &= self::TYPE_RSS_092;
2248 break;
2249
2250 case '0.93':
2251 $this->data['type'] &= self::TYPE_RSS_093;
2252 break;
2253
2254 case '0.94':
2255 $this->data['type'] &= self::TYPE_RSS_094;
2256 break;
2257
2258 case '2.0':
2259 $this->data['type'] &= self::TYPE_RSS_20;
2260 break;
2261 }
2262 }
2263 } else {
2264 $this->data['type'] = self::TYPE_NONE;
2265 }
2266 }
2267 return $this->data['type'];
2268 }
2269
2270 /**
2271 * Get the URL for the feed
2272 *
2273 * When the 'permanent' mode is enabled, returns the original feed URL,
2274 * except in the case of an `HTTP 301 Moved Permanently` status response,
2275 * in which case the location of the first redirection is returned.
2276 *
2277 * When the 'permanent' mode is disabled (default),
2278 * may or may not be different from the URL passed to {@see set_feed_url()},
2279 * depending on whether auto-discovery was used, and whether there were
2280 * any redirects along the way.
2281 *
2282 * @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.)
2283 * @todo Support <itunes:new-feed-url>
2284 * @todo Also, |atom:link|@rel=self
2285 * @param bool $permanent Permanent mode to return only the original URL or the first redirection
2286 * iff it is a 301 redirection
2287 * @return string|null
2288 */
2289 public function subscribe_url(bool $permanent = false)
2290 {
2291 if ($permanent) {
2292 if ($this->permanent_url !== null) {
2293 // sanitize encodes ampersands which are required when used in a url.
2294 return str_replace(
2295 '&',
2296 '&',
2297 $this->sanitize(
2298 $this->permanent_url,
2299 self::CONSTRUCT_IRI
2300 )
2301 );
2302 }
2303 } else {
2304 if ($this->feed_url !== null) {
2305 return str_replace(
2306 '&',
2307 '&',
2308 $this->sanitize(
2309 $this->feed_url,
2310 self::CONSTRUCT_IRI
2311 )
2312 );
2313 }
2314 }
2315 return null;
2316 }
2317
2318 /**
2319 * Get data for an feed-level element
2320 *
2321 * This method allows you to get access to ANY element/attribute that is a
2322 * sub-element of the opening feed tag.
2323 *
2324 * The return value is an indexed array of elements matching the given
2325 * namespace and tag name. Each element has `attribs`, `data` and `child`
2326 * subkeys. For `attribs` and `child`, these contain namespace subkeys.
2327 * `attribs` then has one level of associative name => value data (where
2328 * `value` is a string) after the namespace. `child` has tag-indexed keys
2329 * after the namespace, each member of which is an indexed array matching
2330 * this same format.
2331 *
2332 * For example:
2333 * <pre>
2334 * // This is probably a bad example because we already support
2335 * // <media:content> natively, but it shows you how to parse through
2336 * // the nodes.
2337 * $group = $item->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'group');
2338 * $content = $group[0]['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'];
2339 * $file = $content[0]['attribs']['']['url'];
2340 * echo $file;
2341 * </pre>
2342 *
2343 * @since 1.0
2344 * @see http://simplepie.org/wiki/faq/supported_xml_namespaces
2345 * @param string $namespace The URL of the XML namespace of the elements you're trying to access
2346 * @param string $tag Tag name
2347 * @return array<array<string, mixed>>|null
2348 */
2349 public function get_feed_tags(string $namespace, string $tag)
2350 {
2351 $type = $this->get_type();
2352 if ($type & self::TYPE_ATOM_10) {
2353 if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag])) {
2354 return $this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag];
2355 }
2356 }
2357 if ($type & self::TYPE_ATOM_03) {
2358 if (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag])) {
2359 return $this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag];
2360 }
2361 }
2362 if ($type & self::TYPE_RSS_RDF) {
2363 if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag])) {
2364 return $this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag];
2365 }
2366 }
2367 if ($type & self::TYPE_RSS_SYNDICATION) {
2368 if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag])) {
2369 return $this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag];
2370 }
2371 }
2372 return null;
2373 }
2374
2375 /**
2376 * Get data for an channel-level element
2377 *
2378 * This method allows you to get access to ANY element/attribute in the
2379 * channel/header section of the feed.
2380 *
2381 * See {@see SimplePie::get_feed_tags()} for a description of the return value
2382 *
2383 * @since 1.0
2384 * @see http://simplepie.org/wiki/faq/supported_xml_namespaces
2385 * @param string $namespace The URL of the XML namespace of the elements you're trying to access
2386 * @param string $tag Tag name
2387 * @return array<array<string, mixed>>|null
2388 */
2389 public function get_channel_tags(string $namespace, string $tag)
2390 {
2391 $type = $this->get_type();
2392 if ($type & self::TYPE_ATOM_ALL) {
2393 if ($return = $this->get_feed_tags($namespace, $tag)) {
2394 return $return;
2395 }
2396 }
2397 if ($type & self::TYPE_RSS_10) {
2398 if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'channel')) {
2399 if (isset($channel[0]['child'][$namespace][$tag])) {
2400 return $channel[0]['child'][$namespace][$tag];
2401 }
2402 }
2403 }
2404 if ($type & self::TYPE_RSS_090) {
2405 if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'channel')) {
2406 if (isset($channel[0]['child'][$namespace][$tag])) {
2407 return $channel[0]['child'][$namespace][$tag];
2408 }
2409 }
2410 }
2411 if ($type & self::TYPE_RSS_SYNDICATION) {
2412 if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_20, 'channel')) {
2413 if (isset($channel[0]['child'][$namespace][$tag])) {
2414 return $channel[0]['child'][$namespace][$tag];
2415 }
2416 }
2417 }
2418 return null;
2419 }
2420
2421 /**
2422 * Get data for an channel-level element
2423 *
2424 * This method allows you to get access to ANY element/attribute in the
2425 * image/logo section of the feed.
2426 *
2427 * See {@see SimplePie::get_feed_tags()} for a description of the return value
2428 *
2429 * @since 1.0
2430 * @see http://simplepie.org/wiki/faq/supported_xml_namespaces
2431 * @param string $namespace The URL of the XML namespace of the elements you're trying to access
2432 * @param string $tag Tag name
2433 * @return array<array<string, mixed>>|null
2434 */
2435 public function get_image_tags(string $namespace, string $tag)
2436 {
2437 $type = $this->get_type();
2438 if ($type & self::TYPE_RSS_10) {
2439 if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'image')) {
2440 if (isset($image[0]['child'][$namespace][$tag])) {
2441 return $image[0]['child'][$namespace][$tag];
2442 }
2443 }
2444 }
2445 if ($type & self::TYPE_RSS_090) {
2446 if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'image')) {
2447 if (isset($image[0]['child'][$namespace][$tag])) {
2448 return $image[0]['child'][$namespace][$tag];
2449 }
2450 }
2451 }
2452 if ($type & self::TYPE_RSS_SYNDICATION) {
2453 if ($image = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'image')) {
2454 if (isset($image[0]['child'][$namespace][$tag])) {
2455 return $image[0]['child'][$namespace][$tag];
2456 }
2457 }
2458 }
2459 return null;
2460 }
2461
2462 /**
2463 * Get the base URL value from the feed
2464 *
2465 * Uses `<xml:base>` if available,
2466 * otherwise uses the first 'self' link or the first 'alternate' link of the feed,
2467 * or failing that, the URL of the feed itself.
2468 *
2469 * @see get_link
2470 * @see subscribe_url
2471 *
2472 * @param array<string, mixed> $element
2473 * @return string
2474 */
2475 public function get_base(array $element = [])
2476 {
2477 if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) {
2478 return $element['xml_base'];
2479 }
2480 if (($link = $this->get_link(0, 'alternate')) !== null) {
2481 return $link;
2482 }
2483 if (($link = $this->get_link(0, 'self')) !== null) {
2484 return $link;
2485 }
2486
2487 return $this->subscribe_url() ?? '';
2488 }
2489
2490 /**
2491 * Sanitize feed data
2492 *
2493 * @access private
2494 * @see Sanitize::sanitize()
2495 * @param string $data Data to sanitize
2496 * @param int-mask-of<SimplePie::CONSTRUCT_*> $type
2497 * @param string $base Base URL to resolve URLs against
2498 * @return string Sanitized data
2499 */
2500 public function sanitize(string $data, int $type, string $base = '')
2501 {
2502 try {
2503 // This really returns string|false but changing encoding is uncommon and we are going to deprecate it, so let’s just lie to PHPStan in the interest of cleaner annotations.
2504 return $this->sanitize->sanitize($data, $type, $base);
2505 } catch (SimplePieException $e) {
2506 if (!$this->enable_exceptions) {
2507 $this->error = $e->getMessage();
2508 $this->registry->call(Misc::class, 'error', [$this->error, E_USER_WARNING, $e->getFile(), $e->getLine()]);
2509 return '';
2510 }
2511
2512 throw $e;
2513 }
2514 }
2515
2516 /**
2517 * Get the title of the feed
2518 *
2519 * Uses `<atom:title>`, `<title>` or `<dc:title>`
2520 *
2521 * @since 1.0 (previously called `get_feed_title` since 0.8)
2522 * @return string|null
2523 */
2524 public function get_title()
2525 {
2526 if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'title')) {
2527 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
2528 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'title')) {
2529 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
2530 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'title')) {
2531 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
2532 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'title')) {
2533 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
2534 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'title')) {
2535 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
2536 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'title')) {
2537 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2538 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'title')) {
2539 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2540 }
2541
2542 return null;
2543 }
2544
2545 /**
2546 * Get a category for the feed
2547 *
2548 * @since Unknown
2549 * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1
2550 * @return Category|null
2551 */
2552 public function get_category(int $key = 0)
2553 {
2554 $categories = $this->get_categories();
2555 if (isset($categories[$key])) {
2556 return $categories[$key];
2557 }
2558
2559 return null;
2560 }
2561
2562 /**
2563 * Get all categories for the feed
2564 *
2565 * Uses `<atom:category>`, `<category>` or `<dc:subject>`
2566 *
2567 * @since Unknown
2568 * @return array<Category>|null List of {@see Category} objects
2569 */
2570 public function get_categories()
2571 {
2572 $categories = [];
2573
2574 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'category') as $category) {
2575 $term = null;
2576 $scheme = null;
2577 $label = null;
2578 if (isset($category['attribs']['']['term'])) {
2579 $term = $this->sanitize($category['attribs']['']['term'], self::CONSTRUCT_TEXT);
2580 }
2581 if (isset($category['attribs']['']['scheme'])) {
2582 $scheme = $this->sanitize($category['attribs']['']['scheme'], self::CONSTRUCT_TEXT);
2583 }
2584 if (isset($category['attribs']['']['label'])) {
2585 $label = $this->sanitize($category['attribs']['']['label'], self::CONSTRUCT_TEXT);
2586 }
2587 $categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]);
2588 }
2589 foreach ((array) $this->get_channel_tags(self::NAMESPACE_RSS_20, 'category') as $category) {
2590 // This is really the label, but keep this as the term also for BC.
2591 // Label will also work on retrieving because that falls back to term.
2592 $term = $this->sanitize($category['data'], self::CONSTRUCT_TEXT);
2593 if (isset($category['attribs']['']['domain'])) {
2594 $scheme = $this->sanitize($category['attribs']['']['domain'], self::CONSTRUCT_TEXT);
2595 } else {
2596 $scheme = null;
2597 }
2598 $categories[] = $this->registry->create(Category::class, [$term, $scheme, null]);
2599 }
2600 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'subject') as $category) {
2601 $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]);
2602 }
2603 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'subject') as $category) {
2604 $categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]);
2605 }
2606
2607 if (!empty($categories)) {
2608 return array_unique($categories);
2609 }
2610
2611 return null;
2612 }
2613
2614 /**
2615 * Get an author for the feed
2616 *
2617 * @since 1.1
2618 * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1
2619 * @return Author|null
2620 */
2621 public function get_author(int $key = 0)
2622 {
2623 $authors = $this->get_authors();
2624 if (isset($authors[$key])) {
2625 return $authors[$key];
2626 }
2627
2628 return null;
2629 }
2630
2631 /**
2632 * Get all authors for the feed
2633 *
2634 * Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>`
2635 *
2636 * @since 1.1
2637 * @return array<Author>|null List of {@see Author} objects
2638 */
2639 public function get_authors()
2640 {
2641 $authors = [];
2642 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'author') as $author) {
2643 $name = null;
2644 $uri = null;
2645 $email = null;
2646 if (isset($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) {
2647 $name = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT);
2648 }
2649 if (isset($author['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) {
2650 $uri = $author['child'][self::NAMESPACE_ATOM_10]['uri'][0];
2651 $uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri));
2652 }
2653 if (isset($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) {
2654 $email = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT);
2655 }
2656 if ($name !== null || $email !== null || $uri !== null) {
2657 $authors[] = $this->registry->create(Author::class, [$name, $uri, $email]);
2658 }
2659 }
2660 if ($author = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'author')) {
2661 $name = null;
2662 $url = null;
2663 $email = null;
2664 if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) {
2665 $name = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT);
2666 }
2667 if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) {
2668 $url = $author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0];
2669 $url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url));
2670 }
2671 if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) {
2672 $email = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT);
2673 }
2674 if ($name !== null || $email !== null || $url !== null) {
2675 $authors[] = $this->registry->create(Author::class, [$name, $url, $email]);
2676 }
2677 }
2678 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'creator') as $author) {
2679 $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]);
2680 }
2681 foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'creator') as $author) {
2682 $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]);
2683 }
2684 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ITUNES, 'author') as $author) {
2685 $authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]);
2686 }
2687
2688 if (!empty($authors)) {
2689 return array_unique($authors);
2690 }
2691
2692 return null;
2693 }
2694
2695 /**
2696 * Get a contributor for the feed
2697 *
2698 * @since 1.1
2699 * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1
2700 * @return Author|null
2701 */
2702 public function get_contributor(int $key = 0)
2703 {
2704 $contributors = $this->get_contributors();
2705 if (isset($contributors[$key])) {
2706 return $contributors[$key];
2707 }
2708
2709 return null;
2710 }
2711
2712 /**
2713 * Get all contributors for the feed
2714 *
2715 * Uses `<atom:contributor>`
2716 *
2717 * @since 1.1
2718 * @return array<Author>|null List of {@see Author} objects
2719 */
2720 public function get_contributors()
2721 {
2722 $contributors = [];
2723 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'contributor') as $contributor) {
2724 $name = null;
2725 $uri = null;
2726 $email = null;
2727 if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) {
2728 $name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT);
2729 }
2730 if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) {
2731 $uri = $contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0];
2732 $uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri));
2733 }
2734 if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) {
2735 $email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT);
2736 }
2737 if ($name !== null || $email !== null || $uri !== null) {
2738 $contributors[] = $this->registry->create(Author::class, [$name, $uri, $email]);
2739 }
2740 }
2741 foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'contributor') as $contributor) {
2742 $name = null;
2743 $url = null;
2744 $email = null;
2745 if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) {
2746 $name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT);
2747 }
2748 if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) {
2749 $url = $contributor['child'][self::NAMESPACE_ATOM_03]['url'][0];
2750 $url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url));
2751 }
2752 if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) {
2753 $email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT);
2754 }
2755 if ($name !== null || $email !== null || $url !== null) {
2756 $contributors[] = $this->registry->create(Author::class, [$name, $url, $email]);
2757 }
2758 }
2759
2760 if (!empty($contributors)) {
2761 return array_unique($contributors);
2762 }
2763
2764 return null;
2765 }
2766
2767 /**
2768 * Get a single link for the feed
2769 *
2770 * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8)
2771 * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1
2772 * @param string $rel The relationship of the link to return
2773 * @return string|null Link URL
2774 */
2775 public function get_link(int $key = 0, string $rel = 'alternate')
2776 {
2777 $links = $this->get_links($rel);
2778 if (isset($links[$key])) {
2779 return $links[$key];
2780 }
2781
2782 return null;
2783 }
2784
2785 /**
2786 * Get the permalink for the item
2787 *
2788 * Returns the first link available with a relationship of "alternate".
2789 * Identical to {@see get_link()} with key 0
2790 *
2791 * @see get_link
2792 * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8)
2793 * @internal Added for parity between the parent-level and the item/entry-level.
2794 * @return string|null Link URL
2795 */
2796 public function get_permalink()
2797 {
2798 return $this->get_link(0);
2799 }
2800
2801 /**
2802 * Get all links for the feed
2803 *
2804 * Uses `<atom:link>` or `<link>`
2805 *
2806 * @since Beta 2
2807 * @param string $rel The relationship of links to return
2808 * @return array<string>|null Links found for the feed (strings)
2809 */
2810 public function get_links(string $rel = 'alternate')
2811 {
2812 if (!isset($this->data['links'])) {
2813 $this->data['links'] = [];
2814 if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'link')) {
2815 foreach ($links as $link) {
2816 if (isset($link['attribs']['']['href'])) {
2817 $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate';
2818 $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link));
2819 }
2820 }
2821 }
2822 if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'link')) {
2823 foreach ($links as $link) {
2824 if (isset($link['attribs']['']['href'])) {
2825 $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate';
2826 $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link));
2827 }
2828 }
2829 }
2830 if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'link')) {
2831 $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0]));
2832 }
2833 if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'link')) {
2834 $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0]));
2835 }
2836 if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'link')) {
2837 $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0]));
2838 }
2839
2840 $keys = array_keys($this->data['links']);
2841 foreach ($keys as $key) {
2842 if ($this->registry->call(Misc::class, 'is_isegment_nz_nc', [$key])) {
2843 if (isset($this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key])) {
2844 $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key]);
2845 $this->data['links'][$key] = &$this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key];
2846 } else {
2847 $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key];
2848 }
2849 } elseif (substr($key, 0, 41) === self::IANA_LINK_RELATIONS_REGISTRY) {
2850 $this->data['links'][substr($key, 41)] = &$this->data['links'][$key];
2851 }
2852 $this->data['links'][$key] = array_unique($this->data['links'][$key]);
2853 }
2854 }
2855
2856 if (isset($this->data['headers']['link'])) {
2857 $link_headers = $this->data['headers']['link'];
2858 if (is_array($link_headers)) {
2859 $link_headers = implode(',', $link_headers);
2860 }
2861 // https://datatracker.ietf.org/doc/html/rfc8288
2862 if (is_string($link_headers) &&
2863 preg_match_all('/<(?P<uri>[^>]+)>\s*;\s*rel\s*=\s*(?P<quote>"?)' . preg_quote($rel) . '(?P=quote)\s*(?=,|$)/i', $link_headers, $matches)) {
2864 return $matches['uri'];
2865 }
2866 }
2867
2868 if (isset($this->data['links'][$rel])) {
2869 return $this->data['links'][$rel];
2870 }
2871
2872 return null;
2873 }
2874
2875 /**
2876 * @return ?array<Response>
2877 */
2878 public function get_all_discovered_feeds()
2879 {
2880 return $this->all_discovered_feeds;
2881 }
2882
2883 /**
2884 * Get the content for the item
2885 *
2886 * Uses `<atom:subtitle>`, `<atom:tagline>`, `<description>`,
2887 * `<dc:description>`, `<itunes:summary>` or `<itunes:subtitle>`
2888 *
2889 * @since 1.0 (previously called `get_feed_description()` since 0.8)
2890 * @return string|null
2891 */
2892 public function get_description()
2893 {
2894 if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'subtitle')) {
2895 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
2896 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'tagline')) {
2897 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
2898 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'description')) {
2899 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
2900 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'description')) {
2901 return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
2902 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'description')) {
2903 return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0]));
2904 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'description')) {
2905 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2906 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'description')) {
2907 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2908 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'summary')) {
2909 return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0]));
2910 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'subtitle')) {
2911 return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0]));
2912 }
2913
2914 return null;
2915 }
2916
2917 /**
2918 * Get the copyright info for the feed
2919 *
2920 * Uses `<atom:rights>`, `<atom:copyright>` or `<dc:rights>`
2921 *
2922 * @since 1.0 (previously called `get_feed_copyright()` since 0.8)
2923 * @return string|null
2924 */
2925 public function get_copyright()
2926 {
2927 if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'rights')) {
2928 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
2929 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'copyright')) {
2930 return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
2931 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'copyright')) {
2932 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2933 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'rights')) {
2934 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2935 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'rights')) {
2936 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2937 }
2938
2939 return null;
2940 }
2941
2942 /**
2943 * Get the language for the feed
2944 *
2945 * Uses `<language>`, `<dc:language>`, or @xml_lang
2946 *
2947 * @since 1.0 (previously called `get_feed_language()` since 0.8)
2948 * @return string|null
2949 */
2950 public function get_language()
2951 {
2952 if ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'language')) {
2953 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2954 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'language')) {
2955 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2956 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'language')) {
2957 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
2958 } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'])) {
2959 return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT);
2960 } elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'])) {
2961 return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT);
2962 } elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'])) {
2963 return $this->sanitize($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'], self::CONSTRUCT_TEXT);
2964 } elseif (isset($this->data['headers']['content-language'])) {
2965 return $this->sanitize($this->data['headers']['content-language'], self::CONSTRUCT_TEXT);
2966 }
2967
2968 return null;
2969 }
2970
2971 /**
2972 * Get the latitude coordinates for the item
2973 *
2974 * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications
2975 *
2976 * Uses `<geo:lat>` or `<georss:point>`
2977 *
2978 * @since 1.0
2979 * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo
2980 * @link http://www.georss.org/ GeoRSS
2981 * @return float|null
2982 */
2983 public function get_latitude()
2984 {
2985 if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lat')) {
2986 return (float) $return[0]['data'];
2987 } elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) {
2988 return (float) $match[1];
2989 }
2990
2991 return null;
2992 }
2993
2994 /**
2995 * Get the longitude coordinates for the feed
2996 *
2997 * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications
2998 *
2999 * Uses `<geo:long>`, `<geo:lon>` or `<georss:point>`
3000 *
3001 * @since 1.0
3002 * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo
3003 * @link http://www.georss.org/ GeoRSS
3004 * @return float|null
3005 */
3006 public function get_longitude()
3007 {
3008 if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'long')) {
3009 return (float) $return[0]['data'];
3010 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lon')) {
3011 return (float) $return[0]['data'];
3012 } elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) {
3013 return (float) $match[2];
3014 }
3015
3016 return null;
3017 }
3018
3019 /**
3020 * Get the feed logo's title
3021 *
3022 * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" title.
3023 *
3024 * Uses `<image><title>` or `<image><dc:title>`
3025 *
3026 * @return string|null
3027 */
3028 public function get_image_title()
3029 {
3030 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'title')) {
3031 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
3032 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'title')) {
3033 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
3034 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'title')) {
3035 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
3036 } elseif ($return = $this->get_image_tags(self::NAMESPACE_DC_11, 'title')) {
3037 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
3038 } elseif ($return = $this->get_image_tags(self::NAMESPACE_DC_10, 'title')) {
3039 return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
3040 }
3041
3042 return null;
3043 }
3044
3045 /**
3046 * Get the feed logo's URL
3047 *
3048 * RSS 0.9.0, 2.0, Atom 1.0, and feeds with iTunes RSS tags are allowed to
3049 * have a "feed logo" URL. This points directly to the image itself.
3050 *
3051 * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`,
3052 * `<image><title>` or `<image><dc:title>`
3053 *
3054 * @return string|null
3055 */
3056 public function get_image_url()
3057 {
3058 if ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'image')) {
3059 return $this->sanitize($return[0]['attribs']['']['href'], self::CONSTRUCT_IRI);
3060 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'logo')) {
3061 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3062 } elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'icon')) {
3063 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3064 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'url')) {
3065 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3066 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'url')) {
3067 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3068 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) {
3069 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3070 }
3071
3072 return null;
3073 }
3074
3075
3076 /**
3077 * Get the feed logo's link
3078 *
3079 * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" link. This
3080 * points to a human-readable page that the image should link to.
3081 *
3082 * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`,
3083 * `<image><title>` or `<image><dc:title>`
3084 *
3085 * @return string|null
3086 */
3087 public function get_image_link()
3088 {
3089 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_10, 'link')) {
3090 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3091 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_090, 'link')) {
3092 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3093 } elseif ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'link')) {
3094 return $this->sanitize($return[0]['data'], self::CONSTRUCT_IRI, $this->get_base($return[0]));
3095 }
3096
3097 return null;
3098 }
3099
3100 /**
3101 * Get the feed logo's link
3102 *
3103 * RSS 2.0 feeds are allowed to have a "feed logo" width.
3104 *
3105 * Uses `<image><width>` or defaults to 88 if no width is specified and
3106 * the feed is an RSS 2.0 feed.
3107 *
3108 * @return int|null
3109 */
3110 public function get_image_width()
3111 {
3112 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'width')) {
3113 return intval($return[0]['data']);
3114 } elseif ($this->get_type() & self::TYPE_RSS_SYNDICATION && $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) {
3115 return 88;
3116 }
3117
3118 return null;
3119 }
3120
3121 /**
3122 * Get the feed logo's height
3123 *
3124 * RSS 2.0 feeds are allowed to have a "feed logo" height.
3125 *
3126 * Uses `<image><height>` or defaults to 31 if no height is specified and
3127 * the feed is an RSS 2.0 feed.
3128 *
3129 * @return int|null
3130 */
3131 public function get_image_height()
3132 {
3133 if ($return = $this->get_image_tags(self::NAMESPACE_RSS_20, 'height')) {
3134 return intval($return[0]['data']);
3135 } elseif ($this->get_type() & self::TYPE_RSS_SYNDICATION && $this->get_image_tags(self::NAMESPACE_RSS_20, 'url')) {
3136 return 31;
3137 }
3138
3139 return null;
3140 }
3141
3142 /**
3143 * Get the number of items in the feed
3144 *
3145 * This is well-suited for {@link http://php.net/for for()} loops with
3146 * {@see get_item()}
3147 *
3148 * @param int $max Maximum value to return. 0 for no limit
3149 * @return int Number of items in the feed
3150 */
3151 public function get_item_quantity(int $max = 0)
3152 {
3153 $qty = count($this->get_items());
3154 if ($max === 0) {
3155 return $qty;
3156 }
3157
3158 return min($qty, $max);
3159 }
3160
3161 /**
3162 * Get a single item from the feed
3163 *
3164 * This is better suited for {@link http://php.net/for for()} loops, whereas
3165 * {@see get_items()} is better suited for
3166 * {@link http://php.net/foreach foreach()} loops.
3167 *
3168 * @see get_item_quantity()
3169 * @since Beta 2
3170 * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1
3171 * @return Item|null
3172 */
3173 public function get_item(int $key = 0)
3174 {
3175 $items = $this->get_items();
3176 if (isset($items[$key])) {
3177 return $items[$key];
3178 }
3179
3180 return null;
3181 }
3182
3183 /**
3184 * Get all items from the feed
3185 *
3186 * This is better suited for {@link http://php.net/for for()} loops, whereas
3187 * {@see get_items()} is better suited for
3188 * {@link http://php.net/foreach foreach()} loops.
3189 *
3190 * @see get_item_quantity
3191 * @since Beta 2
3192 * @param int $start Index to start at
3193 * @param int $end Number of items to return. 0 for all items after `$start`
3194 * @return Item[] List of {@see Item} objects
3195 */
3196 public function get_items(int $start = 0, int $end = 0)
3197 {
3198 if (!isset($this->data['items'])) {
3199 if (!empty($this->multifeed_objects)) {
3200 $this->data['items'] = SimplePie::merge_items($this->multifeed_objects, $start, $end, $this->item_limit);
3201 if (empty($this->data['items'])) {
3202 return [];
3203 }
3204 return $this->data['items'];
3205 }
3206 $this->data['items'] = [];
3207 if ($items = $this->get_feed_tags(self::NAMESPACE_ATOM_10, 'entry')) {
3208 $keys = array_keys($items);
3209 foreach ($keys as $key) {
3210 $this->data['items'][] = $this->make_item($items[$key]);
3211 }
3212 }
3213 if ($items = $this->get_feed_tags(self::NAMESPACE_ATOM_03, 'entry')) {
3214 $keys = array_keys($items);
3215 foreach ($keys as $key) {
3216 $this->data['items'][] = $this->make_item($items[$key]);
3217 }
3218 }
3219 if ($items = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'item')) {
3220 $keys = array_keys($items);
3221 foreach ($keys as $key) {
3222 $this->data['items'][] = $this->make_item($items[$key]);
3223 }
3224 }
3225 if ($items = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'item')) {
3226 $keys = array_keys($items);
3227 foreach ($keys as $key) {
3228 $this->data['items'][] = $this->make_item($items[$key]);
3229 }
3230 }
3231 if ($items = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'item')) {
3232 $keys = array_keys($items);
3233 foreach ($keys as $key) {
3234 $this->data['items'][] = $this->make_item($items[$key]);
3235 }
3236 }
3237 }
3238
3239 if (empty($this->data['items'])) {
3240 return [];
3241 }
3242
3243 if ($this->order_by_date) {
3244 if (!isset($this->data['ordered_items'])) {
3245 $this->data['ordered_items'] = $this->data['items'];
3246 usort($this->data['ordered_items'], [get_class($this), 'sort_items']);
3247 }
3248 $items = $this->data['ordered_items'];
3249 } else {
3250 $items = $this->data['items'];
3251 }
3252 // Slice the data as desired
3253 if ($end === 0) {
3254 return array_slice($items, $start);
3255 }
3256
3257 return array_slice($items, $start, $end);
3258 }
3259
3260 /**
3261 * Set the favicon handler
3262 *
3263 * @deprecated Use your own favicon handling instead
3264 * @param string|false $page
3265 * @return bool
3266 */
3267 public function set_favicon_handler($page = false, string $qs = 'i')
3268 {
3269 trigger_error('Favicon handling has been removed since SimplePie 1.3, please use your own handling', \E_USER_DEPRECATED);
3270 return false;
3271 }
3272
3273 /**
3274 * Get the favicon for the current feed
3275 *
3276 * @deprecated Use your own favicon handling instead
3277 * @return string|bool
3278 */
3279 public function get_favicon()
3280 {
3281 trigger_error('Favicon handling has been removed since SimplePie 1.3, please use your own handling', \E_USER_DEPRECATED);
3282
3283 if (($url = $this->get_link()) !== null) {
3284 return 'https://www.google.com/s2/favicons?domain=' . urlencode($url);
3285 }
3286
3287 return false;
3288 }
3289
3290 /**
3291 * Magic method handler
3292 *
3293 * @param string $method Method name
3294 * @param array<mixed> $args Arguments to the method
3295 * @return mixed
3296 */
3297 public function __call(string $method, array $args)
3298 {
3299 if (strpos($method, 'subscribe_') === 0) {
3300 trigger_error('subscribe_*() has been deprecated since SimplePie 1.3, implement the callback yourself', \E_USER_DEPRECATED);
3301 return '';
3302 }
3303 if ($method === 'enable_xml_dump') {
3304 trigger_error('enable_xml_dump() has been deprecated since SimplePie 1.3, use get_raw_data() instead', \E_USER_DEPRECATED);
3305 return false;
3306 }
3307
3308 $class = get_class($this);
3309 $trace = debug_backtrace(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
3310 $file = $trace[0]['file'] ?? '';
3311 $line = $trace[0]['line'] ?? '';
3312 throw new SimplePieException("Call to undefined method $class::$method() in $file on line $line");
3313 }
3314
3315 /**
3316 * Item factory
3317 *
3318 * @param array<string, mixed> $data
3319 */
3320 private function make_item(array $data): Item
3321 {
3322 $item = $this->registry->create(Item::class, [$this, $data]);
3323 $item->set_sanitize($this->sanitize);
3324
3325 return $item;
3326 }
3327
3328 /**
3329 * Sorting callback for items
3330 *
3331 * @access private
3332 * @param Item $a
3333 * @param Item $b
3334 * @return -1|0|1
3335 */
3336 public static function sort_items(Item $a, Item $b)
3337 {
3338 $a_date = $a->get_date('U');
3339 $b_date = $b->get_date('U');
3340 if ($a_date && $b_date) {
3341 return $a_date > $b_date ? -1 : 1;
3342 }
3343 // Sort items without dates to the top.
3344 if ($a_date) {
3345 return 1;
3346 }
3347 if ($b_date) {
3348 return -1;
3349 }
3350 return 0;
3351 }
3352
3353 /**
3354 * Merge items from several feeds into one
3355 *
3356 * If you're merging multiple feeds together, they need to all have dates
3357 * for the items or else SimplePie will refuse to sort them.
3358 *
3359 * @link http://simplepie.org/wiki/tutorial/sort_multiple_feeds_by_time_and_date#if_feeds_require_separate_per-feed_settings
3360 * @param array<SimplePie> $urls List of SimplePie feed objects to merge
3361 * @param int $start Starting item
3362 * @param int $end Number of items to return
3363 * @param int $limit Maximum number of items per feed
3364 * @return array<Item>
3365 */
3366 public static function merge_items(array $urls, int $start = 0, int $end = 0, int $limit = 0)
3367 {
3368 if (count($urls) > 0) {
3369 $items = [];
3370 foreach ($urls as $arg) {
3371 if ($arg instanceof SimplePie) {
3372 $items = array_merge($items, $arg->get_items(0, $limit));
3373
3374 // @phpstan-ignore-next-line Enforce PHPDoc type.
3375 } else {
3376 trigger_error('Arguments must be SimplePie objects', E_USER_WARNING);
3377 }
3378 }
3379
3380 usort($items, [get_class($urls[0]), 'sort_items']);
3381
3382 if ($end === 0) {
3383 return array_slice($items, $start);
3384 }
3385
3386 return array_slice($items, $start, $end);
3387 }
3388
3389 trigger_error('Cannot merge zero SimplePie objects', E_USER_WARNING);
3390 return [];
3391 }
3392
3393 /**
3394 * Store PubSubHubbub links as headers
3395 *
3396 * There is no way to find PuSH links in the body of a microformats feed,
3397 * so they are added to the headers when found, to be used later by get_links.
3398 */
3399 private function store_links(Response $file, ?string $hub, ?string $self): Response
3400 {
3401 $linkHeaderLine = $file->get_header_line('link');
3402 $linkHeader = $file->get_header('link');
3403
3404 if ($hub && !preg_match('/rel=hub/', $linkHeaderLine)) {
3405 $linkHeader[] = '<'.$hub.'>; rel=hub';
3406 }
3407
3408 if ($self && !preg_match('/rel=self/', $linkHeaderLine)) {
3409 $linkHeader[] = '<'.$self.'>; rel=self';
3410 }
3411
3412 if (count($linkHeader) > 0) {
3413 $file = $file->with_header('link', $linkHeader);
3414 }
3415
3416 return $file;
3417 }
3418
3419 /**
3420 * Get a DataCache
3421 *
3422 * @param string $feed_url Only needed for BC, can be removed in SimplePie 2.0.0
3423 *
3424 * @return DataCache
3425 */
3426 private function get_cache(string $feed_url = ''): DataCache
3427 {
3428 if ($this->cache === null) {
3429 // @trigger_error(sprintf('Not providing as PSR-16 cache implementation is deprecated since SimplePie 1.8.0, please use "SimplePie\SimplePie::set_cache()".'), \E_USER_DEPRECATED);
3430 $cache = $this->registry->call(Cache::class, 'get_handler', [
3431 $this->cache_location,
3432 $this->get_cache_filename($feed_url),
3433 Base::TYPE_FEED
3434 ]);
3435
3436 return new BaseDataCache($cache);
3437 }
3438
3439 return $this->cache;
3440 }
3441
3442 /**
3443 * Get a HTTP client
3444 */
3445 private function get_http_client(): Client
3446 {
3447 if ($this->http_client === null) {
3448 $this->http_client = new FileClient(
3449 $this->get_registry(),
3450 [
3451 'timeout' => $this->timeout,
3452 'redirects' => 5,
3453 'useragent' => $this->useragent,
3454 'force_fsockopen' => $this->force_fsockopen,
3455 'curl_options' => $this->curl_options,
3456 ]
3457 );
3458 $this->http_client_injected = true;
3459 }
3460
3461 return $this->http_client;
3462 }
3463}
3464
3465class_alias('SimplePie\SimplePie', 'SimplePie');
3466