From 991de897e4781597f60a9c19d84df740a2b68ed1 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Mon, 29 Sep 2014 13:47:08 -0400 Subject: [PATCH] API: Split list=deletedrevs into prop=deletedrevisions and list=alldeletedrevisions 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 --- RELEASE-NOTES-1.25 | 17 + includes/AutoLoader.php | 3 + includes/api/ApiBase.php | 2 +- includes/api/ApiHelp.php | 1 + includes/api/ApiPageSet.php | 66 ++- includes/api/ApiParamInfo.php | 1 + includes/api/ApiQuery.php | 2 + includes/api/ApiQueryAllDeletedRevisions.php | 384 ++++++++++++++ includes/api/ApiQueryDeletedRevisions.php | 304 ++++++++++++ includes/api/ApiQueryDeletedrevs.php | 11 + includes/api/ApiQueryRevisions.php | 497 +++---------------- includes/api/ApiQueryRevisionsBase.php | 474 ++++++++++++++++++ includes/api/i18n/en.json | 47 +- includes/api/i18n/qqq.json | 50 +- 14 files changed, 1412 insertions(+), 447 deletions(-) create mode 100644 includes/api/ApiQueryAllDeletedRevisions.php create mode 100644 includes/api/ApiQueryDeletedRevisions.php create mode 100644 includes/api/ApiQueryRevisionsBase.php diff --git a/RELEASE-NOTES-1.25 b/RELEASE-NOTES-1.25 index fa870bb414..7a088cfd74 100644 --- a/RELEASE-NOTES-1.25 +++ b/RELEASE-NOTES-1.25 @@ -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 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 36a0fcb917..f0aa11620b 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -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', diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 58bd68db57..3f84f2a786 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -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 diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index fdde4bd683..b33b087e6d 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -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(); } } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index db5eb52f23..ea85cac550 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -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 ); + } } /** diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 07670f6eae..b1c092e0b7 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -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; diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index a091663884..5a0491ae7b 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -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 index 0000000000..0292e9a354 --- /dev/null +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -0,0 +1,384 @@ +.@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 index 0000000000..0271037cfb --- /dev/null +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -0,0 +1,304 @@ +.@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'; + } +} diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 4a5f5fdc7e..f828255bf1 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -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( diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index f5ad9d0fd4..2e980f3bcf 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -32,20 +32,14 @@ * * @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 index 0000000000..3879d7b566 --- /dev/null +++ b/includes/api/ApiQueryRevisionsBase.php @@ -0,0 +1,474 @@ +@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', + ), + ); + } + +} diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 2bec759f56..485c7763e1 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -319,6 +319,23 @@ "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.", @@ -508,6 +525,17 @@ "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.", @@ -763,8 +791,6 @@ "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.", @@ -772,14 +798,7 @@ "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\"", @@ -787,6 +806,16 @@ "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.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 25ba0b2355..ce058e977c 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -291,6 +291,24 @@ "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}}", @@ -466,6 +484,18 @@ "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}}", @@ -692,8 +722,6 @@ "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}}", @@ -701,20 +729,24 @@ "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}}", -- 2.20.1