From fc20c30d20902aef1ce26e1660ca782ab51d05b7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20Dziewo=C5=84ski?= Date: Sun, 2 Feb 2014 17:30:44 +0100 Subject: [PATCH] ChangesListSpecialPage: Separate all functionality for generating feeds I should have done it at the beginning instead of trying to extract it from recent changes. Same for SpecialRecentChanges and SpecialRecentChangesLinked (subclasses). Created a new API module for it: ApiFeedRecentChanges. It's somewhat un-API-like and hackish, but all feed modules are. Old URLs redirect to new ones, so this should be fully backwards-compatible assuming sane feed reader clients. Change-Id: I06ee0f01d896bc66545a1800b24693ce7524e433 --- RELEASE-NOTES-1.23 | 3 + includes/AutoLoader.php | 1 + includes/ChangesFeed.php | 24 ++- includes/api/ApiFeedRecentChanges.php | 203 ++++++++++++++++++ includes/api/ApiMain.php | 1 + .../specialpage/ChangesListSpecialPage.php | 74 ++----- includes/specials/SpecialRecentchanges.php | 92 +++----- .../specials/SpecialRecentchangeslinked.php | 11 - 8 files changed, 277 insertions(+), 132 deletions(-) create mode 100644 includes/api/ApiFeedRecentChanges.php diff --git a/RELEASE-NOTES-1.23 b/RELEASE-NOTES-1.23 index 4f4d414361..ad2f747c8c 100644 --- a/RELEASE-NOTES-1.23 +++ b/RELEASE-NOTES-1.23 @@ -281,6 +281,9 @@ changes to languages because of Bugzilla reports. is transcluded. * (bug 62198) window.$j has been deprecated. * Preference "Disable link title conversion" was removed. +* SpecialRecentChanges no longer includes any functionality for generating feeds + - it has been factored out to ApiFeedRecentChanges. Old URLs redirect to new + ones. ==== Removed classes ==== * FakeMemCachedClient (deprecated in 1.18) diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 87dc95d95e..d4cc9509b2 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -273,6 +273,7 @@ $wgAutoloadLocalClasses = array( 'ApiEmailUser' => 'includes/api/ApiEmailUser.php', 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', 'ApiFeedContributions' => 'includes/api/ApiFeedContributions.php', + 'ApiFeedRecentChanges' => 'includes/api/ApiFeedRecentChanges.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', 'ApiFileRevert' => 'includes/api/ApiFileRevert.php', 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index 0736c5076c..cf05782bd8 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -160,14 +160,28 @@ class ChangesFeed { } /** - * Generate the feed items given a row from the database. + * Generate the feed items given a row from the database, printing the feed. * @param $rows DatabaseBase resource with recentchanges rows * @param $feed Feed object */ public static function generateFeed( $rows, &$feed ) { wfProfileIn( __METHOD__ ); - + $items = self::buildItems( $rows ); $feed->outHeader(); + foreach ( $items as $item ) { + $feed->outItem( $item ); + } + $feed->outFooter(); + wfProfileOut( __METHOD__ ); + } + + /** + * Generate the feed items given a row from the database. + * @param $rows DatabaseBase resource with recentchanges rows + */ + public static function buildItems( $rows ) { + wfProfileIn( __METHOD__ ); + $items = array(); # Merge adjacent edits by one user $sorted = array(); @@ -203,7 +217,7 @@ class ChangesFeed { $url = $title->getFullURL(); } - $item = new FeedItem( + $items[] = new FeedItem( $title->getPrefixedText(), FeedUtils::formatDiff( $obj ), $url, @@ -212,10 +226,10 @@ class ChangesFeed { ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text, $talkpage ); - $feed->outItem( $item ); } - $feed->outFooter(); + wfProfileOut( __METHOD__ ); + return $items; } } diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php new file mode 100644 index 0000000000..f1c1bf3e94 --- /dev/null +++ b/includes/api/ApiFeedRecentChanges.php @@ -0,0 +1,203 @@ +getMain() ); + } + + /** + * Format the rows (generated by SpecialRecentchanges or SpecialRecentchangeslinked) + * as an RSS/Atom feed. + */ + public function execute() { + global $wgFeed, $wgFeedClasses; + + $this->params = $this->extractRequestParams(); + + if ( !$wgFeed ) { + $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); + } + + if ( !isset( $wgFeedClasses[$this->params['feedformat']] ) ) { + $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); + } + + $feedFormat = $this->params['feedformat']; + $specialClass = $this->params['target'] !== null + ? 'SpecialRecentchangeslinked' + : 'SpecialRecentchanges'; + + $formatter = $this->getFeedObject( $feedFormat, $specialClass ); + + // Everything is passed implicitly via $wgRequest… :( + // The row-getting functionality should maybe be factored out of ChangesListSpecialPage too… + $rc = new $specialClass(); + $rows = $rc->getRows(); + + $feedItems = $rows ? ChangesFeed::buildItems( $rows ) : array(); + + ApiFormatFeedWrapper::setResult( $this->getResult(), $formatter, $feedItems ); + } + + /** + * Return a ChannelFeed object. + * + * @param string $feedFormat Feed's format (either 'rss' or 'atom') + * @param string $specialClass Relevant special page name (either 'SpecialRecentchanges' or + * 'SpecialRecentchangeslinked') + * @return ChannelFeed + */ + public function getFeedObject( $feedFormat, $specialClass ) { + if ( $specialClass === 'SpecialRecentchangeslinked' ) { + $title = Title::newFromText( $this->params['target'] ); + $feed = new ChangesFeed( $feedFormat, false ); + $feedObj = $feed->getFeedObject( + $this->msg( 'recentchangeslinked-title', $title->getPrefixedText() ) + ->inContentLanguage()->text(), + $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(), + SpecialPage::getTitleFor( 'Recentchangeslinked' )->getFullURL() + ); + } else { + $feed = new ChangesFeed( $feedFormat, 'rcfeed' ); + $feedObj = $feed->getFeedObject( + $this->msg( 'recentchanges' )->inContentLanguage()->text(), + $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(), + SpecialPage::getTitleFor( 'Recentchanges' )->getFullURL() + ); + } + + return $feedObj; + } + + public function getAllowedParams() { + global $wgFeedClasses, $wgAllowCategorizedRecentChanges, $wgFeedLimit; + $feedFormatNames = array_keys( $wgFeedClasses ); + + $ret = array( + 'feedformat' => array( + ApiBase::PARAM_DFLT => 'rss', + ApiBase::PARAM_TYPE => $feedFormatNames, + ), + + 'namespace' => array( + ApiBase::PARAM_TYPE => 'namespace', + ), + 'invert' => false, + 'associated' => false, + + 'days' => array( + ApiBase::PARAM_DFLT => 7, + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_TYPE => 'integer', + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 50, + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => $wgFeedLimit, + ApiBase::PARAM_TYPE => 'integer', + ), + 'from' => array( + ApiBase::PARAM_TYPE => 'timestamp', + ), + + 'hideminor' => false, + 'hidebots' => false, + 'hideanons' => false, + 'hideliu' => false, + 'hidepatrolled' => false, + 'hidemyself' => false, + + 'tagfilter' => array( + ApiBase::PARAM_TYPE => 'string', + ), + + 'target' => array( + ApiBase::PARAM_TYPE => 'string', + ), + 'showlinkedto' => false, + ); + + if ( $wgAllowCategorizedRecentChanges ) { + $ret += array( + 'categories' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_ISMULTI => true, + ), + 'categories_any' => false, + ); + } + + return $ret; + } + + public function getParamDescription() { + return array( + 'feedformat' => 'The format of the feed', + 'namespace' => 'Namespace to limit the results to', + 'invert' => 'All namespaces but the selected one', + 'associated' => 'Include associated (talk or main) namespace', + 'days' => 'Days to limit the results to', + 'limit' => 'Maximum number of results to return', + 'from' => 'Show changes since then', + 'hideminor' => 'Hide minor changes', + 'hidebots' => 'Hide changes made by bots', + 'hideanons' => 'Hide changes made by anonymous users', + 'hideliu' => 'Hide changes made by registered users', + 'hidepatrolled' => 'Hide patrolled changes', + 'hidemyself' => 'Hide changes made by yourself', + 'tagfilter' => 'Filter by tag', + 'target' => 'Show only changes on pages linked from this page', + 'showlinkedto' => 'Show changes on pages linked to the selected page instead', + 'categories' => 'Show only changes on pages in all of these categories', + 'categories_any' => 'Show only changes on pages in any of the categories instead', + ); + } + + public function getDescription() { + return 'Returns a recent changes feed'; + } + + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), array( + array( 'code' => 'feed-unavailable', 'info' => 'Syndication feeds are not available' ), + array( 'code' => 'feed-invalid', 'info' => 'Invalid subscription feed type' ), + ) ); + } + + public function getExamples() { + return array( + 'api.php?action=feedrecentchanges', + 'api.php?action=feedrecentchanges&days=30' + ); + } +} diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 1a11b527a0..eb24a35b86 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -56,6 +56,7 @@ class ApiMain extends ApiBase { 'parse' => 'ApiParse', 'opensearch' => 'ApiOpenSearch', 'feedcontributions' => 'ApiFeedContributions', + 'feedrecentchanges' => 'ApiFeedRecentChanges', 'feedwatchlist' => 'ApiFeedWatchlist', 'help' => 'ApiHelp', 'paraminfo' => 'ApiParamInfo', diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index 4e2556ccf6..c08d03393f 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -31,14 +31,6 @@ abstract class ChangesListSpecialPage extends SpecialPage { var $rcSubpage, $rcOptions; // @todo Rename these, make protected protected $customFilters; - /** - * The feed format to output as (either 'rss' or 'atom'), or null if no - * feed output was requested - * - * @var string $feedFormat - */ - protected $feedFormat; - /** * Main execution point * @@ -46,19 +38,13 @@ abstract class ChangesListSpecialPage extends SpecialPage { */ public function execute( $subpage ) { $this->rcSubpage = $subpage; - $this->feedFormat = $this->including() ? null : $this->getRequest()->getVal( 'feed' ); - if ( $this->feedFormat !== 'atom' && $this->feedFormat !== 'rss' ) { - $this->feedFormat = null; - } $this->setHeaders(); $this->outputHeader(); $this->addModules(); + $rows = $this->getRows(); $opts = $this->getOptions(); - // Fetch results, prepare a batch link existence check query - $conds = $this->buildMainQueryConds( $opts ); - $rows = $this->doMainQuery( $conds, $opts ); if ( $rows === false ) { if ( !$this->including() ) { $this->doHeader( $opts ); @@ -67,26 +53,30 @@ abstract class ChangesListSpecialPage extends SpecialPage { return; } - if ( !$this->feedFormat ) { - $batch = new LinkBatch; - foreach ( $rows as $row ) { - $batch->add( NS_USER, $row->rc_user_text ); - $batch->add( NS_USER_TALK, $row->rc_user_text ); - $batch->add( $row->rc_namespace, $row->rc_title ); - } - $batch->execute(); - } - if ( $this->feedFormat ) { - list( $changesFeed, $formatter ) = $this->getFeedObject( $this->feedFormat ); - /** @var ChangesFeed $changesFeed */ - $changesFeed->execute( $formatter, $rows, $this->checkLastModified( $this->feedFormat ), $opts ); - } else { - $this->webOutput( $rows, $opts ); + $batch = new LinkBatch; + foreach ( $rows as $row ) { + $batch->add( NS_USER, $row->rc_user_text ); + $batch->add( NS_USER_TALK, $row->rc_user_text ); + $batch->add( $row->rc_namespace, $row->rc_title ); } + $batch->execute(); + + $this->webOutput( $rows, $opts ); $rows->free(); } + /** + * Get the database result for this special page instance. Used by ApiFeedRecentChanges. + * + * @return bool|ResultWrapper Result or false + */ + public function getRows() { + $opts = $this->getOptions(); + $conds = $this->buildMainQueryConds( $opts ); + return $this->doMainQuery( $conds, $opts ); + } + /** * Get the current FormOptions for this request * @@ -461,30 +451,6 @@ abstract class ChangesListSpecialPage extends SpecialPage { $out->addModules( 'mediawiki.special.changeslist.legend.js' ); } - /** - * Return an array with a ChangesFeed object and ChannelFeed object. - * - * This is intentionally not abstract not to require subclasses which don't - * use feeds functionality to implement it. - * - * @param string $feedFormat Feed's format (either 'rss' or 'atom') - * @return array - */ - public function getFeedObject( $feedFormat ) { - throw new MWException( "Not implemented" ); - } - - /** - * Get last-modified date, for client caching. Not implemented by default - * (returns current time). - * - * @param string $feedFormat - * @return string|bool - */ - public function checkLastModified( $feedFormat ) { - return wfTimestampNow(); - } - protected function getGroupName() { return 'changes'; } diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index a5710a96c9..e05c8b89d2 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -38,10 +38,19 @@ class SpecialRecentChanges extends ChangesListSpecialPage { * @param string $subpage */ public function execute( $subpage ) { + // Backwards-compatibility: redirect to new feed URLs + $feedFormat = $this->getRequest()->getVal( 'feed' ); + if ( !$this->including() && $feedFormat ) { + $query = $this->getFeedQuery(); + $query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss'; + $this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) ); + return; + } + // 10 seconds server-side caching max $this->getOutput()->setSquidMaxage( 10 ); // Check if the client has a cached version - $lastmod = $this->checkLastModified( $this->feedFormat ); + $lastmod = $this->checkLastModified(); if ( $lastmod === false ) { return; } @@ -142,8 +151,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } public function validateOptions( FormOptions $opts ) { - global $wgFeedLimit; - $opts->validateIntBounds( 'limit', 0, $this->feedFormat ? $wgFeedLimit : 5000 ); + $opts->validateIntBounds( 'limit', 0, 5000 ); parent::validateOptions( $opts ); } @@ -244,16 +252,26 @@ class SpecialRecentChanges extends ChangesListSpecialPage { return $rows; } + public function outputFeedLinks() { + $this->addFeedLinks( $this->getFeedQuery() ); + } + /** - * Output feed links. + * Get URL query parameters for action=feedrecentchanges API feed of current recent changes view. + * + * @return array */ - public function outputFeedLinks() { - $feedQuery = $this->getFeedQuery(); - if ( $feedQuery !== '' ) { - $this->getOutput()->setFeedAppendQuery( $feedQuery ); - } else { - $this->getOutput()->setFeedAppendQuery( false ); - } + private function getFeedQuery() { + global $wgFeedLimit; + $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) { + // API handles empty parameters in a different way + return $value !== ''; + } ); + $query['action'] = 'feedrecentchanges'; + if ( $query['limit'] > $wgFeedLimit ) { + $query['limit'] = $wgFeedLimit; + } + return $query; } /** @@ -465,64 +483,14 @@ class SpecialRecentChanges extends ChangesListSpecialPage { * Don't use this if we are using the patrol feature, patrol changes don't * update the timestamp * - * @param string $feedFormat * @return string|bool */ - public function checkLastModified( $feedFormat ) { + public function checkLastModified() { $dbr = $this->getDB(); $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ ); - if ( $feedFormat || !$this->getUser()->useRCPatrol() ) { - if ( $lastmod && $this->getOutput()->checkLastModified( $lastmod ) ) { - # Client cache fresh and headers sent, nothing more to do. - return false; - } - } - return $lastmod; } - /** - * Return an array with a ChangesFeed object and ChannelFeed object. - * - * @param string $feedFormat Feed's format (either 'rss' or 'atom') - * @return array - */ - public function getFeedObject( $feedFormat ) { - $changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' ); - $formatter = $changesFeed->getFeedObject( - $this->msg( 'recentchanges' )->inContentLanguage()->text(), - $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(), - $this->getPageTitle()->getFullURL() - ); - - return array( $changesFeed, $formatter ); - } - - /** - * Get the query string to append to feed link URLs. - * - * @return string - */ - public function getFeedQuery() { - global $wgFeedLimit; - - $options = $this->getOptions()->getChangedValues(); - - // wfArrayToCgi() omits options set to null or false - foreach ( $options as &$value ) { - if ( $value === false ) { - $value = '0'; - } - } - unset( $value ); - - if ( isset( $options['limit'] ) && $options['limit'] > $wgFeedLimit ) { - $options['limit'] = $wgFeedLimit; - } - - return wfArrayToCgi( $options ); - } - /** * Creates the choose namespace selection * diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index 7cc8d3071f..34dd51f61b 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -234,17 +234,6 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { return $extraOpts; } - public function getFeedObject( $feedFormat ) { - $feed = new ChangesFeed( $feedFormat, false ); - $feedObj = $feed->getFeedObject( - $this->msg( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ) - ->inContentLanguage()->text(), - $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(), - $this->getPageTitle()->getFullURL() - ); - return array( $feed, $feedObj ); - } - /** * @return Title */ -- 2.20.1