MediaWiki fundraising/REL1_35
ApiQueryAllDeletedRevisions.php
Go to the documentation of this file.
1<?php
30
37
38 public function __construct( ApiQuery $query, $moduleName ) {
39 parent::__construct( $query, $moduleName, 'adr' );
40 }
41
46 protected function run( ApiPageSet $resultPageSet = null ) {
47 $user = $this->getUser();
48 $db = $this->getDB();
49 $params = $this->extractRequestParams( false );
50 $services = MediaWikiServices::getInstance();
51 $revisionStore = $services->getRevisionStore();
52
53 $result = $this->getResult();
54
55 // If the user wants no namespaces, they get no pages.
56 if ( $params['namespace'] === [] ) {
57 if ( $resultPageSet === null ) {
58 $result->addValue( 'query', $this->getModuleName(), [] );
59 }
60 return;
61 }
62
63 // This module operates in two modes:
64 // 'user': List deleted revs by a certain user
65 // 'all': List all deleted revs in NS
66 $mode = 'all';
67 if ( $params['user'] !== null ) {
68 $mode = 'user';
69 }
70
71 if ( $mode == 'user' ) {
72 foreach ( [ 'from', 'to', 'prefix', 'excludeuser' ] as $param ) {
73 if ( $params[$param] !== null ) {
74 $p = $this->getModulePrefix();
75 $this->dieWithError(
76 [ 'apierror-invalidparammix-cannotusewith', $p . $param, "{$p}user" ],
77 'invalidparammix'
78 );
79 }
80 }
81 } else {
82 foreach ( [ 'start', 'end' ] as $param ) {
83 if ( $params[$param] !== null ) {
84 $p = $this->getModulePrefix();
85 $this->dieWithError(
86 [ 'apierror-invalidparammix-mustusewith', $p . $param, "{$p}user" ],
87 'invalidparammix'
88 );
89 }
90 }
91 }
92
93 // If we're generating titles only, we can use DISTINCT for a better
94 // query. But we can't do that in 'user' mode (wrong index), and we can
95 // only do it when sorting ASC (because MySQL apparently can't use an
96 // index backwards for grouping even though it can for ORDER BY, WTF?)
97 $dir = $params['dir'];
98 $optimizeGenerateTitles = false;
99 if ( $mode === 'all' && $params['generatetitles'] && $resultPageSet !== null ) {
100 if ( $dir === 'newer' ) {
101 $optimizeGenerateTitles = true;
102 } else {
103 $p = $this->getModulePrefix();
104 $this->addWarning( [ 'apiwarn-alldeletedrevisions-performance', $p ], 'performance' );
105 }
106 }
107
108 if ( $resultPageSet === null ) {
109 $this->parseParameters( $params );
110 $arQuery = $revisionStore->getArchiveQueryInfo();
111 $this->addTables( $arQuery['tables'] );
112 $this->addJoinConds( $arQuery['joins'] );
113 $this->addFields( $arQuery['fields'] );
114 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
115 } else {
116 $this->limit = $this->getParameter( 'limit' ) ?: 10;
117 $this->addTables( 'archive' );
118 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
119 if ( $optimizeGenerateTitles ) {
120 $this->addOption( 'DISTINCT' );
121 } else {
122 $this->addFields( [ 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
123 }
124 }
125
126 if ( $this->fld_tags ) {
127 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
128 }
129
130 if ( $params['tag'] !== null ) {
131 $this->addTables( 'change_tag' );
132 $this->addJoinConds(
133 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
134 );
135 $changeTagDefStore = $services->getChangeTagDefStore();
136 try {
137 $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
138 } catch ( NameTableAccessException $exception ) {
139 // Return nothing.
140 $this->addWhere( '1=0' );
141 }
142 }
143
144 // This means stricter restrictions
145 if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
146 !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' )
147 ) {
148 $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
149 }
150 if ( $this->fetchContent &&
151 !$this->getPermissionManager()->userHasAnyRight( $user, 'deletedtext', 'undelete' )
152 ) {
153 $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
154 }
155
156 $miser_ns = null;
157
158 if ( $mode == 'all' ) {
159 $namespaces = $params['namespace'] ??
160 $services->getNamespaceInfo()->getValidNamespaces();
161 $this->addWhereFld( 'ar_namespace', $namespaces );
162
163 // For from/to/prefix, we have to consider the potential
164 // transformations of the title in all specified namespaces.
165 // Generally there will be only one transformation, but wikis with
166 // some namespaces case-sensitive could have two.
167 if ( $params['from'] !== null || $params['to'] !== null ) {
168 $isDirNewer = ( $dir === 'newer' );
169 $after = ( $isDirNewer ? '>=' : '<=' );
170 $before = ( $isDirNewer ? '<=' : '>=' );
171 $where = [];
172 foreach ( $namespaces as $ns ) {
173 $w = [];
174 if ( $params['from'] !== null ) {
175 $w[] = 'ar_title' . $after .
176 $db->addQuotes( $this->titlePartToKey( $params['from'], $ns ) );
177 }
178 if ( $params['to'] !== null ) {
179 $w[] = 'ar_title' . $before .
180 $db->addQuotes( $this->titlePartToKey( $params['to'], $ns ) );
181 }
182 $w = $db->makeList( $w, LIST_AND );
183 $where[$w][] = $ns;
184 }
185 if ( count( $where ) == 1 ) {
186 $where = key( $where );
187 $this->addWhere( $where );
188 } else {
189 $where2 = [];
190 foreach ( $where as $w => $ns ) {
191 $where2[] = $db->makeList( [ $w, 'ar_namespace' => $ns ], LIST_AND );
192 }
193 $this->addWhere( $db->makeList( $where2, LIST_OR ) );
194 }
195 }
196
197 if ( isset( $params['prefix'] ) ) {
198 $where = [];
199 foreach ( $namespaces as $ns ) {
200 $w = 'ar_title' . $db->buildLike(
201 $this->titlePartToKey( $params['prefix'], $ns ),
202 $db->anyString() );
203 $where[$w][] = $ns;
204 }
205 if ( count( $where ) == 1 ) {
206 $where = key( $where );
207 $this->addWhere( $where );
208 } else {
209 $where2 = [];
210 foreach ( $where as $w => $ns ) {
211 $where2[] = $db->makeList( [ $w, 'ar_namespace' => $ns ], LIST_AND );
212 }
213 $this->addWhere( $db->makeList( $where2, LIST_OR ) );
214 }
215 }
216 } else {
217 if ( $this->getConfig()->get( 'MiserMode' ) ) {
218 $miser_ns = $params['namespace'];
219 } else {
220 $this->addWhereFld( 'ar_namespace', $params['namespace'] );
221 }
222 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
223 }
224
225 if ( $params['user'] !== null ) {
226 // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
227 $actorQuery = ActorMigration::newMigration()
228 ->getWhere( $db, 'ar_user', $params['user'], false );
229 $this->addTables( $actorQuery['tables'] );
230 $this->addJoinConds( $actorQuery['joins'] );
231 $this->addWhere( $actorQuery['conds'] );
232 } elseif ( $params['excludeuser'] !== null ) {
233 // Here there's no chance of using ar_usertext_timestamp.
234 $actorQuery = ActorMigration::newMigration()
235 ->getWhere( $db, 'ar_user', $params['excludeuser'] );
236 $this->addTables( $actorQuery['tables'] );
237 $this->addJoinConds( $actorQuery['joins'] );
238 $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
239 }
240
241 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
242 // Paranoia: avoid brute force searches (T19342)
243 if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
244 $bitmask = RevisionRecord::DELETED_USER;
245 } elseif ( !$this->getPermissionManager()
246 ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
247 ) {
248 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
249 } else {
250 $bitmask = 0;
251 }
252 if ( $bitmask ) {
253 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
254 }
255 }
256
257 if ( $params['continue'] !== null ) {
258 $cont = explode( '|', $params['continue'] );
259 $op = ( $dir == 'newer' ? '>' : '<' );
260 if ( $optimizeGenerateTitles ) {
261 $this->dieContinueUsageIf( count( $cont ) != 2 );
262 $ns = (int)$cont[0];
263 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
264 $title = $db->addQuotes( $cont[1] );
265 $this->addWhere( "ar_namespace $op $ns OR " .
266 "(ar_namespace = $ns AND ar_title $op= $title)" );
267 } elseif ( $mode == 'all' ) {
268 $this->dieContinueUsageIf( count( $cont ) != 4 );
269 $ns = (int)$cont[0];
270 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
271 $title = $db->addQuotes( $cont[1] );
272 $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
273 $ar_id = (int)$cont[3];
274 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
275 $this->addWhere( "ar_namespace $op $ns OR " .
276 "(ar_namespace = $ns AND " .
277 "(ar_title $op $title OR " .
278 "(ar_title = $title AND " .
279 "(ar_timestamp $op $ts OR " .
280 "(ar_timestamp = $ts AND " .
281 "ar_id $op= $ar_id)))))" );
282 } else {
283 $this->dieContinueUsageIf( count( $cont ) != 2 );
284 $ts = $db->addQuotes( $db->timestamp( $cont[0] ) );
285 $ar_id = (int)$cont[1];
286 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
287 $this->addWhere( "ar_timestamp $op $ts OR " .
288 "(ar_timestamp = $ts AND " .
289 "ar_id $op= $ar_id)" );
290 }
291 }
292
293 $this->addOption( 'LIMIT', $this->limit + 1 );
294
295 $sort = ( $dir == 'newer' ? '' : ' DESC' );
296 $orderby = [];
297 if ( $optimizeGenerateTitles ) {
298 // Targeting index name_title_timestamp
299 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
300 $orderby[] = "ar_namespace $sort";
301 }
302 $orderby[] = "ar_title $sort";
303 } elseif ( $mode == 'all' ) {
304 // Targeting index name_title_timestamp
305 if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
306 $orderby[] = "ar_namespace $sort";
307 }
308 $orderby[] = "ar_title $sort";
309 $orderby[] = "ar_timestamp $sort";
310 $orderby[] = "ar_id $sort";
311 } else {
312 // Targeting index usertext_timestamp
313 // 'user' is always constant.
314 $orderby[] = "ar_timestamp $sort";
315 $orderby[] = "ar_id $sort";
316 }
317 $this->addOption( 'ORDER BY', $orderby );
318
319 $res = $this->select( __METHOD__ );
320
321 if ( $resultPageSet === null ) {
322 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'ar' );
323 }
324
325 $pageMap = []; // Maps ns&title to array index
326 $count = 0;
327 $nextIndex = 0;
328 $generated = [];
329 foreach ( $res as $row ) {
330 if ( ++$count > $this->limit ) {
331 // We've had enough
332 if ( $optimizeGenerateTitles ) {
333 $this->setContinueEnumParameter( 'continue', "$row->ar_namespace|$row->ar_title" );
334 } elseif ( $mode == 'all' ) {
335 $this->setContinueEnumParameter( 'continue',
336 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
337 );
338 } else {
339 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
340 }
341 break;
342 }
343
344 // Miser mode namespace check
345 if ( $miser_ns !== null && !in_array( $row->ar_namespace, $miser_ns ) ) {
346 continue;
347 }
348
349 if ( $resultPageSet !== null ) {
350 if ( $params['generatetitles'] ) {
351 $key = "{$row->ar_namespace}:{$row->ar_title}";
352 if ( !isset( $generated[$key] ) ) {
353 $generated[$key] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
354 }
355 } else {
356 $generated[] = $row->ar_rev_id;
357 }
358 } else {
359 $revision = $revisionStore->newRevisionFromArchiveRow( $row );
360 $rev = $this->extractRevisionInfo( $revision, $row );
361
362 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
363 $index = $nextIndex++;
364 $pageMap[$row->ar_namespace][$row->ar_title] = $index;
365 $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
366 $a = [
367 'pageid' => $title->getArticleID(),
368 'revisions' => [ $rev ],
369 ];
370 ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
372 $fit = $result->addValue( [ 'query', $this->getModuleName() ], $index, $a );
373 } else {
374 $index = $pageMap[$row->ar_namespace][$row->ar_title];
375 $fit = $result->addValue(
376 [ 'query', $this->getModuleName(), $index, 'revisions' ],
377 null, $rev );
378 }
379 if ( !$fit ) {
380 if ( $mode == 'all' ) {
381 $this->setContinueEnumParameter( 'continue',
382 "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
383 );
384 } else {
385 $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
386 }
387 break;
388 }
389 }
390 }
391
392 if ( $resultPageSet !== null ) {
393 if ( $params['generatetitles'] ) {
394 $resultPageSet->populateFromTitles( $generated );
395 } else {
396 $resultPageSet->populateFromRevisionIDs( $generated );
397 }
398 } else {
399 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
400 }
401 }
402
403 public function getAllowedParams() {
404 $ret = parent::getAllowedParams() + [
405 'user' => [
406 ApiBase::PARAM_TYPE => 'user',
407 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
408 UserDef::PARAM_RETURN_OBJECT => true,
409 ],
410 'namespace' => [
412 ApiBase::PARAM_TYPE => 'namespace',
413 ],
414 'start' => [
415 ApiBase::PARAM_TYPE => 'timestamp',
416 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
417 ],
418 'end' => [
419 ApiBase::PARAM_TYPE => 'timestamp',
420 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'useronly' ] ],
421 ],
422 'dir' => [
424 'newer',
425 'older'
426 ],
427 ApiBase::PARAM_DFLT => 'older',
428 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
429 ],
430 'from' => [
431 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
432 ],
433 'to' => [
434 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
435 ],
436 'prefix' => [
437 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
438 ],
439 'excludeuser' => [
440 ApiBase::PARAM_TYPE => 'user',
441 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
442 UserDef::PARAM_RETURN_OBJECT => true,
443 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
444 ],
445 'tag' => null,
446 'continue' => [
447 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
448 ],
449 'generatetitles' => [
450 ApiBase::PARAM_DFLT => false
451 ],
452 ];
453
454 if ( $this->getConfig()->get( 'MiserMode' ) ) {
455 $ret['user'][ApiBase::PARAM_HELP_MSG_APPEND] = [
456 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
457 ];
458 $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [
459 'apihelp-query+alldeletedrevisions-param-miser-user-namespace',
460 ];
461 }
462
463 return $ret;
464 }
465
466 protected function getExamplesMessages() {
467 return [
468 'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
469 => 'apihelp-query+alldeletedrevisions-example-user',
470 'action=query&list=alldeletedrevisions&adrdir=newer&adrnamespace=0&adrlimit=50'
471 => 'apihelp-query+alldeletedrevisions-example-ns-main',
472 ];
473 }
474
475 public function getHelpUrls() {
476 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Alldeletedrevisions';
477 }
478}
getUser()
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1437
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:507
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:892
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1617
const PARAM_TYPE
Definition ApiBase.php:78
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:179
const PARAM_DFLT
Definition ApiBase.php:70
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:169
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:692
getResult()
Get the result object.
Definition ApiBase.php:620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:772
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:1356
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:499
const PARAM_ISMULTI
Definition ApiBase.php:74
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)
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) Stable to override.
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:37
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Type definition for user types.
Definition UserDef.php:23
Page revision base class.
Exception representing a failure to look up a row from a name table.
const LIST_OR
Definition Defines.php:52
const LIST_AND
Definition Defines.php:49