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