MediaWiki REL1_39
ApiQueryAllDeletedRevisions.php
Go to the documentation of this file.
1<?php
37
44
46 private $revisionStore;
47
49 private $changeTagDefStore;
50
52 private $namespaceInfo;
53
66 public function __construct(
67 ApiQuery $query,
68 $moduleName,
69 RevisionStore $revisionStore,
70 IContentHandlerFactory $contentHandlerFactory,
71 ParserFactory $parserFactory,
72 SlotRoleRegistry $slotRoleRegistry,
73 NameTableStore $changeTagDefStore,
74 NamespaceInfo $namespaceInfo,
75 ContentRenderer $contentRenderer,
76 ContentTransformer $contentTransformer
77 ) {
78 parent::__construct(
79 $query,
80 $moduleName,
81 'adr',
82 $revisionStore,
83 $contentHandlerFactory,
84 $parserFactory,
85 $slotRoleRegistry,
86 $contentRenderer,
87 $contentTransformer
88 );
89 $this->revisionStore = $revisionStore;
90 $this->changeTagDefStore = $changeTagDefStore;
91 $this->namespaceInfo = $namespaceInfo;
92 }
93
98 protected function run( ApiPageSet $resultPageSet = null ) {
99 $db = $this->getDB();
100 $params = $this->extractRequestParams( false );
101
102 $result = $this->getResult();
103
104 // If the user wants no namespaces, they get no pages.
105 if ( $params['namespace'] === [] ) {
106 if ( $resultPageSet === null ) {
107 $result->addValue( 'query', $this->getModuleName(), [] );
108 }
109 return;
110 }
111
112 // This module operates in two modes:
113 // 'user': List deleted revs by a certain user
114 // 'all': List all deleted revs in NS
115 $mode = 'all';
116 if ( $params['user'] !== null ) {
117 $mode = 'user';
118 }
119
120 if ( $mode == 'user' ) {
121 foreach ( [ 'from', 'to', 'prefix', 'excludeuser' ] as $param ) {
122 if ( $params[$param] !== null ) {
123 $p = $this->getModulePrefix();
124 $this->dieWithError(
125 [ 'apierror-invalidparammix-cannotusewith', $p . $param, "{$p}user" ],
126 'invalidparammix'
127 );
128 }
129 }
130 } else {
131 foreach ( [ 'start', 'end' ] as $param ) {
132 if ( $params[$param] !== null ) {
133 $p = $this->getModulePrefix();
134 $this->dieWithError(
135 [ 'apierror-invalidparammix-mustusewith', $p . $param, "{$p}user" ],
136 'invalidparammix'
137 );
138 }
139 }
140 }
141
142 // If we're generating titles only, we can use DISTINCT for a better
143 // query. But we can't do that in 'user' mode (wrong index), and we can
144 // only do it when sorting ASC (because MySQL apparently can't use an
145 // index backwards for grouping even though it can for ORDER BY, WTF?)
146 $dir = $params['dir'];
147 $optimizeGenerateTitles = false;
148 if ( $mode === 'all' && $params['generatetitles'] && $resultPageSet !== null ) {
149 if ( $dir === 'newer' ) {
150 $optimizeGenerateTitles = true;
151 } else {
152 $p = $this->getModulePrefix();
153 $this->addWarning( [ 'apiwarn-alldeletedrevisions-performance', $p ], 'performance' );
154 }
155 }
156
157 if ( $resultPageSet === null ) {
158 $this->parseParameters( $params );
159 $arQuery = $this->revisionStore->getArchiveQueryInfo();
160 $this->addTables( $arQuery['tables'] );
161 $this->addJoinConds( $arQuery['joins'] );
162 $this->addFields( $arQuery['fields'] );
163 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
164 } else {
165 $this->limit = $this->getParameter( 'limit' ) ?: 10;
166 $this->addTables( 'archive' );
167 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
168 if ( $optimizeGenerateTitles ) {
169 $this->addOption( 'DISTINCT' );
170 } else {
171 $this->addFields( [ 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
172 }
173 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
174 $this->addTables( 'actor' );
175 $this->addJoinConds( [ 'actor' => 'actor_id=ar_actor' ] );
176 }
177 }
178
179 if ( $this->fld_tags ) {
180 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
181 }
182
183 if ( $params['tag'] !== null ) {
184 $this->addTables( 'change_tag' );
185 $this->addJoinConds(
186 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
187 );
188 try {
189 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
190 } catch ( NameTableAccessException $exception ) {
191 // Return nothing.
192 $this->addWhere( '1=0' );
193 }
194 }
195
196 // This means stricter restrictions
197 if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
198 !$this->getAuthority()->isAllowed( 'deletedhistory' )
199 ) {
200 $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
201 }
202 if ( $this->fetchContent &&
203 !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
204 ) {
205 $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
206 }
207
208 $miser_ns = null;
209
210 if ( $mode == 'all' ) {
211 $namespaces = $params['namespace'] ?? $this->namespaceInfo->getValidNamespaces();
212 $this->addWhereFld( 'ar_namespace', $namespaces );
213
214 // For from/to/prefix, we have to consider the potential
215 // transformations of the title in all specified namespaces.
216 // Generally there will be only one transformation, but wikis with
217 // some namespaces case-sensitive could have two.
218 if ( $params['from'] !== null || $params['to'] !== null ) {
219 $isDirNewer = ( $dir === 'newer' );
220 $after = ( $isDirNewer ? '>=' : '<=' );
221 $before = ( $isDirNewer ? '<=' : '>=' );
222 $where = [];
223 foreach ( $namespaces as $ns ) {
224 $w = [];
225 if ( $params['from'] !== null ) {
226 $w[] = 'ar_title' . $after .
227 $db->addQuotes( $this->titlePartToKey( $params['from'], $ns ) );
228 }
229 if ( $params['to'] !== null ) {
230 $w[] = 'ar_title' . $before .
231 $db->addQuotes( $this->titlePartToKey( $params['to'], $ns ) );
232 }
233 $w = $db->makeList( $w, LIST_AND );
234 $where[$w][] = $ns;
235 }
236 if ( count( $where ) == 1 ) {
237 $where = key( $where );
238 $this->addWhere( $where );
239 } else {
240 $where2 = [];
241 foreach ( $where as $w => $ns ) {
242 $where2[] = $db->makeList( [ $w, 'ar_namespace' => $ns ], LIST_AND );
243 }
244 $this->addWhere( $db->makeList( $where2, LIST_OR ) );
245 }
246 }
247
248 if ( isset( $params['prefix'] ) ) {
249 $where = [];
250 foreach ( $namespaces as $ns ) {
251 $w = 'ar_title' . $db->buildLike(
252 $this->titlePartToKey( $params['prefix'], $ns ),
253 $db->anyString() );
254 $where[$w][] = $ns;
255 }
256 if ( count( $where ) == 1 ) {
257 $where = key( $where );
258 $this->addWhere( $where );
259 } else {
260 $where2 = [];
261 foreach ( $where as $w => $ns ) {
262 $where2[] = $db->makeList( [ $w, 'ar_namespace' => $ns ], LIST_AND );
263 }
264 $this->addWhere( $db->makeList( $where2, LIST_OR ) );
265 }
266 }
267 } else {
268 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
269 $miser_ns = $params['namespace'];
270 } else {
271 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
272 }
273 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
274 }
275
276 if ( $params['user'] !== null ) {
277 // We could get the actor ID from the ActorStore, but it's probably
278 // uncached at this point, and the non-generator case needs an actor
279 // join anyway so adding this join here is normally free. This should
280 // use the ar_actor_timestamp index.
281 $this->addWhereFld( 'actor_name', $params['user'] );
282 } elseif ( $params['excludeuser'] !== null ) {
283 $this->addWhere( 'actor_name<>' . $db->addQuotes( $params['excludeuser'] ) );
284 }
285
286 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
287 // Paranoia: avoid brute force searches (T19342)
288 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
289 $bitmask = RevisionRecord::DELETED_USER;
290 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
291 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
292 } else {
293 $bitmask = 0;
294 }
295 if ( $bitmask ) {
296 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
297 }
298 }
299
300 if ( $params['continue'] !== null ) {
301 $cont = explode( '|', $params['continue'] );
302 $op = ( $dir == 'newer' ? '>' : '<' );
303 if ( $optimizeGenerateTitles ) {
304 $this->dieContinueUsageIf( count( $cont ) != 2 );
305 $ns = (int)$cont[0];
306 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
307 $title = $db->addQuotes( $cont[1] );
308 $this->addWhere( "ar_namespace $op $ns OR " .
309 "(ar_namespace = $ns AND ar_title $op= $title)" );
310 } elseif ( $mode == 'all' ) {
311 $this->dieContinueUsageIf( count( $cont ) != 4 );
312 $ns = (int)$cont[0];
313 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
314 $title = $db->addQuotes( $cont[1] );
315 $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
316 $ar_id = (int)$cont[3];
317 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
318 $this->addWhere( "ar_namespace $op $ns OR " .
319 "(ar_namespace = $ns AND " .
320 "(ar_title $op $title OR " .
321 "(ar_title = $title AND " .
322 "(ar_timestamp $op $ts OR " .
323 "(ar_timestamp = $ts AND " .
324 "ar_id $op= $ar_id)))))" );
325 } else {
326 $this->dieContinueUsageIf( count( $cont ) != 2 );
327 $ts = $db->addQuotes( $db->timestamp( $cont[0] ) );
328 $ar_id = (int)$cont[1];
329 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
330 $this->addWhere( "ar_timestamp $op $ts OR " .
331 "(ar_timestamp = $ts AND " .
332 "ar_id $op= $ar_id)" );
333 }
334 }
335
336 $this->addOption( 'LIMIT', $this->limit + 1 );
337
338 $sort = ( $dir == 'newer' ? '' : ' DESC' );
339 $orderby = [];
340 if ( $optimizeGenerateTitles ) {
341 // Targeting index ar_name_title_timestamp
342 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
343 $orderby[] = "ar_namespace $sort";
344 }
345 $orderby[] = "ar_title $sort";
346 } elseif ( $mode == 'all' ) {
347 // Targeting index ar_name_title_timestamp
348 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
349 $orderby[] = "ar_namespace $sort";
350 }
351 $orderby[] = "ar_title $sort";
352 $orderby[] = "ar_timestamp $sort";
353 $orderby[] = "ar_id $sort";
354 } else {
355 // Targeting index usertext_timestamp
356 // 'user' is always constant.
357 $orderby[] = "ar_timestamp $sort";
358 $orderby[] = "ar_id $sort";
359 }
360 $this->addOption( 'ORDER BY', $orderby );
361
362 $res = $this->select( __METHOD__ );
363
364 if ( $resultPageSet === null ) {
365 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'ar' );
366 }
367
368 $pageMap = []; // Maps ns&title to array index
369 $count = 0;
370 $nextIndex = 0;
371 $generated = [];
372 foreach ( $res as $row ) {
373 if ( ++$count > $this->limit ) {
374 // We've had enough
375 if ( $optimizeGenerateTitles ) {
376 $this->setContinueEnumParameter( 'continue', "$row->ar_namespace|$row->ar_title" );
377 } elseif ( $mode == 'all' ) {
378 $this->setContinueEnumParameter( 'continue',
379 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
380 );
381 } else {
382 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
383 }
384 break;
385 }
386
387 // Miser mode namespace check
388 if ( $miser_ns !== null && !in_array( $row->ar_namespace, $miser_ns ) ) {
389 continue;
390 }
391
392 if ( $resultPageSet !== null ) {
393 if ( $params['generatetitles'] ) {
394 $key = "{$row->ar_namespace}:{$row->ar_title}";
395 if ( !isset( $generated[$key] ) ) {
396 $generated[$key] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
397 }
398 } else {
399 $generated[] = $row->ar_rev_id;
400 }
401 } else {
402 $revision = $this->revisionStore->newRevisionFromArchiveRow( $row );
403 $rev = $this->extractRevisionInfo( $revision, $row );
404
405 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
406 $index = $nextIndex++;
407 $pageMap[$row->ar_namespace][$row->ar_title] = $index;
408 $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
409 $a = [
410 'pageid' => $title->getArticleID(),
411 'revisions' => [ $rev ],
412 ];
413 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
415 $fit = $result->addValue( [ 'query', $this->getModuleName() ], $index, $a );
416 } else {
417 $index = $pageMap[$row->ar_namespace][$row->ar_title];
418 $fit = $result->addValue(
419 [ 'query', $this->getModuleName(), $index, 'revisions' ],
420 null, $rev );
421 }
422 if ( !$fit ) {
423 if ( $mode == 'all' ) {
424 $this->setContinueEnumParameter( 'continue',
425 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
426 );
427 } else {
428 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
429 }
430 break;
431 }
432 }
433 }
434
435 if ( $resultPageSet !== null ) {
436 if ( $params['generatetitles'] ) {
437 $resultPageSet->populateFromTitles( $generated );
438 } else {
439 $resultPageSet->populateFromRevisionIDs( $generated );
440 }
441 } else {
442 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
443 }
444 }
445
446 public function getAllowedParams() {
447 $ret = parent::getAllowedParams() + [
448 'user' => [
449 ParamValidator::PARAM_TYPE => 'user',
450 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
451 ],
452 'namespace' => [
453 ParamValidator::PARAM_ISMULTI => true,
454 ParamValidator::PARAM_TYPE => 'namespace',
455 ],
456 'start' => [
457 ParamValidator::PARAM_TYPE => 'timestamp',
458 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
459 ],
460 'end' => [
461 ParamValidator::PARAM_TYPE => 'timestamp',
462 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
463 ],
464 'dir' => [
465 ParamValidator::PARAM_TYPE => [
466 'newer',
467 'older'
468 ],
469 ParamValidator::PARAM_DEFAULT => 'older',
470 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
472 'newer' => 'api-help-paramvalue-direction-newer',
473 'older' => 'api-help-paramvalue-direction-older',
474 ],
475 ],
476 'from' => [
477 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
478 ],
479 'to' => [
480 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
481 ],
482 'prefix' => [
483 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
484 ],
485 'excludeuser' => [
486 ParamValidator::PARAM_TYPE => 'user',
487 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
488 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
489 ],
490 'tag' => null,
491 'continue' => [
492 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
493 ],
494 'generatetitles' => [
495 ParamValidator::PARAM_DEFAULT => false
496 ],
497 ];
498
499 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
500 $ret['user'][ApiBase::PARAM_HELP_MSG_APPEND] = [
501 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
502 ];
503 $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
504 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
505 ];
506 }
507
508 return $ret;
509 }
510
511 protected function getExamplesMessages() {
512 return [
513 'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
514 => 'apihelp-query+alldeletedrevisions-example-user',
515 'action=query&list=alldeletedrevisions&adrdir=newer&adrnamespace=0&adrlimit=50'
516 => 'apihelp-query+alldeletedrevisions-example-ns-main',
517 ];
518 }
519
520 public function getHelpUrls() {
521 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Alldeletedrevisions';
522 }
523}
getAuthority()
const LIST_OR
Definition Defines.php:46
const LIST_AND
Definition Defines.php:43
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1454
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:506
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:886
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1643
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:180
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:170
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition ApiBase.php:196
getResult()
Get the result object.
Definition ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:765
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:163
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1372
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:498
This class contains a list of pages that the client has requested.
Query module to enumerate all deleted revisions.
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
__construct(ApiQuery $query, $moduleName, RevisionStore $revisionStore, IContentHandlerFactory $contentHandlerFactory, ParserFactory $parserFactory, SlotRoleRegistry $slotRoleRegistry, NameTableStore $changeTagDefStore, NamespaceInfo $namespaceInfo, ContentRenderer $contentRenderer, ContentTransformer $contentTransformer)
run(ApiPageSet $resultPageSet=null)
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
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)
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
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 ] )
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.
setContinueEnumParameter( $paramName, $paramValue)
Overridden to set the generator param if in generator mode.
A base class for functions common to producing a list of revisions.
parseParameters( $params)
Parse the parameters into the various instance fields.
extractRevisionInfo(RevisionRecord $revision, $row)
Extract information from the RevisionRecord.
This is the main query class.
Definition ApiQuery.php:41
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
A class containing constants representing the names of configuration variables.
Type definition for user types.
Definition UserDef.php:27
Page revision base class.
Service for looking up page revisions.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Exception representing a failure to look up a row from a name table.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Service for formatting and validating API parameters.