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