MediaWiki REL1_37
ApiQueryAllDeletedRevisions.php
Go to the documentation of this file.
1<?php
34
41
44
47
50
62 public function __construct(
63 ApiQuery $query,
64 $moduleName,
72 ) {
73 parent::__construct(
74 $query,
75 $moduleName,
76 'adr',
82 );
83 $this->revisionStore = $revisionStore;
84 $this->changeTagDefStore = $changeTagDefStore;
85 $this->namespaceInfo = $namespaceInfo;
86 }
87
92 protected function run( ApiPageSet $resultPageSet = null ) {
93 $user = $this->getUser();
94 $db = $this->getDB();
95 $params = $this->extractRequestParams( false );
96
97 $result = $this->getResult();
98
99 // If the user wants no namespaces, they get no pages.
100 if ( $params['namespace'] === [] ) {
101 if ( $resultPageSet === null ) {
102 $result->addValue( 'query', $this->getModuleName(), [] );
103 }
104 return;
105 }
106
107 // This module operates in two modes:
108 // 'user': List deleted revs by a certain user
109 // 'all': List all deleted revs in NS
110 $mode = 'all';
111 if ( $params['user'] !== null ) {
112 $mode = 'user';
113 }
114
115 if ( $mode == 'user' ) {
116 foreach ( [ 'from', 'to', 'prefix', 'excludeuser' ] as $param ) {
117 if ( $params[$param] !== null ) {
118 $p = $this->getModulePrefix();
119 $this->dieWithError(
120 [ 'apierror-invalidparammix-cannotusewith', $p . $param, "{$p}user" ],
121 'invalidparammix'
122 );
123 }
124 }
125 } else {
126 foreach ( [ 'start', 'end' ] as $param ) {
127 if ( $params[$param] !== null ) {
128 $p = $this->getModulePrefix();
129 $this->dieWithError(
130 [ 'apierror-invalidparammix-mustusewith', $p . $param, "{$p}user" ],
131 'invalidparammix'
132 );
133 }
134 }
135 }
136
137 // If we're generating titles only, we can use DISTINCT for a better
138 // query. But we can't do that in 'user' mode (wrong index), and we can
139 // only do it when sorting ASC (because MySQL apparently can't use an
140 // index backwards for grouping even though it can for ORDER BY, WTF?)
141 $dir = $params['dir'];
142 $optimizeGenerateTitles = false;
143 if ( $mode === 'all' && $params['generatetitles'] && $resultPageSet !== null ) {
144 if ( $dir === 'newer' ) {
145 $optimizeGenerateTitles = true;
146 } else {
147 $p = $this->getModulePrefix();
148 $this->addWarning( [ 'apiwarn-alldeletedrevisions-performance', $p ], 'performance' );
149 }
150 }
151
152 if ( $resultPageSet === null ) {
153 $this->parseParameters( $params );
154 $arQuery = $this->revisionStore->getArchiveQueryInfo();
155 $this->addTables( $arQuery['tables'] );
156 $this->addJoinConds( $arQuery['joins'] );
157 $this->addFields( $arQuery['fields'] );
158 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
159 } else {
160 $this->limit = $this->getParameter( 'limit' ) ?: 10;
161 $this->addTables( 'archive' );
162 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
163 if ( $optimizeGenerateTitles ) {
164 $this->addOption( 'DISTINCT' );
165 } else {
166 $this->addFields( [ 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
167 }
168 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
169 $this->addTables( 'actor' );
170 $this->addJoinConds( [ 'actor' => 'actor_id=ar_actor' ] );
171 }
172 }
173
174 if ( $this->fld_tags ) {
175 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
176 }
177
178 if ( $params['tag'] !== null ) {
179 $this->addTables( 'change_tag' );
180 $this->addJoinConds(
181 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
182 );
183 try {
184 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
185 } catch ( NameTableAccessException $exception ) {
186 // Return nothing.
187 $this->addWhere( '1=0' );
188 }
189 }
190
191 // This means stricter restrictions
192 if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
193 !$this->getAuthority()->isAllowed( 'deletedhistory' )
194 ) {
195 $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
196 }
197 if ( $this->fetchContent &&
198 !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
199 ) {
200 $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
201 }
202
203 $miser_ns = null;
204
205 if ( $mode == 'all' ) {
206 $namespaces = $params['namespace'] ?? $this->namespaceInfo->getValidNamespaces();
207 $this->addWhereFld( 'ar_namespace', $namespaces );
208
209 // For from/to/prefix, we have to consider the potential
210 // transformations of the title in all specified namespaces.
211 // Generally there will be only one transformation, but wikis with
212 // some namespaces case-sensitive could have two.
213 if ( $params['from'] !== null || $params['to'] !== null ) {
214 $isDirNewer = ( $dir === 'newer' );
215 $after = ( $isDirNewer ? '>=' : '<=' );
216 $before = ( $isDirNewer ? '<=' : '>=' );
217 $where = [];
218 foreach ( $namespaces as $ns ) {
219 $w = [];
220 if ( $params['from'] !== null ) {
221 $w[] = 'ar_title' . $after .
222 $db->addQuotes( $this->titlePartToKey( $params['from'], $ns ) );
223 }
224 if ( $params['to'] !== null ) {
225 $w[] = 'ar_title' . $before .
226 $db->addQuotes( $this->titlePartToKey( $params['to'], $ns ) );
227 }
228 $w = $db->makeList( $w, LIST_AND );
229 $where[$w][] = $ns;
230 }
231 if ( count( $where ) == 1 ) {
232 $where = key( $where );
233 $this->addWhere( $where );
234 } else {
235 $where2 = [];
236 foreach ( $where as $w => $ns ) {
237 $where2[] = $db->makeList( [ $w, 'ar_namespace' => $ns ], LIST_AND );
238 }
239 $this->addWhere( $db->makeList( $where2, LIST_OR ) );
240 }
241 }
242
243 if ( isset( $params['prefix'] ) ) {
244 $where = [];
245 foreach ( $namespaces as $ns ) {
246 $w = 'ar_title' . $db->buildLike(
247 $this->titlePartToKey( $params['prefix'], $ns ),
248 $db->anyString() );
249 $where[$w][] = $ns;
250 }
251 if ( count( $where ) == 1 ) {
252 $where = key( $where );
253 $this->addWhere( $where );
254 } else {
255 $where2 = [];
256 foreach ( $where as $w => $ns ) {
257 $where2[] = $db->makeList( [ $w, 'ar_namespace' => $ns ], LIST_AND );
258 }
259 $this->addWhere( $db->makeList( $where2, LIST_OR ) );
260 }
261 }
262 } else {
263 if ( $this->getConfig()->get( 'MiserMode' ) ) {
264 $miser_ns = $params['namespace'];
265 } else {
266 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
267 }
268 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
269 }
270
271 if ( $params['user'] !== null ) {
272 // We could get the actor ID from the ActorStore, but it's probably
273 // uncached at this point, and the non-generator case needs an actor
274 // join anyway so adding this join here is normally free. This should
275 // use the ar_actor_timestamp index.
276 $this->addWhereFld( 'actor_name', $params['user'] );
277 } elseif ( $params['excludeuser'] !== null ) {
278 $this->addWhere( 'actor_name<>' . $db->addQuotes( $params['excludeuser'] ) );
279 }
280
281 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
282 // Paranoia: avoid brute force searches (T19342)
283 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
284 $bitmask = RevisionRecord::DELETED_USER;
285 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
286 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
287 } else {
288 $bitmask = 0;
289 }
290 if ( $bitmask ) {
291 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
292 }
293 }
294
295 if ( $params['continue'] !== null ) {
296 $cont = explode( '|', $params['continue'] );
297 $op = ( $dir == 'newer' ? '>' : '<' );
298 if ( $optimizeGenerateTitles ) {
299 $this->dieContinueUsageIf( count( $cont ) != 2 );
300 $ns = (int)$cont[0];
301 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
302 $title = $db->addQuotes( $cont[1] );
303 $this->addWhere( "ar_namespace $op $ns OR " .
304 "(ar_namespace = $ns AND ar_title $op= $title)" );
305 } elseif ( $mode == 'all' ) {
306 $this->dieContinueUsageIf( count( $cont ) != 4 );
307 $ns = (int)$cont[0];
308 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
309 $title = $db->addQuotes( $cont[1] );
310 $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
311 $ar_id = (int)$cont[3];
312 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
313 $this->addWhere( "ar_namespace $op $ns OR " .
314 "(ar_namespace = $ns AND " .
315 "(ar_title $op $title OR " .
316 "(ar_title = $title AND " .
317 "(ar_timestamp $op $ts OR " .
318 "(ar_timestamp = $ts AND " .
319 "ar_id $op= $ar_id)))))" );
320 } else {
321 $this->dieContinueUsageIf( count( $cont ) != 2 );
322 $ts = $db->addQuotes( $db->timestamp( $cont[0] ) );
323 $ar_id = (int)$cont[1];
324 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
325 $this->addWhere( "ar_timestamp $op $ts OR " .
326 "(ar_timestamp = $ts AND " .
327 "ar_id $op= $ar_id)" );
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::newFromLinkTarget( $revision->getPageAsLinkTarget() );
404 $a = [
405 'pageid' => $title->getArticleID(),
406 'revisions' => [ $rev ],
407 ];
408 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
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
441 public function getAllowedParams() {
442 $ret = parent::getAllowedParams() + [
443 'user' => [
444 ApiBase::PARAM_TYPE => 'user',
445 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
446 ],
447 'namespace' => [
449 ApiBase::PARAM_TYPE => 'namespace',
450 ],
451 'start' => [
452 ApiBase::PARAM_TYPE => 'timestamp',
453 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
454 ],
455 'end' => [
456 ApiBase::PARAM_TYPE => 'timestamp',
457 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
458 ],
459 'dir' => [
461 'newer',
462 'older'
463 ],
464 ApiBase::PARAM_DFLT => 'older',
465 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
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 ApiBase::PARAM_TYPE => 'user',
478 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', '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 ApiBase::PARAM_DFLT => false
487 ],
488 ];
489
490 if ( $this->getConfig()->get( '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
502 protected function getExamplesMessages() {
503 return [
504 'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
505 => 'apihelp-query+alldeletedrevisions-example-user',
506 'action=query&list=alldeletedrevisions&adrdir=newer&adrnamespace=0&adrlimit=50'
507 => 'apihelp-query+alldeletedrevisions-example-ns-main',
508 ];
509 }
510
511 public function getHelpUrls() {
512 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Alldeletedrevisions';
513 }
514}
getAuthority()
const LIST_OR
Definition Defines.php:46
const LIST_AND
Definition Defines.php:43
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1436
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:505
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:884
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1620
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:179
const PARAM_DFLT
Definition ApiBase.php:73
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:169
getResult()
Get the result object.
Definition ApiBase.php:628
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:764
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:162
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1354
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:497
const PARAM_ISMULTI
Definition ApiBase.php:77
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.
run(ApiPageSet $resultPageSet=null)
__construct(ApiQuery $query, $moduleName, RevisionStore $revisionStore, IContentHandlerFactory $contentHandlerFactory, ParserFactory $parserFactory, SlotRoleRegistry $slotRoleRegistry, NameTableStore $changeTagDefStore, NamespaceInfo $namespaceInfo, ContentTransformer $contentTransformer)
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.
IContentHandlerFactory $contentHandlerFactory
ContentTransformer $contentTransformer
SlotRoleRegistry $slotRoleRegistry
extractRevisionInfo(RevisionRecord $revision, $row)
Extract information from the RevisionRecord.
This is the main query class.
Definition ApiQuery.php:37
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Type definition for user types.
Definition UserDef.php:25
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...