From f8bf3687f4e2ea32a3c5c7ac8492e6f573b51e9a Mon Sep 17 00:00:00 2001 From: Amir Sarabadani Date: Mon, 17 Oct 2022 13:10:57 +0200 Subject: Feed: Move feed-related classes to Feed/ and namespace them Bug: T166010 Change-Id: Icdbe003e74d2f31b68b575acfa94c09c24d7aed5 --- includes/Feed/AtomFeed.php | 120 ++++++++++++++++ includes/Feed/ChannelFeed.php | 147 +++++++++++++++++++ includes/Feed/FeedItem.php | 235 ++++++++++++++++++++++++++++++ includes/Feed/FeedUtils.php | 324 ++++++++++++++++++++++++++++++++++++++++++ includes/Feed/RSSFeed.php | 96 +++++++++++++ 5 files changed, 922 insertions(+) create mode 100644 includes/Feed/AtomFeed.php create mode 100644 includes/Feed/ChannelFeed.php create mode 100644 includes/Feed/FeedItem.php create mode 100644 includes/Feed/FeedUtils.php create mode 100644 includes/Feed/RSSFeed.php (limited to 'includes/Feed') diff --git a/includes/Feed/AtomFeed.php b/includes/Feed/AtomFeed.php new file mode 100644 index 000000000000..4574a6230c66 --- /dev/null +++ b/includes/Feed/AtomFeed.php @@ -0,0 +1,120 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Feed; + +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; + +/** + * Generate an Atom feed. + * + * @ingroup Feed + */ +class AtomFeed extends ChannelFeed { + /** + * Format a date given timestamp, if one is given. + * + * @param string|int|null $timestamp + * @return string|null + */ + private function formatTime( $timestamp ) { + if ( $timestamp ) { + // need to use RFC 822 time format at least for rss2.0 + return gmdate( 'Y-m-d\TH:i:s', (int)wfTimestamp( TS_UNIX, $timestamp ) ); + } + return null; + } + + /** + * Outputs a basic header for Atom 1.0 feeds. + */ + public function outHeader() { + $this->outXmlHeader(); + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + 'language' => $this->xmlEncode( $this->getLanguage() ), + 'feedID' => $this->getFeedId(), + 'title' => $this->getTitle(), + 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ), + 'selfUrl' => $this->getSelfUrl(), + 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ), + 'description' => $this->getDescription(), + 'version' => $this->xmlEncode( MW_VERSION ), + ]; + print $this->templateParser->processTemplate( 'AtomHeader', $templateParams ); + } + + /** + * Atom 1.0 requires a unique, opaque IRI as a unique identifier + * for every feed we create. For now just use the URL, but who + * can tell if that's right? If we put options on the feed, do we + * have to change the id? Maybe? Maybe not. + * + * @return string + */ + private function getFeedId() { + return $this->getSelfUrl(); + } + + /** + * Atom 1.0 requests a self-reference to the feed. + * @return string + */ + private function getSelfUrl() { + global $wgRequest; + return htmlspecialchars( $wgRequest->getFullRequestURL() ); + } + + /** + * Output a given item. + * @param FeedItem $item + */ + public function outItem( $item ) { + $mimeType = MediaWikiServices::getInstance()->getMainConfig() + ->get( MainConfigNames::MimeType ); + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + "uniqueID" => $item->getUniqueID(), + "title" => $item->getTitle(), + "mimeType" => $this->xmlEncode( $mimeType ), + "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ), + "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ), + "description" => $item->getDescription(), + "author" => $item->getAuthor() + ]; + print $this->templateParser->processTemplate( 'AtomItem', $templateParams ); + } + + /** + * Outputs the footer for Atom 1.0 feed (basically '\'). + */ + public function outFooter() { + print ""; + } +} + +class_alias( AtomFeed::class, 'AtomFeed' ); diff --git a/includes/Feed/ChannelFeed.php b/includes/Feed/ChannelFeed.php new file mode 100644 index 000000000000..2a58ba90784a --- /dev/null +++ b/includes/Feed/ChannelFeed.php @@ -0,0 +1,147 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Feed; + +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; +use TemplateParser; +use Title; + +/** + * Class to support the outputting of syndication feeds in Atom and RSS format. + * + * @stable to extend + * @ingroup Feed + */ +abstract class ChannelFeed extends FeedItem { + + /** @var TemplateParser */ + protected $templateParser; + + /** + * @stable to call + * + * @param string|Title $title Feed's title + * @param string $description + * @param string $url URL uniquely designating the feed. + * @param string $date Feed's date + * @param string $author Author's user name + * @param string $comments + * + */ + public function __construct( + $title, $description, $url, $date = '', $author = '', $comments = '' + ) { + parent::__construct( $title, $description, $url, $date, $author, $comments ); + $this->templateParser = new TemplateParser(); + } + + /** + * Generate Header of the feed + * @par Example: + * @code + * print ""; + * @endcode + */ + abstract public function outHeader(); + + /** + * Generate an item + * @par Example: + * @code + * print "..."; + * @endcode + * @param FeedItem $item + */ + abstract public function outItem( $item ); + + /** + * Generate Footer of the feed + * @par Example: + * @code + * print ""; + * @endcode + */ + abstract public function outFooter(); + + /** + * Setup and send HTTP headers. Don't send any content; + * content might end up being cached and re-sent with + * these same headers later. + * + * This should be called from the outHeader() method, + * but can also be called separately. + */ + public function httpHeaders() { + global $wgOut; + $varyOnXFP = MediaWikiServices::getInstance()->getMainConfig() + ->get( MainConfigNames::VaryOnXFP ); + # We take over from $wgOut, excepting its cache header info + $wgOut->disable(); + $mimetype = $this->contentType(); + header( "Content-type: $mimetype; charset=UTF-8" ); + + // Set a sensible filename + $mimeAnalyzer = MediaWikiServices::getInstance()->getMimeAnalyzer(); + $ext = $mimeAnalyzer->getExtensionFromMimeTypeOrNull( $mimetype ) ?? 'xml'; + header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" ); + + if ( $varyOnXFP ) { + $wgOut->addVaryHeader( 'X-Forwarded-Proto' ); + } + $wgOut->sendCacheControl(); + } + + /** + * Return an internet media type to be sent in the headers. + * + * @stable to override + * + * @return string + */ + private function contentType() { + global $wgRequest; + + $ctype = $wgRequest->getVal( 'ctype', 'application/xml' ); + $allowedctypes = [ + 'application/xml', + 'text/xml', + 'application/rss+xml', + 'application/atom+xml' + ]; + + return ( in_array( $ctype, $allowedctypes ) ? $ctype : 'application/xml' ); + } + + /** + * Output the initial XML headers. + */ + protected function outXmlHeader() { + $this->httpHeaders(); + echo '' . "\n"; + } +} + +class_alias( ChannelFeed::class, 'ChannelFeed' ); diff --git a/includes/Feed/FeedItem.php b/includes/Feed/FeedItem.php new file mode 100644 index 000000000000..2968b964e228 --- /dev/null +++ b/includes/Feed/FeedItem.php @@ -0,0 +1,235 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Feed; + +use LanguageCode; +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; +use Title; + +/** + * @defgroup Feed Feed + */ + +/** + * A base class for outputting syndication feeds (e.g. RSS and other formats). + * + * @ingroup Feed + */ +class FeedItem { + /** @var Title */ + public $title; + + public $description; + + public $url; + + public $date; + + public $author; + + public $uniqueId; + + public $comments; + + public $rssIsPermalink = false; + + /** + * @param string|Title $title Item's title + * @param string $description + * @param string $url URL uniquely designating the item. + * @param string $date Item's date + * @param string $author Author's user name + * @param string $comments + */ + public function __construct( + $title, $description, $url, $date = '', $author = '', $comments = '' + ) { + $this->title = $title; + $this->description = $description; + $this->url = $url; + $this->uniqueId = $url; + $this->date = $date; + $this->author = $author; + $this->comments = $comments; + } + + /** + * Encode $string so that it can be safely embedded in a XML document + * + * @param string $string String to encode + * @return string + */ + public function xmlEncode( $string ) { + $string = str_replace( "\r\n", "\n", $string ); + $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string ); + return htmlspecialchars( $string ); + } + + /** + * Get the unique id of this item; already xml-encoded + * @return string + */ + public function getUniqueID() { + $id = $this->getUniqueIdUnescaped(); + if ( $id ) { + return $this->xmlEncode( $id ); + } + } + + /** + * Get the unique id of this item, without any escaping + * @return string + */ + public function getUniqueIdUnescaped() { + if ( $this->uniqueId ) { + return wfExpandUrl( $this->uniqueId, PROTO_CURRENT ); + } + } + + /** + * Set the unique id of an item + * + * @param string $uniqueId Unique id for the item + * @param bool $rssIsPermalink Set to true if the guid (unique id) is a permalink (RSS feeds only) + */ + public function setUniqueId( $uniqueId, $rssIsPermalink = false ) { + $this->uniqueId = $uniqueId; + $this->rssIsPermalink = $rssIsPermalink; + } + + /** + * Get the title of this item; already xml-encoded + * + * @return string + */ + public function getTitle() { + return $this->xmlEncode( $this->title ); + } + + /** + * Get the URL of this item; already xml-encoded + * + * @return string + */ + public function getUrl() { + return $this->xmlEncode( $this->url ); + } + + /** Get the URL of this item without any escaping + * + * @return string + */ + public function getUrlUnescaped() { + return $this->url; + } + + /** + * Get the description of this item; already xml-encoded + * + * @return string + */ + public function getDescription() { + return $this->xmlEncode( $this->description ); + } + + /** + * Get the description of this item without any escaping + * + * @return string + */ + public function getDescriptionUnescaped() { + return $this->description; + } + + /** + * Get the language of this item + * + * @return string + */ + public function getLanguage() { + $languageCode = MediaWikiServices::getInstance()->getMainConfig() + ->get( MainConfigNames::LanguageCode ); + return LanguageCode::bcp47( $languageCode ); + } + + /** + * Get the date of this item + * + * @return string + */ + public function getDate() { + return $this->date; + } + + /** + * Get the author of this item; already xml-encoded + * + * @return string + */ + public function getAuthor() { + return $this->xmlEncode( $this->author ); + } + + /** + * Get the author of this item without any escaping + * + * @return string + */ + public function getAuthorUnescaped() { + return $this->author; + } + + /** + * Get the comment of this item; already xml-encoded + * + * @return string + */ + public function getComments() { + return $this->xmlEncode( $this->comments ); + } + + /** + * Get the comment of this item without any escaping + * + * @return string + */ + public function getCommentsUnescaped() { + return $this->comments; + } + + /** + * Quickie hack... strip out wikilinks to more legible form from the comment. + * + * @param string $text Wikitext + * @return string + */ + public static function stripComment( $text ) { + return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); + } + + /** #@- */ +} + +class_alias( FeedItem::class, 'FeedItem' ); diff --git a/includes/Feed/FeedUtils.php b/includes/Feed/FeedUtils.php new file mode 100644 index 000000000000..2e818f680673 --- /dev/null +++ b/includes/Feed/FeedUtils.php @@ -0,0 +1,324 @@ +getMainConfig()->get( MainConfigNames::Feed ); + $feedClasses = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FeedClasses ); + if ( $output === null ) { + // Todo update GoogleNewsSitemap and deprecate + global $wgOut; + $output = $wgOut; + } + + if ( !$feed ) { + $output->addWikiMsg( 'feed-unavailable' ); + return false; + } + + if ( !isset( $feedClasses[$type] ) ) { + $output->addWikiMsg( 'feed-invalid' ); + return false; + } + + return true; + } + + /** + * Format a diff for the newsfeed + * + * @param \stdClass $row Row from the recentchanges table, including fields as + * appropriate for CommentStore + * @param string|null $formattedComment rc_comment in HTML format, or null + * to format it on demand. + * @return string + */ + public static function formatDiff( $row, $formattedComment = null ) { + $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); + $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp ); + $actiontext = ''; + if ( $row->rc_type == RC_LOG ) { + $rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows + $actiontext = LogFormatter::newFromRow( $rcRow )->getActionText(); + } + if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) { + $formattedComment = wfMessage( 'rev-deleted-comment' )->escaped(); + } elseif ( $formattedComment === null ) { + $formattedComment = Linker::formatComment( + CommentStore::getStore()->getComment( 'rc_comment', $row )->text ); + } + return self::formatDiffRow2( $titleObj, + $row->rc_last_oldid, $row->rc_this_oldid, + $timestamp, + $formattedComment, + $actiontext + ); + } + + /** + * Really format a diff for the newsfeed + * + * @param Title $title + * @param int $oldid Old revision's id + * @param int $newid New revision's id + * @param string $timestamp New revision's timestamp + * @param string $comment New revision's comment + * @param string $actiontext Text of the action; in case of log event + * @return string + * @deprecated since 1.38 use formatDiffRow2 + * + */ + public static function formatDiffRow( $title, $oldid, $newid, $timestamp, + $comment, $actiontext = '' + ) { + $formattedComment = MediaWikiServices::getInstance()->getCommentFormatter() + ->format( $comment ); + return self::formatDiffRow2( $title, $oldid, $newid, $timestamp, + $formattedComment, $actiontext ); + } + + /** + * Really really format a diff for the newsfeed. Same as formatDiffRow() + * except with preformatted comments. + * + * @param Title $title + * @param int $oldid Old revision's id + * @param int $newid New revision's id + * @param string $timestamp New revision's timestamp + * @param string $formattedComment New revision's comment in HTML format + * @param string $actiontext Text of the action; in case of log event + * @return string + */ + public static function formatDiffRow2( $title, $oldid, $newid, $timestamp, + $formattedComment, $actiontext = '' + ) { + $feedDiffCutoff = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FeedDiffCutoff ); + + // log entries + $unwrappedText = implode( + ' ', + array_filter( [ $actiontext, $formattedComment ] ) + ); + $completeText = Html::rawElement( 'p', [], $unwrappedText ) . "\n"; + + // NOTE: Check permissions for anonymous users, not current user. + // No "privileged" version should end up in the cache. + // Most feed readers will not log in anyway. + $anon = new User(); + $services = MediaWikiServices::getInstance(); + $permManager = $services->getPermissionManager(); + $accErrors = $permManager->getPermissionErrors( + 'read', + $anon, + $title + ); + + // Can't diff special pages, unreadable pages or pages with no new revision + // to compare against: just return the text. + if ( $title->getNamespace() < 0 || $accErrors || !$newid ) { + return $completeText; + } + + $revLookup = $services->getRevisionLookup(); + $contentHandlerFactory = $services->getContentHandlerFactory(); + if ( $oldid ) { + $diffText = ''; + // Don't bother generating the diff if we won't be able to show it + if ( $feedDiffCutoff > 0 ) { + $revRecord = $revLookup->getRevisionById( $oldid ); + + if ( !$revRecord ) { + $diffText = false; + } else { + $mainContext = RequestContext::getMain(); + $context = clone RequestContext::getMain(); + $context->setTitle( $title ); + + $model = $revRecord->getSlot( + SlotRecord::MAIN, + RevisionRecord::RAW + )->getModel(); + $contentHandler = $contentHandlerFactory->getContentHandler( $model ); + $de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid ); + $lang = $mainContext->getLanguage(); + $user = $mainContext->getUser(); + $diffText = $de->getDiff( + $mainContext->msg( 'previousrevision' )->text(), // hack + $mainContext->msg( 'revisionasof', + $lang->userTimeAndDate( $timestamp, $user ), + $lang->userDate( $timestamp, $user ), + $lang->userTime( $timestamp, $user ) )->text() ); + } + } + + if ( $feedDiffCutoff <= 0 || ( strlen( $diffText ) > $feedDiffCutoff ) ) { + // Omit large diffs + $diffText = self::getDiffLink( $title, $newid, $oldid ); + } elseif ( $diffText === false ) { + // Error in diff engine, probably a missing revision + $diffText = Html::rawElement( + 'p', + [], + "Can't load revision $newid" + ); + } else { + // Diff output fine, clean up any illegal UTF-8 + $diffText = UtfNormal\Validator::cleanUp( $diffText ); + $diffText = self::applyDiffStyle( $diffText ); + } + } else { + $revRecord = $revLookup->getRevisionById( $newid ); + if ( $feedDiffCutoff <= 0 || $revRecord === null ) { + $newContent = $contentHandlerFactory + ->getContentHandler( $title->getContentModel() ) + ->makeEmptyContent(); + } else { + $newContent = $revRecord->getContent( SlotRecord::MAIN ); + } + + if ( $newContent instanceof TextContent ) { + // only textual content has a "source view". + $text = $newContent->getText(); + + if ( $feedDiffCutoff <= 0 || strlen( $text ) > $feedDiffCutoff ) { + $html = null; + } else { + $html = nl2br( htmlspecialchars( $text ) ); + } + } else { + // XXX: we could get an HTML representation of the content via getParserOutput, but that may + // contain JS magic and generally may not be suitable for inclusion in a feed. + // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. + // Compare also ApiFeedContributions::feedItemDesc + $html = null; + } + + if ( $html === null ) { + // Omit large new page diffs, T31110 + // Also use diff link for non-textual content + $diffText = self::getDiffLink( $title, $newid ); + } else { + $diffText = Html::rawElement( + 'p', + [], + Html::rawElement( 'b', [], wfMessage( 'newpage' )->text() ) + ); + $diffText .= Html::rawElement( 'div', [], $html ); + } + } + $completeText .= $diffText; + + return $completeText; + } + + /** + * Generates a diff link. Used when the full diff is not wanted for example + * when $wgFeedDiffCutoff is 0. + * + * @param Title $title Title object: used to generate the diff URL + * @param int $newid Newid for this diff + * @param int|null $oldid Oldid for the diff. Null means it is a new article + * @return string + */ + protected static function getDiffLink( Title $title, $newid, $oldid = null ) { + $queryParameters = [ 'diff' => $newid ]; + if ( $oldid != null ) { + $queryParameters['oldid'] = $oldid; + } + $diffUrl = $title->getFullURL( $queryParameters ); + + $diffLink = Html::element( 'a', [ 'href' => $diffUrl ], + wfMessage( 'showdiff' )->inContentLanguage()->text() ); + + return $diffLink; + } + + /** + * Hacky application of diff styles for the feeds. + * Might be 'cleaner' to use DOM or XSLT or something, + * but *gack* it's a pain in the ass. + * + * @param string $text Diff's HTML output + * @return string Modified HTML + */ + public static function applyDiffStyle( $text ) { + $styles = [ + 'diff' => 'background-color: #fff; color: #202122;', + 'diff-otitle' => 'background-color: #fff; color: #202122; text-align: center;', + 'diff-ntitle' => 'background-color: #fff; color: #202122; text-align: center;', + 'diff-addedline' => 'color: #202122; font-size: 88%; border-style: solid; ' + . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; ' + . 'vertical-align: top; white-space: pre-wrap;', + 'diff-deletedline' => 'color: #202122; font-size: 88%; border-style: solid; ' + . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; ' + . 'vertical-align: top; white-space: pre-wrap;', + 'diff-context' => 'background-color: #f8f9fa; color: #202122; font-size: 88%; ' + . 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; ' + . 'border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;', + 'diffchange' => 'font-weight: bold; text-decoration: none;', + ]; + + foreach ( $styles as $class => $style ) { + $text = preg_replace( '/(<\w+\b[^<>]*)\bclass=([\'"])(?:[^\'"]*\s)?' . + preg_quote( $class ) . '(?:\s[^\'"]*)?\2(?=[^<>]*>)/', + '$1style="' . $style . '"', $text ); + } + + return $text; + } + +} + +class_alias( FeedUtils::class, 'FeedUtils' ); diff --git a/includes/Feed/RSSFeed.php b/includes/Feed/RSSFeed.php new file mode 100644 index 000000000000..1e3efa727c83 --- /dev/null +++ b/includes/Feed/RSSFeed.php @@ -0,0 +1,96 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Feed; + +/** + * Generate an RSS feed. + * + * @ingroup Feed + */ +class RSSFeed extends ChannelFeed { + + /** + * Format a date given a timestamp. If a timestamp is not given, nothing is returned + * + * @param string|int|null $ts Timestamp + * @return string|null Date string + */ + private function formatTime( $ts ) { + if ( $ts ) { + return gmdate( 'D, d M Y H:i:s \G\M\T', (int)wfTimestamp( TS_UNIX, $ts ) ); + } + return null; + } + + /** + * Output an RSS 2.0 header + */ + public function outHeader() { + $this->outXmlHeader(); + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + 'title' => $this->getTitle(), + 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ), + 'description' => $this->getDescription(), + 'language' => $this->xmlEncode( $this->getLanguage() ), + 'version' => $this->xmlEncode( MW_VERSION ), + 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ) + ]; + print $this->templateParser->processTemplate( 'RSSHeader', $templateParams ); + } + + /** + * Output an RSS 2.0 item + * @param FeedItem $item Item to be output + */ + public function outItem( $item ) { + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + "title" => $item->getTitle(), + "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ), + "permalink" => $item->rssIsPermalink, + "uniqueID" => $item->getUniqueID(), + "description" => $item->getDescription(), + "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ), + "author" => $item->getAuthor() + ]; + $comments = $item->getCommentsUnescaped(); + if ( $comments ) { + $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) ); + $templateParams["comments"] = $commentsEscaped; + } + print $this->templateParser->processTemplate( 'RSSItem', $templateParams ); + } + + /** + * Output an RSS 2.0 footer + */ + public function outFooter() { + print ""; + } +} + +class_alias( RSSFeed::class, 'RSSFeed' ); -- cgit v1.2.3