wiki.techinc.nl/includes/api/ApiQueryDeletedrevs.php
Brad Jorsch c2b1525908 API: Use ParamValidator library
This brings significant modularization to the Action API's parameter
validation, and allows the Action API and MW REST API to share
validation code.

Note there are several changes in this patch that may affect other code;
see the entries in RELEASE-NOTES-1.35 for details.

Bug: T142080
Bug: T232672
Bug: T21195
Bug: T34675
Bug: T154774
Change-Id: I1462edc1701278760fa695308007006868b249fc
Depends-On: I10011be060fe6d27c7527312ad41218786b3f40d
2020-02-04 13:36:14 -05:00

513 lines
16 KiB
PHP

<?php
/**
* 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
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\NameTableAccessException;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
/**
* Query module to enumerate all deleted revisions.
*
* @ingroup API
* @deprecated since 1.25
*/
class ApiQueryDeletedrevs extends ApiQueryBase {
public function __construct( ApiQuery $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'dr' );
}
public function execute() {
// Before doing anything at all, let's check permissions
$this->checkUserRightsAny( 'deletedhistory' );
$this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' );
$user = $this->getUser();
$db = $this->getDB();
$commentStore = CommentStore::getStore();
$params = $this->extractRequestParams( false );
$prop = array_flip( $params['prop'] );
$fld_parentid = isset( $prop['parentid'] );
$fld_revid = isset( $prop['revid'] );
$fld_user = isset( $prop['user'] );
$fld_userid = isset( $prop['userid'] );
$fld_comment = isset( $prop['comment'] );
$fld_parsedcomment = isset( $prop['parsedcomment'] );
$fld_minor = isset( $prop['minor'] );
$fld_len = isset( $prop['len'] );
$fld_sha1 = isset( $prop['sha1'] );
$fld_content = isset( $prop['content'] );
$fld_token = isset( $prop['token'] );
$fld_tags = isset( $prop['tags'] );
// If we're in a mode that breaks the same-origin policy, no tokens can
// be obtained
if ( $this->lacksSameOriginSecurity() ) {
$fld_token = false;
}
// If user can't undelete, no tokens
if ( !$this->getPermissionManager()->userHasRight( $user, 'undelete' ) ) {
$fld_token = false;
}
$result = $this->getResult();
$pageSet = $this->getPageSet();
$titles = $pageSet->getTitles();
// This module operates in three modes:
// 'revs': List deleted revs for certain titles (1)
// 'user': List deleted revs by a certain user (2)
// 'all': List all deleted revs in NS (3)
$mode = 'all';
if ( count( $titles ) > 0 ) {
$mode = 'revs';
} elseif ( $params['user'] !== null ) {
$mode = 'user';
}
if ( $mode == 'revs' || $mode == 'user' ) {
// Ignore namespace and unique due to inability to know whether they were purposely set
foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) {
if ( $params[$p] !== null ) {
$this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' );
}
}
} else {
foreach ( [ 'start', 'end' ] as $p ) {
if ( $params[$p] !== null ) {
$this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' );
}
}
}
if ( $params['user'] !== null && $params['excludeuser'] !== null ) {
$this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
}
$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
$arQuery = $revisionStore->getArchiveQueryInfo();
$this->addTables( $arQuery['tables'] );
$this->addFields( $arQuery['fields'] );
$this->addJoinConds( $arQuery['joins'] );
$this->addFields( [ 'ar_title', 'ar_namespace' ] );
if ( $fld_tags ) {
$this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
}
if ( $params['tag'] !== null ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
[ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
);
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
$this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
} catch ( NameTableAccessException $exception ) {
// Return nothing.
$this->addWhere( '1=0' );
}
}
// This means stricter restrictions
if ( $fld_content ) {
$this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
}
// Check limits
$userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
$botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
$limit = $params['limit'];
if ( $limit == 'max' ) {
$limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
}
$limit = $this->getMain()->getParamValidator()->validateValue(
$this, 'limit', $limit, [
ParamValidator::PARAM_TYPE => 'limit',
IntegerDef::PARAM_MIN => 1,
IntegerDef::PARAM_MAX => $userMax,
IntegerDef::PARAM_MAX2 => $botMax,
IntegerDef::PARAM_IGNORE_RANGE => true,
]
);
if ( $fld_token ) {
// Undelete tokens are identical for all pages, so we cache one here
$token = $user->getEditToken( '', $this->getMain()->getRequest() );
}
$dir = $params['dir'];
// We need a custom WHERE clause that matches all titles.
if ( $mode == 'revs' ) {
$lb = new LinkBatch( $titles );
$where = $lb->constructSet( 'ar', $db );
$this->addWhere( $where );
} elseif ( $mode == 'all' ) {
$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() ) );
}
}
if ( $params['user'] !== null ) {
// Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', $params['user'], false );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
// Here there's no chance of using ar_usertext_timestamp.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', $params['excludeuser'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
// Paranoia: avoid brute force searches (T19342)
// (shouldn't be able to get here without 'deletedhistory', but
// check it again just in case)
if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
$bitmask = RevisionRecord::DELETED_USER;
} elseif ( !$this->getPermissionManager()
->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
) {
$bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
} else {
$bitmask = 0;
}
if ( $bitmask ) {
$this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
}
}
if ( $params['continue'] !== null ) {
$cont = explode( '|', $params['continue'] );
$op = ( $dir == 'newer' ? '>' : '<' );
if ( $mode == 'all' || $mode == 'revs' ) {
$this->dieContinueUsageIf( count( $cont ) != 4 );
$ns = (int)$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', $limit + 1 );
if ( $mode == 'all' ) {
if ( $params['unique'] ) {
// @todo Does this work on non-MySQL?
$this->addOption( 'GROUP BY', 'ar_title' );
} else {
$sort = ( $dir == 'newer' ? '' : ' DESC' );
$this->addOption( 'ORDER BY', [
'ar_title' . $sort,
'ar_timestamp' . $sort,
'ar_id' . $sort,
] );
}
} else {
if ( $mode == 'revs' ) {
// Sort by ns and title in the same order as timestamp for efficiency
$this->addWhereRange( 'ar_namespace', $dir, null, null );
$this->addWhereRange( 'ar_title', $dir, null, null );
}
$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__ );
$pageMap = []; // Maps ns&title to (fake) pageid
$count = 0;
$newPageID = 0;
foreach ( $res as $row ) {
if ( ++$count > $limit ) {
// We've had enough
if ( $mode == 'all' || $mode == 'revs' ) {
$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;
}
$rev = [];
$anyHidden = false;
$rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
if ( $fld_revid ) {
$rev['revid'] = (int)$row->ar_rev_id;
}
if ( $fld_parentid && $row->ar_parent_id !== null ) {
$rev['parentid'] = (int)$row->ar_parent_id;
}
if ( $fld_user || $fld_userid ) {
if ( $row->ar_deleted & RevisionRecord::DELETED_USER ) {
$rev['userhidden'] = true;
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_USER, $user ) ) {
if ( $fld_user ) {
$rev['user'] = $row->ar_user_text;
}
if ( $fld_userid ) {
$rev['userid'] = (int)$row->ar_user;
}
}
}
if ( $fld_comment || $fld_parsedcomment ) {
if ( $row->ar_deleted & RevisionRecord::DELETED_COMMENT ) {
$rev['commenthidden'] = true;
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_COMMENT, $user ) ) {
$comment = $commentStore->getComment( 'ar_comment', $row )->text;
if ( $fld_comment ) {
$rev['comment'] = $comment;
}
if ( $fld_parsedcomment ) {
$title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
$rev['parsedcomment'] = Linker::formatComment( $comment, $title );
}
}
}
if ( $fld_minor ) {
$rev['minor'] = $row->ar_minor_edit == 1;
}
if ( $fld_len ) {
$rev['len'] = $row->ar_len;
}
if ( $fld_sha1 ) {
if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
$rev['sha1hidden'] = true;
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_TEXT, $user ) ) {
if ( $row->ar_sha1 != '' ) {
$rev['sha1'] = Wikimedia\base_convert( $row->ar_sha1, 36, 16, 40 );
} else {
$rev['sha1'] = '';
}
}
}
if ( $fld_content ) {
if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
$rev['texthidden'] = true;
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_TEXT, $user ) ) {
ApiResult::setContentValue( $rev, 'text',
$revisionStore->newRevisionFromArchiveRow( $row )
->getContent( SlotRecord::MAIN )->serialize() );
}
}
if ( $fld_tags ) {
if ( $row->ts_tags ) {
$tags = explode( ',', $row->ts_tags );
ApiResult::setIndexedTagName( $tags, 'tag' );
$rev['tags'] = $tags;
} else {
$rev['tags'] = [];
}
}
if ( $anyHidden && ( $row->ar_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
$rev['suppressed'] = true;
}
if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
$pageID = $newPageID++;
$pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
$a = [ 'revisions' => [ $rev ] ];
ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
$title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
ApiQueryBase::addTitleInfo( $a, $title );
if ( $fld_token ) {
$a['token'] = $token;
}
$fit = $result->addValue( [ 'query', $this->getModuleName() ], $pageID, $a );
} else {
$pageID = $pageMap[$row->ar_namespace][$row->ar_title];
$fit = $result->addValue(
[ 'query', $this->getModuleName(), $pageID, 'revisions' ],
null, $rev );
}
if ( !$fit ) {
if ( $mode == 'all' || $mode == 'revs' ) {
$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;
}
}
$result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
}
public function isDeprecated() {
return true;
}
public function getAllowedParams() {
return [
'start' => [
ApiBase::PARAM_TYPE => 'timestamp',
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
],
'end' => [
ApiBase::PARAM_TYPE => 'timestamp',
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
],
'dir' => [
ApiBase::PARAM_TYPE => [
'newer',
'older'
],
ApiBase::PARAM_DFLT => 'older',
ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 3 ] ],
],
'from' => [
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
],
'to' => [
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
],
'prefix' => [
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
],
'unique' => [
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
],
'namespace' => [
ApiBase::PARAM_TYPE => 'namespace',
ApiBase::PARAM_DFLT => NS_MAIN,
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
],
'tag' => null,
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'prop' => [
ApiBase::PARAM_DFLT => 'user|comment',
ApiBase::PARAM_TYPE => [
'revid',
'parentid',
'user',
'userid',
'comment',
'parsedcomment',
'minor',
'len',
'sha1',
'content',
'token',
'tags'
],
ApiBase::PARAM_ISMULTI => true
],
'limit' => [
ApiBase::PARAM_DFLT => 10,
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MIN => 1,
ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
],
'continue' => [
ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
],
];
}
protected function getExamplesMessages() {
return [
'action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&' .
'drprop=user|comment|content'
=> 'apihelp-query+deletedrevs-example-mode1',
'action=query&list=deletedrevs&druser=Bob&drlimit=50'
=> 'apihelp-query+deletedrevs-example-mode2',
'action=query&list=deletedrevs&drdir=newer&drlimit=50'
=> 'apihelp-query+deletedrevs-example-mode3-main',
'action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=1&drunique='
=> 'apihelp-query+deletedrevs-example-mode3-talk',
];
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevs';
}
}