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