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