MediaWiki REL1_34
ApiQueryDeletedrevs.php
Go to the documentation of this file.
1<?php
27
35
36 public function __construct( ApiQuery $query, $moduleName ) {
37 parent::__construct( $query, $moduleName, 'dr' );
38 }
39
40 public function execute() {
41 // Before doing anything at all, let's check permissions
42 $this->checkUserRightsAny( 'deletedhistory' );
43
44 $this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' );
45
46 $user = $this->getUser();
47 $db = $this->getDB();
48 $commentStore = CommentStore::getStore();
49 $params = $this->extractRequestParams( false );
50 $prop = array_flip( $params['prop'] );
51 $fld_parentid = isset( $prop['parentid'] );
52 $fld_revid = isset( $prop['revid'] );
53 $fld_user = isset( $prop['user'] );
54 $fld_userid = isset( $prop['userid'] );
55 $fld_comment = isset( $prop['comment'] );
56 $fld_parsedcomment = isset( $prop['parsedcomment'] );
57 $fld_minor = isset( $prop['minor'] );
58 $fld_len = isset( $prop['len'] );
59 $fld_sha1 = isset( $prop['sha1'] );
60 $fld_content = isset( $prop['content'] );
61 $fld_token = isset( $prop['token'] );
62 $fld_tags = isset( $prop['tags'] );
63
64 // If we're in a mode that breaks the same-origin policy, no tokens can
65 // be obtained
66 if ( $this->lacksSameOriginSecurity() ) {
67 $fld_token = false;
68 }
69
70 // If user can't undelete, no tokens
71 if ( !$this->getPermissionManager()->userHasRight( $user, 'undelete' ) ) {
72 $fld_token = false;
73 }
74
75 $result = $this->getResult();
76 $pageSet = $this->getPageSet();
77 $titles = $pageSet->getTitles();
78
79 // This module operates in three modes:
80 // 'revs': List deleted revs for certain titles (1)
81 // 'user': List deleted revs by a certain user (2)
82 // 'all': List all deleted revs in NS (3)
83 $mode = 'all';
84 if ( count( $titles ) > 0 ) {
85 $mode = 'revs';
86 } elseif ( !is_null( $params['user'] ) ) {
87 $mode = 'user';
88 }
89
90 if ( $mode == 'revs' || $mode == 'user' ) {
91 // Ignore namespace and unique due to inability to know whether they were purposely set
92 foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) {
93 if ( !is_null( $params[$p] ) ) {
94 $this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' );
95 }
96 }
97 } else {
98 foreach ( [ 'start', 'end' ] as $p ) {
99 if ( !is_null( $params[$p] ) ) {
100 $this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' );
101 }
102 }
103 }
104
105 if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
106 $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
107 }
108
109 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
110 $arQuery = $revisionStore->getArchiveQueryInfo();
111 $this->addTables( $arQuery['tables'] );
112 $this->addFields( $arQuery['fields'] );
113 $this->addJoinConds( $arQuery['joins'] );
114 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
115
116 if ( $fld_tags ) {
117 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
118 }
119
120 if ( !is_null( $params['tag'] ) ) {
121 $this->addTables( 'change_tag' );
122 $this->addJoinConds(
123 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
124 );
125 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
126 try {
127 $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
128 } catch ( NameTableAccessException $exception ) {
129 // Return nothing.
130 $this->addWhere( '1=0' );
131 }
132 }
133
134 // This means stricter restrictions
135 if ( $fld_content ) {
136 $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
137 }
138 // Check limits
139 $userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
140 $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
141
142 $limit = $params['limit'];
143
144 if ( $limit == 'max' ) {
145 $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
146 $this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
147 }
148
149 $this->validateLimit( 'limit', $limit, 1, $userMax, $botMax );
150
151 if ( $fld_token ) {
152 // Undelete tokens are identical for all pages, so we cache one here
153 $token = $user->getEditToken( '', $this->getMain()->getRequest() );
154 }
155
156 $dir = $params['dir'];
157
158 // We need a custom WHERE clause that matches all titles.
159 if ( $mode == 'revs' ) {
160 $lb = new LinkBatch( $titles );
161 $where = $lb->constructSet( 'ar', $db );
162 $this->addWhere( $where );
163 } elseif ( $mode == 'all' ) {
164 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
165
166 $from = $params['from'] === null
167 ? null
168 : $this->titlePartToKey( $params['from'], $params['namespace'] );
169 $to = $params['to'] === null
170 ? null
171 : $this->titlePartToKey( $params['to'], $params['namespace'] );
172 $this->addWhereRange( 'ar_title', $dir, $from, $to );
173
174 if ( isset( $params['prefix'] ) ) {
175 $this->addWhere( 'ar_title' . $db->buildLike(
176 $this->titlePartToKey( $params['prefix'], $params['namespace'] ),
177 $db->anyString() ) );
178 }
179 }
180
181 if ( !is_null( $params['user'] ) ) {
182 // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
183 $actorQuery = ActorMigration::newMigration()
184 ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
185 $this->addTables( $actorQuery['tables'] );
186 $this->addJoinConds( $actorQuery['joins'] );
187 $this->addWhere( $actorQuery['conds'] );
188 } elseif ( !is_null( $params['excludeuser'] ) ) {
189 // Here there's no chance of using ar_usertext_timestamp.
190 $actorQuery = ActorMigration::newMigration()
191 ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
192 $this->addTables( $actorQuery['tables'] );
193 $this->addJoinConds( $actorQuery['joins'] );
194 $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
195 }
196
197 if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
198 // Paranoia: avoid brute force searches (T19342)
199 // (shouldn't be able to get here without 'deletedhistory', but
200 // check it again just in case)
201 if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
202 $bitmask = RevisionRecord::DELETED_USER;
203 } elseif ( !$this->getPermissionManager()
204 ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
205 ) {
206 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
207 } else {
208 $bitmask = 0;
209 }
210 if ( $bitmask ) {
211 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
212 }
213 }
214
215 if ( !is_null( $params['continue'] ) ) {
216 $cont = explode( '|', $params['continue'] );
217 $op = ( $dir == 'newer' ? '>' : '<' );
218 if ( $mode == 'all' || $mode == 'revs' ) {
219 $this->dieContinueUsageIf( count( $cont ) != 4 );
220 $ns = (int)$cont[0];
221 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
222 $title = $db->addQuotes( $cont[1] );
223 $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
224 $ar_id = (int)$cont[3];
225 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
226 $this->addWhere( "ar_namespace $op $ns OR " .
227 "(ar_namespace = $ns AND " .
228 "(ar_title $op $title OR " .
229 "(ar_title = $title AND " .
230 "(ar_timestamp $op $ts OR " .
231 "(ar_timestamp = $ts AND " .
232 "ar_id $op= $ar_id)))))" );
233 } else {
234 $this->dieContinueUsageIf( count( $cont ) != 2 );
235 $ts = $db->addQuotes( $db->timestamp( $cont[0] ) );
236 $ar_id = (int)$cont[1];
237 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
238 $this->addWhere( "ar_timestamp $op $ts OR " .
239 "(ar_timestamp = $ts AND " .
240 "ar_id $op= $ar_id)" );
241 }
242 }
243
244 $this->addOption( 'LIMIT', $limit + 1 );
245 if ( $mode == 'all' ) {
246 if ( $params['unique'] ) {
247 // @todo Does this work on non-MySQL?
248 $this->addOption( 'GROUP BY', 'ar_title' );
249 } else {
250 $sort = ( $dir == 'newer' ? '' : ' DESC' );
251 $this->addOption( 'ORDER BY', [
252 'ar_title' . $sort,
253 'ar_timestamp' . $sort,
254 'ar_id' . $sort,
255 ] );
256 }
257 } else {
258 if ( $mode == 'revs' ) {
259 // Sort by ns and title in the same order as timestamp for efficiency
260 $this->addWhereRange( 'ar_namespace', $dir, null, null );
261 $this->addWhereRange( 'ar_title', $dir, null, null );
262 }
263 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
264 // Include in ORDER BY for uniqueness
265 $this->addWhereRange( 'ar_id', $dir, null, null );
266 }
267 $res = $this->select( __METHOD__ );
268 $pageMap = []; // Maps ns&title to (fake) pageid
269 $count = 0;
270 $newPageID = 0;
271 foreach ( $res as $row ) {
272 if ( ++$count > $limit ) {
273 // We've had enough
274 if ( $mode == 'all' || $mode == 'revs' ) {
275 $this->setContinueEnumParameter( 'continue',
276 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
277 );
278 } else {
279 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
280 }
281 break;
282 }
283
284 $rev = [];
285 $anyHidden = false;
286
287 $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
288 if ( $fld_revid ) {
289 $rev['revid'] = (int)$row->ar_rev_id;
290 }
291 if ( $fld_parentid && !is_null( $row->ar_parent_id ) ) {
292 $rev['parentid'] = (int)$row->ar_parent_id;
293 }
294 if ( $fld_user || $fld_userid ) {
295 if ( $row->ar_deleted & RevisionRecord::DELETED_USER ) {
296 $rev['userhidden'] = true;
297 $anyHidden = true;
298 }
299 if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_USER, $user ) ) {
300 if ( $fld_user ) {
301 $rev['user'] = $row->ar_user_text;
302 }
303 if ( $fld_userid ) {
304 $rev['userid'] = (int)$row->ar_user;
305 }
306 }
307 }
308
309 if ( $fld_comment || $fld_parsedcomment ) {
310 if ( $row->ar_deleted & RevisionRecord::DELETED_COMMENT ) {
311 $rev['commenthidden'] = true;
312 $anyHidden = true;
313 }
314 if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_COMMENT, $user ) ) {
315 $comment = $commentStore->getComment( 'ar_comment', $row )->text;
316 if ( $fld_comment ) {
317 $rev['comment'] = $comment;
318 }
319 if ( $fld_parsedcomment ) {
320 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
321 $rev['parsedcomment'] = Linker::formatComment( $comment, $title );
322 }
323 }
324 }
325
326 if ( $fld_minor ) {
327 $rev['minor'] = $row->ar_minor_edit == 1;
328 }
329 if ( $fld_len ) {
330 $rev['len'] = $row->ar_len;
331 }
332 if ( $fld_sha1 ) {
333 if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
334 $rev['sha1hidden'] = true;
335 $anyHidden = true;
336 }
337 if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_TEXT, $user ) ) {
338 if ( $row->ar_sha1 != '' ) {
339 $rev['sha1'] = Wikimedia\base_convert( $row->ar_sha1, 36, 16, 40 );
340 } else {
341 $rev['sha1'] = '';
342 }
343 }
344 }
345 if ( $fld_content ) {
346 if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
347 $rev['texthidden'] = true;
348 $anyHidden = true;
349 }
350 if ( Revision::userCanBitfield( $row->ar_deleted, RevisionRecord::DELETED_TEXT, $user ) ) {
351 ApiResult::setContentValue( $rev, 'text',
352 $revisionStore->newRevisionFromArchiveRow( $row )
353 ->getContent( SlotRecord::MAIN )->serialize() );
354 }
355 }
356
357 if ( $fld_tags ) {
358 if ( $row->ts_tags ) {
359 $tags = explode( ',', $row->ts_tags );
360 ApiResult::setIndexedTagName( $tags, 'tag' );
361 $rev['tags'] = $tags;
362 } else {
363 $rev['tags'] = [];
364 }
365 }
366
367 if ( $anyHidden && ( $row->ar_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
368 $rev['suppressed'] = true;
369 }
370
371 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
372 $pageID = $newPageID++;
373 $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
374 $a = [ 'revisions' => [ $rev ] ];
375 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
376 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
378 if ( $fld_token ) {
379 $a['token'] = $token;
380 }
381 $fit = $result->addValue( [ 'query', $this->getModuleName() ], $pageID, $a );
382 } else {
383 $pageID = $pageMap[$row->ar_namespace][$row->ar_title];
384 $fit = $result->addValue(
385 [ 'query', $this->getModuleName(), $pageID, 'revisions' ],
386 null, $rev );
387 }
388 if ( !$fit ) {
389 if ( $mode == 'all' || $mode == 'revs' ) {
390 $this->setContinueEnumParameter( 'continue',
391 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
392 );
393 } else {
394 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
395 }
396 break;
397 }
398 }
399 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
400 }
401
402 public function isDeprecated() {
403 return true;
404 }
405
406 public function getAllowedParams() {
407 return [
408 'start' => [
409 ApiBase::PARAM_TYPE => 'timestamp',
410 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
411 ],
412 'end' => [
413 ApiBase::PARAM_TYPE => 'timestamp',
414 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
415 ],
416 'dir' => [
418 'newer',
419 'older'
420 ],
421 ApiBase::PARAM_DFLT => 'older',
422 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
423 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 3 ] ],
424 ],
425 'from' => [
426 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
427 ],
428 'to' => [
429 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
430 ],
431 'prefix' => [
432 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
433 ],
434 'unique' => [
435 ApiBase::PARAM_DFLT => false,
436 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
437 ],
438 'namespace' => [
439 ApiBase::PARAM_TYPE => 'namespace',
441 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
442 ],
443 'tag' => null,
444 'user' => [
445 ApiBase::PARAM_TYPE => 'user'
446 ],
447 'excludeuser' => [
448 ApiBase::PARAM_TYPE => 'user'
449 ],
450 'prop' => [
451 ApiBase::PARAM_DFLT => 'user|comment',
453 'revid',
454 'parentid',
455 'user',
456 'userid',
457 'comment',
458 'parsedcomment',
459 'minor',
460 'len',
461 'sha1',
462 'content',
463 'token',
464 'tags'
465 ],
467 ],
468 'limit' => [
470 ApiBase::PARAM_TYPE => 'limit',
474 ],
475 'continue' => [
476 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
477 ],
478 ];
479 }
480
481 protected function getExamplesMessages() {
482 return [
483 'action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&' .
484 'drprop=user|comment|content'
485 => 'apihelp-query+deletedrevs-example-mode1',
486 'action=query&list=deletedrevs&druser=Bob&drlimit=50'
487 => 'apihelp-query+deletedrevs-example-mode2',
488 'action=query&list=deletedrevs&drdir=newer&drlimit=50'
489 => 'apihelp-query+deletedrevs-example-mode3-main',
490 'action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=1&drunique='
491 => 'apihelp-query+deletedrevs-example-mode3-talk',
492 ];
493 }
494
495 public function getHelpUrls() {
496 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevs';
497 }
498}
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const PARAM_MAX2
(integer) Max value allowed for the parameter for users with the apihighlimits right,...
Definition ApiBase.php:103
checkUserRightsAny( $rights, $user=null)
Helper function for permission-denied errors.
Definition ApiBase.php:2130
const PARAM_MAX
(integer) Max value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'.
Definition ApiBase.php:97
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition ApiBase.php:2014
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:2208
getMain()
Get the main module.
Definition ApiBase.php:536
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition ApiBase.php:94
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:148
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition ApiBase.php:55
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:710
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition ApiBase.php:1947
const PARAM_MIN
(integer) Lowest value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'.
Definition ApiBase.php:106
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:259
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:265
validateLimit( $paramName, &$value, $min, $max, $botMax=null, $enforceLimits=false)
Validate the value against the minimum and user/bot maximum limits.
Definition ApiBase.php:1578
getResult()
Get the result object.
Definition ApiBase.php:640
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:761
const LIMIT_SML1
Slow query, standard limit.
Definition ApiBase.php:263
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:131
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:261
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:520
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition ApiBase.php:58
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition ApiBase.php:568
This is a base class for all Query modules.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
addFields( $value)
Add a set of fields to select to the internal array.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
getDB()
Get the Query database connection (read-only)
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
getPageSet()
Get the PageSet object to work on.
titlePartToKey( $titlePart, $namespace=NS_MAIN)
Convert an input title or title prefix into a dbkey.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
Query module to enumerate all deleted revisions.
getExamplesMessages()
Returns usage examples for this module.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getHelpUrls()
Return links to more detailed help pages about the module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
isDeprecated()
Indicates whether this module is deprecated.
__construct(ApiQuery $query, $moduleName)
This is the main query class.
Definition ApiQuery.php:37
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition Linker.php:1165
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Value object representing a content slot associated with a page revision.
Exception representing a failure to look up a row from a name table.
const NS_MAIN
Definition Defines.php:69
$sort
return true
Definition router.php:94