MediaWiki  master
ApiQueryDeletedrevs.php
Go to the documentation of this file.
1 <?php
34 
42 
44  private $commentStore;
45 
47  private $commentFormatter;
48 
50  private $revisionStore;
51 
53  private $changeTagDefStore;
54 
56  private $linkBatchFactory;
57 
67  public function __construct(
68  ApiQuery $query,
69  $moduleName,
70  CommentStore $commentStore,
71  RowCommentFormatter $commentFormatter,
72  RevisionStore $revisionStore,
73  NameTableStore $changeTagDefStore,
74  LinkBatchFactory $linkBatchFactory
75  ) {
76  parent::__construct( $query, $moduleName, 'dr' );
77  $this->commentStore = $commentStore;
78  $this->commentFormatter = $commentFormatter;
79  $this->revisionStore = $revisionStore;
80  $this->changeTagDefStore = $changeTagDefStore;
81  $this->linkBatchFactory = $linkBatchFactory;
82  }
83 
84  public function execute() {
85  // Before doing anything at all, let's check permissions
86  $this->checkUserRightsAny( 'deletedhistory' );
87 
88  $this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' );
89 
90  $user = $this->getUser();
91  $db = $this->getDB();
92  $params = $this->extractRequestParams( false );
93  $prop = array_fill_keys( $params['prop'], true );
94  $fld_parentid = isset( $prop['parentid'] );
95  $fld_revid = isset( $prop['revid'] );
96  $fld_user = isset( $prop['user'] );
97  $fld_userid = isset( $prop['userid'] );
98  $fld_comment = isset( $prop['comment'] );
99  $fld_parsedcomment = isset( $prop['parsedcomment'] );
100  $fld_minor = isset( $prop['minor'] );
101  $fld_len = isset( $prop['len'] );
102  $fld_sha1 = isset( $prop['sha1'] );
103  $fld_content = isset( $prop['content'] );
104  $fld_token = isset( $prop['token'] );
105  $fld_tags = isset( $prop['tags'] );
106 
107  // If we're in a mode that breaks the same-origin policy, no tokens can
108  // be obtained
109  if ( $this->lacksSameOriginSecurity() ) {
110  $fld_token = false;
111  }
112 
113  // If user can't undelete, no tokens
114  if ( !$this->getAuthority()->isAllowed( 'undelete' ) ) {
115  $fld_token = false;
116  }
117 
118  $result = $this->getResult();
119  $pageSet = $this->getPageSet();
120  $titles = $pageSet->getPages();
121 
122  // This module operates in three modes:
123  // 'revs': List deleted revs for certain titles (1)
124  // 'user': List deleted revs by a certain user (2)
125  // 'all': List all deleted revs in NS (3)
126  $mode = 'all';
127  if ( count( $titles ) > 0 ) {
128  $mode = 'revs';
129  } elseif ( $params['user'] !== null ) {
130  $mode = 'user';
131  }
132 
133  if ( $mode == 'revs' || $mode == 'user' ) {
134  // Ignore namespace and unique due to inability to know whether they were purposely set
135  foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) {
136  if ( $params[$p] !== null ) {
137  $this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' );
138  }
139  }
140  } else {
141  foreach ( [ 'start', 'end' ] as $p ) {
142  if ( $params[$p] !== null ) {
143  $this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' );
144  }
145  }
146  }
147 
148  if ( $params['user'] !== null && $params['excludeuser'] !== null ) {
149  $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
150  }
151 
152  $arQuery = $this->revisionStore->getArchiveQueryInfo();
153  $this->addTables( $arQuery['tables'] );
154  $this->addFields( $arQuery['fields'] );
155  $this->addJoinConds( $arQuery['joins'] );
156  $this->addFields( [ 'ar_title', 'ar_namespace' ] );
157 
158  if ( $fld_tags ) {
159  $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
160  }
161 
162  if ( $params['tag'] !== null ) {
163  $this->addTables( 'change_tag' );
164  $this->addJoinConds(
165  [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
166  );
167  try {
168  $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
169  } catch ( NameTableAccessException $exception ) {
170  // Return nothing.
171  $this->addWhere( '1=0' );
172  }
173  }
174 
175  // This means stricter restrictions
176  if ( $fld_content ) {
177  $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
178  }
179  // Check limits
180  $userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
181  $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
182 
183  $limit = $params['limit'];
184 
185  if ( $limit == 'max' ) {
186  $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
187  $this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
188  }
189 
190  $limit = $this->getMain()->getParamValidator()->validateValue(
191  $this, 'limit', $limit, [
192  ParamValidator::PARAM_TYPE => 'limit',
193  IntegerDef::PARAM_MIN => 1,
194  IntegerDef::PARAM_MAX => $userMax,
195  IntegerDef::PARAM_MAX2 => $botMax,
196  IntegerDef::PARAM_IGNORE_RANGE => true,
197  ]
198  );
199 
200  if ( $fld_token ) {
201  // Undelete tokens are identical for all pages, so we cache one here
202  $token = $user->getEditToken( '', $this->getMain()->getRequest() );
203  }
204 
205  $dir = $params['dir'];
206 
207  // We need a custom WHERE clause that matches all titles.
208  if ( $mode == 'revs' ) {
209  $lb = $this->linkBatchFactory->newLinkBatch( $titles );
210  $where = $lb->constructSet( 'ar', $db );
211  $this->addWhere( $where );
212  } elseif ( $mode == 'all' ) {
213  $this->addWhereFld( 'ar_namespace', $params['namespace'] );
214 
215  $from = $params['from'] === null
216  ? null
217  : $this->titlePartToKey( $params['from'], $params['namespace'] );
218  $to = $params['to'] === null
219  ? null
220  : $this->titlePartToKey( $params['to'], $params['namespace'] );
221  $this->addWhereRange( 'ar_title', $dir, $from, $to );
222 
223  if ( isset( $params['prefix'] ) ) {
224  $this->addWhere( 'ar_title' . $db->buildLike(
225  $this->titlePartToKey( $params['prefix'], $params['namespace'] ),
226  $db->anyString() ) );
227  }
228  }
229 
230  if ( $params['user'] !== null ) {
231  // We already join on actor due to getArchiveQueryInfo()
232  $this->addWhereFld( 'actor_name', $params['user'] );
233  } elseif ( $params['excludeuser'] !== null ) {
234  $this->addWhere( 'actor_name<>' . $db->addQuotes( $params['excludeuser'] ) );
235  }
236 
237  if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
238  // Paranoia: avoid brute force searches (T19342)
239  // (shouldn't be able to get here without 'deletedhistory', but
240  // check it again just in case)
241  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
242  $bitmask = RevisionRecord::DELETED_USER;
243  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
244  $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
245  } else {
246  $bitmask = 0;
247  }
248  if ( $bitmask ) {
249  $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
250  }
251  }
252 
253  if ( $params['continue'] !== null ) {
254  $op = ( $dir == 'newer' ? '>=' : '<=' );
255  if ( $mode == 'all' || $mode == 'revs' ) {
256  $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] );
257  $this->addWhere( $db->buildComparison( $op, [
258  'ar_namespace' => $cont[0],
259  'ar_title' => $cont[1],
260  'ar_timestamp' => $db->timestamp( $cont[2] ),
261  'ar_id' => $cont[3],
262  ] ) );
263  } else {
264  $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
265  $this->addWhere( $db->buildComparison( $op, [
266  'ar_timestamp' => $db->timestamp( $cont[0] ),
267  'ar_id' => $cont[1],
268  ] ) );
269  }
270  }
271 
272  $this->addOption( 'LIMIT', $limit + 1 );
273  if ( $mode == 'all' ) {
274  if ( $params['unique'] ) {
275  // @todo Does this work on non-MySQL?
276  $this->addOption( 'GROUP BY', 'ar_title' );
277  } else {
278  $sort = ( $dir == 'newer' ? '' : ' DESC' );
279  $this->addOption( 'ORDER BY', [
280  'ar_title' . $sort,
281  'ar_timestamp' . $sort,
282  'ar_id' . $sort,
283  ] );
284  }
285  } else {
286  if ( $mode == 'revs' ) {
287  // Sort by ns and title in the same order as timestamp for efficiency
288  $this->addWhereRange( 'ar_namespace', $dir, null, null );
289  $this->addWhereRange( 'ar_title', $dir, null, null );
290  }
291  $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
292  // Include in ORDER BY for uniqueness
293  $this->addWhereRange( 'ar_id', $dir, null, null );
294  }
295  $res = $this->select( __METHOD__ );
296 
297  $formattedComments = [];
298  if ( $fld_parsedcomment ) {
299  $formattedComments = $this->commentFormatter->formatItems(
300  $this->commentFormatter->rows( $res )
301  ->indexField( 'ar_id' )
302  ->commentKey( 'ar_comment' )
303  ->namespaceField( 'ar_namespace' )
304  ->titleField( 'ar_title' )
305  );
306  }
307 
308  $pageMap = []; // Maps ns&title to (fake) pageid
309  $count = 0;
310  $newPageID = 0;
311  foreach ( $res as $row ) {
312  if ( ++$count > $limit ) {
313  // We've had enough
314  if ( $mode == 'all' || $mode == 'revs' ) {
315  $this->setContinueEnumParameter( 'continue',
316  "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
317  );
318  } else {
319  $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
320  }
321  break;
322  }
323 
324  $rev = [];
325  $anyHidden = false;
326 
327  $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
328  if ( $fld_revid ) {
329  $rev['revid'] = (int)$row->ar_rev_id;
330  }
331  if ( $fld_parentid && $row->ar_parent_id !== null ) {
332  $rev['parentid'] = (int)$row->ar_parent_id;
333  }
334  if ( $fld_user || $fld_userid ) {
335  if ( $row->ar_deleted & RevisionRecord::DELETED_USER ) {
336  $rev['userhidden'] = true;
337  $anyHidden = true;
338  }
339  if ( RevisionRecord::userCanBitfield(
340  $row->ar_deleted,
341  RevisionRecord::DELETED_USER,
342  $user
343  ) ) {
344  if ( $fld_user ) {
345  $rev['user'] = $row->ar_user_text;
346  }
347  if ( $fld_userid ) {
348  $rev['userid'] = (int)$row->ar_user;
349  }
350  }
351  }
352 
353  if ( $fld_comment || $fld_parsedcomment ) {
354  if ( $row->ar_deleted & RevisionRecord::DELETED_COMMENT ) {
355  $rev['commenthidden'] = true;
356  $anyHidden = true;
357  }
358  if ( RevisionRecord::userCanBitfield(
359  $row->ar_deleted,
360  RevisionRecord::DELETED_COMMENT,
361  $user
362  ) ) {
363  $comment = $this->commentStore->getComment( 'ar_comment', $row )->text;
364  if ( $fld_comment ) {
365  $rev['comment'] = $comment;
366  }
367  if ( $fld_parsedcomment ) {
368  $rev['parsedcomment'] = $formattedComments[$row->ar_id];
369  }
370  }
371  }
372 
373  if ( $fld_minor ) {
374  $rev['minor'] = $row->ar_minor_edit == 1;
375  }
376  if ( $fld_len ) {
377  $rev['len'] = $row->ar_len;
378  }
379  if ( $fld_sha1 ) {
380  if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
381  $rev['sha1hidden'] = true;
382  $anyHidden = true;
383  }
384  if ( RevisionRecord::userCanBitfield(
385  $row->ar_deleted,
386  RevisionRecord::DELETED_TEXT,
387  $user
388  ) ) {
389  if ( $row->ar_sha1 != '' ) {
390  $rev['sha1'] = Wikimedia\base_convert( $row->ar_sha1, 36, 16, 40 );
391  } else {
392  $rev['sha1'] = '';
393  }
394  }
395  }
396  if ( $fld_content ) {
397  if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
398  $rev['texthidden'] = true;
399  $anyHidden = true;
400  }
401  if ( RevisionRecord::userCanBitfield(
402  $row->ar_deleted,
403  RevisionRecord::DELETED_TEXT,
404  $user
405  ) ) {
406  ApiResult::setContentValue( $rev, 'text',
407  $this->revisionStore->newRevisionFromArchiveRow( $row )
408  ->getContent( SlotRecord::MAIN )->serialize() );
409  }
410  }
411 
412  if ( $fld_tags ) {
413  if ( $row->ts_tags ) {
414  $tags = explode( ',', $row->ts_tags );
415  ApiResult::setIndexedTagName( $tags, 'tag' );
416  $rev['tags'] = $tags;
417  } else {
418  $rev['tags'] = [];
419  }
420  }
421 
422  if ( $anyHidden && ( $row->ar_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
423  $rev['suppressed'] = true;
424  }
425 
426  if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
427  $pageID = $newPageID++;
428  $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
429  $a = [ 'revisions' => [ $rev ] ];
430  ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
431  $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
433  if ( $fld_token ) {
434  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable token is set when used
435  $a['token'] = $token;
436  }
437  $fit = $result->addValue( [ 'query', $this->getModuleName() ], $pageID, $a );
438  } else {
439  $pageID = $pageMap[$row->ar_namespace][$row->ar_title];
440  $fit = $result->addValue(
441  [ 'query', $this->getModuleName(), $pageID, 'revisions' ],
442  null, $rev );
443  }
444  if ( !$fit ) {
445  if ( $mode == 'all' || $mode == 'revs' ) {
446  $this->setContinueEnumParameter( 'continue',
447  "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
448  );
449  } else {
450  $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
451  }
452  break;
453  }
454  }
455  $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
456  }
457 
458  public function isDeprecated() {
459  return true;
460  }
461 
462  public function getAllowedParams() {
463  return [
464  'start' => [
465  ParamValidator::PARAM_TYPE => 'timestamp',
466  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
467  ],
468  'end' => [
469  ParamValidator::PARAM_TYPE => 'timestamp',
470  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
471  ],
472  'dir' => [
473  ParamValidator::PARAM_TYPE => [
474  'newer',
475  'older'
476  ],
477  ParamValidator::PARAM_DEFAULT => 'older',
478  ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
479  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 3 ] ],
480  ],
481  'from' => [
482  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
483  ],
484  'to' => [
485  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
486  ],
487  'prefix' => [
488  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
489  ],
490  'unique' => [
491  ParamValidator::PARAM_DEFAULT => false,
492  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
493  ],
494  'namespace' => [
495  ParamValidator::PARAM_TYPE => 'namespace',
496  ParamValidator::PARAM_DEFAULT => NS_MAIN,
497  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
498  ],
499  'tag' => null,
500  'user' => [
501  ParamValidator::PARAM_TYPE => 'user',
502  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
503  ],
504  'excludeuser' => [
505  ParamValidator::PARAM_TYPE => 'user',
506  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
507  ],
508  'prop' => [
509  ParamValidator::PARAM_DEFAULT => 'user|comment',
510  ParamValidator::PARAM_TYPE => [
511  'revid',
512  'parentid',
513  'user',
514  'userid',
515  'comment',
516  'parsedcomment',
517  'minor',
518  'len',
519  'sha1',
520  'content',
521  'token',
522  'tags'
523  ],
524  ParamValidator::PARAM_ISMULTI => true
525  ],
526  'limit' => [
527  ParamValidator::PARAM_DEFAULT => 10,
528  ParamValidator::PARAM_TYPE => 'limit',
529  IntegerDef::PARAM_MIN => 1,
530  IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
531  IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
532  ],
533  'continue' => [
534  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
535  ],
536  ];
537  }
538 
539  protected function getExamplesMessages() {
540  return [
541  'action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&' .
542  'drprop=user|comment|content'
543  => 'apihelp-query+deletedrevs-example-mode1',
544  'action=query&list=deletedrevs&druser=Bob&drlimit=50'
545  => 'apihelp-query+deletedrevs-example-mode2',
546  'action=query&list=deletedrevs&drdir=newer&drlimit=50'
547  => 'apihelp-query+deletedrevs-example-mode3-main',
548  'action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=1&drunique='
549  => 'apihelp-query+deletedrevs-example-mode3-talk',
550  ];
551  }
552 
553  public function getHelpUrls() {
554  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevs';
555  }
556 }
const NS_MAIN
Definition: Defines.php:64
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1459
checkUserRightsAny( $rights, $user=null)
Helper function for permission-denied errors.
Definition: ApiBase.php:1565
getMain()
Get the main module.
Definition: ApiBase.php:521
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition: ApiBase.php:182
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition: ApiBase.php:1648
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition: ApiBase.php:1391
const LIMIT_BIG1
Fast query, standard limit.
Definition: ApiBase.php:228
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition: ApiBase.php:234
getResult()
Get the result object.
Definition: ApiBase.php:636
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:772
const LIMIT_SML1
Slow query, standard limit.
Definition: ApiBase.php:232
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:165
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition: ApiBase.php:230
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:505
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition: ApiBase.php:567
This is a base class for all Query modules.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
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)
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 ] )
getPageSet()
Get the PageSet object to work on.
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.
Query module to enumerate all deleted revisions.
getExamplesMessages()
Returns usage examples for this module.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getHelpUrls()
Return links to more detailed help pages about the module.
__construct(ApiQuery $query, $moduleName, CommentStore $commentStore, RowCommentFormatter $commentFormatter, RevisionStore $revisionStore, NameTableStore $changeTagDefStore, LinkBatchFactory $linkBatchFactory)
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
isDeprecated()
Indicates whether this module is deprecated.
This is the main query class.
Definition: ApiQuery.php:41
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
Definition: ApiResult.php:467
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
This is basically a CommentFormatter with a CommentStore dependency, allowing it to retrieve comment ...
Handle database storage of comments such as edit summaries and log reasons.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Exception representing a failure to look up a row from a name table.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:641
Service for formatting and validating API parameters.
Type definition for integer types.
Definition: IntegerDef.php:23
return true
Definition: router.php:90