MediaWiki  master
ApiQueryInfo.php
Go to the documentation of this file.
1 <?php
32 
38 class ApiQueryInfo extends ApiQueryBase {
39 
41  private $languageConverter;
43  private $linkBatchFactory;
45  private $namespaceInfo;
47  private $titleFactory;
49  private $titleFormatter;
51  private $watchedItemStore;
53  private $restrictionStore;
55  private $linksMigration;
56 
57  private $fld_protection = false, $fld_talkid = false,
58  $fld_subjectid = false, $fld_url = false,
59  $fld_readable = false, $fld_watched = false,
63 
68  private $fld_linkclasses = false;
69 
73  private $fld_associatedpage = false;
74 
75  private $params;
76 
78  private $titles;
80  private $missing;
82  private $everything;
83 
84  private $pageIsRedir, $pageIsNew, $pageTouched,
86 
89 
94  private $watchlistExpiries;
95 
100  private $linkClasses;
101 
102  private $showZeroWatchers = false;
103 
104  private $countTestedActions = 0;
105 
119  public function __construct(
120  ApiQuery $queryModule,
121  $moduleName,
122  Language $contentLanguage,
123  LinkBatchFactory $linkBatchFactory,
124  NamespaceInfo $namespaceInfo,
125  TitleFactory $titleFactory,
126  TitleFormatter $titleFormatter,
127  WatchedItemStore $watchedItemStore,
128  LanguageConverterFactory $languageConverterFactory,
129  RestrictionStore $restrictionStore,
130  LinksMigration $linksMigration
131  ) {
132  parent::__construct( $queryModule, $moduleName, 'in' );
133  $this->languageConverter = $languageConverterFactory->getLanguageConverter( $contentLanguage );
134  $this->linkBatchFactory = $linkBatchFactory;
135  $this->namespaceInfo = $namespaceInfo;
136  $this->titleFactory = $titleFactory;
137  $this->titleFormatter = $titleFormatter;
138  $this->watchedItemStore = $watchedItemStore;
139  $this->restrictionStore = $restrictionStore;
140  $this->linksMigration = $linksMigration;
141  }
142 
147  public function requestExtraData( $pageSet ) {
148  // If the pageset is resolving redirects we won't get page_is_redirect.
149  // But we can't know for sure until the pageset is executed (revids may
150  // turn it off), so request it unconditionally.
151  $pageSet->requestField( 'page_is_redirect' );
152  $pageSet->requestField( 'page_is_new' );
153  $config = $this->getConfig();
154  $pageSet->requestField( 'page_touched' );
155  $pageSet->requestField( 'page_latest' );
156  $pageSet->requestField( 'page_len' );
157  $pageSet->requestField( 'page_content_model' );
158  if ( $config->get( MainConfigNames::PageLanguageUseDB ) ) {
159  $pageSet->requestField( 'page_lang' );
160  }
161  }
162 
163  public function execute() {
164  $this->params = $this->extractRequestParams();
165  if ( $this->params['prop'] !== null ) {
166  $prop = array_fill_keys( $this->params['prop'], true );
167  $this->fld_protection = isset( $prop['protection'] );
168  $this->fld_watched = isset( $prop['watched'] );
169  $this->fld_watchers = isset( $prop['watchers'] );
170  $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
171  $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
172  $this->fld_talkid = isset( $prop['talkid'] );
173  $this->fld_subjectid = isset( $prop['subjectid'] );
174  $this->fld_url = isset( $prop['url'] );
175  $this->fld_readable = isset( $prop['readable'] );
176  $this->fld_preload = isset( $prop['preload'] );
177  $this->fld_displaytitle = isset( $prop['displaytitle'] );
178  $this->fld_varianttitles = isset( $prop['varianttitles'] );
179  $this->fld_linkclasses = isset( $prop['linkclasses'] );
180  $this->fld_associatedpage = isset( $prop['associatedpage'] );
181  }
182 
183  $pageSet = $this->getPageSet();
184  $this->titles = $pageSet->getGoodTitles();
185  $this->missing = $pageSet->getMissingTitles();
186  $this->everything = $this->titles + $this->missing;
187  $result = $this->getResult();
188 
189  uasort( $this->everything, [ Title::class, 'compare' ] );
190  if ( $this->params['continue'] !== null ) {
191  // Throw away any titles we're gonna skip so they don't
192  // clutter queries
193  $cont = explode( '|', $this->params['continue'] );
194  $this->dieContinueUsageIf( count( $cont ) != 2 );
195  $conttitle = $this->titleFactory->makeTitleSafe( (int)$cont[0], $cont[1] );
196  $this->dieContinueUsageIf( !$conttitle );
197  foreach ( $this->everything as $pageid => $title ) {
198  if ( Title::compare( $title, $conttitle ) >= 0 ) {
199  break;
200  }
201  unset( $this->titles[$pageid] );
202  unset( $this->missing[$pageid] );
203  unset( $this->everything[$pageid] );
204  }
205  }
206 
207  // when resolving redirects, no page will have this field
208  $this->pageIsRedir = !$pageSet->isResolvingRedirects()
209  ? $pageSet->getCustomField( 'page_is_redirect' )
210  : [];
211  $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
212 
213  $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
214  $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
215  $this->pageLength = $pageSet->getCustomField( 'page_len' );
216 
217  // Get protection info if requested
218  if ( $this->fld_protection ) {
219  $this->getProtectionInfo();
220  }
221 
222  if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
223  $this->getWatchedInfo();
224  }
225 
226  if ( $this->fld_watchers ) {
227  $this->getWatcherInfo();
228  }
229 
230  if ( $this->fld_visitingwatchers ) {
231  $this->getVisitingWatcherInfo();
232  }
233 
234  // Run the talkid/subjectid query if requested
235  if ( $this->fld_talkid || $this->fld_subjectid ) {
236  $this->getTSIDs();
237  }
238 
239  if ( $this->fld_displaytitle ) {
240  $this->getDisplayTitle();
241  }
242 
243  if ( $this->fld_varianttitles ) {
244  $this->getVariantTitles();
245  }
246 
247  if ( $this->fld_linkclasses ) {
248  $this->getLinkClasses( $this->params['linkcontext'] );
249  }
250 
252  foreach ( $this->everything as $pageid => $title ) {
253  $pageInfo = $this->extractPageInfo( $pageid, $title );
254  $fit = $pageInfo !== null && $result->addValue( [
255  'query',
256  'pages'
257  ], $pageid, $pageInfo );
258  if ( !$fit ) {
259  $this->setContinueEnumParameter( 'continue',
260  $title->getNamespace() . '|' .
261  $title->getText() );
262  break;
263  }
264  }
265  }
266 
273  private function extractPageInfo( $pageid, $title ) {
274  $pageInfo = [];
275  // $title->exists() needs pageid, which is not set for all title objects
276  $titleExists = $pageid > 0;
277  $ns = $title->getNamespace();
278  $dbkey = $title->getDBkey();
279 
280  $pageInfo['contentmodel'] = $title->getContentModel();
281 
282  $pageLanguage = $title->getPageLanguage();
283  $pageInfo['pagelanguage'] = $pageLanguage->getCode();
284  $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
285  $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
286 
287  if ( $titleExists ) {
288  $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
289  $pageInfo['lastrevid'] = (int)$this->pageLatest[$pageid];
290  $pageInfo['length'] = (int)$this->pageLength[$pageid];
291 
292  if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
293  $pageInfo['redirect'] = true;
294  }
295  if ( $this->pageIsNew[$pageid] ) {
296  $pageInfo['new'] = true;
297  }
298  }
299 
300  if ( $this->fld_protection ) {
301  $pageInfo['protection'] = [];
302  if ( isset( $this->protections[$ns][$dbkey] ) ) {
303  $pageInfo['protection'] =
304  $this->protections[$ns][$dbkey];
305  }
306  ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
307 
308  $pageInfo['restrictiontypes'] = [];
309  if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
310  $pageInfo['restrictiontypes'] =
311  $this->restrictionTypes[$ns][$dbkey];
312  }
313  ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
314  }
315 
316  if ( $this->fld_watched ) {
317  $pageInfo['watched'] = false;
318 
319  if ( isset( $this->watched[$ns][$dbkey] ) ) {
320  $pageInfo['watched'] = $this->watched[$ns][$dbkey];
321  }
322 
323  if ( isset( $this->watchlistExpiries[$ns][$dbkey] ) ) {
324  $pageInfo['watchlistexpiry'] = $this->watchlistExpiries[$ns][$dbkey];
325  }
326  }
327 
328  if ( $this->fld_watchers ) {
329  if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
330  $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
331  } elseif ( $this->showZeroWatchers ) {
332  $pageInfo['watchers'] = 0;
333  }
334  }
335 
336  if ( $this->fld_visitingwatchers ) {
337  if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
338  $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
339  } elseif ( $this->showZeroWatchers ) {
340  $pageInfo['visitingwatchers'] = 0;
341  }
342  }
343 
344  if ( $this->fld_notificationtimestamp ) {
345  $pageInfo['notificationtimestamp'] = '';
346  if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
347  $pageInfo['notificationtimestamp'] =
348  wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
349  }
350  }
351 
352  if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
353  $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
354  }
355 
356  if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
357  $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
358  }
359 
360  if ( $this->fld_associatedpage && $ns >= NS_MAIN ) {
361  $pageInfo['associatedpage'] = $this->titleFormatter->getPrefixedText(
362  $this->namespaceInfo->getAssociatedPage( $title )
363  );
364  }
365 
366  if ( $this->fld_url ) {
367  $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
368  $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
369  $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
370  }
371  if ( $this->fld_readable ) {
372  $pageInfo['readable'] = $this->getAuthority()->definitelyCan( 'read', $title );
373  }
374 
375  if ( $this->fld_preload ) {
376  if ( $titleExists ) {
377  $pageInfo['preload'] = '';
378  } else {
379  $text = null;
380  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
381  $this->getHookRunner()->onEditFormPreloadText( $text, $title );
382 
383  $pageInfo['preload'] = $text;
384  }
385  }
386 
387  if ( $this->fld_displaytitle ) {
388  $pageInfo['displaytitle'] = $this->displaytitles[$pageid] ??
389  htmlspecialchars( $title->getPrefixedText(), ENT_NOQUOTES );
390  }
391 
392  if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
393  $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
394  }
395 
396  if ( $this->fld_linkclasses && isset( $this->linkClasses[$pageid] ) ) {
397  $pageInfo['linkclasses'] = $this->linkClasses[$pageid];
398  }
399 
400  if ( $this->params['testactions'] ) {
401  $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
402  if ( $this->countTestedActions >= $limit ) {
403  return null; // force a continuation
404  }
405 
406  $detailLevel = $this->params['testactionsdetail'];
407  $errorFormatter = $this->getErrorFormatter();
408  if ( $errorFormatter->getFormat() === 'bc' ) {
409  // Eew, no. Use a more modern format here.
410  $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
411  }
412 
413  $pageInfo['actions'] = [];
414  foreach ( $this->params['testactions'] as $action ) {
415  $this->countTestedActions++;
416 
417  if ( $detailLevel === 'boolean' ) {
418  $pageInfo['actions'][$action] = $this->getAuthority()->authorizeRead( $action, $title );
419  } else {
420  $status = new PermissionStatus();
421  if ( $detailLevel === 'quick' ) {
422  $this->getAuthority()->probablyCan( $action, $title, $status );
423  } else {
424  $this->getAuthority()->definitelyCan( $action, $title, $status );
425  }
426  $this->addBlockInfoToStatus( $status );
427  $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $status );
428  }
429  }
430  }
431 
432  return $pageInfo;
433  }
434 
438  private function getProtectionInfo() {
439  $this->protections = [];
440  $db = $this->getDB();
441 
442  // Get normal protections for existing titles
443  if ( count( $this->titles ) ) {
444  $this->resetQueryParams();
445  $this->addTables( 'page_restrictions' );
446  $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
447  'pr_expiry', 'pr_cascade' ] );
448  $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
449 
450  $res = $this->select( __METHOD__ );
451  foreach ( $res as $row ) {
453  $title = $this->titles[$row->pr_page];
454  $a = [
455  'type' => $row->pr_type,
456  'level' => $row->pr_level,
457  'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
458  ];
459  if ( $row->pr_cascade ) {
460  $a['cascade'] = true;
461  }
462  $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
463  }
464  }
465 
466  // Get protections for missing titles
467  if ( count( $this->missing ) ) {
468  $this->resetQueryParams();
469  $lb = $this->linkBatchFactory->newLinkBatch( $this->missing );
470  $this->addTables( 'protected_titles' );
471  $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
472  $this->addWhere( $lb->constructSet( 'pt', $db ) );
473  $res = $this->select( __METHOD__ );
474  foreach ( $res as $row ) {
475  $this->protections[$row->pt_namespace][$row->pt_title][] = [
476  'type' => 'create',
477  'level' => $row->pt_create_perm,
478  'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
479  ];
480  }
481  }
482 
483  // Separate good and missing titles into files and other pages
484  // and populate $this->restrictionTypes
485  $images = $others = [];
486  foreach ( $this->everything as $title ) {
487  if ( $title->getNamespace() === NS_FILE ) {
488  $images[] = $title->getDBkey();
489  } else {
490  $others[] = $title;
491  }
492  // Applicable protection types
493  $this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
494  array_values( $this->restrictionStore->listApplicableRestrictionTypes( $title ) );
495  }
496 
497  list( $blNamespace, $blTitle ) = $this->linksMigration->getTitleFields( 'templatelinks' );
498  $queryInfo = $this->linksMigration->getQueryInfo( 'templatelinks' );
499 
500  if ( count( $others ) ) {
501  // Non-images: check templatelinks
502  $lb = $this->linkBatchFactory->newLinkBatch( $others );
503  $this->resetQueryParams();
504  $this->addTables( array_merge( [ 'page_restrictions', 'page' ], $queryInfo['tables'] ) );
505  // templatelinks must use PRIMARY index and not the tl_target_id.
506  $this->addOption( 'USE INDEX', [ 'templatelinks' => 'PRIMARY' ] );
507  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
508  'page_title', 'page_namespace',
509  $blNamespace, $blTitle ] );
510  $this->addWhere( $lb->constructSet( 'tl', $db ) );
511  $this->addWhere( 'pr_page = page_id' );
512  $this->addWhere( 'pr_page = tl_from' );
513  $this->addWhereFld( 'pr_cascade', 1 );
514  $this->addJoinConds( $queryInfo['joins'] );
515 
516  $res = $this->select( __METHOD__ );
517  foreach ( $res as $row ) {
518  $this->protections[$row->$blNamespace][$row->$blTitle][] = [
519  'type' => $row->pr_type,
520  'level' => $row->pr_level,
521  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
522  'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
523  ];
524  }
525  }
526 
527  if ( count( $images ) ) {
528  // Images: check imagelinks
529  $this->resetQueryParams();
530  $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
531  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
532  'page_title', 'page_namespace', 'il_to' ] );
533  $this->addWhere( 'pr_page = page_id' );
534  $this->addWhere( 'pr_page = il_from' );
535  $this->addWhereFld( 'pr_cascade', 1 );
536  $this->addWhereFld( 'il_to', $images );
537 
538  $res = $this->select( __METHOD__ );
539  foreach ( $res as $row ) {
540  $this->protections[NS_FILE][$row->il_to][] = [
541  'type' => $row->pr_type,
542  'level' => $row->pr_level,
543  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
544  'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
545  ];
546  }
547  }
548  }
549 
554  private function getTSIDs() {
555  $getTitles = $this->talkids = $this->subjectids = [];
556  $nsInfo = $this->namespaceInfo;
557 
559  foreach ( $this->everything as $t ) {
560  if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
561  if ( $this->fld_subjectid ) {
562  $getTitles[] = $t->getSubjectPage();
563  }
564  } elseif ( $this->fld_talkid ) {
565  $getTitles[] = $t->getTalkPage();
566  }
567  }
568  if ( $getTitles === [] ) {
569  return;
570  }
571 
572  $db = $this->getDB();
573 
574  // Construct a custom WHERE clause that matches
575  // all titles in $getTitles
576  $lb = $this->linkBatchFactory->newLinkBatch( $getTitles );
577  $this->resetQueryParams();
578  $this->addTables( 'page' );
579  $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
580  $this->addWhere( $lb->constructSet( 'page', $db ) );
581  $res = $this->select( __METHOD__ );
582  foreach ( $res as $row ) {
583  if ( $nsInfo->isTalk( $row->page_namespace ) ) {
584  $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
585  (int)( $row->page_id );
586  } else {
587  $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
588  (int)( $row->page_id );
589  }
590  }
591  }
592 
593  private function getDisplayTitle() {
594  $this->displaytitles = [];
595 
596  $pageIds = array_keys( $this->titles );
597 
598  if ( $pageIds === [] ) {
599  return;
600  }
601 
602  $this->resetQueryParams();
603  $this->addTables( 'page_props' );
604  $this->addFields( [ 'pp_page', 'pp_value' ] );
605  $this->addWhereFld( 'pp_page', $pageIds );
606  $this->addWhereFld( 'pp_propname', 'displaytitle' );
607  $res = $this->select( __METHOD__ );
608 
609  foreach ( $res as $row ) {
610  $this->displaytitles[$row->pp_page] = $row->pp_value;
611  }
612  }
613 
621  private function getLinkClasses( ?LinkTarget $context_title = null ) {
622  if ( $this->titles === [] ) {
623  return;
624  }
625  // For compatibility with legacy GetLinkColours hook:
626  // $pagemap maps from page id to title (as prefixed db key)
627  // $classes maps from title (prefixed db key) to a space-separated
628  // list of link classes ("link colours").
629  // The hook should not modify $pagemap, and should only append to
630  // $classes (being careful to maintain space separation).
631  $classes = [];
632  $pagemap = [];
633  foreach ( $this->titles as $pageId => $title ) {
634  $pdbk = $title->getPrefixedDBkey();
635  $pagemap[$pageId] = $pdbk;
636  $classes[$pdbk] = $title->isRedirect() ? 'mw-redirect' : '';
637  }
638  // legacy hook requires a real Title, not a LinkTarget
639  $context_title = $this->titleFactory->newFromLinkTarget(
640  $context_title ?? $this->titleFactory->newMainPage()
641  );
642  $this->getHookRunner()->onGetLinkColours(
643  $pagemap, $classes, $context_title
644  );
645 
646  // This API class expects the class list to be:
647  // (a) indexed by pageid, not title, and
648  // (b) a proper array of strings (possibly zero-length),
649  // not a single space-separated string (possibly the empty string)
650  $this->linkClasses = [];
651  foreach ( $this->titles as $pageId => $title ) {
652  $pdbk = $title->getPrefixedDBkey();
653  $this->linkClasses[$pageId] = preg_split(
654  '/\s+/', $classes[$pdbk] ?? '', -1, PREG_SPLIT_NO_EMPTY
655  );
656  }
657  }
658 
659  private function getVariantTitles() {
660  if ( $this->titles === [] ) {
661  return;
662  }
663  $this->variantTitles = [];
664  foreach ( $this->titles as $pageId => $t ) {
665  $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
666  ? $this->getAllVariants( $this->displaytitles[$pageId] )
667  : $this->getAllVariants( $t->getText(), $t->getNamespace() );
668  }
669  }
670 
671  private function getAllVariants( $text, $ns = NS_MAIN ) {
672  $result = [];
673  foreach ( $this->languageConverter->getVariants() as $variant ) {
674  $convertTitle = $this->languageConverter->autoConvert( $text, $variant );
675  if ( $ns !== NS_MAIN ) {
676  $convertNs = $this->languageConverter->convertNamespace( $ns, $variant );
677  $convertTitle = $convertNs . ':' . $convertTitle;
678  }
679  $result[$variant] = $convertTitle;
680  }
681  return $result;
682  }
683 
688  private function getWatchedInfo() {
689  $user = $this->getUser();
690 
691  if ( !$user->isRegistered() || count( $this->everything ) == 0
692  || !$this->getAuthority()->isAllowed( 'viewmywatchlist' )
693  ) {
694  return;
695  }
696 
697  $this->watched = [];
698  $this->watchlistExpiries = [];
699  $this->notificationtimestamps = [];
700 
702  $items = $this->watchedItemStore->loadWatchedItemsBatch( $user, $this->everything );
703 
704  foreach ( $items as $item ) {
705  $nsId = $item->getTarget()->getNamespace();
706  $dbKey = $item->getTarget()->getDBkey();
707 
708  if ( $this->fld_watched ) {
709  $this->watched[$nsId][$dbKey] = true;
710 
711  $expiry = $item->getExpiry( TS_ISO_8601 );
712  if ( $expiry ) {
713  $this->watchlistExpiries[$nsId][$dbKey] = $expiry;
714  }
715  }
716 
717  if ( $this->fld_notificationtimestamp ) {
718  $this->notificationtimestamps[$nsId][$dbKey] = $item->getNotificationTimestamp();
719  }
720  }
721  }
722 
726  private function getWatcherInfo() {
727  if ( count( $this->everything ) == 0 ) {
728  return;
729  }
730 
731  $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
732  $unwatchedPageThreshold =
733  $this->getConfig()->get( MainConfigNames::UnwatchedPageThreshold );
734  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
735  return;
736  }
737 
738  $this->showZeroWatchers = $canUnwatchedpages;
739 
740  $countOptions = [];
741  if ( !$canUnwatchedpages ) {
742  $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
743  }
744 
745  $this->watchers = $this->watchedItemStore->countWatchersMultiple(
746  $this->everything,
747  $countOptions
748  );
749  }
750 
757  private function getVisitingWatcherInfo() {
758  $config = $this->getConfig();
759  $db = $this->getDB();
760 
761  $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
762  $unwatchedPageThreshold = $config->get( MainConfigNames::UnwatchedPageThreshold );
763  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
764  return;
765  }
766 
767  $this->showZeroWatchers = $canUnwatchedpages;
768 
769  $titlesWithThresholds = [];
770  if ( $this->titles ) {
771  $lb = $this->linkBatchFactory->newLinkBatch( $this->titles );
772 
773  // Fetch last edit timestamps for pages
774  $this->resetQueryParams();
775  $this->addTables( [ 'page', 'revision' ] );
776  $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
777  $this->addWhere( [
778  'page_latest = rev_id',
779  $lb->constructSet( 'page', $db ),
780  ] );
781  $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
782  $timestampRes = $this->select( __METHOD__ );
783 
784  $age = $config->get( MainConfigNames::WatchersMaxAge );
785  $timestamps = [];
786  foreach ( $timestampRes as $row ) {
787  $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
788  $timestamps[$row->page_namespace][$row->page_title] = (int)$revTimestamp - $age;
789  }
790  $titlesWithThresholds = array_map(
791  static function ( LinkTarget $target ) use ( $timestamps ) {
792  return [
793  $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
794  ];
795  },
796  $this->titles
797  );
798  }
799 
800  if ( $this->missing ) {
801  $titlesWithThresholds = array_merge(
802  $titlesWithThresholds,
803  array_map(
804  static function ( LinkTarget $target ) {
805  return [ $target, null ];
806  },
807  $this->missing
808  )
809  );
810  }
811  $this->visitingwatchers = $this->watchedItemStore->countVisitingWatchersMultiple(
812  $titlesWithThresholds,
813  !$canUnwatchedpages ? $unwatchedPageThreshold : null
814  );
815  }
816 
817  public function getCacheMode( $params ) {
818  // Other props depend on something about the current user
819  $publicProps = [
820  'protection',
821  'talkid',
822  'subjectid',
823  'associatedpage',
824  'url',
825  'preload',
826  'displaytitle',
827  'varianttitles',
828  ];
829  if ( array_diff( (array)$params['prop'], $publicProps ) ) {
830  return 'private';
831  }
832 
833  // testactions also depends on the current user
834  if ( $params['testactions'] ) {
835  return 'private';
836  }
837 
838  return 'public';
839  }
840 
841  public function getAllowedParams() {
842  return [
843  'prop' => [
844  ParamValidator::PARAM_ISMULTI => true,
845  ParamValidator::PARAM_TYPE => [
846  'protection',
847  'talkid',
848  'watched', # private
849  'watchers', # private
850  'visitingwatchers', # private
851  'notificationtimestamp', # private
852  'subjectid',
853  'associatedpage',
854  'url',
855  'readable', # private
856  'preload',
857  'displaytitle',
858  'varianttitles',
859  'linkclasses', # private: stub length (and possibly hook colors)
860  // If you add more properties here, please consider whether they
861  // need to be added to getCacheMode()
862  ],
864  EnumDef::PARAM_DEPRECATED_VALUES => [
865  'readable' => true, // Since 1.32
866  ],
867  ],
868  'linkcontext' => [
869  ParamValidator::PARAM_TYPE => 'title',
870  ParamValidator::PARAM_DEFAULT => $this->titleFactory->newMainPage()->getPrefixedText(),
871  TitleDef::PARAM_RETURN_OBJECT => true,
872  ],
873  'testactions' => [
874  ParamValidator::PARAM_TYPE => 'string',
875  ParamValidator::PARAM_ISMULTI => true,
876  ],
877  'testactionsdetail' => [
878  ParamValidator::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
879  ParamValidator::PARAM_DEFAULT => 'boolean',
881  ],
882  'continue' => [
883  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
884  ],
885  ];
886  }
887 
888  protected function getExamplesMessages() {
889  return [
890  'action=query&prop=info&titles=Main%20Page'
891  => 'apihelp-query+info-example-simple',
892  'action=query&prop=info&inprop=protection&titles=Main%20Page'
893  => 'apihelp-query+info-example-protection',
894  ];
895  }
896 
897  public function getHelpUrls() {
898  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
899  }
900 }
const PROTO_CANONICAL
Definition: Defines.php:199
const NS_FILE
Definition: Defines.php:70
const PROTO_CURRENT
Definition: Defines.php:198
const NS_MAIN
Definition: Defines.php:64
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
addBlockInfoToStatus(StatusValue $status, Authority $user=null)
Add block info to block messages in a Status.
Definition: ApiBase.php:1280
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition: ApiBase.php:1650
getMain()
Get the main module.
Definition: ApiBase.php:514
getErrorFormatter()
Definition: ApiBase.php:640
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:196
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition: ApiBase.php:227
getResult()
Get the result object.
Definition: ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:765
const LIMIT_SML1
Slow query, standard limit.
Definition: ApiBase.php:225
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:163
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:711
This is a base class for all Query modules.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
resetQueryParams()
Blank the internal arrays with query parameters.
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.
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.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
A query module to show basic page information.
getExamplesMessages()
Returns usage examples for this module.
__construct(ApiQuery $queryModule, $moduleName, Language $contentLanguage, LinkBatchFactory $linkBatchFactory, NamespaceInfo $namespaceInfo, TitleFactory $titleFactory, TitleFormatter $titleFormatter, WatchedItemStore $watchedItemStore, LanguageConverterFactory $languageConverterFactory, RestrictionStore $restrictionStore, LinksMigration $linksMigration)
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
requestExtraData( $pageSet)
getHelpUrls()
Return links to more detailed help pages about the module.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getCacheMode( $params)
Get the cache mode for the data generated by this module.
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 formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Definition: ApiResult.php:1199
Base class for language-specific code.
Definition: Language.php:53
An interface for creating language converters.
getLanguageConverter( $language=null)
Provide a LanguageConverter for given language.
Service for compat reading of links tables.
A class containing constants representing the names of configuration variables.
A StatusValue for permission errors.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates Title objects.
static compare( $a, $b)
Callback for usort() to do title sorts by (namespace, title)
Definition: Title.php:881
Storage layer class for WatchedItems.
Service for formatting and validating API parameters.
Type definition for enumeration types.
Definition: EnumDef.php:32
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
A title formatter service for MediaWiki.