MediaWiki REL1_37
ApiQueryDeletedrevs.php
Go to the documentation of this file.
1<?php
32
40
43
46
49
52
61 public function __construct(
62 ApiQuery $query,
63 $moduleName,
68 ) {
69 parent::__construct( $query, $moduleName, 'dr' );
70 $this->commentStore = $commentStore;
71 $this->revisionStore = $revisionStore;
72 $this->changeTagDefStore = $changeTagDefStore;
73 $this->linkBatchFactory = $linkBatchFactory;
74 }
75
76 public function execute() {
77 // Before doing anything at all, let's check permissions
78 $this->checkUserRightsAny( 'deletedhistory' );
79
80 $this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' );
81
82 $user = $this->getUser();
83 $db = $this->getDB();
84 $params = $this->extractRequestParams( false );
85 $prop = array_fill_keys( $params['prop'], true );
86 $fld_parentid = isset( $prop['parentid'] );
87 $fld_revid = isset( $prop['revid'] );
88 $fld_user = isset( $prop['user'] );
89 $fld_userid = isset( $prop['userid'] );
90 $fld_comment = isset( $prop['comment'] );
91 $fld_parsedcomment = isset( $prop['parsedcomment'] );
92 $fld_minor = isset( $prop['minor'] );
93 $fld_len = isset( $prop['len'] );
94 $fld_sha1 = isset( $prop['sha1'] );
95 $fld_content = isset( $prop['content'] );
96 $fld_token = isset( $prop['token'] );
97 $fld_tags = isset( $prop['tags'] );
98
99 // If we're in a mode that breaks the same-origin policy, no tokens can
100 // be obtained
101 if ( $this->lacksSameOriginSecurity() ) {
102 $fld_token = false;
103 }
104
105 // If user can't undelete, no tokens
106 if ( !$this->getAuthority()->isAllowed( 'undelete' ) ) {
107 $fld_token = false;
108 }
109
110 $result = $this->getResult();
111 $pageSet = $this->getPageSet();
112 $titles = $pageSet->getPages();
113
114 // This module operates in three modes:
115 // 'revs': List deleted revs for certain titles (1)
116 // 'user': List deleted revs by a certain user (2)
117 // 'all': List all deleted revs in NS (3)
118 $mode = 'all';
119 if ( count( $titles ) > 0 ) {
120 $mode = 'revs';
121 } elseif ( $params['user'] !== null ) {
122 $mode = 'user';
123 }
124
125 if ( $mode == 'revs' || $mode == 'user' ) {
126 // Ignore namespace and unique due to inability to know whether they were purposely set
127 foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) {
128 if ( $params[$p] !== null ) {
129 $this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' );
130 }
131 }
132 } else {
133 foreach ( [ 'start', 'end' ] as $p ) {
134 if ( $params[$p] !== null ) {
135 $this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' );
136 }
137 }
138 }
139
140 if ( $params['user'] !== null && $params['excludeuser'] !== null ) {
141 $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
142 }
143
144 $arQuery = $this->revisionStore->getArchiveQueryInfo();
145 $this->addTables( $arQuery['tables'] );
146 $this->addFields( $arQuery['fields'] );
147 $this->addJoinConds( $arQuery['joins'] );
148 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
149
150 if ( $fld_tags ) {
151 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
152 }
153
154 if ( $params['tag'] !== null ) {
155 $this->addTables( 'change_tag' );
156 $this->addJoinConds(
157 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
158 );
159 try {
160 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
161 } catch ( NameTableAccessException $exception ) {
162 // Return nothing.
163 $this->addWhere( '1=0' );
164 }
165 }
166
167 // This means stricter restrictions
168 if ( $fld_content ) {
169 $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
170 }
171 // Check limits
172 $userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
173 $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
174
175 $limit = $params['limit'];
176
177 if ( $limit == 'max' ) {
178 $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
179 $this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
180 }
181
182 $limit = $this->getMain()->getParamValidator()->validateValue(
183 $this, 'limit', $limit, [
184 ParamValidator::PARAM_TYPE => 'limit',
185 IntegerDef::PARAM_MIN => 1,
186 IntegerDef::PARAM_MAX => $userMax,
187 IntegerDef::PARAM_MAX2 => $botMax,
188 IntegerDef::PARAM_IGNORE_RANGE => true,
189 ]
190 );
191
192 if ( $fld_token ) {
193 // Undelete tokens are identical for all pages, so we cache one here
194 $token = $user->getEditToken( '', $this->getMain()->getRequest() );
195 }
196
197 $dir = $params['dir'];
198
199 // We need a custom WHERE clause that matches all titles.
200 if ( $mode == 'revs' ) {
201 $lb = $this->linkBatchFactory->newLinkBatch( $titles );
202 $where = $lb->constructSet( 'ar', $db );
203 $this->addWhere( $where );
204 } elseif ( $mode == 'all' ) {
205 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
206
207 $from = $params['from'] === null
208 ? null
209 : $this->titlePartToKey( $params['from'], $params['namespace'] );
210 $to = $params['to'] === null
211 ? null
212 : $this->titlePartToKey( $params['to'], $params['namespace'] );
213 $this->addWhereRange( 'ar_title', $dir, $from, $to );
214
215 if ( isset( $params['prefix'] ) ) {
216 $this->addWhere( 'ar_title' . $db->buildLike(
217 $this->titlePartToKey( $params['prefix'], $params['namespace'] ),
218 $db->anyString() ) );
219 }
220 }
221
222 if ( $params['user'] !== null ) {
223 // We already join on actor due to getArchiveQueryInfo()
224 $this->addWhereFld( 'actor_name', $params['user'] );
225 } elseif ( $params['excludeuser'] !== null ) {
226 $this->addWhere( 'actor_name<>' . $db->addQuotes( $params['excludeuser'] ) );
227 }
228
229 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
230 // Paranoia: avoid brute force searches (T19342)
231 // (shouldn't be able to get here without 'deletedhistory', but
232 // check it again just in case)
233 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
234 $bitmask = RevisionRecord::DELETED_USER;
235 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
236 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
237 } else {
238 $bitmask = 0;
239 }
240 if ( $bitmask ) {
241 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
242 }
243 }
244
245 if ( $params['continue'] !== null ) {
246 $cont = explode( '|', $params['continue'] );
247 $op = ( $dir == 'newer' ? '>' : '<' );
248 if ( $mode == 'all' || $mode == 'revs' ) {
249 $this->dieContinueUsageIf( count( $cont ) != 4 );
250 $ns = (int)$cont[0];
251 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
252 $title = $db->addQuotes( $cont[1] );
253 $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
254 $ar_id = (int)$cont[3];
255 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
256 $this->addWhere( "ar_namespace $op $ns OR " .
257 "(ar_namespace = $ns AND " .
258 "(ar_title $op $title OR " .
259 "(ar_title = $title AND " .
260 "(ar_timestamp $op $ts OR " .
261 "(ar_timestamp = $ts AND " .
262 "ar_id $op= $ar_id)))))" );
263 } else {
264 $this->dieContinueUsageIf( count( $cont ) != 2 );
265 $ts = $db->addQuotes( $db->timestamp( $cont[0] ) );
266 $ar_id = (int)$cont[1];
267 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
268 $this->addWhere( "ar_timestamp $op $ts OR " .
269 "(ar_timestamp = $ts AND " .
270 "ar_id $op= $ar_id)" );
271 }
272 }
273
274 $this->addOption( 'LIMIT', $limit + 1 );
275 if ( $mode == 'all' ) {
276 if ( $params['unique'] ) {
277 // @todo Does this work on non-MySQL?
278 $this->addOption( 'GROUP BY', 'ar_title' );
279 } else {
280 $sort = ( $dir == 'newer' ? '' : ' DESC' );
281 $this->addOption( 'ORDER BY', [
282 'ar_title' . $sort,
283 'ar_timestamp' . $sort,
284 'ar_id' . $sort,
285 ] );
286 }
287 } else {
288 if ( $mode == 'revs' ) {
289 // Sort by ns and title in the same order as timestamp for efficiency
290 $this->addWhereRange( 'ar_namespace', $dir, null, null );
291 $this->addWhereRange( 'ar_title', $dir, null, null );
292 }
293 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
294 // Include in ORDER BY for uniqueness
295 $this->addWhereRange( 'ar_id', $dir, null, null );
296 }
297 $res = $this->select( __METHOD__ );
298 $pageMap = []; // Maps ns&title to (fake) pageid
299 $count = 0;
300 $newPageID = 0;
301 foreach ( $res as $row ) {
302 if ( ++$count > $limit ) {
303 // We've had enough
304 if ( $mode == 'all' || $mode == 'revs' ) {
305 $this->setContinueEnumParameter( 'continue',
306 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
307 );
308 } else {
309 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
310 }
311 break;
312 }
313
314 $rev = [];
315 $anyHidden = false;
316
317 $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
318 if ( $fld_revid ) {
319 $rev['revid'] = (int)$row->ar_rev_id;
320 }
321 if ( $fld_parentid && $row->ar_parent_id !== null ) {
322 $rev['parentid'] = (int)$row->ar_parent_id;
323 }
324 if ( $fld_user || $fld_userid ) {
325 if ( $row->ar_deleted & RevisionRecord::DELETED_USER ) {
326 $rev['userhidden'] = true;
327 $anyHidden = true;
328 }
329 if ( RevisionRecord::userCanBitfield(
330 $row->ar_deleted,
331 RevisionRecord::DELETED_USER,
332 $user
333 ) ) {
334 if ( $fld_user ) {
335 $rev['user'] = $row->ar_user_text;
336 }
337 if ( $fld_userid ) {
338 $rev['userid'] = (int)$row->ar_user;
339 }
340 }
341 }
342
343 if ( $fld_comment || $fld_parsedcomment ) {
344 if ( $row->ar_deleted & RevisionRecord::DELETED_COMMENT ) {
345 $rev['commenthidden'] = true;
346 $anyHidden = true;
347 }
348 if ( RevisionRecord::userCanBitfield(
349 $row->ar_deleted,
350 RevisionRecord::DELETED_COMMENT,
351 $user
352 ) ) {
353 $comment = $this->commentStore->getComment( 'ar_comment', $row )->text;
354 if ( $fld_comment ) {
355 $rev['comment'] = $comment;
356 }
357 if ( $fld_parsedcomment ) {
358 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
359 $rev['parsedcomment'] = Linker::formatComment( $comment, $title );
360 }
361 }
362 }
363
364 if ( $fld_minor ) {
365 $rev['minor'] = $row->ar_minor_edit == 1;
366 }
367 if ( $fld_len ) {
368 $rev['len'] = $row->ar_len;
369 }
370 if ( $fld_sha1 ) {
371 if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
372 $rev['sha1hidden'] = true;
373 $anyHidden = true;
374 }
375 if ( RevisionRecord::userCanBitfield(
376 $row->ar_deleted,
377 RevisionRecord::DELETED_TEXT,
378 $user
379 ) ) {
380 if ( $row->ar_sha1 != '' ) {
381 $rev['sha1'] = Wikimedia\base_convert( $row->ar_sha1, 36, 16, 40 );
382 } else {
383 $rev['sha1'] = '';
384 }
385 }
386 }
387 if ( $fld_content ) {
388 if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
389 $rev['texthidden'] = true;
390 $anyHidden = true;
391 }
392 if ( RevisionRecord::userCanBitfield(
393 $row->ar_deleted,
394 RevisionRecord::DELETED_TEXT,
395 $user
396 ) ) {
397 ApiResult::setContentValue( $rev, 'text',
398 $this->revisionStore->newRevisionFromArchiveRow( $row )
399 ->getContent( SlotRecord::MAIN )->serialize() );
400 }
401 }
402
403 if ( $fld_tags ) {
404 if ( $row->ts_tags ) {
405 $tags = explode( ',', $row->ts_tags );
406 ApiResult::setIndexedTagName( $tags, 'tag' );
407 $rev['tags'] = $tags;
408 } else {
409 $rev['tags'] = [];
410 }
411 }
412
413 if ( $anyHidden && ( $row->ar_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
414 $rev['suppressed'] = true;
415 }
416
417 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
418 $pageID = $newPageID++;
419 $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
420 $a = [ 'revisions' => [ $rev ] ];
421 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
422 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
424 if ( $fld_token ) {
425 $a['token'] = $token;
426 }
427 $fit = $result->addValue( [ 'query', $this->getModuleName() ], $pageID, $a );
428 } else {
429 $pageID = $pageMap[$row->ar_namespace][$row->ar_title];
430 $fit = $result->addValue(
431 [ 'query', $this->getModuleName(), $pageID, 'revisions' ],
432 null, $rev );
433 }
434 if ( !$fit ) {
435 if ( $mode == 'all' || $mode == 'revs' ) {
436 $this->setContinueEnumParameter( 'continue',
437 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
438 );
439 } else {
440 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
441 }
442 break;
443 }
444 }
445 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
446 }
447
448 public function isDeprecated() {
449 return true;
450 }
451
452 public function getAllowedParams() {
453 return [
454 'start' => [
455 ApiBase::PARAM_TYPE => 'timestamp',
456 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
457 ],
458 'end' => [
459 ApiBase::PARAM_TYPE => 'timestamp',
460 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
461 ],
462 'dir' => [
464 'newer',
465 'older'
466 ],
467 ApiBase::PARAM_DFLT => 'older',
468 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
469 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 3 ] ],
470 ],
471 'from' => [
472 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
473 ],
474 'to' => [
475 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
476 ],
477 'prefix' => [
478 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
479 ],
480 'unique' => [
481 ApiBase::PARAM_DFLT => false,
482 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
483 ],
484 'namespace' => [
485 ApiBase::PARAM_TYPE => 'namespace',
487 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
488 ],
489 'tag' => null,
490 'user' => [
491 ApiBase::PARAM_TYPE => 'user',
492 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
493 ],
494 'excludeuser' => [
495 ApiBase::PARAM_TYPE => 'user',
496 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
497 ],
498 'prop' => [
499 ApiBase::PARAM_DFLT => 'user|comment',
501 'revid',
502 'parentid',
503 'user',
504 'userid',
505 'comment',
506 'parsedcomment',
507 'minor',
508 'len',
509 'sha1',
510 'content',
511 'token',
512 'tags'
513 ],
515 ],
516 'limit' => [
518 ApiBase::PARAM_TYPE => 'limit',
522 ],
523 'continue' => [
524 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
525 ],
526 ];
527 }
528
529 protected function getExamplesMessages() {
530 return [
531 'action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&' .
532 'drprop=user|comment|content'
533 => 'apihelp-query+deletedrevs-example-mode1',
534 'action=query&list=deletedrevs&druser=Bob&drlimit=50'
535 => 'apihelp-query+deletedrevs-example-mode2',
536 'action=query&list=deletedrevs&drdir=newer&drlimit=50'
537 => 'apihelp-query+deletedrevs-example-mode3-main',
538 'action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=1&drunique='
539 => 'apihelp-query+deletedrevs-example-mode3-talk',
540 ];
541 }
542
543 public function getHelpUrls() {
544 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevs';
545 }
546}
const NS_MAIN
Definition Defines.php:64
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1436
const PARAM_MAX2
Definition ApiBase.php:89
checkUserRightsAny( $rights, $user=null)
Helper function for permission-denied errors.
Definition ApiBase.php:1539
const PARAM_MAX
Definition ApiBase.php:85
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1620
getMain()
Get the main module.
Definition ApiBase.php:513
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:179
const PARAM_DFLT
Definition ApiBase.php:73
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition ApiBase.php:1368
const PARAM_MIN
Definition ApiBase.php:93
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:220
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:226
getResult()
Get the result object.
Definition ApiBase.php:628
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:764
const LIMIT_SML1
Slow query, standard limit.
Definition ApiBase.php:224
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:162
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:222
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:497
const PARAM_ISMULTI
Definition ApiBase.php:77
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition ApiBase.php:559
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.
NameTableStore $changeTagDefStore
LinkBatchFactory $linkBatchFactory
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
__construct(ApiQuery $query, $moduleName, CommentStore $commentStore, RevisionStore $revisionStore, NameTableStore $changeTagDefStore, LinkBatchFactory $linkBatchFactory)
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.
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.
Handle database storage of comments such as edit summaries and log reasons.
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:1372
Type definition for user types.
Definition UserDef.php:25
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Exception representing a failure to look up a row from a name table.
Service for formatting and validating API parameters.
Type definition for integer types.
return true
Definition router.php:92