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