Forráskód Böngészése

Better enclosures (#4944)

* Better enclosures
#fix https://github.com/FreshRSS/FreshRSS/issues/4702
Improvement of https://github.com/FreshRSS/FreshRSS/pull/2898

* A few fixes

* Better enclosure titles

* Improve thumbnails

* Implement thumbnail for HTML+XPath

* Avoid duplicate enclosures
#fix https://github.com/FreshRSS/FreshRSS/issues/1668

* Fix regex

* Add basic support for media:credit
And use <figure> for enclosures

* Fix link encoding + simplify code

* Fix some SimplePie bugs
Encoding errors in enclosure links

* Remove debugging syslog

* Remove debugging syslog

* SimplePie fix multiple RSS2 enclosures
#fix https://github.com/FreshRSS/FreshRSS/issues/4974

* Improve thumbnails

* Performance with yield
Avoid generating all enclosures if not used

* API keep providing enclosures inside content
Clients are typically not showing the enclosures to the users (tested with News+, FeedMe, Readrops, Fluent Reader Lite)

* Lint

* Fix API output enclosure

* Fix API content strcut

* API tolerate enclosures without a type
Alexandre Alapetite 3 éve
szülő
commit
8f9c4143fc

+ 1 - 1
app/Controllers/feedController.php

@@ -949,7 +949,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 				$this->view->htmlContent = $fullContent;
 			} else {
 				$this->view->selectorSuccess = false;
-				$this->view->htmlContent = $entry->content();
+				$this->view->htmlContent = $entry->content(false);
 			}
 		} catch (Exception $e) {
 			$this->view->fatalError = _t('feedback.sub.feed.selector_preview.http_error');

+ 140 - 27
app/Models/Entry.php

@@ -67,7 +67,9 @@ class FreshRSS_Entry extends Minz_Model {
 			$dao['content'] = '';
 		}
 		if (!empty($dao['thumbnail'])) {
-			$dao['content'] .= '<p class="enclosure-content"><img src="' . $dao['thumbnail'] . '" alt="" /></p>';
+			$dao['attributes']['thumbnail'] = [
+				'url' => $dao['thumbnail'],
+			];
 		}
 		$entry = new FreshRSS_Entry(
 			$dao['id_feed'] ?? 0,
@@ -116,15 +118,117 @@ class FreshRSS_Entry extends Minz_Model {
 			return $this->authors;
 		}
 	}
-	public function content(): string {
-		return $this->content;
+
+	/**
+	 * Basic test without ambition to catch all cases such as unquoted addresses, variants of entities, HTML comments, etc.
+	 */
+	private static function containsLink(string $html, string $link): bool {
+		return preg_match('/(?P<delim>[\'"])' . preg_quote($link, '/') . '(?P=delim)/', $html) == 1;
+	}
+
+	private static function enclosureIsImage(array $enclosure): bool {
+		$elink = $enclosure['url'] ?? '';
+		$length = $enclosure['length'] ?? 0;
+		$medium = $enclosure['medium'] ?? '';
+		$mime = $enclosure['type'] ?? '';
+
+		return $elink != '' && $medium === 'image' || strpos($mime, 'image') === 0 ||
+			($mime == '' && $length == 0 && preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink));
 	}
 
-	/** @return array<array<string,string>> */
-	public function enclosures(bool $searchBodyImages = false): array {
-		$results = [];
+	/**
+	 * @param bool $withEnclosures Set to true to include the enclosures in the returned HTML, false otherwise.
+	 * @param bool $allowDuplicateEnclosures Set to false to remove obvious enclosure duplicates (based on simple string comparison), true otherwise.
+	 * @return string HTML content
+	 */
+	public function content(bool $withEnclosures = true, bool $allowDuplicateEnclosures = false): string {
+		if (!$withEnclosures) {
+			return $this->content;
+		}
+
+		$content = $this->content;
+
+		$thumbnail = $this->attributes('thumbnail');
+		if (!empty($thumbnail['url'])) {
+			$elink = $thumbnail['url'];
+			if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) {
+			$content .= <<<HTML
+<figure class="enclosure">
+	<p class="enclosure-content">
+		<img class="enclosure-thumbnail" src="{$elink}" alt="" />
+	</p>
+</figure>
+HTML;
+			}
+		}
+
+		$attributeEnclosures = $this->attributes('enclosures');
+		if (empty($attributeEnclosures)) {
+			return $content;
+		}
+
+		foreach ($attributeEnclosures as $enclosure) {
+			$elink = $enclosure['url'] ?? '';
+			if ($elink == '') {
+				continue;
+			}
+			if (!$allowDuplicateEnclosures && self::containsLink($content, $elink)) {
+				continue;
+			}
+			$credit = $enclosure['credit'] ?? '';
+			$description = $enclosure['description'] ?? '';
+			$length = $enclosure['length'] ?? 0;
+			$medium = $enclosure['medium'] ?? '';
+			$mime = $enclosure['type'] ?? '';
+			$thumbnails = $enclosure['thumbnails'] ?? [];
+			$etitle = $enclosure['title'] ?? '';
+
+			$content .= '<figure class="enclosure">';
+
+			foreach ($thumbnails as $thumbnail) {
+				$content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>';
+			}
+
+			if (self::enclosureIsImage($enclosure)) {
+				$content .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" title="' . $etitle . '" /></p>';
+			} elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
+				$content .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
+					. ($length == null ? '' : '" data-length="' . intval($length))
+					. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+					. '" controls="controls" title="' . $etitle . '"></audio> <a download="" href="' . $elink . '">💾</a></p>';
+			} elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
+				$content .= '<p class="enclosure-content"><video preload="none" src="' . $elink
+					. ($length == null ? '' : '" data-length="' . intval($length))
+					. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+					. '" controls="controls" title="' . $etitle . '"></video> <a download="" href="' . $elink . '">💾</a></p>';
+			} else {	//e.g. application, text, unknown
+				$content .= '<p class="enclosure-content"><a download="" href="' . $elink
+					. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+					. ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
+					. '" title="' . $etitle . '">💾</a></p>';
+			}
+
+			if ($credit != '') {
+				$content .= '<p class="enclosure-credits">© ' . $credit . '</p>';
+			}
+			if ($description != '') {
+				$content .= '<figcaption class="enclosure-description">' . $description . '</figcaption>';
+			}
+			$content .= "</figure>\n";
+		}
+
+		return $content;
+	}
+
+	/** @return iterable<array<string,string>> */
+	public function enclosures(bool $searchBodyImages = false) {
+		$attributeEnclosures = $this->attributes('enclosures');
+		if (is_array($attributeEnclosures)) {
+			// FreshRSS 1.20.1+: The enclosures are saved as attributes
+			yield from $attributeEnclosures;
+		}
 		try {
-			$searchEnclosures = strpos($this->content, '<p class="enclosure-content') !== false;
+			$searchEnclosures = !is_array($attributeEnclosures) && (strpos($this->content, '<p class="enclosure-content') !== false);
 			$searchBodyImages &= (stripos($this->content, '<img') !== false);
 			$xpath = null;
 			if ($searchEnclosures || $searchBodyImages) {
@@ -133,6 +237,7 @@ class FreshRSS_Entry extends Minz_Model {
 				$xpath = new DOMXpath($dom);
 			}
 			if ($searchEnclosures) {
+				// Legacy code for database entries < FreshRSS 1.20.1
 				$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
 				foreach ($enclosures as $enclosure) {
 					$result = [
@@ -148,7 +253,7 @@ class FreshRSS_Entry extends Minz_Model {
 							case 'audio': $result['medium'] = 'audio'; break;
 						}
 					}
-					$results[] = $result;
+					yield Minz_Helper::htmlspecialchars_utf8($result);
 				}
 			}
 			if ($searchBodyImages) {
@@ -159,26 +264,31 @@ class FreshRSS_Entry extends Minz_Model {
 						$src = $img->getAttribute('data-src');
 					}
 					if ($src != null) {
-						$results[] = [
+						$result = [
 							'url' => $src,
-							'alt' => $img->getAttribute('alt'),
 						];
+						yield Minz_Helper::htmlspecialchars_utf8($result);
 					}
 				}
 			}
-			return $results;
 		} catch (Exception $ex) {
-			return $results;
+			Minz_Log::debug(__METHOD__ . ' ' . $ex->getMessage());
 		}
 	}
 
 	/**
 	 * @return array<string,string>|null
 	 */
-	public function thumbnail() {
-		foreach ($this->enclosures(true) as $enclosure) {
-			if (!empty($enclosure['url']) && empty($enclosure['type'])) {
-				return $enclosure;
+	public function thumbnail(bool $searchEnclosures = true) {
+		$thumbnail = $this->attributes('thumbnail');
+		if (!empty($thumbnail['url'])) {
+			return $thumbnail;
+		}
+		if ($searchEnclosures) {
+			foreach ($this->enclosures(true) as $enclosure) {
+				if (self::enclosureIsImage($enclosure)) {
+					return $enclosure;
+				}
 			}
 		}
 		return null;
@@ -587,7 +697,7 @@ class FreshRSS_Entry extends Minz_Model {
 
 			if ($entry) {
 				// l’article existe déjà en BDD, en se contente de recharger ce contenu
-				$this->content = $entry->content();
+				$this->content = $entry->content(false);
 			} else {
 				try {
 					// The article is not yet in the database, so let’s fetch it
@@ -629,7 +739,7 @@ class FreshRSS_Entry extends Minz_Model {
 			'guid' => $this->guid(),
 			'title' => $this->title(),
 			'author' => $this->authors(true),
-			'content' => $this->content(),
+			'content' => $this->content(false),
 			'link' => $this->link(),
 			'date' => $this->date(true),
 			'hash' => $this->hash(),
@@ -677,7 +787,6 @@ class FreshRSS_Entry extends Minz_Model {
 			'published' => $this->date(true),
 			// 'updated' => $this->date(true),
 			'title' => $this->title(),
-			'summary' => ['content' => $this->content()],
 			'canonical' => [
 				['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)],
 			],
@@ -697,13 +806,16 @@ class FreshRSS_Entry extends Minz_Model {
 		if ($mode === 'compat') {
 			$item['title'] = escapeToUnicodeAlternative($this->title(), false);
 			unset($item['alternate'][0]['type']);
-			if (mb_strlen($this->content(), 'UTF-8') > self::API_MAX_COMPAT_CONTENT_LENGTH) {
-				$item['summary']['content'] = mb_strcut($this->content(), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8');
-			}
-		} elseif ($mode === 'freshrss') {
+			$item['summary'] = [
+				'content' => mb_strcut($this->content(true), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'),
+			];
+		} else {
+			$item['content'] = [
+				'content' => $this->content(false),
+			];
+		}
+		if ($mode === 'freshrss') {
 			$item['guid'] = $this->guid();
-			unset($item['summary']);
-			$item['content'] = ['content' => $this->content()];
 		}
 		if ($category != null && $mode !== 'freshrss') {
 			$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES);
@@ -718,10 +830,11 @@ class FreshRSS_Entry extends Minz_Model {
 			}
 		}
 		foreach ($this->enclosures() as $enclosure) {
-			if (!empty($enclosure['url']) && !empty($enclosure['type'])) {
+			if (!empty($enclosure['url'])) {
 				$media = [
 						'href' => $enclosure['url'],
-						'type' => $enclosure['type'],
+						'type' => $enclosure['type'] ?? $enclosure['medium'] ??
+							(self::enclosureIsImage($enclosure) ? 'image' : ''),
 					];
 				if (!empty($enclosure['length'])) {
 					$media['length'] = intval($enclosure['length']);

+ 31 - 42
app/Models/Feed.php

@@ -502,61 +502,46 @@ class FreshRSS_Feed extends Minz_Model {
 
 			$content = html_only_entity_decode($item->get_content());
 
-			if ($item->get_enclosures() != null) {
-				$elinks = array();
+			$attributeThumbnail = $item->get_thumbnail() ?? [];
+			if (empty($attributeThumbnail['url'])) {
+				$attributeThumbnail['url'] = '';
+			}
+
+			$attributeEnclosures = [];
+			if (!empty($item->get_enclosures())) {
 				foreach ($item->get_enclosures() as $enclosure) {
 					$elink = $enclosure->get_link();
-					if ($elink != '' && empty($elinks[$elink])) {
-						$content .= '<div class="enclosure">';
-
-						if ($enclosure->get_title() != '') {
-							$content .= '<p class="enclosure-title">' . $enclosure->get_title() . '</p>';
-						}
-
-						$enclosureContent = '';
-						$elinks[$elink] = true;
+					if ($elink != '') {
+						$etitle = $enclosure->get_title() ?? '';
+						$credit = $enclosure->get_credit() ?? null;
+						$description = $enclosure->get_description() ?? '';
 						$mime = strtolower($enclosure->get_type() ?? '');
 						$medium = strtolower($enclosure->get_medium() ?? '');
 						$height = $enclosure->get_height();
 						$width = $enclosure->get_width();
 						$length = $enclosure->get_length();
-						if ($medium === 'image' || strpos($mime, 'image') === 0 ||
-							($mime == '' && $length == null && ($width != 0 || $height != 0 || preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink)))) {
-							$enclosureContent .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" /></p>';
-						} elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
-							$enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
-								. ($length == null ? '' : '" data-length="' . intval($length))
-								. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
-								. '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
-						} elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
-							$enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink
-								. ($length == null ? '' : '" data-length="' . intval($length))
-								. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
-								. '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
-						} else {	//e.g. application, text, unknown
-							$enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink
-								. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
-								. ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
-								. '">💾</a></p>';
-						}
 
-						$thumbnailContent = '';
-						if ($enclosure->get_thumbnails() != null) {
+						$attributeEnclosure = [
+							'url' => $elink,
+						];
+						if ($etitle != '') $attributeEnclosure['title'] = $etitle;
+						if ($credit != null) $attributeEnclosure['credit'] = $credit->get_name();
+						if ($description != '') $attributeEnclosure['description'] = $description;
+						if ($mime != '') $attributeEnclosure['type'] = $mime;
+						if ($medium != '') $attributeEnclosure['medium'] = $medium;
+						if ($length != '') $attributeEnclosure['length'] = intval($length);
+						if ($height != '') $attributeEnclosure['height'] = intval($height);
+						if ($width != '') $attributeEnclosure['width'] = intval($width);
+
+						if (!empty($enclosure->get_thumbnails())) {
 							foreach ($enclosure->get_thumbnails() as $thumbnail) {
-								if (empty($elinks[$thumbnail])) {
-									$elinks[$thumbnail] = true;
-									$thumbnailContent .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" /></p>';
+								if ($thumbnail !== $attributeThumbnail['url']) {
+									$attributeEnclosure['thumbnails'][] = $thumbnail;
 								}
 							}
 						}
 
-						$content .= $thumbnailContent;
-						$content .= $enclosureContent;
-
-						if ($enclosure->get_description() != '') {
-							$content .= '<p class="enclosure-description">' . $enclosure->get_description() . '</p>';
-						}
-						$content .= "</div>\n";
+						$attributeEnclosures[] = $attributeEnclosure;
 					}
 				}
 			}
@@ -586,6 +571,10 @@ class FreshRSS_Feed extends Minz_Model {
 			);
 			$entry->_tags($tags);
 			$entry->_feed($this);
+			if (!empty($attributeThumbnail['url'])) {
+				$entry->_attributes('thumbnail', $attributeThumbnail);
+			}
+			$entry->_attributes('enclosures', $attributeEnclosures);
 			$entry->hash();	//Must be computed before loading full content
 			$entry->loadCompleteContent();	// Optionally load full content for truncated feeds
 

+ 2 - 3
app/views/helpers/index/normal/entry_header.phtml

@@ -42,8 +42,7 @@
 	?><li class="item thumbnail <?= $topline_thumbnail ?> <?= $topline_summary ? '' : 'small' ?>"><?php
 		$thumbnail = $this->entry->thumbnail();
 		if ($thumbnail != null):
-			?><img src="<?= htmlspecialchars($thumbnail['url'], ENT_COMPAT, 'UTF-8') ?>" class="item-element "<?= $lazyload ? ' loading="lazy"' : '' ?><?=
-				empty($thumbnail['alt']) ? '' : ' alt="' . htmlspecialchars(strip_tags($thumbnail['alt']), ENT_COMPAT, 'UTF-8') . '"' ?> /><?php
+			?><img src="<?= $thumbnail['url'] ?>" class="item-element "<?= $lazyload ? ' loading="lazy"' : '' ?> alt="" /><?php
 		endif;
 	?></li><?php
 	endif; ?>
@@ -62,7 +61,7 @@
 			?></span><?php
 		endif;
 		if ($topline_summary):
-			?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content()), 0, 500, 'UTF-8')) ?></div><?php
+			?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content(false)), 0, 500, 'UTF-8')) ?></div><?php
 		endif;
 	?></a></li>
 	<?php if ($topline_date) { ?><li class="item date"><time datetime="<?= $this->entry->machineReadableDate() ?>" class="item-element"><?= $this->entry->date() ?></time>&nbsp;</li><?php } ?>

+ 1 - 1
app/views/index/normal.phtml

@@ -162,7 +162,7 @@ $today = @strtotime('today');
 					<?php } ?>
 				</header>
 				<div class="text"><?php
-					echo $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content();
+					echo $lazyload && $hidePosts ? lazyimg($this->entry->content(true)) : $this->entry->content(true);
 				?></div>
 				<?php
 				$display_authors_date = FreshRSS_Context::$user_conf->show_author_date === 'f' || FreshRSS_Context::$user_conf->show_author_date === 'b';

+ 1 - 1
app/views/index/reader.phtml

@@ -136,7 +136,7 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
 				</header>
 
 				<div class="text">
-					<?= $item->content() ?>
+					<?= $item->content(true) ?>
 				</div>
 				<?php
 				$display_authors_date = FreshRSS_Context::$user_conf->show_author_date === 'f' || FreshRSS_Context::$user_conf->show_author_date === 'b';

+ 17 - 5
app/views/index/rss.phtml

@@ -29,29 +29,41 @@ foreach ($this->entries as $item) {
 				$authors = $item->authors();
 				if (is_array($authors)) {
 					foreach ($authors as $author) {
-						echo "\t\t\t" , '<dc:creator>', $author, '</dc:creator>', "\n";
+						echo "\t\t\t", '<dc:creator>', $author, '</dc:creator>', "\n";
 					}
 				}
 				$categories = $item->tags();
 				if (is_array($categories)) {
 					foreach ($categories as $category) {
-						echo "\t\t\t" , '<category>', $category, '</category>', "\n";
+						echo "\t\t\t", '<category>', $category, '</category>', "\n";
 					}
 				}
+				$thumbnail = $item->thumbnail(false);
+				if (!empty($thumbnail['url'])) {
+					// https://www.rssboard.org/media-rss#media-thumbnails
+					echo "\t\t\t", '<media:thumbnail url="' . $thumbnail['url']
+						. (empty($thumbnail['width']) ? '' : '" width="' . $thumbnail['width'])
+						. (empty($thumbnail['height']) ? '' : '" height="' . $thumbnail['height'])
+						. (empty($thumbnail['time']) ? '' : '" time="' . $thumbnail['time'])
+						. '" />', "\n";
+				}
 				$enclosures = $item->enclosures(false);
 				if (is_array($enclosures)) {
 					foreach ($enclosures as $enclosure) {
 						// https://www.rssboard.org/media-rss
-						echo "\t\t\t" , '<media:content url="' . $enclosure['url']
+						echo "\t\t\t", '<media:content url="' . $enclosure['url']
 							. (empty($enclosure['medium']) ? '' : '" medium="' . $enclosure['medium'])
 							. (empty($enclosure['type']) ? '' : '" type="' . $enclosure['type'])
 							. (empty($enclosure['length']) ? '' : '" length="' . $enclosure['length'])
-							. '"></media:content>', "\n";
+							. '">'
+							. (empty($enclosure['title']) ? '' : '<media:title type="html">' . $enclosure['title'] . '</media:title>')
+							. (empty($enclosure['credit']) ? '' : '<media:credit>' . $enclosure['credit'] . '</media:credit>')
+							. '</media:content>', "\n";
 					}
 				}
 			?>
 			<description><![CDATA[<?php
-	echo $item->content();
+	echo $item->content(false);
 ?>]]></description>
 			<pubDate><?= date('D, d M Y H:i:s O', $item->date(true)) ?></pubDate>
 			<guid isPermaLink="false"><?= $item->id() > 0 ? $item->id() : $item->guid() ?></guid>

+ 1 - 1
lib/SimplePie/SimplePie/Enclosure.php

@@ -627,7 +627,7 @@ class SimplePie_Enclosure
 	{
 		if ($this->link !== null)
 		{
-			return urldecode($this->link);
+			return $this->link;
 		}
 
 		return null;

+ 17 - 8
lib/SimplePie/SimplePie/Item.php

@@ -427,7 +427,16 @@ class SimplePie_Item
 		{
 			if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail'))
 			{
-				$this->data['thumbnail'] = $return[0]['attribs'][''];
+				$thumbnail = $return[0]['attribs'][''];
+				if (empty($thumbnail['url']))
+				{
+					$this->data['thumbnail'] = null;
+				}
+				else
+				{
+					$thumbnail['url'] = $this->sanitize($thumbnail['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0]));
+					$this->data['thumbnail'] = $thumbnail;
+				}
 			}
 			else
 			{
@@ -2847,9 +2856,9 @@ class SimplePie_Item
 				}
 			}
 
-			if ($enclosure = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'enclosure'))
+			foreach ($this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'enclosure') ?? [] as $enclosure)
 			{
-				if (isset($enclosure[0]['attribs']['']['url']))
+				if (isset($enclosure['attribs']['']['url']))
 				{
 					// Attributes
 					$bitrate = null;
@@ -2867,15 +2876,15 @@ class SimplePie_Item
 					$url = null;
 					$width = null;
 
-					$url = $this->sanitize($enclosure[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure[0]));
+					$url = $this->sanitize($enclosure['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure));
 					$url = $this->feed->sanitize->https_url($url);
-					if (isset($enclosure[0]['attribs']['']['type']))
+					if (isset($enclosure['attribs']['']['type']))
 					{
-						$type = $this->sanitize($enclosure[0]['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT);
+						$type = $this->sanitize($enclosure['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT);
 					}
-					if (isset($enclosure[0]['attribs']['']['length']))
+					if (isset($enclosure['attribs']['']['length']))
 					{
-						$length = intval($enclosure[0]['attribs']['']['length']);
+						$length = intval($enclosure['attribs']['']['length']);
 					}
 
 					// Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor