MediaWiki  master
ApiQueryInfo.php
Go to the documentation of this file.
1 <?php
24 
30 class ApiQueryInfo extends ApiQueryBase {
31 
32  private $fld_protection = false, $fld_talkid = false,
33  $fld_subjectid = false, $fld_url = false,
34  $fld_readable = false, $fld_watched = false,
38 
39  private $params;
40 
42  private $titles;
44  private $missing;
46  private $everything;
47 
50 
53  private $showZeroWatchers = false;
54 
55  private $tokenFunctions;
56 
57  private $countTestedActions = 0;
58 
59  public function __construct( ApiQuery $query, $moduleName ) {
60  parent::__construct( $query, $moduleName, 'in' );
61  }
62 
67  public function requestExtraData( $pageSet ) {
68  $pageSet->requestField( 'page_restrictions' );
69  // If the pageset is resolving redirects we won't get page_is_redirect.
70  // But we can't know for sure until the pageset is executed (revids may
71  // turn it off), so request it unconditionally.
72  $pageSet->requestField( 'page_is_redirect' );
73  $pageSet->requestField( 'page_is_new' );
74  $config = $this->getConfig();
75  $pageSet->requestField( 'page_touched' );
76  $pageSet->requestField( 'page_latest' );
77  $pageSet->requestField( 'page_len' );
78  if ( $config->get( 'ContentHandlerUseDB' ) ) {
79  $pageSet->requestField( 'page_content_model' );
80  }
81  if ( $config->get( 'PageLanguageUseDB' ) ) {
82  $pageSet->requestField( 'page_lang' );
83  }
84  }
85 
93  protected function getTokenFunctions() {
94  // Don't call the hooks twice
95  if ( isset( $this->tokenFunctions ) ) {
96  return $this->tokenFunctions;
97  }
98 
99  // If we're in a mode that breaks the same-origin policy, no tokens can
100  // be obtained
101  if ( $this->lacksSameOriginSecurity() ) {
102  return [];
103  }
104 
105  $this->tokenFunctions = [
106  'edit' => [ self::class, 'getEditToken' ],
107  'delete' => [ self::class, 'getDeleteToken' ],
108  'protect' => [ self::class, 'getProtectToken' ],
109  'move' => [ self::class, 'getMoveToken' ],
110  'block' => [ self::class, 'getBlockToken' ],
111  'unblock' => [ self::class, 'getUnblockToken' ],
112  'email' => [ self::class, 'getEmailToken' ],
113  'import' => [ self::class, 'getImportToken' ],
114  'watch' => [ self::class, 'getWatchToken' ],
115  ];
116  Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
117 
118  return $this->tokenFunctions;
119  }
120 
122  protected static $cachedTokens = [];
123 
127  public static function resetTokenCache() {
128  self::$cachedTokens = [];
129  }
130 
134  public static function getEditToken( $pageid, $title ) {
135  // We could check for $title->userCan('edit') here,
136  // but that's too expensive for this purpose
137  // and would break caching
138  global $wgUser;
139  if ( !MediaWikiServices::getInstance()->getPermissionManager()
140  ->userHasRight( $wgUser, 'edit' ) ) {
141  return false;
142  }
143 
144  // The token is always the same, let's exploit that
145  if ( !isset( self::$cachedTokens['edit'] ) ) {
146  self::$cachedTokens['edit'] = $wgUser->getEditToken();
147  }
148 
149  return self::$cachedTokens['edit'];
150  }
151 
155  public static function getDeleteToken( $pageid, $title ) {
156  global $wgUser;
157  if ( !MediaWikiServices::getInstance()->getPermissionManager()
158  ->userHasRight( $wgUser, 'delete' ) ) {
159  return false;
160  }
161 
162  // The token is always the same, let's exploit that
163  if ( !isset( self::$cachedTokens['delete'] ) ) {
164  self::$cachedTokens['delete'] = $wgUser->getEditToken();
165  }
166 
167  return self::$cachedTokens['delete'];
168  }
169 
173  public static function getProtectToken( $pageid, $title ) {
174  global $wgUser;
175  if ( !MediaWikiServices::getInstance()->getPermissionManager()
176  ->userHasRight( $wgUser, 'protect' ) ) {
177  return false;
178  }
179 
180  // The token is always the same, let's exploit that
181  if ( !isset( self::$cachedTokens['protect'] ) ) {
182  self::$cachedTokens['protect'] = $wgUser->getEditToken();
183  }
184 
185  return self::$cachedTokens['protect'];
186  }
187 
191  public static function getMoveToken( $pageid, $title ) {
192  global $wgUser;
193  if ( !MediaWikiServices::getInstance()->getPermissionManager()
194  ->userHasRight( $wgUser, 'move' ) ) {
195  return false;
196  }
197 
198  // The token is always the same, let's exploit that
199  if ( !isset( self::$cachedTokens['move'] ) ) {
200  self::$cachedTokens['move'] = $wgUser->getEditToken();
201  }
202 
203  return self::$cachedTokens['move'];
204  }
205 
209  public static function getBlockToken( $pageid, $title ) {
210  global $wgUser;
211  if ( !MediaWikiServices::getInstance()->getPermissionManager()
212  ->userHasRight( $wgUser, 'block' ) ) {
213  return false;
214  }
215 
216  // The token is always the same, let's exploit that
217  if ( !isset( self::$cachedTokens['block'] ) ) {
218  self::$cachedTokens['block'] = $wgUser->getEditToken();
219  }
220 
221  return self::$cachedTokens['block'];
222  }
223 
227  public static function getUnblockToken( $pageid, $title ) {
228  // Currently, this is exactly the same as the block token
229  return self::getBlockToken( $pageid, $title );
230  }
231 
235  public static function getEmailToken( $pageid, $title ) {
236  global $wgUser;
237  if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailuser() ) {
238  return false;
239  }
240 
241  // The token is always the same, let's exploit that
242  if ( !isset( self::$cachedTokens['email'] ) ) {
243  self::$cachedTokens['email'] = $wgUser->getEditToken();
244  }
245 
246  return self::$cachedTokens['email'];
247  }
248 
252  public static function getImportToken( $pageid, $title ) {
253  global $wgUser;
254  if ( !MediaWikiServices::getInstance()
256  ->userHasAnyRight( $wgUser, 'import', 'importupload' ) ) {
257  return false;
258  }
259 
260  // The token is always the same, let's exploit that
261  if ( !isset( self::$cachedTokens['import'] ) ) {
262  self::$cachedTokens['import'] = $wgUser->getEditToken();
263  }
264 
265  return self::$cachedTokens['import'];
266  }
267 
271  public static function getWatchToken( $pageid, $title ) {
272  global $wgUser;
273  if ( !$wgUser->isLoggedIn() ) {
274  return false;
275  }
276 
277  // The token is always the same, let's exploit that
278  if ( !isset( self::$cachedTokens['watch'] ) ) {
279  self::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
280  }
281 
282  return self::$cachedTokens['watch'];
283  }
284 
288  public static function getOptionsToken( $pageid, $title ) {
289  global $wgUser;
290  if ( !$wgUser->isLoggedIn() ) {
291  return false;
292  }
293 
294  // The token is always the same, let's exploit that
295  if ( !isset( self::$cachedTokens['options'] ) ) {
296  self::$cachedTokens['options'] = $wgUser->getEditToken();
297  }
298 
299  return self::$cachedTokens['options'];
300  }
301 
302  public function execute() {
303  $this->params = $this->extractRequestParams();
304  if ( !is_null( $this->params['prop'] ) ) {
305  $prop = array_flip( $this->params['prop'] );
306  $this->fld_protection = isset( $prop['protection'] );
307  $this->fld_watched = isset( $prop['watched'] );
308  $this->fld_watchers = isset( $prop['watchers'] );
309  $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
310  $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
311  $this->fld_talkid = isset( $prop['talkid'] );
312  $this->fld_subjectid = isset( $prop['subjectid'] );
313  $this->fld_url = isset( $prop['url'] );
314  $this->fld_readable = isset( $prop['readable'] );
315  $this->fld_preload = isset( $prop['preload'] );
316  $this->fld_displaytitle = isset( $prop['displaytitle'] );
317  $this->fld_varianttitles = isset( $prop['varianttitles'] );
318  }
319 
320  $pageSet = $this->getPageSet();
321  $this->titles = $pageSet->getGoodTitles();
322  $this->missing = $pageSet->getMissingTitles();
323  $this->everything = $this->titles + $this->missing;
324  $result = $this->getResult();
325 
326  uasort( $this->everything, [ Title::class, 'compare' ] );
327  if ( !is_null( $this->params['continue'] ) ) {
328  // Throw away any titles we're gonna skip so they don't
329  // clutter queries
330  $cont = explode( '|', $this->params['continue'] );
331  $this->dieContinueUsageIf( count( $cont ) != 2 );
332  $conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
333  foreach ( $this->everything as $pageid => $title ) {
334  if ( Title::compare( $title, $conttitle ) >= 0 ) {
335  break;
336  }
337  unset( $this->titles[$pageid] );
338  unset( $this->missing[$pageid] );
339  unset( $this->everything[$pageid] );
340  }
341  }
342 
343  $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
344  // when resolving redirects, no page will have this field
345  $this->pageIsRedir = !$pageSet->isResolvingRedirects()
346  ? $pageSet->getCustomField( 'page_is_redirect' )
347  : [];
348  $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
349 
350  $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
351  $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
352  $this->pageLength = $pageSet->getCustomField( 'page_len' );
353 
354  // Get protection info if requested
355  if ( $this->fld_protection ) {
356  $this->getProtectionInfo();
357  }
358 
359  if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
360  $this->getWatchedInfo();
361  }
362 
363  if ( $this->fld_watchers ) {
364  $this->getWatcherInfo();
365  }
366 
367  if ( $this->fld_visitingwatchers ) {
368  $this->getVisitingWatcherInfo();
369  }
370 
371  // Run the talkid/subjectid query if requested
372  if ( $this->fld_talkid || $this->fld_subjectid ) {
373  $this->getTSIDs();
374  }
375 
376  if ( $this->fld_displaytitle ) {
377  $this->getDisplayTitle();
378  }
379 
380  if ( $this->fld_varianttitles ) {
381  $this->getVariantTitles();
382  }
383 
385  foreach ( $this->everything as $pageid => $title ) {
386  $pageInfo = $this->extractPageInfo( $pageid, $title );
387  $fit = $pageInfo !== null && $result->addValue( [
388  'query',
389  'pages'
390  ], $pageid, $pageInfo );
391  if ( !$fit ) {
392  $this->setContinueEnumParameter( 'continue',
393  $title->getNamespace() . '|' .
394  $title->getText() );
395  break;
396  }
397  }
398  }
399 
406  private function extractPageInfo( $pageid, $title ) {
407  $pageInfo = [];
408  // $title->exists() needs pageid, which is not set for all title objects
409  $titleExists = $pageid > 0;
410  $ns = $title->getNamespace();
411  $dbkey = $title->getDBkey();
412 
413  $pageInfo['contentmodel'] = $title->getContentModel();
414 
415  $pageLanguage = $title->getPageLanguage();
416  $pageInfo['pagelanguage'] = $pageLanguage->getCode();
417  $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
418  $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
419 
420  $user = $this->getUser();
421 
422  if ( $titleExists ) {
423  $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
424  $pageInfo['lastrevid'] = (int)$this->pageLatest[$pageid];
425  $pageInfo['length'] = (int)$this->pageLength[$pageid];
426 
427  if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
428  $pageInfo['redirect'] = true;
429  }
430  if ( $this->pageIsNew[$pageid] ) {
431  $pageInfo['new'] = true;
432  }
433  }
434 
435  if ( !is_null( $this->params['token'] ) ) {
437  $pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
438  foreach ( $this->params['token'] as $t ) {
439  $val = call_user_func( $tokenFunctions[$t], $pageid, $title );
440  if ( $val === false ) {
441  $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
442  } else {
443  $pageInfo[$t . 'token'] = $val;
444  }
445  }
446  }
447 
448  if ( $this->fld_protection ) {
449  $pageInfo['protection'] = [];
450  if ( isset( $this->protections[$ns][$dbkey] ) ) {
451  $pageInfo['protection'] =
452  $this->protections[$ns][$dbkey];
453  }
454  ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
455 
456  $pageInfo['restrictiontypes'] = [];
457  if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
458  $pageInfo['restrictiontypes'] =
459  $this->restrictionTypes[$ns][$dbkey];
460  }
461  ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
462  }
463 
464  if ( $this->fld_watched && $this->watched !== null ) {
465  $pageInfo['watched'] = $this->watched[$ns][$dbkey];
466  }
467 
468  if ( $this->fld_watchers ) {
469  if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
470  $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
471  } elseif ( $this->showZeroWatchers ) {
472  $pageInfo['watchers'] = 0;
473  }
474  }
475 
476  if ( $this->fld_visitingwatchers ) {
477  if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
478  $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
479  } elseif ( $this->showZeroWatchers ) {
480  $pageInfo['visitingwatchers'] = 0;
481  }
482  }
483 
484  if ( $this->fld_notificationtimestamp ) {
485  $pageInfo['notificationtimestamp'] = '';
486  if ( $this->notificationtimestamps[$ns][$dbkey] ) {
487  $pageInfo['notificationtimestamp'] =
488  wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
489  }
490  }
491 
492  if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
493  $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
494  }
495 
496  if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
497  $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
498  }
499 
500  if ( $this->fld_url ) {
501  $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
502  $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
503  $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
504  }
505  if ( $this->fld_readable ) {
506  $pageInfo['readable'] = $this->getPermissionManager()->userCan(
507  'read', $user, $title
508  );
509  }
510 
511  if ( $this->fld_preload ) {
512  if ( $titleExists ) {
513  $pageInfo['preload'] = '';
514  } else {
515  $text = null;
516  Hooks::run( 'EditFormPreloadText', [ &$text, &$title ] );
517 
518  $pageInfo['preload'] = $text;
519  }
520  }
521 
522  if ( $this->fld_displaytitle ) {
523  if ( isset( $this->displaytitles[$pageid] ) ) {
524  $pageInfo['displaytitle'] = $this->displaytitles[$pageid];
525  } else {
526  $pageInfo['displaytitle'] = $title->getPrefixedText();
527  }
528  }
529 
530  if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
531  $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
532  }
533 
534  if ( $this->params['testactions'] ) {
535  $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
536  if ( $this->countTestedActions >= $limit ) {
537  return null; // force a continuation
538  }
539 
540  $detailLevel = $this->params['testactionsdetail'];
541  $rigor = $detailLevel === 'quick' ? 'quick' : 'secure';
542  $errorFormatter = $this->getErrorFormatter();
543  if ( $errorFormatter->getFormat() === 'bc' ) {
544  // Eew, no. Use a more modern format here.
545  $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
546  }
547 
548  $user = $this->getUser();
549  $pageInfo['actions'] = [];
550  foreach ( $this->params['testactions'] as $action ) {
551  $this->countTestedActions++;
552 
553  if ( $detailLevel === 'boolean' ) {
554  $pageInfo['actions'][$action] = $this->getPermissionManager()->userCan(
555  $action, $user, $title
556  );
557  } else {
558  $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $this->errorArrayToStatus(
559  $this->getPermissionManager()->getPermissionErrors(
560  $action, $user, $title, $rigor
561  ),
562  $user
563  ) );
564  }
565  }
566  }
567 
568  return $pageInfo;
569  }
570 
574  private function getProtectionInfo() {
575  $this->protections = [];
576  $db = $this->getDB();
577 
578  // Get normal protections for existing titles
579  if ( count( $this->titles ) ) {
580  $this->resetQueryParams();
581  $this->addTables( 'page_restrictions' );
582  $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
583  'pr_expiry', 'pr_cascade' ] );
584  $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
585 
586  $res = $this->select( __METHOD__ );
587  foreach ( $res as $row ) {
589  $title = $this->titles[$row->pr_page];
590  $a = [
591  'type' => $row->pr_type,
592  'level' => $row->pr_level,
593  'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
594  ];
595  if ( $row->pr_cascade ) {
596  $a['cascade'] = true;
597  }
598  $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
599  }
600  // Also check old restrictions
601  foreach ( $this->titles as $pageId => $title ) {
602  if ( $this->pageRestrictions[$pageId] ) {
603  $namespace = $title->getNamespace();
604  $dbKey = $title->getDBkey();
605  $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
606  foreach ( $restrictions as $restrict ) {
607  $temp = explode( '=', trim( $restrict ) );
608  if ( count( $temp ) == 1 ) {
609  // old old format should be treated as edit/move restriction
610  $restriction = trim( $temp[0] );
611 
612  if ( $restriction == '' ) {
613  continue;
614  }
615  $this->protections[$namespace][$dbKey][] = [
616  'type' => 'edit',
617  'level' => $restriction,
618  'expiry' => 'infinity',
619  ];
620  $this->protections[$namespace][$dbKey][] = [
621  'type' => 'move',
622  'level' => $restriction,
623  'expiry' => 'infinity',
624  ];
625  } else {
626  $restriction = trim( $temp[1] );
627  if ( $restriction == '' ) {
628  continue;
629  }
630  $this->protections[$namespace][$dbKey][] = [
631  'type' => $temp[0],
632  'level' => $restriction,
633  'expiry' => 'infinity',
634  ];
635  }
636  }
637  }
638  }
639  }
640 
641  // Get protections for missing titles
642  if ( count( $this->missing ) ) {
643  $this->resetQueryParams();
644  $lb = new LinkBatch( $this->missing );
645  $this->addTables( 'protected_titles' );
646  $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
647  $this->addWhere( $lb->constructSet( 'pt', $db ) );
648  $res = $this->select( __METHOD__ );
649  foreach ( $res as $row ) {
650  $this->protections[$row->pt_namespace][$row->pt_title][] = [
651  'type' => 'create',
652  'level' => $row->pt_create_perm,
653  'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
654  ];
655  }
656  }
657 
658  // Separate good and missing titles into files and other pages
659  // and populate $this->restrictionTypes
660  $images = $others = [];
661  foreach ( $this->everything as $title ) {
662  if ( $title->getNamespace() == NS_FILE ) {
663  $images[] = $title->getDBkey();
664  } else {
665  $others[] = $title;
666  }
667  // Applicable protection types
668  $this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
669  array_values( $title->getRestrictionTypes() );
670  }
671 
672  if ( count( $others ) ) {
673  // Non-images: check templatelinks
674  $lb = new LinkBatch( $others );
675  $this->resetQueryParams();
676  $this->addTables( [ 'page_restrictions', 'page', 'templatelinks' ] );
677  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
678  'page_title', 'page_namespace',
679  'tl_title', 'tl_namespace' ] );
680  $this->addWhere( $lb->constructSet( 'tl', $db ) );
681  $this->addWhere( 'pr_page = page_id' );
682  $this->addWhere( 'pr_page = tl_from' );
683  $this->addWhereFld( 'pr_cascade', 1 );
684 
685  $res = $this->select( __METHOD__ );
686  foreach ( $res as $row ) {
687  $source = Title::makeTitle( $row->page_namespace, $row->page_title );
688  $this->protections[$row->tl_namespace][$row->tl_title][] = [
689  'type' => $row->pr_type,
690  'level' => $row->pr_level,
691  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
692  'source' => $source->getPrefixedText()
693  ];
694  }
695  }
696 
697  if ( count( $images ) ) {
698  // Images: check imagelinks
699  $this->resetQueryParams();
700  $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
701  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
702  'page_title', 'page_namespace', 'il_to' ] );
703  $this->addWhere( 'pr_page = page_id' );
704  $this->addWhere( 'pr_page = il_from' );
705  $this->addWhereFld( 'pr_cascade', 1 );
706  $this->addWhereFld( 'il_to', $images );
707 
708  $res = $this->select( __METHOD__ );
709  foreach ( $res as $row ) {
710  $source = Title::makeTitle( $row->page_namespace, $row->page_title );
711  $this->protections[NS_FILE][$row->il_to][] = [
712  'type' => $row->pr_type,
713  'level' => $row->pr_level,
714  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
715  'source' => $source->getPrefixedText()
716  ];
717  }
718  }
719  }
720 
725  private function getTSIDs() {
726  $getTitles = $this->talkids = $this->subjectids = [];
727  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
728 
730  foreach ( $this->everything as $t ) {
731  if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
732  if ( $this->fld_subjectid ) {
733  $getTitles[] = $t->getSubjectPage();
734  }
735  } elseif ( $this->fld_talkid ) {
736  $getTitles[] = $t->getTalkPage();
737  }
738  }
739  if ( $getTitles === [] ) {
740  return;
741  }
742 
743  $db = $this->getDB();
744 
745  // Construct a custom WHERE clause that matches
746  // all titles in $getTitles
747  $lb = new LinkBatch( $getTitles );
748  $this->resetQueryParams();
749  $this->addTables( 'page' );
750  $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
751  $this->addWhere( $lb->constructSet( 'page', $db ) );
752  $res = $this->select( __METHOD__ );
753  foreach ( $res as $row ) {
754  if ( $nsInfo->isTalk( $row->page_namespace ) ) {
755  $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
756  (int)( $row->page_id );
757  } else {
758  $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
759  (int)( $row->page_id );
760  }
761  }
762  }
763 
764  private function getDisplayTitle() {
765  $this->displaytitles = [];
766 
767  $pageIds = array_keys( $this->titles );
768 
769  if ( $pageIds === [] ) {
770  return;
771  }
772 
773  $this->resetQueryParams();
774  $this->addTables( 'page_props' );
775  $this->addFields( [ 'pp_page', 'pp_value' ] );
776  $this->addWhereFld( 'pp_page', $pageIds );
777  $this->addWhereFld( 'pp_propname', 'displaytitle' );
778  $res = $this->select( __METHOD__ );
779 
780  foreach ( $res as $row ) {
781  $this->displaytitles[$row->pp_page] = $row->pp_value;
782  }
783  }
784 
785  private function getVariantTitles() {
786  if ( $this->titles === [] ) {
787  return;
788  }
789  $this->variantTitles = [];
790  foreach ( $this->titles as $pageId => $t ) {
791  $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
792  ? $this->getAllVariants( $this->displaytitles[$pageId] )
793  : $this->getAllVariants( $t->getText(), $t->getNamespace() );
794  }
795  }
796 
797  private function getAllVariants( $text, $ns = NS_MAIN ) {
798  $result = [];
799  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
800  foreach ( $contLang->getVariants() as $variant ) {
801  $convertTitle = $contLang->autoConvert( $text, $variant );
802  if ( $ns !== NS_MAIN ) {
803  $convertNs = $contLang->convertNamespace( $ns, $variant );
804  $convertTitle = $convertNs . ':' . $convertTitle;
805  }
806  $result[$variant] = $convertTitle;
807  }
808  return $result;
809  }
810 
815  private function getWatchedInfo() {
816  $user = $this->getUser();
817 
818  if ( $user->isAnon() || count( $this->everything ) == 0
819  || !$this->getPermissionManager()->userHasRight( $user, 'viewmywatchlist' )
820  ) {
821  return;
822  }
823 
824  $this->watched = [];
825  $this->notificationtimestamps = [];
826 
827  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
828  $timestamps = $store->getNotificationTimestampsBatch( $user, $this->everything );
829 
830  if ( $this->fld_watched ) {
831  foreach ( $timestamps as $namespaceId => $dbKeys ) {
832  $this->watched[$namespaceId] = array_map(
833  function ( $x ) {
834  return $x !== false;
835  },
836  $dbKeys
837  );
838  }
839  }
840  if ( $this->fld_notificationtimestamp ) {
841  $this->notificationtimestamps = $timestamps;
842  }
843  }
844 
848  private function getWatcherInfo() {
849  if ( count( $this->everything ) == 0 ) {
850  return;
851  }
852 
853  $user = $this->getUser();
854  $canUnwatchedpages = $this->getPermissionManager()->userHasRight( $user, 'unwatchedpages' );
855  $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
856  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
857  return;
858  }
859 
860  $this->showZeroWatchers = $canUnwatchedpages;
861 
862  $countOptions = [];
863  if ( !$canUnwatchedpages ) {
864  $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
865  }
866 
867  $this->watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchersMultiple(
868  $this->everything,
869  $countOptions
870  );
871  }
872 
879  private function getVisitingWatcherInfo() {
880  $config = $this->getConfig();
881  $user = $this->getUser();
882  $db = $this->getDB();
883 
884  $canUnwatchedpages = $this->getPermissionManager()->userHasRight( $user, 'unwatchedpages' );
885  $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
886  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
887  return;
888  }
889 
890  $this->showZeroWatchers = $canUnwatchedpages;
891 
892  $titlesWithThresholds = [];
893  if ( $this->titles ) {
894  $lb = new LinkBatch( $this->titles );
895 
896  // Fetch last edit timestamps for pages
897  $this->resetQueryParams();
898  $this->addTables( [ 'page', 'revision' ] );
899  $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
900  $this->addWhere( [
901  'page_latest = rev_id',
902  $lb->constructSet( 'page', $db ),
903  ] );
904  $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
905  $timestampRes = $this->select( __METHOD__ );
906 
907  $age = $config->get( 'WatchersMaxAge' );
908  $timestamps = [];
909  foreach ( $timestampRes as $row ) {
910  $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
911  $timestamps[$row->page_namespace][$row->page_title] = $revTimestamp - $age;
912  }
913  $titlesWithThresholds = array_map(
914  function ( LinkTarget $target ) use ( $timestamps ) {
915  return [
916  $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
917  ];
918  },
920  );
921  }
922 
923  if ( $this->missing ) {
924  $titlesWithThresholds = array_merge(
925  $titlesWithThresholds,
926  array_map(
927  function ( LinkTarget $target ) {
928  return [ $target, null ];
929  },
931  )
932  );
933  }
934  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
935  $this->visitingwatchers = $store->countVisitingWatchersMultiple(
936  $titlesWithThresholds,
937  !$canUnwatchedpages ? $unwatchedPageThreshold : null
938  );
939  }
940 
941  public function getCacheMode( $params ) {
942  // Other props depend on something about the current user
943  $publicProps = [
944  'protection',
945  'talkid',
946  'subjectid',
947  'url',
948  'preload',
949  'displaytitle',
950  'varianttitles',
951  ];
952  if ( array_diff( (array)$params['prop'], $publicProps ) ) {
953  return 'private';
954  }
955 
956  // testactions also depends on the current user
957  if ( $params['testactions'] ) {
958  return 'private';
959  }
960 
961  if ( !is_null( $params['token'] ) ) {
962  return 'private';
963  }
964 
965  return 'public';
966  }
967 
968  public function getAllowedParams() {
969  return [
970  'prop' => [
971  ApiBase::PARAM_ISMULTI => true,
973  'protection',
974  'talkid',
975  'watched', # private
976  'watchers', # private
977  'visitingwatchers', # private
978  'notificationtimestamp', # private
979  'subjectid',
980  'url',
981  'readable', # private
982  'preload',
983  'displaytitle',
984  'varianttitles',
985  // If you add more properties here, please consider whether they
986  // need to be added to getCacheMode()
987  ],
990  'readable' => true, // Since 1.32
991  ],
992  ],
993  'testactions' => [
994  ApiBase::PARAM_TYPE => 'string',
995  ApiBase::PARAM_ISMULTI => true,
996  ],
997  'testactionsdetail' => [
998  ApiBase::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
999  ApiBase::PARAM_DFLT => 'boolean',
1001  ],
1002  'token' => [
1003  ApiBase::PARAM_DEPRECATED => true,
1004  ApiBase::PARAM_ISMULTI => true,
1005  ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() )
1006  ],
1007  'continue' => [
1008  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
1009  ],
1010  ];
1011  }
1012 
1013  protected function getExamplesMessages() {
1014  return [
1015  'action=query&prop=info&titles=Main%20Page'
1016  => 'apihelp-query+info-example-simple',
1017  'action=query&prop=info&inprop=protection&titles=Main%20Page'
1018  => 'apihelp-query+info-example-protection',
1019  ];
1020  }
1021 
1022  public function getHelpUrls() {
1023  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
1024  }
1025 }
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below...
Definition: ApiBase.php:94
requestExtraData( $pageSet)
getErrorFormatter()
Get the error formatter.
Definition: ApiBase.php:654
getDB()
Get the Query database connection (read-only)
static getUnblockToken( $pageid, $title)
getResult()
Get the result object.
Definition: ApiBase.php:640
const NS_MAIN
Definition: Defines.php:60
static getWatchToken( $pageid, $title)
static string [] $cachedTokens
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition: ApiBase.php:55
getMain()
Get the main module.
Definition: ApiBase.php:536
static getImportToken( $pageid, $title)
Title [] $missing
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
This is a base class for all Query modules.
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition: ApiBase.php:568
$source
const PROTO_CURRENT
Definition: Defines.php:202
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user...
Definition: ApiBase.php:761
getPageSet()
Get the PageSet object to work on.
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition: ApiBase.php:164
static getOptionsToken( $pageid, $title)
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:616
getNamespace()
Get the namespace index.
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Definition: ApiResult.php:1205
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:34
extractPageInfo( $pageid, $title)
Get a result array with information about a title.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
A query module to show basic page information.
static getEditToken( $pageid, $title)
getWatchedInfo()
Get information about watched status and put it in $this->watched and $this->notificationtimestamps.
dieContinueUsageIf( $condition)
Die with the &#39;badcontinue&#39; error.
Definition: ApiBase.php:2204
getDBkey()
Get the main part with underscores.
addFields( $value)
Add a set of fields to select to the internal array.
getTSIDs()
Get talk page IDs (if requested) and subject page IDs (if requested) and put them in $talkids and $su...
const NS_FILE
Definition: Defines.php:66
__construct(ApiQuery $query, $moduleName)
static getMoveToken( $pageid, $title)
This is the main query class.
Definition: ApiQuery.php:37
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter...
Definition: ApiBase.php:131
Title [] $everything
getWatcherInfo()
Get the count of watchers and put it in $this->watchers.
Title [] $titles
errorArrayToStatus(array $errors, User $user=null)
Turn an array of message keys or key+param arrays into a Status.
Definition: ApiBase.php:1822
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:613
static getDeleteToken( $pageid, $title)
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
getTokenFunctions()
Get an array mapping token names to their handler functions.
const PROTO_CANONICAL
Definition: Defines.php:203
getProtectionInfo()
Get information about protections and put it in $protections.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition: ApiBase.php:1930
const PARAM_DEPRECATED_VALUES
(array) When PARAM_TYPE is an array, this indicates which of the values are deprecated.
Definition: ApiBase.php:209
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition: ApiBase.php:58
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks...
Definition: ApiBase.php:710
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
static compare(LinkTarget $a, LinkTarget $b)
Callback for usort() to do title sorts by (namespace, title)
Definition: Title.php:841
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
const PARAM_DEPRECATED
(boolean) Is the parameter deprecated (will show a warning)?
Definition: ApiBase.php:112
getVisitingWatcherInfo()
Get the count of watchers who have visited recent edits and put it in $this->visitingwatchers.
static getProtectToken( $pageid, $title)
static getBlockToken( $pageid, $title)
static resetTokenCache()
static getEmailToken( $pageid, $title)
getCacheMode( $params)
getAllVariants( $text, $ns=NS_MAIN)
resetQueryParams()
Blank the internal arrays with query parameters.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.