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', $this->getAuthority() )
162 ] );
163 }
164
165 if ( $params['tag'] !== null ) {
166 if ( !$this->changeTagsStore->canViewTag( $params['tag'], $this->getAuthority() ) ) {
167 $this->addWhere( '1=0' );
168 }
169
170 $this->addTables( 'change_tag' );
171 $this->addJoinConds(
172 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
173 );
174 try {
175 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
176 } catch ( NameTableAccessException ) {
177 // Return nothing.
178 $this->addWhere( '1=0' );
179 }
180 }
181
182 // This means stricter restrictions
183 if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
184 !$this->getAuthority()->isAllowed( 'deletedhistory' )
185 ) {
186 $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
187 }
188 if ( $this->fetchContent &&
189 !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
190 ) {
191 $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
192 }
193
194 $miser_ns = null;
195
196 if ( $mode == 'all' ) {
197 $namespaces = $params['namespace'] ?? $this->namespaceInfo->getValidNamespaces();
198 $this->addWhereFld( 'ar_namespace', $namespaces );
199
200 // For from/to/prefix, we have to consider the potential
201 // transformations of the title in all specified namespaces.
202 // Generally there will be only one transformation, but wikis with
203 // some namespaces case-sensitive could have two.
204 if ( $params['from'] !== null || $params['to'] !== null ) {
205 $isDirNewer = ( $dir === 'newer' );
206 $after = ( $isDirNewer ? '>=' : '<=' );
207 $before = ( $isDirNewer ? '<=' : '>=' );
208 $titleParts = [];
209 foreach ( $namespaces as $ns ) {
210 if ( $params['from'] !== null ) {
211 $fromTitlePart = $this->titlePartToKey( $params['from'], $ns );
212 } else {
213 $fromTitlePart = '';
214 }
215 if ( $params['to'] !== null ) {
216 $toTitlePart = $this->titlePartToKey( $params['to'], $ns );
217 } else {
218 $toTitlePart = '';
219 }
220 $titleParts[$fromTitlePart . '|' . $toTitlePart][] = $ns;
221 }
222 if ( count( $titleParts ) === 1 ) {
223 [ $fromTitlePart, $toTitlePart, ] = explode( '|', key( $titleParts ), 2 );
224 if ( $fromTitlePart !== '' ) {
225 $this->addWhere( $db->expr( 'ar_title', $after, $fromTitlePart ) );
226 }
227 if ( $toTitlePart !== '' ) {
228 $this->addWhere( $db->expr( 'ar_title', $before, $toTitlePart ) );
229 }
230 } else {
231 $where = [];
232 foreach ( $titleParts as $titlePart => $ns ) {
233 [ $fromTitlePart, $toTitlePart, ] = explode( '|', $titlePart, 2 );
234 $expr = $db->expr( 'ar_namespace', '=', $ns );
235 if ( $fromTitlePart !== '' ) {
236 $expr = $expr->and( 'ar_title', $after, $fromTitlePart );
237 }
238 if ( $toTitlePart !== '' ) {
239 $expr = $expr->and( 'ar_title', $before, $toTitlePart );
240 }
241 $where[] = $expr;
242 }
243 $this->addWhere( $db->orExpr( $where ) );
244 }
245 }
246
247 if ( isset( $params['prefix'] ) ) {
248 $titleParts = [];
249 foreach ( $namespaces as $ns ) {
250 $prefixTitlePart = $this->titlePartToKey( $params['prefix'], $ns );
251 $titleParts[$prefixTitlePart][] = $ns;
252 }
253 if ( count( $titleParts ) === 1 ) {
254 $prefixTitlePart = key( $titleParts );
255 $this->addWhere( $db->expr( 'ar_title', IExpression::LIKE,
256 new LikeValue( $prefixTitlePart, $db->anyString() )
257 ) );
258 } else {
259 $where = [];
260 foreach ( $titleParts as $prefixTitlePart => $ns ) {
261 $where[] = $db->expr( 'ar_namespace', '=', $ns )
262 ->and( 'ar_title', IExpression::LIKE,
263 new LikeValue( $prefixTitlePart, $db->anyString() ) );
264 }
265 $this->addWhere( $db->orExpr( $where ) );
266 }
267 }
268 } else {
269 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
270 $miser_ns = $params['namespace'];
271 } else {
272 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
273 }
274 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
275 }
276
277 if ( $params['user'] !== null ) {
278 // We could get the actor ID from the ActorStore, but it's probably
279 // uncached at this point, and the non-generator case needs an actor
280 // join anyway so adding this join here is normally free. This should
281 // use the ar_actor_timestamp index.
282 $this->addWhereFld( 'actor_name', $params['user'] );
283 } elseif ( $params['excludeuser'] !== null ) {
284 $this->addWhere( $db->expr( 'actor_name', '!=', $params['excludeuser'] ) );
285 }
286
287 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
288 // Paranoia: avoid brute force searches (T19342)
289 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
290 $bitmask = RevisionRecord::DELETED_USER;
291 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
292 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
293 } else {
294 $bitmask = 0;
295 }
296 if ( $bitmask ) {
297 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
298 }
299 }
300
301 if ( $params['continue'] !== null ) {
302 $op = ( $dir == 'newer' ? '>=' : '<=' );
303 if ( $optimizeGenerateTitles ) {
304 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string' ] );
305 $this->addWhere( $db->buildComparison( $op, [
306 'ar_namespace' => $cont[0],
307 'ar_title' => $cont[1],
308 ] ) );
309 } elseif ( $mode == 'all' ) {
310 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] );
311 $this->addWhere( $db->buildComparison( $op, [
312 'ar_namespace' => $cont[0],
313 'ar_title' => $cont[1],
314 'ar_timestamp' => $db->timestamp( $cont[2] ),
315 'ar_id' => $cont[3],
316 ] ) );
317 } else {
318 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
319 $this->addWhere( $db->buildComparison( $op, [
320 'ar_timestamp' => $db->timestamp( $cont[0] ),
321 'ar_id' => $cont[1],
322 ] ) );
323 }
324 }
325
326 $this->addOption( 'LIMIT', $this->limit + 1 );
327
328 $sort = ( $dir == 'newer' ? '' : ' DESC' );
329 $orderby = [];
330 if ( $optimizeGenerateTitles ) {
331 // Targeting index ar_name_title_timestamp
332 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
333 $orderby[] = "ar_namespace $sort";
334 }
335 $orderby[] = "ar_title $sort";
336 } elseif ( $mode == 'all' ) {
337 // Targeting index ar_name_title_timestamp
338 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
339 $orderby[] = "ar_namespace $sort";
340 }
341 $orderby[] = "ar_title $sort";
342 $orderby[] = "ar_timestamp $sort";
343 $orderby[] = "ar_id $sort";
344 } else {
345 // Targeting index usertext_timestamp
346 // 'user' is always constant.
347 $orderby[] = "ar_timestamp $sort";
348 $orderby[] = "ar_id $sort";
349 }
350 $this->addOption( 'ORDER BY', $orderby );
351
352 $res = $this->select( __METHOD__ );
353
354 if ( $resultPageSet === null ) {
355 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'ar' );
356 }
357
358 $pageMap = []; // Maps ns&title to array index
359 $count = 0;
360 $nextIndex = 0;
361 $generated = [];
362 foreach ( $res as $row ) {
363 if ( ++$count > $this->limit ) {
364 // We've had enough
365 if ( $optimizeGenerateTitles ) {
366 $this->setContinueEnumParameter( 'continue', "$row->ar_namespace|$row->ar_title" );
367 } elseif ( $mode == 'all' ) {
368 $this->setContinueEnumParameter( 'continue',
369 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
370 );
371 } else {
372 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
373 }
374 break;
375 }
376
377 // Miser mode namespace check
378 if ( $miser_ns !== null && !in_array( $row->ar_namespace, $miser_ns ) ) {
379 continue;
380 }
381
382 if ( $resultPageSet !== null ) {
383 if ( $params['generatetitles'] ) {
384 $key = "{$row->ar_namespace}:{$row->ar_title}";
385 if ( !isset( $generated[$key] ) ) {
386 $generated[$key] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
387 }
388 } else {
389 $generated[] = $row->ar_rev_id;
390 }
391 } else {
392 $revision = $this->revisionStore->newRevisionFromArchiveRow( $row );
393 $rev = $this->extractRevisionInfo( $revision, $row );
394
395 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
396 $index = $nextIndex++;
397 $pageMap[$row->ar_namespace][$row->ar_title] = $index;
398 $title = Title::newFromPageIdentity( $revision->getPage() );
399 $a = [
400 'pageid' => $title->getArticleID(),
401 'revisions' => [ $rev ],
402 ];
403 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
404 ApiQueryBase::addTitleInfo( $a, $title );
405 $fit = $result->addValue( [ 'query', $this->getModuleName() ], $index, $a );
406 } else {
407 $index = $pageMap[$row->ar_namespace][$row->ar_title];
408 $fit = $result->addValue(
409 [ 'query', $this->getModuleName(), $index, 'revisions' ],
410 null, $rev );
411 }
412 if ( !$fit ) {
413 if ( $mode == 'all' ) {
414 $this->setContinueEnumParameter( 'continue',
415 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
416 );
417 } else {
418 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
419 }
420 break;
421 }
422 }
423 }
424
425 if ( $resultPageSet !== null ) {
426 if ( $params['generatetitles'] ) {
427 $resultPageSet->populateFromTitles( $generated );
428 } else {
429 $resultPageSet->populateFromRevisionIDs( $generated );
430 }
431 } else {
432 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
433 }
434 }
435
437 public function getAllowedParams() {
438 $ret = parent::getAllowedParams() + [
439 'user' => [
440 ParamValidator::PARAM_TYPE => 'user',
441 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
442 ],
443 'namespace' => [
444 ParamValidator::PARAM_ISMULTI => true,
445 ParamValidator::PARAM_TYPE => 'namespace',
446 ],
447 'start' => [
448 ParamValidator::PARAM_TYPE => 'timestamp',
449 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
450 ],
451 'end' => [
452 ParamValidator::PARAM_TYPE => 'timestamp',
453 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
454 ],
455 'dir' => [
456 ParamValidator::PARAM_TYPE => [
457 'newer',
458 'older'
459 ],
460 ParamValidator::PARAM_DEFAULT => 'older',
461 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
463 'newer' => 'api-help-paramvalue-direction-newer',
464 'older' => 'api-help-paramvalue-direction-older',
465 ],
466 ],
467 'from' => [
468 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
469 ],
470 'to' => [
471 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
472 ],
473 'prefix' => [
474 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
475 ],
476 'excludeuser' => [
477 ParamValidator::PARAM_TYPE => 'user',
478 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
479 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
480 ],
481 'tag' => null,
482 'continue' => [
483 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
484 ],
485 'generatetitles' => [
486 ParamValidator::PARAM_DEFAULT => false
487 ],
488 ];
489
490 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
491 $ret['user'][ApiBase::PARAM_HELP_MSG_APPEND] = [
492 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
493 ];
494 $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
495 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
496 ];
497 }
498
499 return $ret;
500 }
501
503 protected function getExamplesMessages() {
504 return [
505 'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
506 => 'apihelp-query+alldeletedrevisions-example-user',
507 'action=query&list=alldeletedrevisions&adrdir=newer&adrnamespace=0&adrlimit=50'
508 => 'apihelp-query+alldeletedrevisions-example-ns-main',
509 ];
510 }
511
513 public function getHelpUrls() {
514 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Alldeletedrevisions';
515 }
516}
517
519class_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)