API: Split list=deletedrevs into prop=deletedrevisions and list=alldeletedrevisions
authorBrad Jorsch <bjorsch@wikimedia.org>
Mon, 29 Sep 2014 17:47:08 +0000 (13:47 -0400)
committerBrad Jorsch <bjorsch@wikimedia.org>
Tue, 4 Nov 2014 20:02:03 +0000 (15:02 -0500)
list=deletedrevs has always been an odd one: it pretends to be a prop
module sometimes in taking titles from ApiPageSet, but when the pageset
supplies no titles it acts like a list module. This causes problems
such as bug 71389, and makes the whole thing unnecessarily confusing.

The solution is to split the "prop" and "list" aspects into separate
modules: prop=deletedrevisions when input should come from ApiPageSet
and list=alldeletedrevisions when not.

At the same time, let's take advantage of the situation to clear up some
other bugs. And let's share the revision-formatting code with
ApiQueryRevisions instead of partially reimplementing it.

Bug: 23489
Bug: 27193
Bug: 44190
Bug: 71396
Bug: 71389
Change-Id: I3e960d5c655bc57885d6d4ee227e67104808add7

14 files changed:
RELEASE-NOTES-1.25
includes/AutoLoader.php
includes/api/ApiBase.php
includes/api/ApiHelp.php
includes/api/ApiPageSet.php
includes/api/ApiParamInfo.php
includes/api/ApiQuery.php
includes/api/ApiQueryAllDeletedRevisions.php [new file with mode: 0644]
includes/api/ApiQueryDeletedRevisions.php [new file with mode: 0644]
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryRevisionsBase.php [new file with mode: 0644]
includes/api/i18n/en.json
includes/api/i18n/qqq.json

index fa870bb..7a088cf 100644 (file)
@@ -85,6 +85,15 @@ production.
   when a batch of pages is complete.
 * Pretty-printed HTML output now has nicer formatting and (if available)
   better syntax highlighting.
+* Deprecated list=deletedrevs in favor of newly-added prop=deletedrevisions and
+  list=alldeletedrevisions.
+* prop=revisions will gracefully continue when given too many revids or titles,
+  rather than just ignoring the extras.
+* prop=revisions will no longer die if rvcontentformat doesn't match a
+  revision's content model; it will instead warn and omit the content.
+* If the user has the 'deletedhistory' right, action=query's revids parameter
+  will now recognize deleted revids.
+* prop=revisions may be used as a generator, generating revids.
 
 === Action API internal changes in 1.25 ===
 * ApiHelp has been rewritten to support i18n and paginated HTML output.
@@ -109,6 +118,11 @@ production.
   has been removed.
 * ApiFormatBase now always buffers. Output is done when
   ApiFormatBase::closePrinter is called.
+* Much of the logic in ApiQueryRevisions has been split into ApiQueryRevisionsBase.
+* The 'revids' parameter supplied by ApiPageSet will now count deleted
+  revisions as "good" if the user has the 'deletedhistory' right. New methods
+  ApiPageSet::getLiveRevisionIDs() and ApiPageSet::getDeletedRevisionIDs() are
+  provided to access just the live or just the deleted revids.
 * The following methods have been deprecated and may be removed in a future
   release:
   * ApiBase::getDescription
@@ -127,6 +141,9 @@ production.
   * ApiMain::reallyMakeHelpMsg
   * ApiMain::makeHelpMsgHeader
   * ApiQueryImageInfo::getPropertyDescriptions
+* The following classes have been deprecated and may be removed in a future
+  release:
+  * ApiQueryDeletedrevs
 
 === Languages updated in 1.25 ===
 
index 36a0fcb..f0aa116 100644 (file)
@@ -262,6 +262,7 @@ $wgAutoloadLocalClasses = array(
        'ApiPurge' => 'includes/api/ApiPurge.php',
        'ApiQuery' => 'includes/api/ApiQuery.php',
        'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
+       'ApiQueryAllDeletedRevisions' => 'includes/api/ApiQueryAllDeletedRevisions.php',
        'ApiQueryAllImages' => 'includes/api/ApiQueryAllImages.php',
        'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
        'ApiQueryAllMessages' => 'includes/api/ApiQueryAllMessages.php',
@@ -276,6 +277,7 @@ $wgAutoloadLocalClasses = array(
        'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
        'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php',
        'ApiQueryContributors' => 'includes/api/ApiQueryContributors.php',
+       'ApiQueryDeletedRevisions' => 'includes/api/ApiQueryDeletedRevisions.php',
        'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
        'ApiQueryDisabled' => 'includes/api/ApiQueryDisabled.php',
        'ApiQueryDuplicateFiles' => 'includes/api/ApiQueryDuplicateFiles.php',
@@ -303,6 +305,7 @@ $wgAutoloadLocalClasses = array(
        'ApiQueryRecentChanges' => 'includes/api/ApiQueryRecentChanges.php',
        'ApiQueryFileRepoInfo' => 'includes/api/ApiQueryFileRepoInfo.php',
        'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
+       'ApiQueryRevisionsBase' => 'includes/api/ApiQueryRevisionsBase.php',
        'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
        'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
        'ApiQueryStashImageInfo' => 'includes/api/ApiQueryStashImageInfo.php',
index 58bd68d..3f84f2a 100644 (file)
@@ -79,7 +79,7 @@ abstract class ApiBase extends ContextSource {
        // of arrays, with the first member being the 'tag' for the info and the
        // remaining members being the values. In the help, this is formatted using
        // apihelp-{$path}-paraminfo-{$tag}, which is passed $1 = count, $2 =
-       // comma-joined list of values.
+       // comma-joined list of values, $3 = module prefix.
        const PARAM_HELP_MSG_INFO = 12;
        /// @since 1.25
        // When PARAM_DFLT is an array, this may be an array mapping those values
index fdde4bd..b33b087 100644 (file)
@@ -376,6 +376,7 @@ class ApiHelp extends ApiBase {
                                                        $info[] = $context->msg( "apihelp-{$path}-paraminfo-{$tag}" )
                                                                ->numParams( count( $i ) )
                                                                ->params( $context->getLanguage()->commaList( $i ) )
+                                                               ->params( $module->getModulePrefix() )
                                                                ->parse();
                                                }
                                        }
index db5eb52..ea85cac 100644 (file)
@@ -68,6 +68,8 @@ class ApiPageSet extends ApiBase {
        private $mPendingRedirectIDs = array();
        private $mConvertedTitles = array();
        private $mGoodRevIDs = array();
+       private $mLiveRevIDs = array();
+       private $mDeletedRevIDs = array();
        private $mMissingRevIDs = array();
        private $mFakePageId = -1;
        private $mCacheMode = 'public';
@@ -596,13 +598,29 @@ class ApiPageSet extends ApiBase {
        }
 
        /**
-        * Get the list of revision IDs (requested with the revids= parameter)
+        * Get the list of valid revision IDs (requested with the revids= parameter)
         * @return array Array of revID (int) => pageID (int)
         */
        public function getRevisionIDs() {
                return $this->mGoodRevIDs;
        }
 
+       /**
+        * Get the list of non-deleted revision IDs (requested with the revids= parameter)
+        * @return array Array of revID (int) => pageID (int)
+        */
+       public function getLiveRevisionIDs() {
+               return $this->mLiveRevIDs;
+       }
+
+       /**
+        * Get the list of revision IDs that were associated with deleted titles.
+        * @return array Array of revID (int) => pageID (int)
+        */
+       public function getDeletedRevisionIDs() {
+               return $this->mDeletedRevIDs;
+       }
+
        /**
         * Revision IDs that were not found in the database
         * @return array Array of revision IDs
@@ -901,6 +919,7 @@ class ApiPageSet extends ApiBase {
                                $revid = intval( $row->rev_id );
                                $pageid = intval( $row->rev_page );
                                $this->mGoodRevIDs[$revid] = $pageid;
+                               $this->mLiveRevIDs[$revid] = $pageid;
                                $pageids[$pageid] = '';
                                unset( $remaining[$revid] );
                        }
@@ -911,6 +930,51 @@ class ApiPageSet extends ApiBase {
 
                // Populate all the page information
                $this->initFromPageIds( array_keys( $pageids ) );
+
+               // If the user can see deleted revisions, pull out the corresponding
+               // titles from the archive table and include them too. We ignore
+               // ar_page_id because deleted revisions are tied by title, not page_id.
+               if ( !empty( $this->mMissingRevIDs ) && $this->getUser()->isAllowed( 'deletedhistory' ) ) {
+                       $remaining = array_flip( $this->mMissingRevIDs );
+                       $tables = array( 'archive' );
+                       $fields = array( 'ar_rev_id', 'ar_namespace', 'ar_title' );
+                       $where = array( 'ar_rev_id' => $this->mMissingRevIDs );
+
+                       $this->profileDBIn();
+                       $res = $db->select( $tables, $fields, $where, __METHOD__ );
+                       $titles = array();
+                       foreach ( $res as $row ) {
+                               $revid = intval( $row->ar_rev_id );
+                               $titles[$revid] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                               unset( $remaining[$revid] );
+                       }
+                       $this->profileDBOut();
+
+                       $this->initFromTitles( $titles );
+
+                       foreach ( $titles as $revid => $title ) {
+                               $ns = $title->getNamespace();
+                               $dbkey = $title->getDBkey();
+
+                               // Handle converted titles
+                               if ( !isset( $this->mAllPages[$ns][$dbkey] ) &&
+                                       isset( $this->mConvertedTitles[$title->getPrefixedText()] )
+                               ) {
+                                       $title = Title::newFromText( $this->mConvertedTitles[$title->getPrefixedText()] );
+                                       $ns = $title->getNamespace();
+                                       $dbkey = $title->getDBkey();
+                               }
+
+                               if ( isset( $this->mAllPages[$ns][$dbkey] ) ) {
+                                       $this->mGoodRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
+                                       $this->mDeletedRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
+                               } else {
+                                       $remaining[$revid] = true;
+                               }
+                       }
+
+                       $this->mMissingRevIDs = array_keys( $remaining );
+               }
        }
 
        /**
index 07670f6..b1c092e 100644 (file)
@@ -331,6 +331,7 @@ class ApiParamInfo extends ApiBase {
                                                $this->context->msg( "apihelp-{$path}-paraminfo-{$tag}" )
                                                        ->numParams( count( $i ) )
                                                        ->params( $this->context->getLanguage()->commaList( $i ) )
+                                                       ->params( $module->getModulePrefix() )
                                        ) );
                                        $result->setSubelements( $info, 'text' );
                                        $item['info'][] = $info;
index a091663..5a0491a 100644 (file)
@@ -45,6 +45,7 @@ class ApiQuery extends ApiBase {
                'categories' => 'ApiQueryCategories',
                'categoryinfo' => 'ApiQueryCategoryInfo',
                'contributors' => 'ApiQueryContributors',
+               'deletedrevisions' => 'ApiQueryDeletedRevisions',
                'duplicatefiles' => 'ApiQueryDuplicateFiles',
                'extlinks' => 'ApiQueryExternalLinks',
                'fileusage' => 'ApiQueryBacklinksprop',
@@ -69,6 +70,7 @@ class ApiQuery extends ApiBase {
         */
        private static $QueryListModules = array(
                'allcategories' => 'ApiQueryAllCategories',
+               'alldeletedrevisions' => 'ApiQueryAllDeletedRevisions',
                'allfileusages' => 'ApiQueryAllLinks',
                'allimages' => 'ApiQueryAllImages',
                'alllinks' => 'ApiQueryAllLinks',
diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php
new file mode 100644 (file)
index 0000000..0292e9a
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+/**
+ * Created on Oct 3, 2014
+ *
+ * Copyright Â© 2014 Brad Jorsch "bjorsch@wikimedia.org"
+ *
+ * Heavily based on ApiQueryDeletedrevs,
+ * Copyright Â© 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+/**
+ * Query module to enumerate all deleted revisions.
+ *
+ * @ingroup API
+ */
+class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
+
+       public function __construct( ApiQuery $query, $moduleName ) {
+               parent::__construct( $query, $moduleName, 'adr' );
+       }
+
+       /**
+        * @param ApiPageSet $resultPageSet
+        * @return void
+        */
+       protected function run( ApiPageSet $resultPageSet = null ) {
+               $user = $this->getUser();
+               // Before doing anything at all, let's check permissions
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $this->dieUsage(
+                               'You don\'t have permission to view deleted revision information',
+                               'permissiondenied'
+                       );
+               }
+
+               $db = $this->getDB();
+               $params = $this->extractRequestParams( false );
+
+               $result = $this->getResult();
+               $pageSet = $this->getPageSet();
+               $titles = $pageSet->getTitles();
+
+               // This module operates in two modes:
+               // 'user': List deleted revs by a certain user
+               // 'all': List all deleted revs in NS
+               $mode = 'all';
+               if ( !is_null( $params['user'] ) ) {
+                       $mode = 'user';
+               }
+
+               if ( $mode == 'user' ) {
+                       foreach ( array( 'from', 'to', 'prefix', 'excludeuser' ) as $param ) {
+                               if ( !is_null( $params[$param] ) ) {
+                                       $p = $this->getModulePrefix();
+                                       $this->dieUsage( "The '{$p}{$param}' parameter cannot be used with '{$p}user'",
+                                               'badparams' );
+                               }
+                       }
+               } else {
+                       foreach ( array( 'start', 'end' ) as $param ) {
+                               if ( !is_null( $params[$param] ) ) {
+                                       $p = $this->getModulePrefix();
+                                       $this->dieUsage( "The '{$p}{$param}' parameter may only be used with '{$p}user'",
+                                               'badparams' );
+                               }
+                       }
+               }
+
+               $this->addTables( 'archive' );
+               if ( $resultPageSet === null ) {
+                       $this->parseParameters( $params );
+                       $this->addFields( Revision::selectArchiveFields() );
+                       $this->addFields( array( 'ar_title', 'ar_namespace' ) );
+               } else {
+                       $this->limit = $this->getParameter( 'limit' ) ?: 10;
+                       $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ) );
+               }
+
+               if ( $this->fld_tags ) {
+                       $this->addTables( 'tag_summary' );
+                       $this->addJoinConds(
+                               array( 'tag_summary' => array( 'LEFT JOIN', array( 'ar_rev_id=ts_rev_id' ) ) )
+                       );
+                       $this->addFields( 'ts_tags' );
+               }
+
+               if ( !is_null( $params['tag'] ) ) {
+                       $this->addTables( 'change_tag' );
+                       $this->addJoinConds(
+                               array( 'change_tag' => array( 'INNER JOIN', array( 'ar_rev_id=ct_rev_id' ) ) )
+                       );
+                       $this->addWhereFld( 'ct_tag', $params['tag'] );
+               }
+
+               if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+                       // Modern MediaWiki has the content for deleted revs in the 'text'
+                       // table using fields old_text and old_flags. But revisions deleted
+                       // pre-1.5 store the content in the 'archive' table directly using
+                       // fields ar_text and ar_flags, and no corresponding 'text' row. So
+                       // we have to LEFT JOIN and fetch all four fields.
+                       $this->addTables( 'text' );
+                       $this->addJoinConds(
+                               array( 'text' => array( 'LEFT JOIN', array( 'ar_text_id=old_id' ) ) )
+                       );
+                       $this->addFields( array( 'ar_text', 'ar_flags', 'old_text', 'old_flags' ) );
+
+                       // This also means stricter restrictions
+                       if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
+                               $this->dieUsage(
+                                       'You don\'t have permission to view deleted revision content',
+                                       'permissiondenied'
+                               );
+                       }
+               }
+
+               $dir = $params['dir'];
+               $miser_ns = null;
+
+               if ( $mode == 'all' ) {
+                       if ( $params['namespace'] !== null ) {
+                               $this->addWhereFld( 'ar_namespace', $params['namespace'] );
+                       }
+
+                       $from = $params['from'] === null
+                               ? null
+                               : $this->titlePartToKey( $params['from'], $params['namespace'] );
+                       $to = $params['to'] === null
+                               ? null
+                               : $this->titlePartToKey( $params['to'], $params['namespace'] );
+                       $this->addWhereRange( 'ar_title', $dir, $from, $to );
+
+                       if ( isset( $params['prefix'] ) ) {
+                               $this->addWhere( 'ar_title' . $db->buildLike(
+                                       $this->titlePartToKey( $params['prefix'], $params['namespace'] ),
+                                       $db->anyString() ) );
+                       }
+               } else {
+                       if ( $this->getConfig()->get( 'MiserMode' ) ) {
+                               $miser_ns = $params['namespace'];
+                       } else {
+                               $this->addWhereFld( 'ar_namespace', $params['namespace'] );
+                       }
+                       $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
+               }
+
+               if ( !is_null( $params['user'] ) ) {
+                       $this->addWhereFld( 'ar_user_text', $params['user'] );
+               } elseif ( !is_null( $params['excludeuser'] ) ) {
+                       $this->addWhere( 'ar_user_text != ' .
+                               $db->addQuotes( $params['excludeuser'] ) );
+               }
+
+               if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
+                       // Paranoia: avoid brute force searches (bug 17342)
+                       // (shouldn't be able to get here without 'deletedhistory', but
+                       // check it again just in case)
+                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                               $bitmask = Revision::DELETED_USER;
+                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                               $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+                       } else {
+                               $bitmask = 0;
+                       }
+                       if ( $bitmask ) {
+                               $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
+                       }
+               }
+
+               if ( !is_null( $params['continue'] ) ) {
+                       $cont = explode( '|', $params['continue'] );
+                       $op = ( $dir == 'newer' ? '>' : '<' );
+                       if ( $mode == 'all' ) {
+                               $this->dieContinueUsageIf( count( $cont ) != 4 );
+                               $ns = intval( $cont[0] );
+                               $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
+                               $title = $db->addQuotes( $cont[1] );
+                               $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
+                               $ar_id = (int)$cont[3];
+                               $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
+                               $this->addWhere( "ar_namespace $op $ns OR " .
+                                       "(ar_namespace = $ns AND " .
+                                       "(ar_title $op $title OR " .
+                                       "(ar_title = $title AND " .
+                                       "(ar_timestamp $op $ts OR " .
+                                       "(ar_timestamp = $ts AND " .
+                                       "ar_id $op= $ar_id)))))" );
+                       } else {
+                               $this->dieContinueUsageIf( count( $cont ) != 2 );
+                               $ts = $db->addQuotes( $db->timestamp( $cont[0] ) );
+                               $ar_id = (int)$cont[1];
+                               $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
+                               $this->addWhere( "ar_timestamp $op $ts OR " .
+                                       "(ar_timestamp = $ts AND " .
+                                       "ar_id $op= $ar_id)" );
+                       }
+               }
+
+               $this->addOption( 'LIMIT', $this->limit + 1 );
+
+               $sort = ( $dir == 'newer' ? '' : ' DESC' );
+               $orderby = array();
+               if ( $mode == 'all' ) {
+                       // Targeting index name_title_timestamp
+                       if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
+                               $orderby[] = "ar_namespace $sort";
+                       }
+                       $orderby[] = "ar_title $sort";
+                       $orderby[] = "ar_timestamp $sort";
+                       $orderby[] = "ar_id $sort";
+               } else {
+                       // Targeting index usertext_timestamp
+                       // 'user' is always constant.
+                       $orderby[] = "ar_timestamp $sort";
+                       $orderby[] = "ar_id $sort";
+               }
+               $this->addOption( 'ORDER BY', $orderby );
+
+               $res = $this->select( __METHOD__ );
+               $pageMap = array(); // Maps ns&title to array index
+               $count = 0;
+               $nextIndex = 0;
+               $generated = array();
+               foreach ( $res as $row ) {
+                       if ( ++$count > $this->limit ) {
+                               // We've had enough
+                               if ( $mode == 'all' ) {
+                                       $this->setContinueEnumParameter( 'continue',
+                                               "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
+                                       );
+                               } else {
+                                       $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
+                               }
+                               break;
+                       }
+
+                       // Miser mode namespace check
+                       if ( $miser_ns !== null && !in_array( $row->ar_namespace, $miser_ns ) ) {
+                               continue;
+                       }
+
+                       if ( $resultPageSet !== null ) {
+                               if ( $params['generatetitles'] ) {
+                                       $key = "{$row->ar_namespace}:{$row->ar_title}";
+                                       if ( !isset( $generated[$key] ) ) {
+                                               $generated[$key] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                                       }
+                               } else {
+                                       $generated[] = $row->ar_rev_id;
+                               }
+                       } else {
+                               $revision = Revision::newFromArchiveRow( $row );
+                               $rev = $this->extractRevisionInfo( $revision, $row );
+
+                               if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
+                                       $index = $nextIndex++;
+                                       $pageMap[$row->ar_namespace][$row->ar_title] = $index;
+                                       $title = $revision->getTitle();
+                                       $a = array(
+                                               'pageid' => $title->getArticleID(),
+                                               'revisions' => array( $rev ),
+                                       );
+                                       $result->setIndexedTagName( $a['revisions'], 'rev' );
+                                       ApiQueryBase::addTitleInfo( $a, $title );
+                                       $fit = $result->addValue( array( 'query', $this->getModuleName() ), $index, $a );
+                               } else {
+                                       $index = $pageMap[$row->ar_namespace][$row->ar_title];
+                                       $fit = $result->addValue(
+                                               array( 'query', $this->getModuleName(), $index, 'revisions' ),
+                                               null, $rev );
+                               }
+                               if ( !$fit ) {
+                                       if ( $mode == 'all' ) {
+                                               $this->setContinueEnumParameter( 'continue',
+                                                       "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
+                                               );
+                                       } else {
+                                               $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
+                                       }
+                                       break;
+                               }
+                       }
+               }
+
+               if ( $resultPageSet !== null ) {
+                       if ( $params['generatetitles'] ) {
+                               $resultPageSet->populateFromTitles( $generated );
+                       } else {
+                               $resultPageSet->populateFromRevisionIDs( $generated );
+                       }
+               } else {
+                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
+               }
+       }
+
+       public function getAllowedParams() {
+               $ret = parent::getAllowedParams() + array(
+                       'user' => array(
+                               ApiBase::PARAM_TYPE => 'user'
+                       ),
+                       'namespace' => array(
+                               ApiBase::PARAM_ISMULTI => true,
+                               ApiBase::PARAM_TYPE => 'namespace',
+                               ApiBase::PARAM_DFLT => null,
+                       ),
+                       'start' => array(
+                               ApiBase::PARAM_TYPE => 'timestamp',
+                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'useronly' ) ),
+                       ),
+                       'end' => array(
+                               ApiBase::PARAM_TYPE => 'timestamp',
+                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'useronly' ) ),
+                       ),
+                       'dir' => array(
+                               ApiBase::PARAM_TYPE => array(
+                                       'newer',
+                                       'older'
+                               ),
+                               ApiBase::PARAM_DFLT => 'older',
+                               ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
+                       ),
+                       'from' => array(
+                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'nonuseronly' ) ),
+                       ),
+                       'to' => array(
+                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'nonuseronly' ) ),
+                       ),
+                       'prefix' => array(
+                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'nonuseronly' ) ),
+                       ),
+                       'excludeuser' => array(
+                               ApiBase::PARAM_TYPE => 'user',
+                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'nonuseronly' ) ),
+                       ),
+                       'tag' => null,
+                       'continue' => array(
+                               ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+                       ),
+                       'generatetitles' => array(
+                               ApiBase::PARAM_DFLT => false
+                       ),
+               );
+
+               if ( $this->getConfig()->get( 'MiserMode' ) ) {
+                       $ret['user'][ApiBase::PARAM_HELP_MSG_APPEND] = array(
+                               'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
+                       );
+                       $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = array(
+                               'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
+                       );
+               }
+
+               return $ret;
+       }
+
+       protected function getExamplesMessages() {
+               return array(
+                       'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
+                               => 'apihelp-query+alldeletedrevisions-example-user',
+                       'action=query&list=alldeletedrevisions&adrdir=newer&adrlimit=50'
+                               => 'apihelp-query+alldeletedrevisions-example-ns-main',
+               );
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Alldeletedrevisions';
+       }
+}
diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php
new file mode 100644 (file)
index 0000000..0271037
--- /dev/null
@@ -0,0 +1,304 @@
+<?php
+/**
+ * Created on Oct 3, 2014
+ *
+ * Copyright Â© 2014 Brad Jorsch "bjorsch@wikimedia.org"
+ *
+ * Heavily based on ApiQueryDeletedrevs,
+ * Copyright Â© 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+/**
+ * Query module to enumerate deleted revisions for pages.
+ *
+ * @ingroup API
+ */
+class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
+
+       public function __construct( ApiQuery $query, $moduleName ) {
+               parent::__construct( $query, $moduleName, 'drv' );
+       }
+
+       protected function run( ApiPageSet $resultPageSet = null ) {
+               $user = $this->getUser();
+               // Before doing anything at all, let's check permissions
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $this->dieUsage(
+                               'You don\'t have permission to view deleted revision information',
+                               'permissiondenied'
+                       );
+               }
+
+               $result = $this->getResult();
+               $pageSet = $this->getPageSet();
+               $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace();
+               $pageCount = count( $pageSet->getGoodAndMissingTitles() );
+               $revCount = $pageSet->getRevisionCount();
+               if ( $revCount === 0 && $pageCount === 0 ) {
+                       // Nothing to do
+                       return;
+               }
+               if ( $revCount !== 0 && count( $pageSet->getDeletedRevisionIDs() ) === 0 ) {
+                       // Nothing to do, revisions were supplied but none are deleted
+                       return;
+               }
+
+               $params = $this->extractRequestParams( false );
+
+               $db = $this->getDB();
+
+               if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
+                       $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
+               }
+
+               $this->addTables( 'archive' );
+               if ( $resultPageSet === null ) {
+                       $this->parseParameters( $params );
+                       $this->addFields( Revision::selectArchiveFields() );
+                       $this->addFields( array( 'ar_title', 'ar_namespace' ) );
+               } else {
+                       $this->limit = $this->getParameter( 'limit' ) ?: 10;
+                       $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ) );
+               }
+
+               if ( $this->fld_tags ) {
+                       $this->addTables( 'tag_summary' );
+                       $this->addJoinConds(
+                               array( 'tag_summary' => array( 'LEFT JOIN', array( 'ar_rev_id=ts_rev_id' ) ) )
+                       );
+                       $this->addFields( 'ts_tags' );
+               }
+
+               if ( !is_null( $params['tag'] ) ) {
+                       $this->addTables( 'change_tag' );
+                       $this->addJoinConds(
+                               array( 'change_tag' => array( 'INNER JOIN', array( 'ar_rev_id=ct_rev_id' ) ) )
+                       );
+                       $this->addWhereFld( 'ct_tag', $params['tag'] );
+               }
+
+               if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+                       // Modern MediaWiki has the content for deleted revs in the 'text'
+                       // table using fields old_text and old_flags. But revisions deleted
+                       // pre-1.5 store the content in the 'archive' table directly using
+                       // fields ar_text and ar_flags, and no corresponding 'text' row. So
+                       // we have to LEFT JOIN and fetch all four fields.
+                       $this->addTables( 'text' );
+                       $this->addJoinConds(
+                               array( 'text' => array( 'LEFT JOIN', array( 'ar_text_id=old_id' ) ) )
+                       );
+                       $this->addFields( array( 'ar_text', 'ar_flags', 'old_text', 'old_flags' ) );
+
+                       // This also means stricter restrictions
+                       if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
+                               $this->dieUsage(
+                                       'You don\'t have permission to view deleted revision content',
+                                       'permissiondenied'
+                               );
+                       }
+               }
+
+               $dir = $params['dir'];
+
+               if ( $revCount !== 0 ) {
+                       $this->addWhere( array(
+                               'ar_rev_id' => array_keys( $pageSet->getDeletedRevisionIDs() )
+                       ) );
+               } else {
+                       // We need a custom WHERE clause that matches all titles.
+                       $lb = new LinkBatch( $pageSet->getGoodAndMissingTitles() );
+                       $where = $lb->constructSet( 'ar', $db );
+                       $this->addWhere( $where );
+               }
+
+               if ( !is_null( $params['user'] ) ) {
+                       $this->addWhereFld( 'ar_user_text', $params['user'] );
+               } elseif ( !is_null( $params['excludeuser'] ) ) {
+                       $this->addWhere( 'ar_user_text != ' .
+                               $db->addQuotes( $params['excludeuser'] ) );
+               }
+
+               if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
+                       // Paranoia: avoid brute force searches (bug 17342)
+                       // (shouldn't be able to get here without 'deletedhistory', but
+                       // check it again just in case)
+                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                               $bitmask = Revision::DELETED_USER;
+                       } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                               $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+                       } else {
+                               $bitmask = 0;
+                       }
+                       if ( $bitmask ) {
+                               $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
+                       }
+               }
+
+               if ( !is_null( $params['continue'] ) ) {
+                       $cont = explode( '|', $params['continue'] );
+                       $op = ( $dir == 'newer' ? '>' : '<' );
+                       if ( $revCount !== 0 ) {
+                               $this->dieContinueUsageIf( count( $cont ) != 2 );
+                               $rev = intval( $cont[0] );
+                               $this->dieContinueUsageIf( strval( $rev ) !== $cont[0] );
+                               $ar_id = (int)$cont[1];
+                               $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
+                               $this->addWhere( "ar_rev_id $op $rev OR " .
+                                       "(ar_rev_id = $rev AND " .
+                                       "ar_id $op= $ar_id)" );
+                       } else {
+                               $this->dieContinueUsageIf( count( $cont ) != 4 );
+                               $ns = intval( $cont[0] );
+                               $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
+                               $title = $db->addQuotes( $cont[1] );
+                               $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
+                               $ar_id = (int)$cont[3];
+                               $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
+                               $this->addWhere( "ar_namespace $op $ns OR " .
+                                       "(ar_namespace = $ns AND " .
+                                       "(ar_title $op $title OR " .
+                                       "(ar_title = $title AND " .
+                                       "(ar_timestamp $op $ts OR " .
+                                       "(ar_timestamp = $ts AND " .
+                                       "ar_id $op= $ar_id)))))" );
+                       }
+               }
+
+               $this->addOption( 'LIMIT', $this->limit + 1 );
+
+               if ( $revCount !== 0 ) {
+                       // Sort by ar_rev_id when querying by ar_rev_id
+                       $this->addWhereRange( 'ar_rev_id', $dir, null, null );
+               } else {
+                       // Sort by ns and title in the same order as timestamp for efficiency
+                       // But only when not already unique in the query
+                       if ( count( $pageMap ) > 1 ) {
+                               $this->addWhereRange( 'ar_namespace', $dir, null, null );
+                       }
+                       $oneTitle = key( reset( $pageMap ) );
+                       foreach ( $pageMap as $pages ) {
+                               if ( count( $pages ) > 1 || key( $pages ) !== $oneTitle ) {
+                                       $this->addWhereRange( 'ar_title', $dir, null, null );
+                                       break;
+                               }
+                       }
+                       $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
+               }
+               // Include in ORDER BY for uniqueness
+               $this->addWhereRange( 'ar_id', $dir, null, null );
+
+               $res = $this->select( __METHOD__ );
+               $count = 0;
+               $generated = array();
+               foreach ( $res as $row ) {
+                       if ( ++$count > $this->limit ) {
+                               // We've had enough
+                               $this->setContinueEnumParameter( 'continue',
+                                       $revCount
+                                               ? "$row->ar_rev_id|$row->ar_id"
+                                               : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
+                               );
+                               break;
+                       }
+
+                       if ( $resultPageSet !== null ) {
+                               $generated[] = $row->ar_rev_id;
+                       } else {
+                               if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
+                                       // Was it converted?
+                                       $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                                       $converted = $pageSet->getConvertedTitles();
+                                       if ( $title && isset( $converted[$title->getPrefixedText()] ) ) {
+                                               $title = Title::newFromText( $converted[$title->getPrefixedText()] );
+                                               if ( $title && isset( $pageMap[$title->getNamespace()][$title->getDBkey()] ) ) {
+                                                       $pageMap[$row->ar_namespace][$row->ar_title] =
+                                                               $pageMap[$title->getNamespace()][$title->getDBkey()];
+                                               }
+                                       }
+                               }
+                               if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
+                                       ApiBase::dieDebug( "Found row in archive (ar_id={$row->ar_id}) that didn't " .
+                                               "get processed by ApiPageSet" );
+                               }
+
+                               $fit = $this->addPageSubItem(
+                                       $pageMap[$row->ar_namespace][$row->ar_title],
+                                       $this->extractRevisionInfo( Revision::newFromArchiveRow( $row ), $row ),
+                                       'rev'
+                               );
+                               if ( !$fit ) {
+                                       $this->setContinueEnumParameter( 'continue',
+                                               $revCount
+                                                       ? "$row->ar_rev_id|$row->ar_id"
+                                                       : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
+                                       );
+                                       break;
+                               }
+                       }
+               }
+
+               if ( $resultPageSet !== null ) {
+                       $resultPageSet->populateFromRevisionIDs( $generated );
+               }
+       }
+
+       public function getAllowedParams() {
+               return parent::getAllowedParams() + array(
+                       'start' => array(
+                               ApiBase::PARAM_TYPE => 'timestamp',
+                       ),
+                       'end' => array(
+                               ApiBase::PARAM_TYPE => 'timestamp',
+                       ),
+                       'dir' => array(
+                               ApiBase::PARAM_TYPE => array(
+                                       'newer',
+                                       'older'
+                               ),
+                               ApiBase::PARAM_DFLT => 'older',
+                               ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
+                       ),
+                       'tag' => null,
+                       'user' => array(
+                               ApiBase::PARAM_TYPE => 'user'
+                       ),
+                       'excludeuser' => array(
+                               ApiBase::PARAM_TYPE => 'user'
+                       ),
+                       'continue' => array(
+                               ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+                       ),
+               );
+       }
+
+       protected function getExamplesMessages() {
+               return array(
+                       'action=query&prop=deletedrevisions&titles=Main%20Page|Talk:Main%20Page&' .
+                               'drvprop=user|comment|content'
+                               => 'apihelp-query+deletedrevisions-example-titles',
+                       'action=query&prop=deletedrevisions&revids=123456'
+                               => 'apihelp-query+deletedrevisions-example-revids',
+               );
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Properties#deletedrevisions_.2F_drv';
+       }
+}
index 4a5f5fd..f828255 100644 (file)
@@ -28,6 +28,7 @@
  * Query module to enumerate all deleted revisions.
  *
  * @ingroup API
+ * @deprecated since 1.25
  */
 class ApiQueryDeletedrevs extends ApiQueryBase {
 
@@ -45,6 +46,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        );
                }
 
+               $this->setWarning(
+                       'list=deletedrevs has been deprecated. Please use prop=deletedrevisions or ' .
+                       'list=alldeletedrevisions instead.'
+               );
+               $this->logFeatureUsage( 'action=query&list=deletedrevs' );
+
                $db = $this->getDB();
                $params = $this->extractRequestParams( false );
                $prop = array_flip( $params['prop'] );
@@ -420,6 +427,10 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
        }
 
+       public function isDeprecated() {
+               return true;
+       }
+
        public function getAllowedParams() {
                return array(
                        'start' => array(
index f5ad9d0..2e980f3 100644 (file)
  *
  * @ingroup API
  */
-class ApiQueryRevisions extends ApiQueryBase {
+class ApiQueryRevisions extends ApiQueryRevisionsBase {
 
-       private $diffto, $difftotext, $expandTemplates, $generateXML, $section,
-               $token, $parseContent, $contentFormat;
+       private $token = null;
 
        public function __construct( ApiQuery $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'rv' );
        }
 
-       private $fld_ids = false, $fld_flags = false, $fld_timestamp = false,
-               $fld_size = false, $fld_sha1 = false, $fld_comment = false,
-               $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
-               $fld_content = false, $fld_tags = false, $fld_contentmodel = false;
-
        private $tokenFunctions;
 
        /** @deprecated since 1.24 */
@@ -89,7 +83,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                        array( $title->getPrefixedText(), $rev->getUserText() ) );
        }
 
-       public function execute() {
+       protected function run( ApiPageSet $resultPageSet = null ) {
                $params = $this->extractRequestParams( false );
 
                // If any of those parameters are used, work in 'enumeration' mode.
@@ -107,6 +101,11 @@ class ApiQueryRevisions extends ApiQueryBase {
 
                // Optimization -- nothing to do
                if ( $revCount === 0 && $pageCount === 0 ) {
+                       // Nothing to do
+                       return;
+               }
+               if ( $revCount > 0 && count( $pageSet->getLiveRevisionIDs() ) === 0 ) {
+                       // We're in revisions mode but all given revisions are deleted
                        return;
                }
 
@@ -127,75 +126,32 @@ class ApiQueryRevisions extends ApiQueryBase {
                        );
                }
 
-               if ( !is_null( $params['difftotext'] ) ) {
-                       $this->difftotext = $params['difftotext'];
-               } elseif ( !is_null( $params['diffto'] ) ) {
-                       if ( $params['diffto'] == 'cur' ) {
-                               $params['diffto'] = 0;
-                       }
-                       if ( ( !ctype_digit( $params['diffto'] ) || $params['diffto'] < 0 )
-                               && $params['diffto'] != 'prev' && $params['diffto'] != 'next'
-                       ) {
-                               $this->dieUsage(
-                                       'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"',
-                                       'diffto'
-                               );
-                       }
-                       // Check whether the revision exists and is readable,
-                       // DifferenceEngine returns a rather ambiguous empty
-                       // string if that's not the case
-                       if ( $params['diffto'] != 0 ) {
-                               $difftoRev = Revision::newFromID( $params['diffto'] );
-                               if ( !$difftoRev ) {
-                                       $this->dieUsageMsg( array( 'nosuchrevid', $params['diffto'] ) );
-                               }
-                               if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
-                                       $this->setWarning( "Couldn't diff to r{$difftoRev->getID()}: content is hidden" );
-                                       $params['diffto'] = null;
-                               }
-                       }
-                       $this->diffto = $params['diffto'];
+               // In non-enum mode, rvlimit can't be directly used. Use the maximum
+               // allowed value.
+               if ( !$enumRevMode ) {
+                       $this->setParsedLimit = false;
+                       $params['limit'] = 'max';
                }
 
                $db = $this->getDB();
-               $this->addTables( 'page' );
-               $this->addFields( Revision::selectFields() );
-               $this->addWhere( 'page_id = rev_page' );
-
-               $prop = array_flip( $params['prop'] );
-
-               // Optional fields
-               $this->fld_ids = isset( $prop['ids'] );
-               // $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed?
-               $this->fld_flags = isset( $prop['flags'] );
-               $this->fld_timestamp = isset( $prop['timestamp'] );
-               $this->fld_comment = isset( $prop['comment'] );
-               $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
-               $this->fld_size = isset( $prop['size'] );
-               $this->fld_sha1 = isset( $prop['sha1'] );
-               $this->fld_contentmodel = isset( $prop['contentmodel'] );
-               $this->fld_userid = isset( $prop['userid'] );
-               $this->fld_user = isset( $prop['user'] );
-               $this->token = $params['token'];
-
-               if ( !empty( $params['contentformat'] ) ) {
-                       $this->contentFormat = $params['contentformat'];
-               }
-
-               $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
-               $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
-               $limit = $params['limit'];
-               if ( $limit == 'max' ) {
-                       $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
-                       $this->getResult()->setParsedLimit( $this->getModuleName(), $limit );
-               }
+               $this->addTables( array( 'revision', 'page' ) );
+               $this->addJoinConds(
+                       array( 'page' => array( 'INNER JOIN', array( 'page_id = rev_page' ) ) )
+               );
 
-               if ( !is_null( $this->token ) || $pageCount > 0 ) {
-                       $this->addFields( Revision::selectPageFields() );
+               if ( $resultPageSet === null ) {
+                       $this->parseParameters( $params );
+                       $this->token = $params['token'];
+                       $this->addFields( Revision::selectFields() );
+                       if ( $this->token !== null || $pageCount > 0 ) {
+                               $this->addFields( Revision::selectPageFields() );
+                       }
+               } else {
+                       $this->limit = $this->getParameter( 'limit' ) ?: 10;
+                       $this->addFields( array( 'rev_id', 'rev_page' ) );
                }
 
-               if ( isset( $prop['tags'] ) ) {
-                       $this->fld_tags = true;
+               if ( $this->fld_tags ) {
                        $this->addTables( 'tag_summary' );
                        $this->addJoinConds(
                                array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) )
@@ -211,7 +167,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                        $this->addWhereFld( 'ct_tag', $params['tag'] );
                }
 
-               if ( isset( $prop['content'] ) || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+               if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
                        // For each page we will request, the user must have read rights for that page
                        $user = $this->getUser();
                        /** @var $title Title */
@@ -224,28 +180,11 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
 
                        $this->addTables( 'text' );
-                       $this->addWhere( 'rev_text_id=old_id' );
+                       $this->addJoinConds(
+                               array( 'text' => array( 'INNER JOIN', array( 'rev_text_id=old_id' ) ) )
+                       );
                        $this->addFields( 'old_id' );
                        $this->addFields( Revision::selectTextFields() );
-
-                       $this->fld_content = isset( $prop['content'] );
-
-                       $this->expandTemplates = $params['expandtemplates'];
-                       $this->generateXML = $params['generatexml'];
-                       $this->parseContent = $params['parse'];
-                       if ( $this->parseContent ) {
-                               // Must manually initialize unset limit
-                               if ( is_null( $limit ) ) {
-                                       $limit = 1;
-                               }
-                               // We are only going to parse 1 revision per request
-                               $this->validateLimit( 'limit', $limit, 1, 1, 1 );
-                       }
-                       if ( isset( $params['section'] ) ) {
-                               $this->section = $params['section'];
-                       } else {
-                               $this->section = false;
-                       }
                }
 
                // add user name, if needed
@@ -255,9 +194,6 @@ class ApiQueryRevisions extends ApiQueryBase {
                        $this->addFields( Revision::selectUserFields() );
                }
 
-               // Bug 24166 - API error when using rvprop=tags
-               $this->addTables( 'revision' );
-
                if ( $enumRevMode ) {
                        // This is mostly to prevent parameter errors (and optimize SQL?)
                        if ( !is_null( $params['startid'] ) && !is_null( $params['start'] ) ) {
@@ -300,12 +236,6 @@ class ApiQueryRevisions extends ApiQueryBase {
                                        $params['start'], $params['end'], false );
                        }
 
-                       // must manually initialize unset limit
-                       if ( is_null( $limit ) ) {
-                               $limit = 10;
-                       }
-                       $this->validateLimit( 'limit', $limit, 1, $userMax, $botMax );
-
                        // There is only one ID, use it
                        $ids = array_keys( $pageSet->getGoodTitles() );
                        $this->addWhereFld( 'rev_page', reset( $ids ) );
@@ -330,11 +260,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                                }
                        }
                } elseif ( $revCount > 0 ) {
-                       $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
-                       $revs = $pageSet->getRevisionIDs();
-                       if ( self::truncateArray( $revs, $max ) ) {
-                               $this->setWarning( "Too many values supplied for parameter 'revids': the limit is $max" );
-                       }
+                       $revs = $pageSet->getLiveRevisionIDs();
 
                        // Get all revision IDs
                        $this->addWhereFld( 'rev_id', array_keys( $revs ) );
@@ -343,19 +269,11 @@ class ApiQueryRevisions extends ApiQueryBase {
                                $this->addWhere( 'rev_id >= ' . intval( $params['continue'] ) );
                        }
                        $this->addOption( 'ORDER BY', 'rev_id' );
-
-                       // assumption testing -- we should never get more then $revCount rows.
-                       $limit = $revCount;
                } elseif ( $pageCount > 0 ) {
-                       $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
                        $titles = $pageSet->getGoodTitles();
-                       if ( self::truncateArray( $titles, $max ) ) {
-                               $this->setWarning( "Too many values supplied for parameter 'titles': the limit is $max" );
-                       }
 
                        // When working in multi-page non-enumeration mode,
                        // limit to the latest revision only
-                       $this->addWhere( 'page_id=rev_page' );
                        $this->addWhere( 'page_latest=rev_id' );
 
                        // Get all page IDs
@@ -378,31 +296,20 @@ class ApiQueryRevisions extends ApiQueryBase {
                                'rev_page',
                                'rev_id'
                        ) );
-
-                       // assumption testing -- we should never get more then $pageCount rows.
-                       $limit = $pageCount;
                } else {
                        ApiBase::dieDebug( __METHOD__, 'param validation?' );
                }
 
-               $this->addOption( 'LIMIT', $limit + 1 );
+               $this->addOption( 'LIMIT', $this->limit + 1 );
 
                $count = 0;
+               $generated = array();
                $res = $this->select( __METHOD__ );
 
                foreach ( $res as $row ) {
-                       if ( ++$count > $limit ) {
+                       if ( ++$count > $this->limit ) {
                                // We've reached the one extra which shows that there are
                                // additional pages to be had. Stop here...
-                               if ( !$enumRevMode ) {
-                                       ApiBase::dieDebug( __METHOD__, 'Got more rows then expected' ); // bug report
-                               }
-                               $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
-                               break;
-                       }
-
-                       $fit = $this->addPageSubItem( $row->rev_page, $this->extractRowInfo( $row ), 'rev' );
-                       if ( !$fit ) {
                                if ( $enumRevMode ) {
                                        $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
                                } elseif ( $revCount > 0 ) {
@@ -413,313 +320,55 @@ class ApiQueryRevisions extends ApiQueryBase {
                                }
                                break;
                        }
-               }
-       }
-
-       private function extractRowInfo( $row ) {
-               $revision = new Revision( $row );
-               $title = $revision->getTitle();
-               $user = $this->getUser();
-               $vals = array();
-               $anyHidden = false;
-
-               if ( $this->fld_ids ) {
-                       $vals['revid'] = intval( $revision->getId() );
-                       // $vals['oldid'] = intval( $row->rev_text_id ); // todo: should this be exposed?
-                       if ( !is_null( $revision->getParentId() ) ) {
-                               $vals['parentid'] = intval( $revision->getParentId() );
-                       }
-               }
-
-               if ( $this->fld_flags && $revision->isMinor() ) {
-                       $vals['minor'] = '';
-               }
-
-               if ( $this->fld_user || $this->fld_userid ) {
-                       if ( $revision->isDeleted( Revision::DELETED_USER ) ) {
-                               $vals['userhidden'] = '';
-                               $anyHidden = true;
-                       }
-                       if ( $revision->userCan( Revision::DELETED_USER, $user ) ) {
-                               if ( $this->fld_user ) {
-                                       $vals['user'] = $revision->getRawUserText();
-                               }
-                               $userid = $revision->getRawUser();
-                               if ( !$userid ) {
-                                       $vals['anon'] = '';
-                               }
-
-                               if ( $this->fld_userid ) {
-                                       $vals['userid'] = $userid;
-                               }
-                       }
-               }
-
-               if ( $this->fld_timestamp ) {
-                       $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $revision->getTimestamp() );
-               }
-
-               if ( $this->fld_size ) {
-                       if ( !is_null( $revision->getSize() ) ) {
-                               $vals['size'] = intval( $revision->getSize() );
-                       } else {
-                               $vals['size'] = 0;
-                       }
-               }
-
-               if ( $this->fld_sha1 ) {
-                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $vals['sha1hidden'] = '';
-                               $anyHidden = true;
-                       }
-                       if ( $revision->userCan( Revision::DELETED_TEXT, $user ) ) {
-                               if ( $revision->getSha1() != '' ) {
-                                       $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 );
-                               } else {
-                                       $vals['sha1'] = '';
-                               }
-                       }
-               }
-
-               if ( $this->fld_contentmodel ) {
-                       $vals['contentmodel'] = $revision->getContentModel();
-               }
-
-               if ( $this->fld_comment || $this->fld_parsedcomment ) {
-                       if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
-                               $vals['commenthidden'] = '';
-                               $anyHidden = true;
-                       }
-                       if ( $revision->userCan( Revision::DELETED_COMMENT, $user ) ) {
-                               $comment = $revision->getRawComment();
 
-                               if ( $this->fld_comment ) {
-                                       $vals['comment'] = $comment;
-                               }
-
-                               if ( $this->fld_parsedcomment ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
-                               }
-                       }
-               }
-
-               if ( $this->fld_tags ) {
-                       if ( $row->ts_tags ) {
-                               $tags = explode( ',', $row->ts_tags );
-                               $this->getResult()->setIndexedTagName( $tags, 'tag' );
-                               $vals['tags'] = $tags;
+                       if ( $resultPageSet !== null ) {
+                               $generated[] = $row->rev_id;
                        } else {
-                               $vals['tags'] = array();
-                       }
-               }
-
-               if ( !is_null( $this->token ) ) {
-                       $tokenFunctions = $this->getTokenFunctions();
-                       foreach ( $this->token as $t ) {
-                               $val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revision );
-                               if ( $val === false ) {
-                                       $this->setWarning( "Action '$t' is not allowed for the current user" );
-                               } else {
-                                       $vals[$t . 'token'] = $val;
-                               }
-                       }
-               }
-
-               $content = null;
-               global $wgParser;
-               if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
-                       $content = $revision->getContent( Revision::FOR_THIS_USER, $this->getUser() );
-                       // Expand templates after getting section content because
-                       // template-added sections don't count and Parser::preprocess()
-                       // will have less input
-                       if ( $content && $this->section !== false ) {
-                               $content = $content->getSection( $this->section, false );
-                               if ( !$content ) {
-                                       $this->dieUsage(
-                                               "There is no section {$this->section} in r" . $revision->getId(),
-                                               'nosuchsection'
-                                       );
-                               }
-                       }
-                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $vals['texthidden'] = '';
-                               $anyHidden = true;
-                       } elseif ( !$content ) {
-                               $vals['textmissing'] = '';
-                       }
-               }
-               if ( $this->fld_content && $content ) {
-                       $text = null;
-
-                       if ( $this->generateXML ) {
-                               if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
-                                       $t = $content->getNativeData(); # note: don't set $text
-
-                                       $wgParser->startExternalParse(
-                                               $title,
-                                               ParserOptions::newFromContext( $this->getContext() ),
-                                               Parser::OT_PREPROCESS
-                                       );
-                                       $dom = $wgParser->preprocessToDom( $t );
-                                       if ( is_callable( array( $dom, 'saveXML' ) ) ) {
-                                               $xml = $dom->saveXML();
-                                       } else {
-                                               $xml = $dom->__toString();
+                               $revision = new Revision( $row );
+                               $rev = $this->extractRevisionInfo( $revision, $row );
+
+                               if ( $this->token !== null ) {
+                                       $title = $revision->getTitle();
+                                       $tokenFunctions = $this->getTokenFunctions();
+                                       foreach ( $this->token as $t ) {
+                                               $val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revision );
+                                               if ( $val === false ) {
+                                                       $this->setWarning( "Action '$t' is not allowed for the current user" );
+                                               } else {
+                                                       $rev[$t . 'token'] = $val;
+                                               }
                                        }
-                                       $vals['parsetree'] = $xml;
-                               } else {
-                                       $this->setWarning( "Conversion to XML is supported for wikitext only, " .
-                                               $title->getPrefixedDBkey() .
-                                               " uses content model " . $content->getModel() );
                                }
-                       }
-
-                       if ( $this->expandTemplates && !$this->parseContent ) {
-                               #XXX: implement template expansion for all content types in ContentHandler?
-                               if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
-                                       $text = $content->getNativeData();
-
-                                       $text = $wgParser->preprocess(
-                                               $text,
-                                               $title,
-                                               ParserOptions::newFromContext( $this->getContext() )
-                                       );
-                               } else {
-                                       $this->setWarning( "Template expansion is supported for wikitext only, " .
-                                               $title->getPrefixedDBkey() .
-                                               " uses content model " . $content->getModel() );
-
-                                       $text = false;
-                               }
-                       }
-                       if ( $this->parseContent ) {
-                               $po = $content->getParserOutput(
-                                       $title,
-                                       $revision->getId(),
-                                       ParserOptions::newFromContext( $this->getContext() )
-                               );
-                               $text = $po->getText();
-                       }
-
-                       if ( $text === null ) {
-                               $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat();
-                               $model = $content->getModel();
-
-                               if ( !$content->isSupportedFormat( $format ) ) {
-                                       $name = $title->getPrefixedDBkey();
 
-                                       $this->dieUsage( "The requested format {$this->contentFormat} is not supported " .
-                                               "for content model $model used by $name", 'badformat' );
-                               }
-
-                               $text = $content->serialize( $format );
-
-                               // always include format and model.
-                               // Format is needed to deserialize, model is needed to interpret.
-                               $vals['contentformat'] = $format;
-                               $vals['contentmodel'] = $model;
-                       }
-
-                       if ( $text !== false ) {
-                               ApiResult::setContent( $vals, $text );
-                       }
-               }
-
-               if ( $content && ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) ) {
-                       static $n = 0; // Number of uncached diffs we've had
-
-                       if ( $n < $this->getConfig()->get( 'APIMaxUncachedDiffs' ) ) {
-                               $vals['diff'] = array();
-                               $context = new DerivativeContext( $this->getContext() );
-                               $context->setTitle( $title );
-                               $handler = $revision->getContentHandler();
-
-                               if ( !is_null( $this->difftotext ) ) {
-                                       $model = $title->getContentModel();
-
-                                       if ( $this->contentFormat
-                                               && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat )
-                                       ) {
-
-                                               $name = $title->getPrefixedDBkey();
-
-                                               $this->dieUsage( "The requested format {$this->contentFormat} is not supported for " .
-                                                       "content model $model used by $name", 'badformat' );
+                               $fit = $this->addPageSubItem( $row->rev_page, $rev, 'rev' );
+                               if ( !$fit ) {
+                                       if ( $enumRevMode ) {
+                                               $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
+                                       } elseif ( $revCount > 0 ) {
+                                               $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
+                                       } else {
+                                               $this->setContinueEnumParameter( 'continue', intval( $row->rev_page ) .
+                                                       '|' . intval( $row->rev_id ) );
                                        }
-
-                                       $difftocontent = ContentHandler::makeContent(
-                                               $this->difftotext,
-                                               $title,
-                                               $model,
-                                               $this->contentFormat
-                                       );
-
-                                       $engine = $handler->createDifferenceEngine( $context );
-                                       $engine->setContent( $content, $difftocontent );
-                               } else {
-                                       $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
-                                       $vals['diff']['from'] = $engine->getOldid();
-                                       $vals['diff']['to'] = $engine->getNewid();
+                                       break;
                                }
-                               $difftext = $engine->getDiffBody();
-                               ApiResult::setContent( $vals['diff'], $difftext );
-                               if ( !$engine->wasCacheHit() ) {
-                                       $n++;
-                               }
-                       } else {
-                               $vals['diff']['notcached'] = '';
                        }
                }
 
-               if ( $anyHidden && $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) {
-                       $vals['suppressed'] = '';
+               if ( $resultPageSet !== null ) {
+                       $resultPageSet->populateFromRevisionIDs( $generated );
                }
-
-               return $vals;
        }
 
        public function getCacheMode( $params ) {
                if ( isset( $params['token'] ) ) {
                        return 'private';
                }
-               if ( $this->userCanSeeRevDel() ) {
-                       return 'private';
-               }
-               if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
-                       // formatComment() calls wfMessage() among other things
-                       return 'anon-public-user-private';
-               }
-
-               return 'public';
+               return parent::getCacheMode( $params );
        }
 
        public function getAllowedParams() {
-               return array(
-                       'prop' => array(
-                               ApiBase::PARAM_ISMULTI => true,
-                               ApiBase::PARAM_DFLT => 'ids|timestamp|flags|comment|user',
-                               ApiBase::PARAM_TYPE => array(
-                                       'ids',
-                                       'flags',
-                                       'timestamp',
-                                       'user',
-                                       'userid',
-                                       'size',
-                                       'sha1',
-                                       'contentmodel',
-                                       'comment',
-                                       'parsedcomment',
-                                       'content',
-                                       'tags'
-                               )
-                       ),
-                       'limit' => array(
-                               ApiBase::PARAM_TYPE => 'limit',
-                               ApiBase::PARAM_MIN => 1,
-                               ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
-                               ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
-                               ApiBase::PARAM_HELP_MSG_INFO => array( array( 'singlepageonly' ) ),
-                       ),
+               $ret = parent::getAllowedParams() + array(
                        'startid' => array(
                                ApiBase::PARAM_TYPE => 'integer',
                                ApiBase::PARAM_HELP_MSG_INFO => array( array( 'singlepageonly' ) ),
@@ -742,7 +391,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                                        'newer',
                                        'older'
                                ),
-                               ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+                               ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
                                ApiBase::PARAM_HELP_MSG_INFO => array( array( 'singlepageonly' ) ),
                        ),
                        'user' => array(
@@ -754,10 +403,6 @@ class ApiQueryRevisions extends ApiQueryBase {
                                ApiBase::PARAM_HELP_MSG_INFO => array( array( 'singlepageonly' ) ),
                        ),
                        'tag' => null,
-                       'expandtemplates' => false,
-                       'generatexml' => false,
-                       'parse' => false,
-                       'section' => null,
                        'token' => array(
                                ApiBase::PARAM_DEPRECATED => true,
                                ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
@@ -766,13 +411,11 @@ class ApiQueryRevisions extends ApiQueryBase {
                        'continue' => array(
                                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
                        ),
-                       'diffto' => null,
-                       'difftotext' => null,
-                       'contentformat' => array(
-                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
-                               ApiBase::PARAM_DFLT => null
-                       ),
                );
+
+               $ret['limit'][ApiBase::PARAM_HELP_MSG_INFO] = array( array( 'singlepageonly' ) );
+
+               return $ret;
        }
 
        protected function getExamplesMessages() {
diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php
new file mode 100644 (file)
index 0000000..3879d7b
--- /dev/null
@@ -0,0 +1,474 @@
+<?php
+/**
+ *
+ *
+ * Created on Oct 3, 2014 as a split from ApiQueryRevisions
+ *
+ * Copyright Â© 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+/**
+ * A base class for functions common to producing a list of revisions.
+ *
+ * @ingroup API
+ */
+abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
+
+       protected $limit, $diffto, $difftotext, $expandTemplates, $generateXML, $section,
+               $parseContent, $contentFormat, $setParsedLimit = true;
+
+       protected $fld_ids = false, $fld_flags = false, $fld_timestamp = false,
+               $fld_size = false, $fld_sha1 = false, $fld_comment = false,
+               $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
+               $fld_content = false, $fld_tags = false, $fld_contentmodel = false;
+
+       public function execute() {
+               $this->run();
+       }
+
+       public function executeGenerator( $resultPageSet ) {
+               $this->run( $resultPageSet );
+       }
+
+       /**
+        * @param ApiPageSet $resultPageSet
+        * @return void
+        */
+       abstract protected function run( ApiPageSet $resultPageSet = null );
+
+       /**
+        * Parse the parameters into the various instance fields.
+        *
+        * @param array $params
+        */
+       protected function parseParameters( $params ) {
+               if ( !is_null( $params['difftotext'] ) ) {
+                       $this->difftotext = $params['difftotext'];
+               } elseif ( !is_null( $params['diffto'] ) ) {
+                       if ( $params['diffto'] == 'cur' ) {
+                               $params['diffto'] = 0;
+                       }
+                       if ( ( !ctype_digit( $params['diffto'] ) || $params['diffto'] < 0 )
+                               && $params['diffto'] != 'prev' && $params['diffto'] != 'next'
+                       ) {
+                               $p = $this->getModulePrefix();
+                               $this->dieUsage(
+                                       "{$p}diffto must be set to a non-negative number, \"prev\", \"next\" or \"cur\"",
+                                       'diffto'
+                               );
+                       }
+                       // Check whether the revision exists and is readable,
+                       // DifferenceEngine returns a rather ambiguous empty
+                       // string if that's not the case
+                       if ( $params['diffto'] != 0 ) {
+                               $difftoRev = Revision::newFromID( $params['diffto'] );
+                               if ( !$difftoRev ) {
+                                       $this->dieUsageMsg( array( 'nosuchrevid', $params['diffto'] ) );
+                               }
+                               if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+                                       $this->setWarning( "Couldn't diff to r{$difftoRev->getID()}: content is hidden" );
+                                       $params['diffto'] = null;
+                               }
+                       }
+                       $this->diffto = $params['diffto'];
+               }
+
+               $prop = array_flip( $params['prop'] );
+
+               $this->fld_ids = isset( $prop['ids'] );
+               $this->fld_flags = isset( $prop['flags'] );
+               $this->fld_timestamp = isset( $prop['timestamp'] );
+               $this->fld_comment = isset( $prop['comment'] );
+               $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
+               $this->fld_size = isset( $prop['size'] );
+               $this->fld_sha1 = isset( $prop['sha1'] );
+               $this->fld_content = isset( $prop['content'] );
+               $this->fld_contentmodel = isset( $prop['contentmodel'] );
+               $this->fld_userid = isset( $prop['userid'] );
+               $this->fld_user = isset( $prop['user'] );
+               $this->fld_tags = isset( $prop['tags'] );
+
+               if ( !empty( $params['contentformat'] ) ) {
+                       $this->contentFormat = $params['contentformat'];
+               }
+
+               $this->limit = $params['limit'];
+
+               $smallLimit = false;
+               if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+                       $smallLimit = true;
+                       $this->expandTemplates = $params['expandtemplates'];
+                       $this->generateXML = $params['generatexml'];
+                       $this->parseContent = $params['parse'];
+                       if ( $this->parseContent ) {
+                               // Must manually initialize unset limit
+                               if ( is_null( $this->limit ) ) {
+                                       $this->limit = 1;
+                               }
+                       }
+                       if ( isset( $params['section'] ) ) {
+                               $this->section = $params['section'];
+                       } else {
+                               $this->section = false;
+                       }
+               }
+
+               $userMax = $this->parseContent ? 1 : ( $smallLimit ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
+               $botMax = $this->parseContent ? 1 : ( $smallLimit ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
+               if ( $this->limit == 'max' ) {
+                       $this->limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
+                       if ( $this->setParsedLimit ) {
+                               $this->getResult()->setParsedLimit( $this->getModuleName(), $this->limit );
+                       }
+               }
+
+               if ( is_null( $this->limit ) ) {
+                       $this->limit = 10;
+               }
+               $this->validateLimit( 'limit', $this->limit, 1, $userMax, $botMax );
+       }
+
+       /**
+        * Extract information from the Revision
+        *
+        * @param Revision $revision
+        * @param object $row Should have a field 'ts_tags' if $this->fld_tags is set
+        * @return array
+        */
+       protected function extractRevisionInfo( Revision $revision, $row ) {
+               $title = $revision->getTitle();
+               $user = $this->getUser();
+               $vals = array();
+               $anyHidden = false;
+
+               if ( $this->fld_ids ) {
+                       $vals['revid'] = intval( $revision->getId() );
+                       if ( !is_null( $revision->getParentId() ) ) {
+                               $vals['parentid'] = intval( $revision->getParentId() );
+                       }
+               }
+
+               if ( $this->fld_flags && $revision->isMinor() ) {
+                       $vals['minor'] = '';
+               }
+
+               if ( $this->fld_user || $this->fld_userid ) {
+                       if ( $revision->isDeleted( Revision::DELETED_USER ) ) {
+                               $vals['userhidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( $revision->userCan( Revision::DELETED_USER, $user ) ) {
+                               if ( $this->fld_user ) {
+                                       $vals['user'] = $revision->getRawUserText();
+                               }
+                               $userid = $revision->getRawUser();
+                               if ( !$userid ) {
+                                       $vals['anon'] = '';
+                               }
+
+                               if ( $this->fld_userid ) {
+                                       $vals['userid'] = $userid;
+                               }
+                       }
+               }
+
+               if ( $this->fld_timestamp ) {
+                       $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $revision->getTimestamp() );
+               }
+
+               if ( $this->fld_size ) {
+                       if ( !is_null( $revision->getSize() ) ) {
+                               $vals['size'] = intval( $revision->getSize() );
+                       } else {
+                               $vals['size'] = 0;
+                       }
+               }
+
+               if ( $this->fld_sha1 ) {
+                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $vals['sha1hidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( $revision->userCan( Revision::DELETED_TEXT, $user ) ) {
+                               if ( $revision->getSha1() != '' ) {
+                                       $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 );
+                               } else {
+                                       $vals['sha1'] = '';
+                               }
+                       }
+               }
+
+               if ( $this->fld_contentmodel ) {
+                       $vals['contentmodel'] = $revision->getContentModel();
+               }
+
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                       if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
+                               $vals['commenthidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( $revision->userCan( Revision::DELETED_COMMENT, $user ) ) {
+                               $comment = $revision->getRawComment();
+
+                               if ( $this->fld_comment ) {
+                                       $vals['comment'] = $comment;
+                               }
+
+                               if ( $this->fld_parsedcomment ) {
+                                       $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
+                               }
+                       }
+               }
+
+               if ( $this->fld_tags ) {
+                       if ( $row->ts_tags ) {
+                               $tags = explode( ',', $row->ts_tags );
+                               $this->getResult()->setIndexedTagName( $tags, 'tag' );
+                               $vals['tags'] = $tags;
+                       } else {
+                               $vals['tags'] = array();
+                       }
+               }
+
+               $content = null;
+               global $wgParser;
+               if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+                       $content = $revision->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+                       // Expand templates after getting section content because
+                       // template-added sections don't count and Parser::preprocess()
+                       // will have less input
+                       if ( $content && $this->section !== false ) {
+                               $content = $content->getSection( $this->section, false );
+                               if ( !$content ) {
+                                       $this->dieUsage(
+                                               "There is no section {$this->section} in r" . $revision->getId(),
+                                               'nosuchsection'
+                                       );
+                               }
+                       }
+                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $vals['texthidden'] = '';
+                               $anyHidden = true;
+                       } elseif ( !$content ) {
+                               $vals['textmissing'] = '';
+                       }
+               }
+               if ( $this->fld_content && $content ) {
+                       $text = null;
+
+                       if ( $this->generateXML ) {
+                               if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+                                       $t = $content->getNativeData(); # note: don't set $text
+
+                                       $wgParser->startExternalParse(
+                                               $title,
+                                               ParserOptions::newFromContext( $this->getContext() ),
+                                               Parser::OT_PREPROCESS
+                                       );
+                                       $dom = $wgParser->preprocessToDom( $t );
+                                       if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+                                               $xml = $dom->saveXML();
+                                       } else {
+                                               $xml = $dom->__toString();
+                                       }
+                                       $vals['parsetree'] = $xml;
+                               } else {
+                                       $vals['badcontentformatforparsetree'] = '';
+                                       $this->setWarning( "Conversion to XML is supported for wikitext only, " .
+                                               $title->getPrefixedDBkey() .
+                                               " uses content model " . $content->getModel() );
+                               }
+                       }
+
+                       if ( $this->expandTemplates && !$this->parseContent ) {
+                               #XXX: implement template expansion for all content types in ContentHandler?
+                               if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+                                       $text = $content->getNativeData();
+
+                                       $text = $wgParser->preprocess(
+                                               $text,
+                                               $title,
+                                               ParserOptions::newFromContext( $this->getContext() )
+                                       );
+                               } else {
+                                       $this->setWarning( "Template expansion is supported for wikitext only, " .
+                                               $title->getPrefixedDBkey() .
+                                               " uses content model " . $content->getModel() );
+                                       $vals['badcontentformat'] = '';
+                                       $text = false;
+                               }
+                       }
+                       if ( $this->parseContent ) {
+                               $po = $content->getParserOutput(
+                                       $title,
+                                       $revision->getId(),
+                                       ParserOptions::newFromContext( $this->getContext() )
+                               );
+                               $text = $po->getText();
+                       }
+
+                       if ( $text === null ) {
+                               $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat();
+                               $model = $content->getModel();
+
+                               if ( !$content->isSupportedFormat( $format ) ) {
+                                       $name = $title->getPrefixedDBkey();
+                                       $this->setWarning( "The requested format {$this->contentFormat} is not " .
+                                               "supported for content model $model used by $name" );
+                                       $vals['badcontentformat'] = '';
+                                       $text = false;
+                               } else {
+                                       $text = $content->serialize( $format );
+                                       // always include format and model.
+                                       // Format is needed to deserialize, model is needed to interpret.
+                                       $vals['contentformat'] = $format;
+                                       $vals['contentmodel'] = $model;
+                               }
+                       }
+
+                       if ( $text !== false ) {
+                               ApiResult::setContent( $vals, $text );
+                       }
+               }
+
+               if ( $content && ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) ) {
+                       static $n = 0; // Number of uncached diffs we've had
+
+                       if ( $n < $this->getConfig()->get( 'APIMaxUncachedDiffs' ) ) {
+                               $vals['diff'] = array();
+                               $context = new DerivativeContext( $this->getContext() );
+                               $context->setTitle( $title );
+                               $handler = $revision->getContentHandler();
+
+                               if ( !is_null( $this->difftotext ) ) {
+                                       $model = $title->getContentModel();
+
+                                       if ( $this->contentFormat
+                                               && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat )
+                                       ) {
+                                               $name = $title->getPrefixedDBkey();
+                                               $this->setWarning( "The requested format {$this->contentFormat} is not " .
+                                                       "supported for content model $model used by $name" );
+                                               $vals['diff']['badcontentformat'] = '';
+                                               $engine = null;
+                                       } else {
+                                               $difftocontent = ContentHandler::makeContent(
+                                                       $this->difftotext,
+                                                       $title,
+                                                       $model,
+                                                       $this->contentFormat
+                                               );
+
+                                               $engine = $handler->createDifferenceEngine( $context );
+                                               $engine->setContent( $content, $difftocontent );
+                                       }
+                               } else {
+                                       $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
+                                       $vals['diff']['from'] = $engine->getOldid();
+                                       $vals['diff']['to'] = $engine->getNewid();
+                               }
+                               if ( $engine ) {
+                                       $difftext = $engine->getDiffBody();
+                                       ApiResult::setContent( $vals['diff'], $difftext );
+                                       if ( !$engine->wasCacheHit() ) {
+                                               $n++;
+                                       }
+                               }
+                       } else {
+                               $vals['diff']['notcached'] = '';
+                       }
+               }
+
+               if ( $anyHidden && $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+                       $vals['suppressed'] = '';
+               }
+
+               return $vals;
+       }
+
+       public function getCacheMode( $params ) {
+               if ( $this->userCanSeeRevDel() ) {
+                       return 'private';
+               }
+
+               return 'public';
+       }
+
+       public function getAllowedParams() {
+               return array(
+                       'prop' => array(
+                               ApiBase::PARAM_ISMULTI => true,
+                               ApiBase::PARAM_DFLT => 'ids|timestamp|flags|comment|user',
+                               ApiBase::PARAM_TYPE => array(
+                                       'ids',
+                                       'flags',
+                                       'timestamp',
+                                       'user',
+                                       'userid',
+                                       'size',
+                                       'sha1',
+                                       'contentmodel',
+                                       'comment',
+                                       'parsedcomment',
+                                       'content',
+                                       'tags'
+                               ),
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop',
+                       ),
+                       'limit' => array(
+                               ApiBase::PARAM_TYPE => 'limit',
+                               ApiBase::PARAM_MIN => 1,
+                               ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+                               ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-limit',
+                       ),
+                       'expandtemplates' => array(
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-expandtemplates',
+                       ),
+                       'generatexml' => array(
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-generatexml',
+                       ),
+                       'parse' => array(
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-parse',
+                       ),
+                       'section' => array(
+                               ApiBase::PARAM_DFLT => null,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-section',
+                       ),
+                       'diffto' => array(
+                               ApiBase::PARAM_DFLT => null,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-diffto',
+                       ),
+                       'difftotext' => array(
+                               ApiBase::PARAM_DFLT => null,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotext',
+                       ),
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                               ApiBase::PARAM_DFLT => null,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat',
+                       ),
+               );
+       }
+
+}
index 2bec759..485c776 100644 (file)
        "apihelp-query+allcategories-example-size": "List categories with information on the number of pages in each",
        "apihelp-query+allcategories-example-generator": "Retrieve info about the category page itself for categories beginning \"List\"",
 
+       "apihelp-query+alldeletedrevisions-description": "List all deleted revisions by a user or in a namespace.",
+       "apihelp-query+alldeletedrevisions-paraminfo-useronly": "May only be used with $3user.",
+       "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "Cannot be used with $3user.",
+       "apihelp-query+alldeletedrevisions-param-start": "The timestamp to start enumerating from.",
+       "apihelp-query+alldeletedrevisions-param-end": "The timestamp to stop enumerating at.",
+       "apihelp-query+alldeletedrevisions-param-from": "Start listing at this title.",
+       "apihelp-query+alldeletedrevisions-param-to": "Stop listing at this title.",
+       "apihelp-query+alldeletedrevisions-param-prefix": "Search for all page titles that begin with this value.",
+       "apihelp-query+alldeletedrevisions-param-tag": "Only list revisions tagged with this tag.",
+       "apihelp-query+alldeletedrevisions-param-user": "Only list revisions by this user.",
+       "apihelp-query+alldeletedrevisions-param-excludeuser": "Don't list revisions by this user.",
+       "apihelp-query+alldeletedrevisions-param-namespace": "Only list pages in this namespace.",
+       "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "'''NOTE:''' Due to [https://www.mediawiki.org/wiki/Manual:$wgMiserMode miser mode], using $1user and $1namespace together may result in fewer than \"$1limit\" results returned before continuing; in extreme cases, zero results may be returned.",
+       "apihelp-query+alldeletedrevisions-param-generatetitles": "When being used as a generator, generate titles rather than revision IDs.",
+       "apihelp-query+alldeletedrevisions-example-user": "List the last 50 deleted contributions by User:Example",
+       "apihelp-query+alldeletedrevisions-example-ns-main": "List the first 50 deleted revisions in the main namespace",
+
        "apihelp-query+allfileusages-description": "List all file usages, including non-existing.",
        "apihelp-query+allfileusages-param-from": "The title of the file to start enumerating from.",
        "apihelp-query+allfileusages-param-to": "The title of the file to stop enumerating at.",
        "apihelp-query+contributors-param-limit": "How many contributors to return.",
        "apihelp-query+contributors-example-simple": "Show contributors to the [[Main Page]]",
 
+       "apihelp-query+deletedrevisions-description": "Get deleted revision information.\n\nMay be used in several ways:\n# Get deleted revisions for a set of pages, by setting titles or pageids. Ordered by title and timestamp.\n# Get data about a set of deleted revisions by setting their IDs with revids. Ordered by revision ID.",
+       "apihelp-query+deletedrevisions-param-start": "The timestamp to start enumerating from. Ignored when processing a list of revision IDs.",
+       "apihelp-query+deletedrevisions-param-end": "The timestamp to stop enumerating at. Ignored when processing a list of revision IDs.",
+       "apihelp-query+deletedrevisions-param-tag": "Only list revisions tagged with this tag.",
+       "apihelp-query+deletedrevisions-param-user": "Only list revisions by this user.",
+       "apihelp-query+deletedrevisions-param-excludeuser": "Don't list revisions by this user.",
+       "apihelp-query+deletedrevisions-param-limit": "The maximum amount of revisions to list.",
+       "apihelp-query+deletedrevisions-param-prop": "Which properties to get:\n;revid:Adds the revision ID of the deleted revision.\n;parentid:Adds the revision ID of the previous revision to the page.\n;user:Adds the user who made the revision.\n;userid:Adds the user ID who made the revision.\n;comment:Adds the comment of the revision.\n;parsedcomment:Adds the parsed comment of the revision.\n;minor:Tags if the revision is minor.\n;len:Adds the length (bytes) of the revision.\n;sha1:Adds the SHA-1 (base 16) of the revision.\n;content:Adds the content of the revision.\n;tags:Tags for the revision.",
+       "apihelp-query+deletedrevisions-example-titles": "List the deleted revisions of [[Main Page]] and [[Talk:Main Page]], with content",
+       "apihelp-query+deletedrevisions-example-revids": "List the information for deleted revision 123456",
+
        "apihelp-query+deletedrevs-description": "List deleted revisions.\n\nOperates in three modes:\n# List deleted revisions for the given titles, sorted by timestamp.\n# List deleted contributions for the given user, sorted by timestamp (no titles specified).\n# List all deleted revisions in the given namespace, sorted by title and timestamp (no titles specified, $1user not set).\n\nCertain parameters only apply to some modes and are ignored in others.",
        "apihelp-query+deletedrevs-paraminfo-modes": "{{PLURAL:$1|Mode|Modes}}: $2",
        "apihelp-query+deletedrevs-param-start": "The timestamp to start enumerating from.",
 
        "apihelp-query+revisions-description": "Get revision information.\n\nMay be used in several ways:\n# Get data about a set of pages (last revision), by setting titles or pageids.\n# Get revisions for one given page, by using titles or pageids with start, end, or limit.\n# Get data about a set of revisions by setting their IDs with revids.",
        "apihelp-query+revisions-paraminfo-singlepageonly": "May only be used with a single page (mode #2).",
-       "apihelp-query+revisions-param-prop": "Which properties to get for each revision:\n;ids:The ID of the revision.\n;flags:Revision flags (minor).\n;timestamp:The timestamp of the revision.\n;user:User that made the revision.\n;userid:User ID of revision creator.\n;size:Length (bytes) of the revision.\n;sha1:SHA-1 (base 16) of the revision.\n;contentmodel:Content model ID.\n;comment:Comment by the user for revision.\n;parsedcomment:Parsed comment by the user for the revision.\n;content:Text of the revision.\n;tags:Tags for the revision.",
-       "apihelp-query+revisions-param-limit": "Limit how many revisions will be returned.",
        "apihelp-query+revisions-param-startid": "From which revision ID to start enumeration.",
        "apihelp-query+revisions-param-endid": "Stop revision enumeration on this revision ID.",
        "apihelp-query+revisions-param-start": "From which revision timestamp to start enumeration.",
        "apihelp-query+revisions-param-user": "Only include revisions made by user.",
        "apihelp-query+revisions-param-excludeuser": "Exclude revisions made by user.",
        "apihelp-query+revisions-param-tag": "Only list revisions tagged with this tag.",
-       "apihelp-query+revisions-param-expandtemplates": "Expand templates in revision content (requires $1prop=content).",
-       "apihelp-query+revisions-param-generatexml": "Generate XML parse tree for revision content (requires $1prop=content).",
-       "apihelp-query+revisions-param-parse": "Parse revision content (requires $1prop=content). For performance reasons, if this option is used, $1limit is enforced to 1.",
-       "apihelp-query+revisions-param-section": "Only retrieve the content of this section number.",
        "apihelp-query+revisions-param-token": "Which tokens to obtain for each revision.",
-       "apihelp-query+revisions-param-diffto": "Revision ID to diff each revision to. Use \"prev\", \"next\" and \"cur\" for the previous, next and current revision respectively.",
-       "apihelp-query+revisions-param-difftotext": "Text to diff each revision to. Only diffs a limited number of revisions. Overrides $1diffto. If $1section is set, only that section will be diffed against this text.",
-       "apihelp-query+revisions-param-contentformat": "Serialization format used for $1difftotext and expected for output of content.",
        "apihelp-query+revisions-example-content": "Get data with content for the last revision of titles \"API\" and \"Main Page\"",
        "apihelp-query+revisions-example-last5": "Get last 5 revisions of the \"Main Page\"",
        "apihelp-query+revisions-example-first5": "Get first 5 revisions of the \"Main Page\"",
        "apihelp-query+revisions-example-first5-not-localhost": "Get first 5 revisions of the \"Main Page\" that were not made made by anonymous user \"127.0.0.1\"",
        "apihelp-query+revisions-example-first5-user": "Get first 5 revisions of the \"Main Page\" that were made by the user \"MediaWiki default\"",
 
+       "apihelp-query+revisions+base-param-prop": "Which properties to get for each revision:\n;ids:The ID of the revision.\n;flags:Revision flags (minor).\n;timestamp:The timestamp of the revision.\n;user:User that made the revision.\n;userid:User ID of the revision creator.\n;size:Length (bytes) of the revision.\n;sha1:SHA-1 (base 16) of the revision.\n;contentmodel:Content model ID of the revision.\n;comment:Comment by the user for the revision.\n;parsedcomment:Parsed comment by the user for the revision.\n;content:Text of the revision.\n;tags:Tags for the revision.",
+       "apihelp-query+revisions+base-param-limit": "Limit how many revisions will be returned.",
+       "apihelp-query+revisions+base-param-expandtemplates": "Expand templates in revision content (requires $1prop=content).",
+       "apihelp-query+revisions+base-param-generatexml": "Generate XML parse tree for revision content (requires $1prop=content).",
+       "apihelp-query+revisions+base-param-parse": "Parse revision content (requires $1prop=content). For performance reasons, if this option is used, $1limit is enforced to 1.",
+       "apihelp-query+revisions+base-param-section": "Only retrieve the content of this section number.",
+       "apihelp-query+revisions+base-param-diffto": "Revision ID to diff each revision to. Use \"prev\", \"next\" and \"cur\" for the previous, next and current revision respectively.",
+       "apihelp-query+revisions+base-param-difftotext": "Text to diff each revision to. Only diffs a limited number of revisions. Overrides $1diffto. If $1section is set, only that section will be diffed against this text",
+       "apihelp-query+revisions+base-param-contentformat": "Serialization format used for $1difftotext and expected for output of content.",
+
        "apihelp-query+search-description": "Perform a full text search.",
        "apihelp-query+search-param-search": "Search for all page titles (or content) that have this value.",
        "apihelp-query+search-param-namespace": "Search only within these namespaces.",
index 25ba0b2..ce058e9 100644 (file)
        "apihelp-query+allcategories-param-prop": "{{doc-apihelp-param|query+allcategories|prop}}",
        "apihelp-query+allcategories-example-size": "{{doc-apihelp-example|query+allcategories}}",
        "apihelp-query+allcategories-example-generator": "{{doc-apihelp-example|query+allcategories}}",
+
+       "apihelp-query+alldeletedrevisions-description": "{{doc-apihelp-description|query+alldeletedrevisions}}",
+       "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "{{doc-apihelp-paraminfo|query+alldeletedrevisions|nonuseronly}}",
+       "apihelp-query+alldeletedrevisions-paraminfo-useronly": "{{doc-apihelp-paraminfo|query+alldeletedrevisions|useronly}}",
+       "apihelp-query+alldeletedrevisions-param-end": "{{doc-apihelp-param|query+alldeletedrevisions|end}}",
+       "apihelp-query+alldeletedrevisions-param-excludeuser": "{{doc-apihelp-param|query+alldeletedrevisions|excludeuser}}",
+       "apihelp-query+alldeletedrevisions-param-from": "{{doc-apihelp-param|query+alldeletedrevisions|from}}",
+       "apihelp-query+alldeletedrevisions-param-generatetitles": "{{doc-apihelp-param|query+alldeletedrevisions|generatetitles}}",
+       "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "{{doc-apihelp-param|query+alldeletedrevisions|miser-user-namespace}}",
+       "apihelp-query+alldeletedrevisions-param-namespace": "{{doc-apihelp-param|query+alldeletedrevisions|namespace}}",
+       "apihelp-query+alldeletedrevisions-param-prefix": "{{doc-apihelp-param|query+alldeletedrevisions|prefix}}",
+       "apihelp-query+alldeletedrevisions-param-start": "{{doc-apihelp-param|query+alldeletedrevisions|start}}",
+       "apihelp-query+alldeletedrevisions-param-tag": "{{doc-apihelp-param|query+alldeletedrevisions|tag}}",
+       "apihelp-query+alldeletedrevisions-param-to": "{{doc-apihelp-param|query+alldeletedrevisions|to}}",
+       "apihelp-query+alldeletedrevisions-param-user": "{{doc-apihelp-param|query+alldeletedrevisions|user}}",
+       "apihelp-query+alldeletedrevisions-example-ns-main": "{{doc-apihelp-example|query+alldeletedrevisions}}",
+       "apihelp-query+alldeletedrevisions-example-user": "{{doc-apihelp-example|query+alldeletedrevisions}}",
+
        "apihelp-query+allfileusages-description": "{{doc-apihelp-description|query+allfileusages}}",
        "apihelp-query+allfileusages-param-from": "{{doc-apihelp-param|query+allfileusages|from}}",
        "apihelp-query+allfileusages-param-to": "{{doc-apihelp-param|query+allfileusages|to}}",
        "apihelp-query+contributors-param-excluderights": "{{doc-apihelp-param|query+contributors|excluderights}}",
        "apihelp-query+contributors-param-limit": "{{doc-apihelp-param|query+contributors|limit}}",
        "apihelp-query+contributors-example-simple": "{{doc-apihelp-example|query+contributors}}",
+
+       "apihelp-query+deletedrevisions-description": "{{doc-apihelp-description|query+deletedrevisions}}",
+       "apihelp-query+deletedrevisions-param-end": "{{doc-apihelp-param|query+deletedrevisions|end}}",
+       "apihelp-query+deletedrevisions-param-excludeuser": "{{doc-apihelp-param|query+deletedrevisions|excludeuser}}",
+       "apihelp-query+deletedrevisions-param-limit": "{{doc-apihelp-param|query+deletedrevisions|limit}}",
+       "apihelp-query+deletedrevisions-param-prop": "{{doc-apihelp-param|query+deletedrevisions|prop}}",
+       "apihelp-query+deletedrevisions-param-start": "{{doc-apihelp-param|query+deletedrevisions|start}}",
+       "apihelp-query+deletedrevisions-param-tag": "{{doc-apihelp-param|query+deletedrevisions|tag}}",
+       "apihelp-query+deletedrevisions-param-user": "{{doc-apihelp-param|query+deletedrevisions|user}}",
+       "apihelp-query+deletedrevisions-example-revids": "{{doc-apihelp-example|query+deletedrevisions}}",
+       "apihelp-query+deletedrevisions-example-titles": "{{doc-apihelp-example|query+deletedrevisions}}",
+
        "apihelp-query+deletedrevs-description": "{{doc-apihelp-description|query+deletedrevs}}",
        "apihelp-query+deletedrevs-paraminfo-modes": "{{doc-apihelp-paraminfo|query+deletedrevs|modes}}\n{{Identical|Mode}}",
        "apihelp-query+deletedrevs-param-start": "{{doc-apihelp-param|query+deletedrevs|start}}",
        "apihelp-query+redirects-example-generator": "{{doc-apihelp-example|query+redirects}}",
        "apihelp-query+revisions-description": "{{doc-apihelp-description|query+revisions}}",
        "apihelp-query+revisions-paraminfo-singlepageonly": "{{doc-apihelp-paraminfo|query+revisions|singlepageonly}}",
-       "apihelp-query+revisions-param-prop": "{{doc-apihelp-param|query+revisions|prop}}",
-       "apihelp-query+revisions-param-limit": "{{doc-apihelp-param|query+revisions|limit}}",
        "apihelp-query+revisions-param-startid": "{{doc-apihelp-param|query+revisions|startid}}",
        "apihelp-query+revisions-param-endid": "{{doc-apihelp-param|query+revisions|endid}}",
        "apihelp-query+revisions-param-start": "{{doc-apihelp-param|query+revisions|start}}",
        "apihelp-query+revisions-param-user": "{{doc-apihelp-param|query+revisions|user}}",
        "apihelp-query+revisions-param-excludeuser": "{{doc-apihelp-param|query+revisions|excludeuser}}",
        "apihelp-query+revisions-param-tag": "{{doc-apihelp-param|query+revisions|tag}}",
-       "apihelp-query+revisions-param-expandtemplates": "{{doc-apihelp-param|query+revisions|expandtemplates}}",
-       "apihelp-query+revisions-param-generatexml": "{{doc-apihelp-param|query+revisions|generatexml}}",
-       "apihelp-query+revisions-param-parse": "{{doc-apihelp-param|query+revisions|parse}}",
-       "apihelp-query+revisions-param-section": "{{doc-apihelp-param|query+revisions|section}}",
        "apihelp-query+revisions-param-token": "{{doc-apihelp-param|query+revisions|token}}",
-       "apihelp-query+revisions-param-diffto": "{{doc-apihelp-param|query+revisions|diffto}}",
-       "apihelp-query+revisions-param-difftotext": "{{doc-apihelp-param|query+revisions|difftotext}}",
-       "apihelp-query+revisions-param-contentformat": "{{doc-apihelp-param|query+revisions|contentformat}}",
        "apihelp-query+revisions-example-content": "{{doc-apihelp-example|query+revisions}}",
        "apihelp-query+revisions-example-last5": "{{doc-apihelp-example|query+revisions}}",
        "apihelp-query+revisions-example-first5": "{{doc-apihelp-example|query+revisions}}",
        "apihelp-query+revisions-example-first5-after": "{{doc-apihelp-example|query+revisions}}",
        "apihelp-query+revisions-example-first5-not-localhost": "{{doc-apihelp-example|query+revisions}}",
        "apihelp-query+revisions-example-first5-user": "{{doc-apihelp-example|query+revisions}}",
+
+       "apihelp-query+revisions+base-param-contentformat": "{{doc-apihelp-param|query+revisions+base|contentformat|description=the \"contentformat\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-diffto": "{{doc-apihelp-param|query+revisions+base|diffto|description=the \"diffto\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-difftotext": "{{doc-apihelp-param|query+revisions+base|difftotext|description=the \"difftotext\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-expandtemplates": "{{doc-apihelp-param|query+revisions+base|expandtemplates|description=the \"expandtemplates\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-generatexml": "{{doc-apihelp-param|query+revisions+base|generatexml|description=the \"generatexml\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-limit": "{{doc-apihelp-param|query+revisions+base|limit|description=the \"limit\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-parse": "{{doc-apihelp-param|query+revisions+base|parse|description=the \"parse\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-prop": "{{doc-apihelp-param|query+revisions+base|prop|description=the \"prop\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-section": "{{doc-apihelp-param|query+revisions+base|section|description=the \"section\" parameter to revision querying modules|noseealso=1}}",
+
        "apihelp-query+search-description": "{{doc-apihelp-description|query+search}}",
        "apihelp-query+search-param-search": "{{doc-apihelp-param|query+search|search}}",
        "apihelp-query+search-param-namespace": "{{doc-apihelp-param|query+search|namespace}}",