MediaWiki 1.43.1
ApiQueryAllDeletedRevisions.php
Go to the documentation of this file.
1<?php
26namespace MediaWiki\Api;
27
48
55
56 private RevisionStore $revisionStore;
57 private NameTableStore $changeTagDefStore;
58 private NamespaceInfo $namespaceInfo;
59
60 public function __construct(
61 ApiQuery $query,
62 string $moduleName,
63 RevisionStore $revisionStore,
64 IContentHandlerFactory $contentHandlerFactory,
65 ParserFactory $parserFactory,
66 SlotRoleRegistry $slotRoleRegistry,
67 NameTableStore $changeTagDefStore,
68 NamespaceInfo $namespaceInfo,
69 ContentRenderer $contentRenderer,
70 ContentTransformer $contentTransformer,
71 CommentFormatter $commentFormatter,
72 TempUserCreator $tempUserCreator,
73 UserFactory $userFactory
74 ) {
75 parent::__construct(
76 $query,
77 $moduleName,
78 'adr',
79 $revisionStore,
80 $contentHandlerFactory,
81 $parserFactory,
82 $slotRoleRegistry,
83 $contentRenderer,
84 $contentTransformer,
85 $commentFormatter,
86 $tempUserCreator,
87 $userFactory
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( [
181 'ts_tags' => MediaWikiServices::getInstance()->getChangeTagsStore()
182 ->makeTagSummarySubquery( 'archive' )
183 ] );
184 }
185
186 if ( $params['tag'] !== null ) {
187 $this->addTables( 'change_tag' );
188 $this->addJoinConds(
189 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
190 );
191 try {
192 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
193 } catch ( NameTableAccessException $exception ) {
194 // Return nothing.
195 $this->addWhere( '1=0' );
196 }
197 }
198
199 // This means stricter restrictions
200 if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
201 !$this->getAuthority()->isAllowed( 'deletedhistory' )
202 ) {
203 $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
204 }
205 if ( $this->fetchContent &&
206 !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
207 ) {
208 $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
209 }
210
211 $miser_ns = null;
212
213 if ( $mode == 'all' ) {
214 $namespaces = $params['namespace'] ?? $this->namespaceInfo->getValidNamespaces();
215 $this->addWhereFld( 'ar_namespace', $namespaces );
216
217 // For from/to/prefix, we have to consider the potential
218 // transformations of the title in all specified namespaces.
219 // Generally there will be only one transformation, but wikis with
220 // some namespaces case-sensitive could have two.
221 if ( $params['from'] !== null || $params['to'] !== null ) {
222 $isDirNewer = ( $dir === 'newer' );
223 $after = ( $isDirNewer ? '>=' : '<=' );
224 $before = ( $isDirNewer ? '<=' : '>=' );
225 $titleParts = [];
226 foreach ( $namespaces as $ns ) {
227 if ( $params['from'] !== null ) {
228 $fromTitlePart = $this->titlePartToKey( $params['from'], $ns );
229 } else {
230 $fromTitlePart = '';
231 }
232 if ( $params['to'] !== null ) {
233 $toTitlePart = $this->titlePartToKey( $params['to'], $ns );
234 } else {
235 $toTitlePart = '';
236 }
237 $titleParts[$fromTitlePart . '|' . $toTitlePart][] = $ns;
238 }
239 if ( count( $titleParts ) === 1 ) {
240 [ $fromTitlePart, $toTitlePart, ] = explode( '|', key( $titleParts ), 2 );
241 if ( $fromTitlePart !== '' ) {
242 $this->addWhere( $db->expr( 'ar_title', $after, $fromTitlePart ) );
243 }
244 if ( $toTitlePart !== '' ) {
245 $this->addWhere( $db->expr( 'ar_title', $before, $toTitlePart ) );
246 }
247 } else {
248 $where = [];
249 foreach ( $titleParts as $titlePart => $ns ) {
250 [ $fromTitlePart, $toTitlePart, ] = explode( '|', $titlePart, 2 );
251 $expr = $db->expr( 'ar_namespace', '=', $ns );
252 if ( $fromTitlePart !== '' ) {
253 $expr = $expr->and( 'ar_title', $after, $fromTitlePart );
254 }
255 if ( $toTitlePart !== '' ) {
256 $expr = $expr->and( 'ar_title', $before, $toTitlePart );
257 }
258 $where[] = $expr;
259 }
260 $this->addWhere( $db->orExpr( $where ) );
261 }
262 }
263
264 if ( isset( $params['prefix'] ) ) {
265 $titleParts = [];
266 foreach ( $namespaces as $ns ) {
267 $prefixTitlePart = $this->titlePartToKey( $params['prefix'], $ns );
268 $titleParts[$prefixTitlePart][] = $ns;
269 }
270 if ( count( $titleParts ) === 1 ) {
271 $prefixTitlePart = key( $titleParts );
272 $this->addWhere( $db->expr( 'ar_title', IExpression::LIKE,
273 new LikeValue( $prefixTitlePart, $db->anyString() )
274 ) );
275 } else {
276 $where = [];
277 foreach ( $titleParts as $prefixTitlePart => $ns ) {
278 $where[] = $db->expr( 'ar_namespace', '=', $ns )
279 ->and( 'ar_title', IExpression::LIKE,
280 new LikeValue( $prefixTitlePart, $db->anyString() ) );
281 }
282 $this->addWhere( $db->orExpr( $where ) );
283 }
284 }
285 } else {
286 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
287 $miser_ns = $params['namespace'];
288 } else {
289 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
290 }
291 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
292 }
293
294 if ( $params['user'] !== null ) {
295 // We could get the actor ID from the ActorStore, but it's probably
296 // uncached at this point, and the non-generator case needs an actor
297 // join anyway so adding this join here is normally free. This should
298 // use the ar_actor_timestamp index.
299 $this->addWhereFld( 'actor_name', $params['user'] );
300 } elseif ( $params['excludeuser'] !== null ) {
301 $this->addWhere( $db->expr( 'actor_name', '!=', $params['excludeuser'] ) );
302 }
303
304 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
305 // Paranoia: avoid brute force searches (T19342)
306 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
307 $bitmask = RevisionRecord::DELETED_USER;
308 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
309 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
310 } else {
311 $bitmask = 0;
312 }
313 if ( $bitmask ) {
314 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
315 }
316 }
317
318 if ( $params['continue'] !== null ) {
319 $op = ( $dir == 'newer' ? '>=' : '<=' );
320 if ( $optimizeGenerateTitles ) {
321 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string' ] );
322 $this->addWhere( $db->buildComparison( $op, [
323 'ar_namespace' => $cont[0],
324 'ar_title' => $cont[1],
325 ] ) );
326 } elseif ( $mode == 'all' ) {
327 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] );
328 $this->addWhere( $db->buildComparison( $op, [
329 'ar_namespace' => $cont[0],
330 'ar_title' => $cont[1],
331 'ar_timestamp' => $db->timestamp( $cont[2] ),
332 'ar_id' => $cont[3],
333 ] ) );
334 } else {
335 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
336 $this->addWhere( $db->buildComparison( $op, [
337 'ar_timestamp' => $db->timestamp( $cont[0] ),
338 'ar_id' => $cont[1],
339 ] ) );
340 }
341 }
342
343 $this->addOption( 'LIMIT', $this->limit + 1 );
344
345 $sort = ( $dir == 'newer' ? '' : ' DESC' );
346 $orderby = [];
347 if ( $optimizeGenerateTitles ) {
348 // Targeting index ar_name_title_timestamp
349 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
350 $orderby[] = "ar_namespace $sort";
351 }
352 $orderby[] = "ar_title $sort";
353 } elseif ( $mode == 'all' ) {
354 // Targeting index ar_name_title_timestamp
355 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
356 $orderby[] = "ar_namespace $sort";
357 }
358 $orderby[] = "ar_title $sort";
359 $orderby[] = "ar_timestamp $sort";
360 $orderby[] = "ar_id $sort";
361 } else {
362 // Targeting index usertext_timestamp
363 // 'user' is always constant.
364 $orderby[] = "ar_timestamp $sort";
365 $orderby[] = "ar_id $sort";
366 }
367 $this->addOption( 'ORDER BY', $orderby );
368
369 $res = $this->select( __METHOD__ );
370
371 if ( $resultPageSet === null ) {
372 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'ar' );
373 }
374
375 $pageMap = []; // Maps ns&title to array index
376 $count = 0;
377 $nextIndex = 0;
378 $generated = [];
379 foreach ( $res as $row ) {
380 if ( ++$count > $this->limit ) {
381 // We've had enough
382 if ( $optimizeGenerateTitles ) {
383 $this->setContinueEnumParameter( 'continue', "$row->ar_namespace|$row->ar_title" );
384 } elseif ( $mode == 'all' ) {
385 $this->setContinueEnumParameter( 'continue',
386 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
387 );
388 } else {
389 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
390 }
391 break;
392 }
393
394 // Miser mode namespace check
395 if ( $miser_ns !== null && !in_array( $row->ar_namespace, $miser_ns ) ) {
396 continue;
397 }
398
399 if ( $resultPageSet !== null ) {
400 if ( $params['generatetitles'] ) {
401 $key = "{$row->ar_namespace}:{$row->ar_title}";
402 if ( !isset( $generated[$key] ) ) {
403 $generated[$key] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
404 }
405 } else {
406 $generated[] = $row->ar_rev_id;
407 }
408 } else {
409 $revision = $this->revisionStore->newRevisionFromArchiveRow( $row );
410 $rev = $this->extractRevisionInfo( $revision, $row );
411
412 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
413 $index = $nextIndex++;
414 $pageMap[$row->ar_namespace][$row->ar_title] = $index;
415 $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
416 $a = [
417 'pageid' => $title->getArticleID(),
418 'revisions' => [ $rev ],
419 ];
420 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
421 ApiQueryBase::addTitleInfo( $a, $title );
422 $fit = $result->addValue( [ 'query', $this->getModuleName() ], $index, $a );
423 } else {
424 $index = $pageMap[$row->ar_namespace][$row->ar_title];
425 $fit = $result->addValue(
426 [ 'query', $this->getModuleName(), $index, 'revisions' ],
427 null, $rev );
428 }
429 if ( !$fit ) {
430 if ( $mode == 'all' ) {
431 $this->setContinueEnumParameter( 'continue',
432 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
433 );
434 } else {
435 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
436 }
437 break;
438 }
439 }
440 }
441
442 if ( $resultPageSet !== null ) {
443 if ( $params['generatetitles'] ) {
444 $resultPageSet->populateFromTitles( $generated );
445 } else {
446 $resultPageSet->populateFromRevisionIDs( $generated );
447 }
448 } else {
449 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
450 }
451 }
452
453 public function getAllowedParams() {
454 $ret = parent::getAllowedParams() + [
455 'user' => [
456 ParamValidator::PARAM_TYPE => 'user',
457 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
458 ],
459 'namespace' => [
460 ParamValidator::PARAM_ISMULTI => true,
461 ParamValidator::PARAM_TYPE => 'namespace',
462 ],
463 'start' => [
464 ParamValidator::PARAM_TYPE => 'timestamp',
465 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
466 ],
467 'end' => [
468 ParamValidator::PARAM_TYPE => 'timestamp',
469 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
470 ],
471 'dir' => [
472 ParamValidator::PARAM_TYPE => [
473 'newer',
474 'older'
475 ],
476 ParamValidator::PARAM_DEFAULT => 'older',
477 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
479 'newer' => 'api-help-paramvalue-direction-newer',
480 'older' => 'api-help-paramvalue-direction-older',
481 ],
482 ],
483 'from' => [
484 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
485 ],
486 'to' => [
487 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
488 ],
489 'prefix' => [
490 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
491 ],
492 'excludeuser' => [
493 ParamValidator::PARAM_TYPE => 'user',
494 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
495 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
496 ],
497 'tag' => null,
498 'continue' => [
499 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
500 ],
501 'generatetitles' => [
502 ParamValidator::PARAM_DEFAULT => false
503 ],
504 ];
505
506 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
507 $ret['user'][ApiBase::PARAM_HELP_MSG_APPEND] = [
508 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
509 ];
510 $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
511 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
512 ];
513 }
514
515 return $ret;
516 }
517
518 protected function getExamplesMessages() {
519 return [
520 'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
521 => 'apihelp-query+alldeletedrevisions-example-user',
522 'action=query&list=alldeletedrevisions&adrdir=newer&adrnamespace=0&adrlimit=50'
523 => 'apihelp-query+alldeletedrevisions-example-ns-main',
524 ];
525 }
526
527 public function getHelpUrls() {
528 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Alldeletedrevisions';
529 }
530}
531
533class_alias( ApiQueryAllDeletedRevisions::class, 'ApiQueryAllDeletedRevisions' );
array $params
The job parameters.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1577
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:580
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:571
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:202
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1768
getResult()
Get the result object.
Definition ApiBase.php:710
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:224
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1495
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:192
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:184
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:851
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:973
This class contains a list of pages that the client has requested.
Query module to enumerate all deleted revisions.
getHelpUrls()
Return links to more detailed help pages about the module.
__construct(ApiQuery $query, string $moduleName, RevisionStore $revisionStore, IContentHandlerFactory $contentHandlerFactory, ParserFactory $parserFactory, SlotRoleRegistry $slotRoleRegistry, NameTableStore $changeTagDefStore, NamespaceInfo $namespaceInfo, ContentRenderer $contentRenderer, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter, TempUserCreator $tempUserCreator, UserFactory $userFactory)
getExamplesMessages()
Returns usage examples for this module.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
titlePartToKey( $titlePart, $namespace=NS_MAIN)
Convert an input title or title prefix into a dbkey.
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
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.
extractRevisionInfo(RevisionRecord $revision, $row)
Extract information from the RevisionRecord.
parseParameters( $params)
Parse the parameters into the various instance fields.
This is the main query class.
Definition ApiQuery.php:48
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
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.
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
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.
Content of like value.
Definition LikeValue.php:14
addTables( $tables, $alias=null)
addWhere( $conds)
addJoinConds( $conds)
addFields( $fields)