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