rss-bridge/formats/MrssFormat.php

206 lines
9.1 KiB
PHP

<?php
/**
* MrssFormat - RSS 2.0 + Media RSS
* http://www.rssboard.org/rss-specification
* http://www.rssboard.org/media-rss
*
* Validators:
* https://validator.w3.org/feed/
* http://www.rssboard.org/rss-validator/
*
* Notes about the implementation:
*
* - The item author is not supported as it needs to be an e-mail address to be
* valid.
* - The RSS specification does not explicitly allow to have more than one
* enclosure as every item is meant to provide one "story", thus having
* multiple enclosures per item may lead to unexpected behavior.
* On top of that, it requires to have a length specified, which RSS-Bridge
* can't provide.
* - The Media RSS extension comes in handy, since it allows to have multiple
* enclosures, even though they recommend to have only one enclosure because
* of the one-story-per-item reason. It only requires to specify the URL,
* everything else is optional.
* - Since the Media RSS extension has its own namespace, the output is a valid
* RSS 2.0 feed that works with feed readers that don't support the extension.
*/
class MrssFormat extends FormatAbstract
{
const MIME_TYPE = 'application/rss+xml';
protected const ATOM_NS = 'http://www.w3.org/2005/Atom';
protected const MRSS_NS = 'http://search.yahoo.com/mrss/';
public function stringify()
{
$document = new \DomDocument('1.0', $this->getCharset());
$document->formatOutput = true;
$feed = $document->createElement('rss');
$document->appendChild($feed);
$feed->setAttribute('version', '2.0');
$feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', self::ATOM_NS);
$feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS);
$channel = $document->createElement('channel');
$feed->appendChild($channel);
$feedArray = $this->getFeed();
$uri = $feedArray['uri'];
$title = $feedArray['name'];
foreach ($feedArray as $feedKey => $feedValue) {
if (in_array($feedKey, ['atom', 'donationUri'])) {
continue;
}
if ($feedKey === 'name') {
$channelTitle = $document->createElement('title');
$channel->appendChild($channelTitle);
$channelTitle->appendChild($document->createTextNode($title));
$description = $document->createElement('description');
$channel->appendChild($description);
$description->appendChild($document->createTextNode($title));
} elseif ($feedKey === 'uri') {
$link = $document->createElement('link');
$channel->appendChild($link);
$link->appendChild($document->createTextNode($uri));
$linkAlternate = $document->createElementNS(self::ATOM_NS, 'link');
$channel->appendChild($linkAlternate);
$linkAlternate->setAttribute('rel', 'alternate');
$linkAlternate->setAttribute('type', 'text/html');
$linkAlternate->setAttribute('href', $uri);
$linkSelf = $document->createElementNS(self::ATOM_NS, 'link');
$channel->appendChild($linkSelf);
$linkSelf->setAttribute('rel', 'self');
$linkSelf->setAttribute('type', 'application/atom+xml');
$feedUrl = get_current_url();
$linkSelf->setAttribute('href', $feedUrl);
} elseif ($feedKey === 'icon') {
$allowedIconExtensions = [
'.gif',
'.jpg',
'.png',
'.ico',
];
$icon = $feedValue;
if ($icon && in_array(substr($icon, -4), $allowedIconExtensions)) {
$feedImage = $document->createElement('image');
$channel->appendChild($feedImage);
$iconUrl = $document->createElement('url');
$iconUrl->appendChild($document->createTextNode($icon));
$feedImage->appendChild($iconUrl);
$iconTitle = $document->createElement('title');
$iconTitle->appendChild($document->createTextNode($title));
$feedImage->appendChild($iconTitle);
$iconLink = $document->createElement('link');
$iconLink->appendChild($document->createTextNode($uri));
$feedImage->appendChild($iconLink);
}
} elseif ($feedKey === 'itunes') {
$feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:itunes', self::ITUNES_NS);
foreach ($feedValue as $itunesKey => $itunesValue) {
$itunesProperty = $document->createElementNS(self::ITUNES_NS, $itunesKey);
$channel->appendChild($itunesProperty);
$itunesProperty->appendChild($document->createTextNode($itunesValue));
}
} else {
$element = $document->createElement($feedKey);
$channel->appendChild($element);
$element->appendChild($document->createTextNode($feedValue));
}
}
foreach ($this->getItems() as $item) {
$itemArray = $item->toArray();
$itemTimestamp = $item->getTimestamp();
$itemTitle = $item->getTitle();
$itemUri = $item->getURI();
$itemContent = $item->getContent() ?? '';
$itemUid = $item->getUid();
$isPermaLink = 'false';
if (empty($itemUid) && !empty($itemUri)) {
// Fallback to provided URI
$itemUid = $itemUri;
$isPermaLink = 'true';
}
if (empty($itemUid)) {
// Fallback to title and content
$itemUid = hash('sha1', $itemTitle . $itemContent);
}
$entry = $document->createElement('item');
$channel->appendChild($entry);
if (!empty($itemTitle)) {
$entryTitle = $document->createElement('title');
$entry->appendChild($entryTitle);
$entryTitle->appendChild($document->createTextNode($itemTitle));
}
if (isset($itemArray['itunes'])) {
$feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:itunes', self::ITUNES_NS);
foreach ($itemArray['itunes'] as $itunesKey => $itunesValue) {
$itunesProperty = $document->createElementNS(self::ITUNES_NS, $itunesKey);
$entry->appendChild($itunesProperty);
$itunesProperty->appendChild($document->createTextNode($itunesValue));
}
if (isset($itemArray['enclosure'])) {
$itunesEnclosure = $document->createElement('enclosure');
$entry->appendChild($itunesEnclosure);
$itunesEnclosure->setAttribute('url', $itemArray['enclosure']['url']);
$itunesEnclosure->setAttribute('length', $itemArray['enclosure']['length']);
$itunesEnclosure->setAttribute('type', $itemArray['enclosure']['type']);
}
}
if (!empty($itemUri)) {
$entryLink = $document->createElement('link');
$entry->appendChild($entryLink);
$entryLink->appendChild($document->createTextNode($itemUri));
}
$entryGuid = $document->createElement('guid');
$entryGuid->setAttribute('isPermaLink', $isPermaLink);
$entry->appendChild($entryGuid);
$entryGuid->appendChild($document->createTextNode($itemUid));
if (!empty($itemTimestamp)) {
$entryPublished = $document->createElement('pubDate');
$entry->appendChild($entryPublished);
$entryPublished->appendChild($document->createTextNode(gmdate(\DATE_RFC2822, $itemTimestamp)));
}
if (!empty($itemContent)) {
$entryDescription = $document->createElement('description');
$entry->appendChild($entryDescription);
$entryDescription->appendChild($document->createTextNode($itemContent));
}
foreach ($item->getEnclosures() as $enclosure) {
$entryEnclosure = $document->createElementNS(self::MRSS_NS, 'content');
$entry->appendChild($entryEnclosure);
$entryEnclosure->setAttribute('url', $enclosure);
$entryEnclosure->setAttribute('type', parse_mime_type($enclosure));
}
foreach ($item->getCategories() as $category) {
$entryCategory = $document->createElement('category');
$entry->appendChild($entryCategory);
$entryCategory->appendChild($document->createTextNode($category));
}
}
$xml = $document->saveXML();
// Remove invalid non-UTF8 characters
ini_set('mbstring.substitute_character', 'none');
$xml = mb_convert_encoding($xml, $this->getCharset(), 'UTF-8');
return $xml;
}
}