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  if ( isset( $this->displaytitles[$pageid] ) ) {
374  $pageInfo['displaytitle'] = $this->displaytitles[$pageid];
375  } else {
376  $pageInfo['displaytitle'] = $title->getPrefixedText();
377  }
378  }
379 
380  if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
381  $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
382  }
383 
384  if ( $this->fld_linkclasses && isset( $this->linkClasses[$pageid] ) ) {
385  $pageInfo['linkclasses'] = $this->linkClasses[$pageid];
386  }
387 
388  if ( $this->params['testactions'] ) {
389  $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
390  if ( $this->countTestedActions >= $limit ) {
391  return null; // force a continuation
392  }
393 
394  $detailLevel = $this->params['testactionsdetail'];
395  $errorFormatter = $this->getErrorFormatter();
396  if ( $errorFormatter->getFormat() === 'bc' ) {
397  // Eew, no. Use a more modern format here.
398  $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
399  }
400 
401  $pageInfo['actions'] = [];
402  foreach ( $this->params['testactions'] as $action ) {
403  $this->countTestedActions++;
404 
405  if ( $detailLevel === 'boolean' ) {
406  $pageInfo['actions'][$action] = $this->getAuthority()->authorizeRead( $action, $title );
407  } else {
408  $status = new PermissionStatus();
409  if ( $detailLevel === 'quick' ) {
410  $this->getAuthority()->probablyCan( $action, $title, $status );
411  } else {
412  $this->getAuthority()->definitelyCan( $action, $title, $status );
413  }
414  $this->addBlockInfoToStatus( $status );
415  $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $status );
416  }
417  }
418  }
419 
420  return $pageInfo;
421  }
422 
426  private function getProtectionInfo() {
427  $this->protections = [];
428  $db = $this->getDB();
429 
430  // Get normal protections for existing titles
431  if ( count( $this->titles ) ) {
432  $this->resetQueryParams();
433  $this->addTables( 'page_restrictions' );
434  $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
435  'pr_expiry', 'pr_cascade' ] );
436  $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
437 
438  $res = $this->select( __METHOD__ );
439  foreach ( $res as $row ) {
441  $title = $this->titles[$row->pr_page];
442  $a = [
443  'type' => $row->pr_type,
444  'level' => $row->pr_level,
445  'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
446  ];
447  if ( $row->pr_cascade ) {
448  $a['cascade'] = true;
449  }
450  $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
451  }
452  // Also check old restrictions
453  foreach ( $this->titles as $pageId => $title ) {
454  if ( $this->pageRestrictions[$pageId] ) {
455  $namespace = $title->getNamespace();
456  $dbKey = $title->getDBkey();
457  $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
458  foreach ( $restrictions as $restrict ) {
459  $temp = explode( '=', trim( $restrict ) );
460  if ( count( $temp ) == 1 ) {
461  // old old format should be treated as edit/move restriction
462  $restriction = trim( $temp[0] );
463 
464  if ( $restriction == '' ) {
465  continue;
466  }
467  $this->protections[$namespace][$dbKey][] = [
468  'type' => 'edit',
469  'level' => $restriction,
470  'expiry' => 'infinity',
471  ];
472  $this->protections[$namespace][$dbKey][] = [
473  'type' => 'move',
474  'level' => $restriction,
475  'expiry' => 'infinity',
476  ];
477  } else {
478  $restriction = trim( $temp[1] );
479  if ( $restriction == '' ) {
480  continue;
481  }
482  $this->protections[$namespace][$dbKey][] = [
483  'type' => $temp[0],
484  'level' => $restriction,
485  'expiry' => 'infinity',
486  ];
487  }
488  }
489  }
490  }
491  }
492 
493  // Get protections for missing titles
494  if ( count( $this->missing ) ) {
495  $this->resetQueryParams();
496  $lb = $this->linkBatchFactory->newLinkBatch( $this->missing );
497  $this->addTables( 'protected_titles' );
498  $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
499  $this->addWhere( $lb->constructSet( 'pt', $db ) );
500  $res = $this->select( __METHOD__ );
501  foreach ( $res as $row ) {
502  $this->protections[$row->pt_namespace][$row->pt_title][] = [
503  'type' => 'create',
504  'level' => $row->pt_create_perm,
505  'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
506  ];
507  }
508  }
509 
510  // Separate good and missing titles into files and other pages
511  // and populate $this->restrictionTypes
512  $images = $others = [];
513  foreach ( $this->everything as $title ) {
514  if ( $title->getNamespace() === NS_FILE ) {
515  $images[] = $title->getDBkey();
516  } else {
517  $others[] = $title;
518  }
519  // Applicable protection types
520  $this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
521  array_values( $title->getRestrictionTypes() );
522  }
523 
524  if ( count( $others ) ) {
525  // Non-images: check templatelinks
526  $lb = $this->linkBatchFactory->newLinkBatch( $others );
527  $this->resetQueryParams();
528  $this->addTables( [ 'page_restrictions', 'page', 'templatelinks' ] );
529  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
530  'page_title', 'page_namespace',
531  'tl_title', 'tl_namespace' ] );
532  $this->addWhere( $lb->constructSet( 'tl', $db ) );
533  $this->addWhere( 'pr_page = page_id' );
534  $this->addWhere( 'pr_page = tl_from' );
535  $this->addWhereFld( 'pr_cascade', 1 );
536 
537  $res = $this->select( __METHOD__ );
538  foreach ( $res as $row ) {
539  $source = $this->titleFactory->makeTitle( $row->page_namespace, $row->page_title );
540  $this->protections[$row->tl_namespace][$row->tl_title][] = [
541  'type' => $row->pr_type,
542  'level' => $row->pr_level,
543  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
544  'source' => $source->getPrefixedText()
545  ];
546  }
547  }
548 
549  if ( count( $images ) ) {
550  // Images: check imagelinks
551  $this->resetQueryParams();
552  $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
553  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
554  'page_title', 'page_namespace', 'il_to' ] );
555  $this->addWhere( 'pr_page = page_id' );
556  $this->addWhere( 'pr_page = il_from' );
557  $this->addWhereFld( 'pr_cascade', 1 );
558  $this->addWhereFld( 'il_to', $images );
559 
560  $res = $this->select( __METHOD__ );
561  foreach ( $res as $row ) {
562  $source = $this->titleFactory->makeTitle( $row->page_namespace, $row->page_title );
563  $this->protections[NS_FILE][$row->il_to][] = [
564  'type' => $row->pr_type,
565  'level' => $row->pr_level,
566  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
567  'source' => $source->getPrefixedText()
568  ];
569  }
570  }
571  }
572 
577  private function getTSIDs() {
578  $getTitles = $this->talkids = $this->subjectids = [];
579  $nsInfo = $this->namespaceInfo;
580 
582  foreach ( $this->everything as $t ) {
583  if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
584  if ( $this->fld_subjectid ) {
585  $getTitles[] = $t->getSubjectPage();
586  }
587  } elseif ( $this->fld_talkid ) {
588  $getTitles[] = $t->getTalkPage();
589  }
590  }
591  if ( $getTitles === [] ) {
592  return;
593  }
594 
595  $db = $this->getDB();
596 
597  // Construct a custom WHERE clause that matches
598  // all titles in $getTitles
599  $lb = $this->linkBatchFactory->newLinkBatch( $getTitles );
600  $this->resetQueryParams();
601  $this->addTables( 'page' );
602  $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
603  $this->addWhere( $lb->constructSet( 'page', $db ) );
604  $res = $this->select( __METHOD__ );
605  foreach ( $res as $row ) {
606  if ( $nsInfo->isTalk( $row->page_namespace ) ) {
607  $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
608  (int)( $row->page_id );
609  } else {
610  $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
611  (int)( $row->page_id );
612  }
613  }
614  }
615 
616  private function getDisplayTitle() {
617  $this->displaytitles = [];
618 
619  $pageIds = array_keys( $this->titles );
620 
621  if ( $pageIds === [] ) {
622  return;
623  }
624 
625  $this->resetQueryParams();
626  $this->addTables( 'page_props' );
627  $this->addFields( [ 'pp_page', 'pp_value' ] );
628  $this->addWhereFld( 'pp_page', $pageIds );
629  $this->addWhereFld( 'pp_propname', 'displaytitle' );
630  $res = $this->select( __METHOD__ );
631 
632  foreach ( $res as $row ) {
633  $this->displaytitles[$row->pp_page] = $row->pp_value;
634  }
635  }
636 
644  private function getLinkClasses( ?LinkTarget $context_title = null ) {
645  if ( $this->titles === [] ) {
646  return;
647  }
648  // For compatibility with legacy GetLinkColours hook:
649  // $pagemap maps from page id to title (as prefixed db key)
650  // $classes maps from title (prefixed db key) to a space-separated
651  // list of link classes ("link colours").
652  // The hook should not modify $pagemap, and should only append to
653  // $classes (being careful to maintain space separation).
654  $classes = [];
655  $pagemap = [];
656  foreach ( $this->titles as $pageId => $title ) {
657  $pdbk = $title->getPrefixedDBkey();
658  $pagemap[$pageId] = $pdbk;
659  $classes[$pdbk] = $title->isRedirect() ? 'mw-redirect' : '';
660  }
661  // legacy hook requires a real Title, not a LinkTarget
662  $context_title = $this->titleFactory->newFromLinkTarget(
663  $context_title ?? $this->titleFactory->newMainPage()
664  );
665  $this->getHookRunner()->onGetLinkColours(
666  $pagemap, $classes, $context_title
667  );
668 
669  // This API class expects the class list to be:
670  // (a) indexed by pageid, not title, and
671  // (b) a proper array of strings (possibly zero-length),
672  // not a single space-separated string (possibly the empty string)
673  $this->linkClasses = [];
674  foreach ( $this->titles as $pageId => $title ) {
675  $pdbk = $title->getPrefixedDBkey();
676  $this->linkClasses[$pageId] = preg_split(
677  '/\s+/', $classes[$pdbk] ?? '', -1, PREG_SPLIT_NO_EMPTY
678  );
679  }
680  }
681 
682  private function getVariantTitles() {
683  if ( $this->titles === [] ) {
684  return;
685  }
686  $this->variantTitles = [];
687  foreach ( $this->titles as $pageId => $t ) {
688  $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
689  ? $this->getAllVariants( $this->displaytitles[$pageId] )
690  : $this->getAllVariants( $t->getText(), $t->getNamespace() );
691  }
692  }
693 
694  private function getAllVariants( $text, $ns = NS_MAIN ) {
695  $result = [];
696  foreach ( $this->languageConverter->getVariants() as $variant ) {
697  $convertTitle = $this->languageConverter->autoConvert( $text, $variant );
698  if ( $ns !== NS_MAIN ) {
699  $convertNs = $this->languageConverter->convertNamespace( $ns, $variant );
700  $convertTitle = $convertNs . ':' . $convertTitle;
701  }
702  $result[$variant] = $convertTitle;
703  }
704  return $result;
705  }
706 
711  private function getWatchedInfo() {
712  $user = $this->getUser();
713 
714  if ( !$user->isRegistered() || count( $this->everything ) == 0
715  || !$this->getAuthority()->isAllowed( 'viewmywatchlist' )
716  ) {
717  return;
718  }
719 
720  $this->watched = [];
721  $this->watchlistExpiries = [];
722  $this->notificationtimestamps = [];
723 
725  $items = $this->watchedItemStore->loadWatchedItemsBatch( $user, $this->everything );
726 
727  foreach ( $items as $item ) {
728  $nsId = $item->getTarget()->getNamespace();
729  $dbKey = $item->getTarget()->getDBkey();
730 
731  if ( $this->fld_watched ) {
732  $this->watched[$nsId][$dbKey] = true;
733 
734  $expiry = $item->getExpiry( TS_ISO_8601 );
735  if ( $expiry ) {
736  $this->watchlistExpiries[$nsId][$dbKey] = $expiry;
737  }
738  }
739 
740  if ( $this->fld_notificationtimestamp ) {
741  $this->notificationtimestamps[$nsId][$dbKey] = $item->getNotificationTimestamp();
742  }
743  }
744  }
745 
749  private function getWatcherInfo() {
750  if ( count( $this->everything ) == 0 ) {
751  return;
752  }
753 
754  $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
755  $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
756  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
757  return;
758  }
759 
760  $this->showZeroWatchers = $canUnwatchedpages;
761 
762  $countOptions = [];
763  if ( !$canUnwatchedpages ) {
764  $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
765  }
766 
767  $this->watchers = $this->watchedItemStore->countWatchersMultiple(
768  $this->everything,
769  $countOptions
770  );
771  }
772 
779  private function getVisitingWatcherInfo() {
780  $config = $this->getConfig();
781  $db = $this->getDB();
782 
783  $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
784  $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
785  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
786  return;
787  }
788 
789  $this->showZeroWatchers = $canUnwatchedpages;
790 
791  $titlesWithThresholds = [];
792  if ( $this->titles ) {
793  $lb = $this->linkBatchFactory->newLinkBatch( $this->titles );
794 
795  // Fetch last edit timestamps for pages
796  $this->resetQueryParams();
797  $this->addTables( [ 'page', 'revision' ] );
798  $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
799  $this->addWhere( [
800  'page_latest = rev_id',
801  $lb->constructSet( 'page', $db ),
802  ] );
803  $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
804  $timestampRes = $this->select( __METHOD__ );
805 
806  $age = $config->get( 'WatchersMaxAge' );
807  $timestamps = [];
808  foreach ( $timestampRes as $row ) {
809  $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
810  $timestamps[$row->page_namespace][$row->page_title] = (int)$revTimestamp - $age;
811  }
812  $titlesWithThresholds = array_map(
813  static function ( LinkTarget $target ) use ( $timestamps ) {
814  return [
815  $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
816  ];
817  },
819  );
820  }
821 
822  if ( $this->missing ) {
823  $titlesWithThresholds = array_merge(
824  $titlesWithThresholds,
825  array_map(
826  static function ( LinkTarget $target ) {
827  return [ $target, null ];
828  },
830  )
831  );
832  }
833  $this->visitingwatchers = $this->watchedItemStore->countVisitingWatchersMultiple(
834  $titlesWithThresholds,
835  !$canUnwatchedpages ? $unwatchedPageThreshold : null
836  );
837  }
838 
839  public function getCacheMode( $params ) {
840  // Other props depend on something about the current user
841  $publicProps = [
842  'protection',
843  'talkid',
844  'subjectid',
845  'associatedpage',
846  'url',
847  'preload',
848  'displaytitle',
849  'varianttitles',
850  ];
851  if ( array_diff( (array)$params['prop'], $publicProps ) ) {
852  return 'private';
853  }
854 
855  // testactions also depends on the current user
856  if ( $params['testactions'] ) {
857  return 'private';
858  }
859 
860  return 'public';
861  }
862 
863  public function getAllowedParams() {
864  return [
865  'prop' => [
866  ApiBase::PARAM_ISMULTI => true,
868  'protection',
869  'talkid',
870  'watched', # private
871  'watchers', # private
872  'visitingwatchers', # private
873  'notificationtimestamp', # private
874  'subjectid',
875  'associatedpage',
876  'url',
877  'readable', # private
878  'preload',
879  'displaytitle',
880  'varianttitles',
881  'linkclasses', # private: stub length (and possibly hook colors)
882  // If you add more properties here, please consider whether they
883  // need to be added to getCacheMode()
884  ],
887  'readable' => true, // Since 1.32
888  ],
889  ],
890  'linkcontext' => [
891  ApiBase::PARAM_TYPE => 'title',
892  ApiBase::PARAM_DFLT => $this->titleFactory->newMainPage()->getPrefixedText(),
893  TitleDef::PARAM_RETURN_OBJECT => true,
894  ],
895  'testactions' => [
896  ApiBase::PARAM_TYPE => 'string',
897  ApiBase::PARAM_ISMULTI => true,
898  ],
899  'testactionsdetail' => [
900  ApiBase::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
901  ApiBase::PARAM_DFLT => 'boolean',
903  ],
904  'continue' => [
905  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
906  ],
907  ];
908  }
909 
910  protected function getExamplesMessages() {
911  return [
912  'action=query&prop=info&titles=Main%20Page'
913  => 'apihelp-query+info-example-simple',
914  'action=query&prop=info&inprop=protection&titles=Main%20Page'
915  => 'apihelp-query+info-example-protection',
916  ];
917  }
918 
919  public function getHelpUrls() {
920  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
921  }
922 }
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:37
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:426
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:105
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1668
ApiBase\PARAM_TYPE
const PARAM_TYPE
Definition: ApiBase.php:72
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:571
ApiQueryInfo\getVisitingWatcherInfo
getVisitingWatcherInfo()
Get the count of watchers who have visited recent edits and put it in $this->visitingwatchers.
Definition: ApiQueryInfo.php:779
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:84
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:694
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:1213
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:644
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:707
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:577
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:711
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:884
ApiQueryInfo\$params
$params
Definition: ApiQueryInfo.php:66
ApiQueryInfo\getHelpUrls
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiQueryInfo.php:919
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:910
ApiBase\dieContinueUsageIf
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition: ApiBase.php:1569
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
ApiBase\LIMIT_SML2
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition: ApiBase.php:169
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:839
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:48
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:616
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:70
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:863
ApiBase\PARAM_ISMULTI
const PARAM_ISMULTI
Definition: ApiBase.php:71
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:456
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:138
ApiQueryInfo\getVariantTitles
getVariantTitles()
Definition: ApiQueryInfo.php:682
ApiBase\getHookRunner
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:653
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:749
ApiQueryInfo\$pageLength
$pageLength
Definition: ApiQueryInfo.php:76
ApiBase\getErrorFormatter
getErrorFormatter()
Definition: ApiBase.php:582
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:167
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:474