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