changes: Split Feed.php into a class per file
authorTimo Tijhof <krinklemail@gmail.com>
Sat, 13 Apr 2019 22:56:06 +0000 (23:56 +0100)
committerReedy <reedy@wikimedia.org>
Sat, 13 Apr 2019 23:29:04 +0000 (23:29 +0000)
Change-Id: I1f11a52871dcb249b3ba790e484dd10fe8bb049f

.phpcs.xml
autoload.php
includes/Feed.php [deleted file]
includes/changes/AtomFeed.php [new file with mode: 0644]
includes/changes/ChannelFeed.php [new file with mode: 0644]
includes/changes/FeedItem.php [new file with mode: 0644]
includes/changes/RSSFeed.php [new file with mode: 0644]

index 170e16d..df80740 100644 (file)
@@ -70,7 +70,6 @@
                        Whitelist existing violations, but enable the sniff to prevent
                        any new occurrences.
                -->
-               <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
                <exclude-pattern>*/includes/installer/PhpBugTests\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/SpecialMostinterwikis\.php</exclude-pattern>
                <exclude-pattern>*/includes/compat/XMPReader\.php</exclude-pattern>
                <exclude-pattern>*/includes/api/ApiRsd\.php</exclude-pattern>
                <exclude-pattern>*/includes/compat/XMPReader\.php</exclude-pattern>
                <exclude-pattern>*/includes/diff/DairikiDiff\.php</exclude-pattern>
-               <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
                <exclude-pattern>*/includes/filerepo/file/LocalFile\.php</exclude-pattern>
                <exclude-pattern>*/includes/htmlform/HTMLFormElement\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FileBackendStore\.php</exclude-pattern>
index a74a0b8..e8ae399 100644 (file)
@@ -161,7 +161,7 @@ $wgAutoloadLocalClasses = [
        'ArrayUtils' => __DIR__ . '/includes/libs/ArrayUtils.php',
        'Article' => __DIR__ . '/includes/page/Article.php',
        'AssembleUploadChunksJob' => __DIR__ . '/includes/jobqueue/jobs/AssembleUploadChunksJob.php',
-       'AtomFeed' => __DIR__ . '/includes/Feed.php',
+       'AtomFeed' => __DIR__ . '/includes/changes/AtomFeed.php',
        'AtomicSectionUpdate' => __DIR__ . '/includes/deferred/AtomicSectionUpdate.php',
        'AttachLatest' => __DIR__ . '/maintenance/attachLatest.php',
        'AugmentPageProps' => __DIR__ . '/includes/search/AugmentPageProps.php',
@@ -253,7 +253,7 @@ $wgAutoloadLocalClasses = [
        'ChangesListSpecialPage' => __DIR__ . '/includes/specialpage/ChangesListSpecialPage.php',
        'ChangesListStringOptionsFilter' => __DIR__ . '/includes/changes/ChangesListStringOptionsFilter.php',
        'ChangesListStringOptionsFilterGroup' => __DIR__ . '/includes/changes/ChangesListStringOptionsFilterGroup.php',
-       'ChannelFeed' => __DIR__ . '/includes/Feed.php',
+       'ChannelFeed' => __DIR__ . '/includes/changes/ChannelFeed.php',
        'CheckBadRedirects' => __DIR__ . '/maintenance/checkBadRedirects.php',
        'CheckComposerLockUpToDate' => __DIR__ . '/maintenance/checkComposerLockUpToDate.php',
        'CheckExtensionsCLI' => __DIR__ . '/maintenance/language/checkLanguage.inc',
@@ -498,7 +498,7 @@ $wgAutoloadLocalClasses = [
        'FatalError' => __DIR__ . '/includes/exception/FatalError.php',
        'FauxRequest' => __DIR__ . '/includes/FauxRequest.php',
        'FauxResponse' => __DIR__ . '/includes/FauxResponse.php',
-       'FeedItem' => __DIR__ . '/includes/Feed.php',
+       'FeedItem' => __DIR__ . '/includes/changes/FeedItem.php',
        'FeedUtils' => __DIR__ . '/includes/FeedUtils.php',
        'FetchText' => __DIR__ . '/maintenance/fetchText.php',
        'FewestrevisionsPage' => __DIR__ . '/includes/specials/SpecialFewestrevisions.php',
@@ -1184,7 +1184,7 @@ $wgAutoloadLocalClasses = [
        'RCFeedEngine' => __DIR__ . '/includes/rcfeed/RCFeedEngine.php',
        'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php',
        'RESTBagOStuff' => __DIR__ . '/includes/libs/objectcache/RESTBagOStuff.php',
-       'RSSFeed' => __DIR__ . '/includes/Feed.php',
+       'RSSFeed' => __DIR__ . '/includes/changes/RSSFeed.php',
        'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php',
        'RangeChronologicalPager' => __DIR__ . '/includes/pager/RangeChronologicalPager.php',
        'RangeDifference' => __DIR__ . '/includes/diff/RangeDifference.php',
diff --git a/includes/Feed.php b/includes/Feed.php
deleted file mode 100644 (file)
index 86e9bee..0000000
+++ /dev/null
@@ -1,494 +0,0 @@
-<?php
-/**
- * Basic support for outputting syndication feeds in RSS, other formats.
- *
- * Contain a feed class as well as classes to build rss / atom ... feeds
- * Available feeds are defined in Defines.php
- *
- * Copyright © 2004 Brion Vibber <brion@pobox.com>
- * 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
- */
-
-/**
- * @defgroup Feed Feed
- */
-
-/**
- * A base class for basic support for outputting syndication feeds in 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
-        */
-       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() {
-               global $wgLanguageCode;
-               return LanguageCode::bcp47( $wgLanguageCode );
-       }
-
-       /**
-        * 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 to support the outputting of syndication feeds in Atom and RSS format.
- *
- * @ingroup Feed
- */
-abstract class ChannelFeed extends FeedItem {
-
-       /** @var TemplateParser */
-       protected $templateParser;
-
-       /**
-        * @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
-        */
-       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 "<feed>";
-        * @endcode
-        */
-       abstract public function outHeader();
-
-       /**
-        * Generate an item
-        * @par Example:
-        * @code
-        * print "<item>...</item>";
-        * @endcode
-        * @param FeedItem $item
-        */
-       abstract public function outItem( $item );
-
-       /**
-        * Generate Footer of the feed
-        * @par Example:
-        * @code
-        * print "</feed>";
-        * @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, $wgVaryOnXFP;
-
-               # We take over from $wgOut, excepting its cache header info
-               $wgOut->disable();
-               $mimetype = $this->contentType();
-               header( "Content-type: $mimetype; charset=UTF-8" );
-
-               // Set a sane filename
-               $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
-                       ->getExtensionsForType( $mimetype );
-               $ext = $exts ? strtok( $exts, ' ' ) : 'xml';
-               header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
-
-               if ( $wgVaryOnXFP ) {
-                       $wgOut->addVaryHeader( 'X-Forwarded-Proto' );
-               }
-               $wgOut->sendCacheControl();
-       }
-
-       /**
-        * Return an internet media type to be sent in the headers.
-        *
-        * @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 '<?xml version="1.0"?>' . "\n";
-       }
-}
-
-/**
- * Generate a RSS feed
- *
- * @ingroup Feed
- */
-class RSSFeed extends ChannelFeed {
-
-       /**
-        * Format a date given a timestamp. If a timestamp is not given, nothing is returned
-        *
-        * @param int|null $ts Timestamp
-        * @return string|null Date string
-        */
-       function formatTime( $ts ) {
-               if ( $ts ) {
-                       return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
-               }
-       }
-
-       /**
-        * Output an RSS 2.0 header
-        */
-       function outHeader() {
-               global $wgVersion;
-
-               $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( $wgVersion ),
-                       '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
-        */
-       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
-        */
-       function outFooter() {
-               print "</channel></rss>";
-       }
-}
-
-/**
- * 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
-        */
-       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', wfTimestamp( TS_UNIX, $timestamp ) );
-               }
-       }
-
-       /**
-        * Outputs a basic header for Atom 1.0 feeds.
-        */
-       function outHeader() {
-               global $wgVersion;
-               $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( $wgVersion ),
-               ];
-               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
-        */
-       function outItem( $item ) {
-               global $wgMimeType;
-               // 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( $wgMimeType ),
-                       "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 '\</feed\>').
-        */
-       function outFooter() {
-               print "</feed>";
-       }
-}
diff --git a/includes/changes/AtomFeed.php b/includes/changes/AtomFeed.php
new file mode 100644 (file)
index 0000000..a4ce0c1
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * 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
+ */
+
+/**
+ * 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
+        */
+       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', wfTimestamp( TS_UNIX, $timestamp ) );
+               }
+       }
+
+       /**
+        * Outputs a basic header for Atom 1.0 feeds.
+        */
+       function outHeader() {
+               global $wgVersion;
+               $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( $wgVersion ),
+               ];
+               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
+        */
+       function outItem( $item ) {
+               global $wgMimeType;
+               // 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( $wgMimeType ),
+                       "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 '\</feed\>').
+        */
+       function outFooter() {
+               print "</feed>";
+       }
+}
diff --git a/includes/changes/ChannelFeed.php b/includes/changes/ChannelFeed.php
new file mode 100644 (file)
index 0000000..a1b832e
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/**
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * 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
+ */
+
+/**
+ * Class to support the outputting of syndication feeds in Atom and RSS format.
+ *
+ * @ingroup Feed
+ */
+abstract class ChannelFeed extends FeedItem {
+
+       /** @var TemplateParser */
+       protected $templateParser;
+
+       /**
+        * @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
+        */
+       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 "<feed>";
+        * @endcode
+        */
+       abstract public function outHeader();
+
+       /**
+        * Generate an item
+        * @par Example:
+        * @code
+        * print "<item>...</item>";
+        * @endcode
+        * @param FeedItem $item
+        */
+       abstract public function outItem( $item );
+
+       /**
+        * Generate Footer of the feed
+        * @par Example:
+        * @code
+        * print "</feed>";
+        * @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, $wgVaryOnXFP;
+
+               # We take over from $wgOut, excepting its cache header info
+               $wgOut->disable();
+               $mimetype = $this->contentType();
+               header( "Content-type: $mimetype; charset=UTF-8" );
+
+               // Set a sane filename
+               $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
+                       ->getExtensionsForType( $mimetype );
+               $ext = $exts ? strtok( $exts, ' ' ) : 'xml';
+               header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
+
+               if ( $wgVaryOnXFP ) {
+                       $wgOut->addVaryHeader( 'X-Forwarded-Proto' );
+               }
+               $wgOut->sendCacheControl();
+       }
+
+       /**
+        * Return an internet media type to be sent in the headers.
+        *
+        * @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 '<?xml version="1.0"?>' . "\n";
+       }
+}
diff --git a/includes/changes/FeedItem.php b/includes/changes/FeedItem.php
new file mode 100644 (file)
index 0000000..a6a2615
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * 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
+ */
+
+/**
+ * @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
+        */
+       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() {
+               global $wgLanguageCode;
+               return LanguageCode::bcp47( $wgLanguageCode );
+       }
+
+       /**
+        * 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 );
+       }
+       /**#@-*/
+}
diff --git a/includes/changes/RSSFeed.php b/includes/changes/RSSFeed.php
new file mode 100644 (file)
index 0000000..3b34500
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * 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
+ */
+
+/**
+ * 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 int|null $ts Timestamp
+        * @return string|null Date string
+        */
+       function formatTime( $ts ) {
+               if ( $ts ) {
+                       return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+               }
+       }
+
+       /**
+        * Output an RSS 2.0 header
+        */
+       function outHeader() {
+               global $wgVersion;
+
+               $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( $wgVersion ),
+                       '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
+        */
+       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
+        */
+       function outFooter() {
+               print "</channel></rss>";
+       }
+}