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