MediaWiki 1.40.4
ApiQueryRevisions.php
Go to the documentation of this file.
1<?php
37
47
49 private $revisionStore;
50
52 private $changeTagDefStore;
53
55 private $actorMigration;
56
70 public function __construct(
71 ApiQuery $query,
72 $moduleName,
73 RevisionStore $revisionStore,
74 IContentHandlerFactory $contentHandlerFactory,
75 ParserFactory $parserFactory,
76 SlotRoleRegistry $slotRoleRegistry,
77 NameTableStore $changeTagDefStore,
78 ActorMigration $actorMigration,
79 ContentRenderer $contentRenderer,
80 ContentTransformer $contentTransformer,
81 CommentFormatter $commentFormatter
82 ) {
83 parent::__construct(
84 $query,
85 $moduleName,
86 'rv',
87 $revisionStore,
88 $contentHandlerFactory,
89 $parserFactory,
90 $slotRoleRegistry,
91 $contentRenderer,
92 $contentTransformer,
93 $commentFormatter
94 );
95 $this->revisionStore = $revisionStore;
96 $this->changeTagDefStore = $changeTagDefStore;
97 $this->actorMigration = $actorMigration;
98 }
99
100 protected function run( ApiPageSet $resultPageSet = null ) {
101 $params = $this->extractRequestParams( false );
102
103 // If any of those parameters are used, work in 'enumeration' mode.
104 // Enum mode can only be used when exactly one page is provided.
105 // Enumerating revisions on multiple pages make it extremely
106 // difficult to manage continuations and require additional SQL indexes
107 $enumRevMode = ( $params['user'] !== null || $params['excludeuser'] !== null ||
108 $params['limit'] !== null || $params['startid'] !== null ||
109 $params['endid'] !== null || $params['dir'] === 'newer' ||
110 $params['start'] !== null || $params['end'] !== null );
111
112 $pageSet = $this->getPageSet();
113 $pageCount = $pageSet->getGoodTitleCount();
114 $revCount = $pageSet->getRevisionCount();
115
116 // Optimization -- nothing to do
117 if ( $revCount === 0 && $pageCount === 0 ) {
118 // Nothing to do
119 return;
120 }
121 if ( $revCount > 0 && count( $pageSet->getLiveRevisionIDs() ) === 0 ) {
122 // We're in revisions mode but all given revisions are deleted
123 return;
124 }
125
126 if ( $revCount > 0 && $enumRevMode ) {
127 $this->dieWithError(
128 [ 'apierror-revisions-norevids', $this->getModulePrefix() ], 'invalidparammix'
129 );
130 }
131
132 if ( $pageCount > 1 && $enumRevMode ) {
133 $this->dieWithError(
134 [ 'apierror-revisions-singlepage', $this->getModulePrefix() ], 'invalidparammix'
135 );
136 }
137
138 // In non-enum mode, rvlimit can't be directly used. Use the maximum
139 // allowed value.
140 if ( !$enumRevMode ) {
141 $this->setParsedLimit = false;
142 $params['limit'] = 'max';
143 }
144
145 $db = $this->getDB();
146
147 $idField = 'rev_id';
148 $tsField = 'rev_timestamp';
149 $pageField = 'rev_page';
150
151 $ignoreIndex = [
152 // T224017: `rev_timestamp` is never the correct index to use for this module, but
153 // MariaDB sometimes insists on trying to use it anyway. Tell it not to.
154 // Last checked with MariaDB 10.4.13
155 'revision' => 'rev_timestamp',
156 ];
157 $useIndex = [];
158 if ( $resultPageSet === null ) {
159 $this->parseParameters( $params );
160 $opts = [ 'page' ];
161 if ( $this->fld_user ) {
162 $opts[] = 'user';
163 }
164 $revQuery = $this->revisionStore->getQueryInfo( $opts );
165 $this->addTables( $revQuery['tables'] );
166 $this->addFields( $revQuery['fields'] );
167 $this->addJoinConds( $revQuery['joins'] );
168 } else {
169 $this->limit = $this->getParameter( 'limit' ) ?: 10;
170 // Always join 'page' so orphaned revisions are filtered out
171 $this->addTables( [ 'revision', 'page' ] );
172 $this->addJoinConds(
173 [ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ] ]
174 );
175 $this->addFields( [
176 'rev_id' => $idField, 'rev_timestamp' => $tsField, 'rev_page' => $pageField
177 ] );
178 }
179
180 if ( $this->fld_tags ) {
181 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'revision' ) ] );
182 }
183
184 if ( $params['tag'] !== null ) {
185 $this->addTables( 'change_tag' );
186 $this->addJoinConds(
187 [ 'change_tag' => [ 'JOIN', [ 'rev_id=ct_rev_id' ] ] ]
188 );
189 try {
190 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
191 } catch ( NameTableAccessException $exception ) {
192 // Return nothing.
193 $this->addWhere( '1=0' );
194 }
195 }
196
197 if ( $resultPageSet === null && $this->fetchContent ) {
198 // For each page we will request, the user must have read rights for that page
199 $status = Status::newGood();
200
202 foreach ( $pageSet->getGoodTitles() as $title ) {
203 if ( !$this->getAuthority()->authorizeRead( 'read', $title ) ) {
204 $status->fatal( ApiMessage::create(
205 [ 'apierror-cannotviewtitle', wfEscapeWikiText( $title->getPrefixedText() ) ],
206 'accessdenied'
207 ) );
208 }
209 }
210 if ( !$status->isGood() ) {
211 $this->dieStatus( $status );
212 }
213 }
214
215 if ( $enumRevMode ) {
216 // Indexes targeted:
217 // page_timestamp if we don't have rvuser
218 // page_actor_timestamp (on revision_actor_temp) if we have rvuser in READ_NEW mode
219 // page_user_timestamp if we have a logged-in rvuser
220 // page_timestamp or usertext_timestamp if we have an IP rvuser
221
222 // This is mostly to prevent parameter errors (and optimize SQL?)
223 $this->requireMaxOneParameter( $params, 'startid', 'start' );
224 $this->requireMaxOneParameter( $params, 'endid', 'end' );
225 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
226
227 if ( $params['continue'] !== null ) {
228 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
229 $op = ( $params['dir'] === 'newer' ? '>=' : '<=' );
230 $continueTimestamp = $db->timestamp( $cont[0] );
231 $continueId = (int)$cont[1];
232 $this->addWhere( $db->buildComparison( $op, [
233 $tsField => $continueTimestamp,
234 $idField => $continueId,
235 ] ) );
236 }
237
238 // Convert startid/endid to timestamps (T163532)
239 $revids = [];
240 if ( $params['startid'] !== null ) {
241 $revids[] = (int)$params['startid'];
242 }
243 if ( $params['endid'] !== null ) {
244 $revids[] = (int)$params['endid'];
245 }
246 if ( $revids ) {
247 $db = $this->getDB();
248 $sql = $db->unionQueries( [
249 $db->selectSQLText(
250 'revision',
251 [ 'id' => 'rev_id', 'ts' => 'rev_timestamp' ],
252 [ 'rev_id' => $revids ],
253 __METHOD__
254 ),
255 $db->selectSQLText(
256 'archive',
257 [ 'id' => 'ar_rev_id', 'ts' => 'ar_timestamp' ],
258 [ 'ar_rev_id' => $revids ],
259 __METHOD__
260 ),
261 ], $db::UNION_DISTINCT );
262 $res = $db->query( $sql, __METHOD__, ISQLPlatform::QUERY_CHANGE_NONE );
263 foreach ( $res as $row ) {
264 if ( (int)$row->id === (int)$params['startid'] ) {
265 $params['start'] = $row->ts;
266 }
267 if ( (int)$row->id === (int)$params['endid'] ) {
268 $params['end'] = $row->ts;
269 }
270 }
271 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
272 if ( $params['startid'] !== null && $params['start'] === null ) {
273 $p = $this->encodeParamName( 'startid' );
274 $this->dieWithError( [ 'apierror-revisions-badid', $p ], "badid_$p" );
275 }
276 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
277 if ( $params['endid'] !== null && $params['end'] === null ) {
278 $p = $this->encodeParamName( 'endid' );
279 $this->dieWithError( [ 'apierror-revisions-badid', $p ], "badid_$p" );
280 }
281
282 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
283 if ( $params['start'] !== null ) {
284 $op = ( $params['dir'] === 'newer' ? '>=' : '<=' );
285 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
286 $ts = $db->timestampOrNull( $params['start'] );
287 if ( $params['startid'] !== null ) {
288 $this->addWhere( $db->buildComparison( $op, [
289 $tsField => $ts,
290 $idField => (int)$params['startid'],
291 ] ) );
292 } else {
293 $this->addWhere( $db->buildComparison( $op, [ $tsField => $ts ] ) );
294 }
295 }
296 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
297 if ( $params['end'] !== null ) {
298 $op = ( $params['dir'] === 'newer' ? '<=' : '>=' ); // Yes, opposite of the above
299 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
300 $ts = $db->timestampOrNull( $params['end'] );
301 if ( $params['endid'] !== null ) {
302 $this->addWhere( $db->buildComparison( $op, [
303 $tsField => $ts,
304 $idField => (int)$params['endid'],
305 ] ) );
306 } else {
307 $this->addWhere( $db->buildComparison( $op, [ $tsField => $ts ] ) );
308 }
309 }
310 } else {
311 $this->addTimestampWhereRange( $tsField, $params['dir'],
312 $params['start'], $params['end'] );
313 }
314
315 $sort = ( $params['dir'] === 'newer' ? '' : 'DESC' );
316 $this->addOption( 'ORDER BY', [ "rev_timestamp $sort", "rev_id $sort" ] );
317
318 // There is only one ID, use it
319 $ids = array_keys( $pageSet->getGoodPages() );
320 $this->addWhereFld( $pageField, reset( $ids ) );
321
322 if ( $params['user'] !== null ) {
323 $actorQuery = $this->actorMigration->getWhere( $db, 'rev_user', $params['user'] );
324 $this->addTables( $actorQuery['tables'] );
325 $this->addJoinConds( $actorQuery['joins'] );
326 $this->addWhere( $actorQuery['conds'] );
327 } elseif ( $params['excludeuser'] !== null ) {
328 $actorQuery = $this->actorMigration->getWhere( $db, 'rev_user', $params['excludeuser'] );
329 $this->addTables( $actorQuery['tables'] );
330 $this->addJoinConds( $actorQuery['joins'] );
331 $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
332 } else {
333 // T258480: MariaDB ends up using rev_page_actor_timestamp in some cases here.
334 // Last checked with MariaDB 10.4.13
335 // Unless we are filtering by user (see above), we always want to use the
336 // "history" index on the revision table, namely page_timestamp.
337 $useIndex['revision'] = 'rev_page_timestamp';
338 }
339
340 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
341 // Paranoia: avoid brute force searches (T19342)
342 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
343 $bitmask = RevisionRecord::DELETED_USER;
344 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' )
345 ) {
346 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
347 } else {
348 $bitmask = 0;
349 }
350 if ( $bitmask ) {
351 $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
352 }
353 }
354 } elseif ( $revCount > 0 ) {
355 // Always targets the PRIMARY index
356
357 $revs = $pageSet->getLiveRevisionIDs();
358
359 // Get all revision IDs
360 $this->addWhereFld( 'rev_id', array_keys( $revs ) );
361
362 if ( $params['continue'] !== null ) {
363 $this->addWhere( 'rev_id >= ' . (int)$params['continue'] );
364 }
365 $this->addOption( 'ORDER BY', 'rev_id' );
366 } elseif ( $pageCount > 0 ) {
367 // Always targets the rev_page_id index
368
369 $pageids = array_keys( $pageSet->getGoodPages() );
370
371 // When working in multi-page non-enumeration mode,
372 // limit to the latest revision only
373 $this->addWhere( 'page_latest=rev_id' );
374
375 // Get all page IDs
376 $this->addWhereFld( 'page_id', $pageids );
377 // Every time someone relies on equality propagation, god kills a kitten :)
378 $this->addWhereFld( 'rev_page', $pageids );
379
380 if ( $params['continue'] !== null ) {
381 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
382 $this->addWhere( $db->buildComparison( '>=', [
383 'rev_page' => $cont[0],
384 'rev_id' => $cont[1],
385 ] ) );
386 }
387 $this->addOption( 'ORDER BY', [
388 'rev_page',
389 'rev_id'
390 ] );
391 } else {
392 ApiBase::dieDebug( __METHOD__, 'param validation?' );
393 }
394
395 $this->addOption( 'LIMIT', $this->limit + 1 );
396
397 $this->addOption( 'IGNORE INDEX', $ignoreIndex );
398
399 if ( $useIndex ) {
400 $this->addOption( 'USE INDEX', $useIndex );
401 }
402
403 $count = 0;
404 $generated = [];
405 $hookData = [];
406 $res = $this->select( __METHOD__, [], $hookData );
407
408 foreach ( $res as $row ) {
409 if ( ++$count > $this->limit ) {
410 // We've reached the one extra which shows that there are
411 // additional pages to be had. Stop here...
412 if ( $enumRevMode ) {
413 $this->setContinueEnumParameter( 'continue',
414 $row->rev_timestamp . '|' . (int)$row->rev_id );
415 } elseif ( $revCount > 0 ) {
416 $this->setContinueEnumParameter( 'continue', (int)$row->rev_id );
417 } else {
418 $this->setContinueEnumParameter( 'continue', (int)$row->rev_page .
419 '|' . (int)$row->rev_id );
420 }
421 break;
422 }
423
424 if ( $resultPageSet !== null ) {
425 $generated[] = $row->rev_id;
426 } else {
427 $revision = $this->revisionStore->newRevisionFromRow( $row, 0, Title::newFromRow( $row ) );
428 $rev = $this->extractRevisionInfo( $revision, $row );
429 $fit = $this->processRow( $row, $rev, $hookData ) &&
430 $this->addPageSubItem( $row->rev_page, $rev, 'rev' );
431 if ( !$fit ) {
432 if ( $enumRevMode ) {
433 $this->setContinueEnumParameter( 'continue',
434 $row->rev_timestamp . '|' . (int)$row->rev_id );
435 } elseif ( $revCount > 0 ) {
436 $this->setContinueEnumParameter( 'continue', (int)$row->rev_id );
437 } else {
438 $this->setContinueEnumParameter( 'continue', (int)$row->rev_page .
439 '|' . (int)$row->rev_id );
440 }
441 break;
442 }
443 }
444 }
445
446 if ( $resultPageSet !== null ) {
447 $resultPageSet->populateFromRevisionIDs( $generated );
448 }
449 }
450
451 public function getAllowedParams() {
452 $ret = parent::getAllowedParams() + [
453 'startid' => [
454 ParamValidator::PARAM_TYPE => 'integer',
455 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
456 ],
457 'endid' => [
458 ParamValidator::PARAM_TYPE => 'integer',
459 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
460 ],
461 'start' => [
462 ParamValidator::PARAM_TYPE => 'timestamp',
463 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
464 ],
465 'end' => [
466 ParamValidator::PARAM_TYPE => 'timestamp',
467 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
468 ],
469 'dir' => [
470 ParamValidator::PARAM_DEFAULT => 'older',
471 ParamValidator::PARAM_TYPE => [
472 'newer',
473 'older'
474 ],
475 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
477 'newer' => 'api-help-paramvalue-direction-newer',
478 'older' => 'api-help-paramvalue-direction-older',
479 ],
480 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
481 ],
482 'user' => [
483 ParamValidator::PARAM_TYPE => 'user',
484 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
485 UserDef::PARAM_RETURN_OBJECT => true,
486 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
487 ],
488 'excludeuser' => [
489 ParamValidator::PARAM_TYPE => 'user',
490 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
491 UserDef::PARAM_RETURN_OBJECT => true,
492 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
493 ],
494 'tag' => null,
495 'continue' => [
496 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
497 ],
498 ];
499
500 $ret['limit'][ApiBase::PARAM_HELP_MSG_INFO] = [ [ 'singlepageonly' ] ];
501
502 return $ret;
503 }
504
505 protected function getExamplesMessages() {
506 $title = Title::newMainPage()->getPrefixedText();
507 $mp = rawurlencode( $title );
508
509 return [
510 "action=query&prop=revisions&titles=API|{$mp}&" .
511 'rvslots=*&rvprop=timestamp|user|comment|content'
512 => 'apihelp-query+revisions-example-content',
513 "action=query&prop=revisions&titles={$mp}&rvlimit=5&" .
514 'rvprop=timestamp|user|comment'
515 => 'apihelp-query+revisions-example-last5',
516 "action=query&prop=revisions&titles={$mp}&rvlimit=5&" .
517 'rvprop=timestamp|user|comment&rvdir=newer'
518 => 'apihelp-query+revisions-example-first5',
519 "action=query&prop=revisions&titles={$mp}&rvlimit=5&" .
520 'rvprop=timestamp|user|comment&rvdir=newer&rvstart=2006-05-01T00:00:00Z'
521 => 'apihelp-query+revisions-example-first5-after',
522 "action=query&prop=revisions&titles={$mp}&rvlimit=5&" .
523 'rvprop=timestamp|user|comment&rvexcludeuser=127.0.0.1'
524 => 'apihelp-query+revisions-example-first5-not-localhost',
525 "action=query&prop=revisions&titles={$mp}&rvlimit=5&" .
526 'rvprop=timestamp|user|comment&rvuser=MediaWiki%20default'
527 => 'apihelp-query+revisions-example-first5-user',
528 ];
529 }
530
531 public function getHelpUrls() {
532 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Revisions';
533 }
534}
getAuthority()
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1460
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:514
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:894
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1701
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:183
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1649
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:204
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:946
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:773
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:166
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1521
This class contains a list of pages that the client has requested.
processRow( $row, array &$data, array &$hookData)
Call the ApiQueryBaseProcessRow hook.
addFields( $value)
Add a set of fields to select to the internal array.
addPageSubItem( $pageId, $item, $elemname=null)
Same as addPageSubItems(), but one element of $data at a time.
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)
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 ] )
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.
getPageSet()
Get the PageSet object to work on.
encodeParamName( $paramName)
Overrides ApiBase to prepend 'g' to every generator parameter.
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.
A query action to enumerate revisions of a given page, or show top revisions of multiple pages.
__construct(ApiQuery $query, $moduleName, RevisionStore $revisionStore, IContentHandlerFactory $contentHandlerFactory, ParserFactory $parserFactory, SlotRoleRegistry $slotRoleRegistry, NameTableStore $changeTagDefStore, ActorMigration $actorMigration, ContentRenderer $contentRenderer, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter)
getHelpUrls()
Return links to more detailed help pages about the module.
run(ApiPageSet $resultPageSet=null)
getExamplesMessages()
Returns usage examples for this module.
This is the main query class.
Definition ApiQuery.php:42
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...
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.
Represents a title within MediaWiki.
Definition Title.php:82
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Service for formatting and validating API parameters.
Interface for query language.