MediaWiki  master
ApiQueryInfo.php
Go to the documentation of this file.
1 <?php
44 
50 class ApiQueryInfo extends ApiQueryBase {
51 
52  private ILanguageConverter $languageConverter;
53  private LinkBatchFactory $linkBatchFactory;
54  private NamespaceInfo $namespaceInfo;
55  private TitleFactory $titleFactory;
56  private TitleFormatter $titleFormatter;
57  private WatchedItemStore $watchedItemStore;
58  private RestrictionStore $restrictionStore;
59  private LinksMigration $linksMigration;
60  private TempUserCreator $tempUserCreator;
61  private IntroMessageBuilder $introMessageBuilder;
62  private PreloadedContentBuilder $preloadedContentBuilder;
63  private RevisionLookup $revisionLookup;
64  private UrlUtils $urlUtils;
65 
66  private $fld_protection = false, $fld_talkid = false,
67  $fld_subjectid = false, $fld_url = false,
68  $fld_readable = false, $fld_watched = false,
71  $fld_preload = false, $fld_preloadcontent = false, $fld_editintro = false,
73 
78  private $fld_linkclasses = false;
79 
83  private $fld_associatedpage = false;
84 
85  private $params;
86 
88  private $titles;
90  private $missing;
92  private $everything;
93 
94  private $pageIsRedir, $pageIsNew, $pageTouched,
96 
99 
104  private $watchlistExpiries;
105 
110  private $linkClasses;
111 
112  private $showZeroWatchers = false;
113 
114  private $countTestedActions = 0;
115 
134  public function __construct(
135  ApiQuery $queryModule,
136  $moduleName,
137  Language $contentLanguage,
138  LinkBatchFactory $linkBatchFactory,
139  NamespaceInfo $namespaceInfo,
140  TitleFactory $titleFactory,
141  TitleFormatter $titleFormatter,
142  WatchedItemStore $watchedItemStore,
143  LanguageConverterFactory $languageConverterFactory,
144  RestrictionStore $restrictionStore,
145  LinksMigration $linksMigration,
146  TempUserCreator $tempUserCreator,
147  IntroMessageBuilder $introMessageBuilder,
148  PreloadedContentBuilder $preloadedContentBuilder,
149  RevisionLookup $revisionLookup,
150  UrlUtils $urlUtils
151  ) {
152  parent::__construct( $queryModule, $moduleName, 'in' );
153  $this->languageConverter = $languageConverterFactory->getLanguageConverter( $contentLanguage );
154  $this->linkBatchFactory = $linkBatchFactory;
155  $this->namespaceInfo = $namespaceInfo;
156  $this->titleFactory = $titleFactory;
157  $this->titleFormatter = $titleFormatter;
158  $this->watchedItemStore = $watchedItemStore;
159  $this->restrictionStore = $restrictionStore;
160  $this->linksMigration = $linksMigration;
161  $this->tempUserCreator = $tempUserCreator;
162  $this->introMessageBuilder = $introMessageBuilder;
163  $this->preloadedContentBuilder = $preloadedContentBuilder;
164  $this->revisionLookup = $revisionLookup;
165  $this->urlUtils = $urlUtils;
166  }
167 
172  public function requestExtraData( $pageSet ) {
173  // If the pageset is resolving redirects we won't get page_is_redirect.
174  // But we can't know for sure until the pageset is executed (revids may
175  // turn it off), so request it unconditionally.
176  $pageSet->requestField( 'page_is_redirect' );
177  $pageSet->requestField( 'page_is_new' );
178  $config = $this->getConfig();
179  $pageSet->requestField( 'page_touched' );
180  $pageSet->requestField( 'page_latest' );
181  $pageSet->requestField( 'page_len' );
182  $pageSet->requestField( 'page_content_model' );
183  if ( $config->get( MainConfigNames::PageLanguageUseDB ) ) {
184  $pageSet->requestField( 'page_lang' );
185  }
186  }
187 
188  public function execute() {
189  $this->params = $this->extractRequestParams();
190  if ( $this->params['prop'] !== null ) {
191  $prop = array_fill_keys( $this->params['prop'], true );
192  $this->fld_protection = isset( $prop['protection'] );
193  $this->fld_watched = isset( $prop['watched'] );
194  $this->fld_watchers = isset( $prop['watchers'] );
195  $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
196  $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
197  $this->fld_talkid = isset( $prop['talkid'] );
198  $this->fld_subjectid = isset( $prop['subjectid'] );
199  $this->fld_url = isset( $prop['url'] );
200  $this->fld_readable = isset( $prop['readable'] );
201  $this->fld_preload = isset( $prop['preload'] );
202  $this->fld_preloadcontent = isset( $prop['preloadcontent'] );
203  $this->fld_editintro = isset( $prop['editintro'] );
204  $this->fld_displaytitle = isset( $prop['displaytitle'] );
205  $this->fld_varianttitles = isset( $prop['varianttitles'] );
206  $this->fld_linkclasses = isset( $prop['linkclasses'] );
207  $this->fld_associatedpage = isset( $prop['associatedpage'] );
208  }
209 
210  $pageSet = $this->getPageSet();
211  $this->titles = $pageSet->getGoodPages();
212  $this->missing = $pageSet->getMissingPages();
213  $this->everything = $this->titles + $this->missing;
214  $result = $this->getResult();
215 
216  if (
217  ( $this->fld_preloadcontent || $this->fld_editintro ) &&
218  ( count( $this->everything ) > 1 || count( $this->getPageSet()->getRevisionIDs() ) > 1 )
219  ) {
220  // This is relatively slow, so disallow doing it for multiple pages, just in case.
221  // (Also, handling multiple revisions would be tricky.)
222  $this->dieWithError(
223  [ 'apierror-info-singlepagerevision', $this->getModulePrefix() ], 'invalidparammix'
224  );
225  }
226 
227  uasort( $this->everything, [ Title::class, 'compare' ] );
228  if ( $this->params['continue'] !== null ) {
229  // Throw away any titles we're gonna skip so they don't
230  // clutter queries
231  $cont = $this->parseContinueParamOrDie( $this->params['continue'], [ 'int', 'string' ] );
232  $conttitle = $this->titleFactory->makeTitleSafe( $cont[0], $cont[1] );
233  $this->dieContinueUsageIf( !$conttitle );
234  foreach ( $this->everything as $pageid => $page ) {
235  if ( Title::compare( $page, $conttitle ) >= 0 ) {
236  break;
237  }
238  unset( $this->titles[$pageid] );
239  unset( $this->missing[$pageid] );
240  unset( $this->everything[$pageid] );
241  }
242  }
243 
244  // when resolving redirects, no page will have this field
245  $this->pageIsRedir = !$pageSet->isResolvingRedirects()
246  ? $pageSet->getCustomField( 'page_is_redirect' )
247  : [];
248  $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
249 
250  $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
251  $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
252  $this->pageLength = $pageSet->getCustomField( 'page_len' );
253 
254  // Get protection info if requested
255  if ( $this->fld_protection ) {
256  $this->getProtectionInfo();
257  }
258 
259  if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
260  $this->getWatchedInfo();
261  }
262 
263  if ( $this->fld_watchers ) {
264  $this->getWatcherInfo();
265  }
266 
267  if ( $this->fld_visitingwatchers ) {
268  $this->getVisitingWatcherInfo();
269  }
270 
271  // Run the talkid/subjectid query if requested
272  if ( $this->fld_talkid || $this->fld_subjectid ) {
273  $this->getTSIDs();
274  }
275 
276  if ( $this->fld_displaytitle ) {
277  $this->getDisplayTitle();
278  }
279 
280  if ( $this->fld_varianttitles ) {
281  $this->getVariantTitles();
282  }
283 
284  if ( $this->fld_linkclasses ) {
285  $this->getLinkClasses( $this->params['linkcontext'] );
286  }
287 
289  foreach ( $this->everything as $pageid => $page ) {
290  $pageInfo = $this->extractPageInfo( $pageid, $page );
291  $fit = $pageInfo !== null && $result->addValue( [
292  'query',
293  'pages'
294  ], $pageid, $pageInfo );
295  if ( !$fit ) {
296  $this->setContinueEnumParameter( 'continue',
297  $page->getNamespace() . '|' .
298  $this->titleFormatter->getText( $page ) );
299  break;
300  }
301  }
302  }
303 
310  private function extractPageInfo( $pageid, $page ) {
311  $title = $this->titleFactory->newFromPageIdentity( $page );
312  $pageInfo = [];
313  // $page->exists() needs pageid, which is not set for all title objects
314  $pageExists = $pageid > 0;
315  $ns = $page->getNamespace();
316  $dbkey = $page->getDBkey();
317 
318  $pageInfo['contentmodel'] = $title->getContentModel();
319 
320  $pageLanguage = $title->getPageLanguage();
321  $pageInfo['pagelanguage'] = $pageLanguage->getCode();
322  $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
323  $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
324 
325  if ( $pageExists ) {
326  $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
327  $pageInfo['lastrevid'] = (int)$this->pageLatest[$pageid];
328  $pageInfo['length'] = (int)$this->pageLength[$pageid];
329 
330  if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
331  $pageInfo['redirect'] = true;
332  }
333  if ( $this->pageIsNew[$pageid] ) {
334  $pageInfo['new'] = true;
335  }
336  }
337 
338  if ( $this->fld_protection ) {
339  $pageInfo['protection'] = [];
340  if ( isset( $this->protections[$ns][$dbkey] ) ) {
341  $pageInfo['protection'] =
342  $this->protections[$ns][$dbkey];
343  }
344  ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
345 
346  $pageInfo['restrictiontypes'] = [];
347  if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
348  $pageInfo['restrictiontypes'] =
349  $this->restrictionTypes[$ns][$dbkey];
350  }
351  ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
352  }
353 
354  if ( $this->fld_watched ) {
355  $pageInfo['watched'] = false;
356 
357  if ( isset( $this->watched[$ns][$dbkey] ) ) {
358  $pageInfo['watched'] = $this->watched[$ns][$dbkey];
359  }
360 
361  if ( isset( $this->watchlistExpiries[$ns][$dbkey] ) ) {
362  $pageInfo['watchlistexpiry'] = $this->watchlistExpiries[$ns][$dbkey];
363  }
364  }
365 
366  if ( $this->fld_watchers ) {
367  if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
368  $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
369  } elseif ( $this->showZeroWatchers ) {
370  $pageInfo['watchers'] = 0;
371  }
372  }
373 
374  if ( $this->fld_visitingwatchers ) {
375  if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
376  $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
377  } elseif ( $this->showZeroWatchers ) {
378  $pageInfo['visitingwatchers'] = 0;
379  }
380  }
381 
382  if ( $this->fld_notificationtimestamp ) {
383  $pageInfo['notificationtimestamp'] = '';
384  if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
385  $pageInfo['notificationtimestamp'] =
386  wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
387  }
388  }
389 
390  if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
391  $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
392  }
393 
394  if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
395  $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
396  }
397 
398  if ( $this->fld_associatedpage && $ns >= NS_MAIN ) {
399  $pageInfo['associatedpage'] = $this->titleFormatter->getPrefixedText(
400  $this->namespaceInfo->getAssociatedPage( TitleValue::newFromPage( $page ) )
401  );
402  }
403 
404  if ( $this->fld_url ) {
405  $pageInfo['fullurl'] = (string)$this->urlUtils->expand(
406  $title->getFullURL(), PROTO_CURRENT
407  );
408  $pageInfo['editurl'] = (string)$this->urlUtils->expand(
409  $title->getFullURL( 'action=edit' ), PROTO_CURRENT
410  );
411  $pageInfo['canonicalurl'] = (string)$this->urlUtils->expand(
412  $title->getFullURL(), PROTO_CANONICAL
413  );
414  }
415  if ( $this->fld_readable ) {
416  $pageInfo['readable'] = $this->getAuthority()->definitelyCan( 'read', $page );
417  }
418 
419  if ( $this->fld_preload ) {
420  if ( $pageExists ) {
421  $pageInfo['preload'] = '';
422  } else {
423  $text = null;
424  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
425  $this->getHookRunner()->onEditFormPreloadText( $text, $title );
426 
427  $pageInfo['preload'] = $text;
428  }
429  }
430 
431  if ( $this->fld_preloadcontent ) {
432  $newSection = $this->params['preloadnewsection'];
433  // Preloaded content is not supported for already existing pages or sections.
434  // The actual page/section content should be shown for editing (from prop=revisions API).
435  if ( !$pageExists || $newSection ) {
436  $content = $this->preloadedContentBuilder->getPreloadedContent(
437  $title->toPageIdentity(),
438  $this->getAuthority(),
439  $this->params['preloadcustom'],
440  $this->params['preloadparams'] ?? [],
441  $newSection ? 'new' : null
442  );
443  $defaultContent = $newSection ? null :
444  $this->preloadedContentBuilder->getDefaultContent( $title->toPageIdentity() );
445  $contentIsDefault = $defaultContent ? $content->equals( $defaultContent ) : $content->isEmpty();
446  // Adapted from ApiQueryRevisionsBase::extractAllSlotInfo.
447  // The preloaded content fills the main slot.
448  $pageInfo['preloadcontent']['contentmodel'] = $content->getModel();
449  $pageInfo['preloadcontent']['contentformat'] = $content->getDefaultFormat();
450  ApiResult::setContentValue( $pageInfo['preloadcontent'], 'content', $content->serialize() );
451  // If the preloaded content generated from these parameters is the same as
452  // the default page content, the user should be discouraged from saving the page
453  // (e.g. by disabling the save button until changes are made, or displaying a warning).
454  $pageInfo['preloadisdefault'] = $contentIsDefault;
455  }
456  }
457 
458  if ( $this->fld_editintro ) {
459  // Use $page as the context page in every processed message (T300184)
460  $localizerWithPage = new class( $this, $page ) implements MessageLocalizer {
461  private MessageLocalizer $base;
462  private PageReference $page;
463 
464  public function __construct( MessageLocalizer $base, PageReference $page ) {
465  $this->base = $base;
466  $this->page = $page;
467  }
468 
472  public function msg( $key, ...$params ) {
473  return $this->base->msg( $key, ...$params )->page( $this->page );
474  }
475  };
476 
477  $styleParamMap = [
478  'lessframes' => IntroMessageBuilder::LESS_FRAMES,
479  'moreframes' => IntroMessageBuilder::MORE_FRAMES,
480  ];
481  // If we got here, there is exactly one page and revision in the query
482  $revId = array_key_first( $this->getPageSet()->getLiveRevisionIDs() );
483  $revRecord = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
484 
485  $messages = $this->introMessageBuilder->getIntroMessages(
486  $styleParamMap[ $this->params['editintrostyle'] ],
487  $this->params['editintroskip'] ?? [],
488  $localizerWithPage,
489  $title->toPageIdentity(),
490  $revRecord,
491  $this->getAuthority(),
492  $this->params['editintrocustom'],
493  // Maybe expose these as parameters in the future, but for now it doesn't seem worth it:
494  null,
495  false
496  );
497  ApiResult::setIndexedTagName( $messages, 'ei' );
498  ApiResult::setArrayType( $messages, 'kvp', 'key' );
499 
500  $pageInfo['editintro'] = $messages;
501  }
502 
503  if ( $this->fld_displaytitle ) {
504  $pageInfo['displaytitle'] = $this->displaytitles[$pageid] ??
505  htmlspecialchars( $this->titleFormatter->getPrefixedText( $page ), ENT_NOQUOTES );
506  }
507 
508  if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
509  $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
510  }
511 
512  if ( $this->fld_linkclasses && isset( $this->linkClasses[$pageid] ) ) {
513  $pageInfo['linkclasses'] = $this->linkClasses[$pageid];
514  }
515 
516  if ( $this->params['testactions'] ) {
517  $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
518  if ( $this->countTestedActions >= $limit ) {
519  return null; // force a continuation
520  }
521 
522  $detailLevel = $this->params['testactionsdetail'];
523  $errorFormatter = $this->getErrorFormatter();
524  if ( $errorFormatter->getFormat() === 'bc' ) {
525  // Eew, no. Use a more modern format here.
526  $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
527  }
528 
529  $pageInfo['actions'] = [];
530  foreach ( $this->params['testactions'] as $action ) {
531  $this->countTestedActions++;
532 
533  if ( $detailLevel === 'boolean' ) {
534  $pageInfo['actions'][$action] = $this->getAuthority()->authorizeRead( $action, $page );
535  } else {
536  $status = new PermissionStatus();
537  if ( $detailLevel === 'quick' ) {
538  $this->getAuthority()->probablyCan( $action, $page, $status );
539  } else {
540  $this->getAuthority()->definitelyCan( $action, $page, $status );
541  }
542  $this->addBlockInfoToStatus( $status );
543  $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $status );
544  }
545  }
546 
547  if ( $this->params['testactionsautocreate'] ) {
548  $pageInfo['wouldautocreate'] = [];
549  foreach ( $this->params['testactions'] as $action ) {
550  // Copied from EditPage::maybeActivateTempUserCreate
551  $pageInfo['wouldautocreate'][$action] =
552  $this->tempUserCreator->shouldAutoCreate( $this->getUser(), $action );
553  }
554  }
555  }
556 
557  return $pageInfo;
558  }
559 
563  private function getProtectionInfo() {
564  $this->protections = [];
565  $db = $this->getDB();
566 
567  // Get normal protections for existing titles
568  if ( count( $this->titles ) ) {
569  $this->resetQueryParams();
570  $this->addTables( 'page_restrictions' );
571  $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
572  'pr_expiry', 'pr_cascade' ] );
573  $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
574 
575  $res = $this->select( __METHOD__ );
576  foreach ( $res as $row ) {
578  $page = $this->titles[$row->pr_page];
579  $a = [
580  'type' => $row->pr_type,
581  'level' => $row->pr_level,
582  'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
583  ];
584  if ( $row->pr_cascade ) {
585  $a['cascade'] = true;
586  }
587  $this->protections[$page->getNamespace()][$page->getDBkey()][] = $a;
588  }
589  }
590 
591  // Get protections for missing titles
592  if ( count( $this->missing ) ) {
593  $this->resetQueryParams();
594  $lb = $this->linkBatchFactory->newLinkBatch( $this->missing );
595  $this->addTables( 'protected_titles' );
596  $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
597  $this->addWhere( $lb->constructSet( 'pt', $db ) );
598  $res = $this->select( __METHOD__ );
599  foreach ( $res as $row ) {
600  $this->protections[$row->pt_namespace][$row->pt_title][] = [
601  'type' => 'create',
602  'level' => $row->pt_create_perm,
603  'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
604  ];
605  }
606  }
607 
608  // Separate good and missing titles into files and other pages
609  // and populate $this->restrictionTypes
610  $images = $others = [];
611  foreach ( $this->everything as $page ) {
612  if ( $page->getNamespace() === NS_FILE ) {
613  $images[] = $page->getDBkey();
614  } else {
615  $others[] = $page;
616  }
617  // Applicable protection types
618  $this->restrictionTypes[$page->getNamespace()][$page->getDBkey()] =
619  array_values( $this->restrictionStore->listApplicableRestrictionTypes( $page ) );
620  }
621 
622  [ $blNamespace, $blTitle ] = $this->linksMigration->getTitleFields( 'templatelinks' );
623  $queryInfo = $this->linksMigration->getQueryInfo( 'templatelinks' );
624 
625  if ( count( $others ) ) {
626  // Non-images: check templatelinks
627  $lb = $this->linkBatchFactory->newLinkBatch( $others );
628  $this->resetQueryParams();
629  $this->addTables( array_merge( [ 'page_restrictions', 'page' ], $queryInfo['tables'] ) );
630  // templatelinks must use PRIMARY index and not the tl_target_id.
631  $this->addOption( 'USE INDEX', [ 'templatelinks' => 'PRIMARY' ] );
632  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
633  'page_title', 'page_namespace',
634  $blNamespace, $blTitle ] );
635  $this->addWhere( $lb->constructSet( 'tl', $db ) );
636  $this->addWhere( 'pr_page = page_id' );
637  $this->addWhere( 'pr_page = tl_from' );
638  $this->addWhereFld( 'pr_cascade', 1 );
639  $this->addJoinConds( $queryInfo['joins'] );
640 
641  $res = $this->select( __METHOD__ );
642  foreach ( $res as $row ) {
643  $this->protections[$row->$blNamespace][$row->$blTitle][] = [
644  'type' => $row->pr_type,
645  'level' => $row->pr_level,
646  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
647  'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
648  ];
649  }
650  }
651 
652  if ( count( $images ) ) {
653  // Images: check imagelinks
654  $this->resetQueryParams();
655  $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
656  $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
657  'page_title', 'page_namespace', 'il_to' ] );
658  $this->addWhere( 'pr_page = page_id' );
659  $this->addWhere( 'pr_page = il_from' );
660  $this->addWhereFld( 'pr_cascade', 1 );
661  $this->addWhereFld( 'il_to', $images );
662 
663  $res = $this->select( __METHOD__ );
664  foreach ( $res as $row ) {
665  $this->protections[NS_FILE][$row->il_to][] = [
666  'type' => $row->pr_type,
667  'level' => $row->pr_level,
668  'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
669  'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
670  ];
671  }
672  }
673  }
674 
679  private function getTSIDs() {
680  $getTitles = $this->talkids = $this->subjectids = [];
681  $nsInfo = $this->namespaceInfo;
682 
684  foreach ( $this->everything as $page ) {
685  if ( $nsInfo->isTalk( $page->getNamespace() ) ) {
686  if ( $this->fld_subjectid ) {
687  $getTitles[] = $nsInfo->getSubjectPage( TitleValue::newFromPage( $page ) );
688  }
689  } elseif ( $this->fld_talkid ) {
690  $getTitles[] = $nsInfo->getTalkPage( TitleValue::newFromPage( $page ) );
691  }
692  }
693  if ( $getTitles === [] ) {
694  return;
695  }
696 
697  $db = $this->getDB();
698 
699  // Construct a custom WHERE clause that matches
700  // all titles in $getTitles
701  $lb = $this->linkBatchFactory->newLinkBatch( $getTitles );
702  $this->resetQueryParams();
703  $this->addTables( 'page' );
704  $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
705  $this->addWhere( $lb->constructSet( 'page', $db ) );
706  $res = $this->select( __METHOD__ );
707  foreach ( $res as $row ) {
708  if ( $nsInfo->isTalk( $row->page_namespace ) ) {
709  $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
710  (int)( $row->page_id );
711  } else {
712  $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
713  (int)( $row->page_id );
714  }
715  }
716  }
717 
718  private function getDisplayTitle() {
719  $this->displaytitles = [];
720 
721  $pageIds = array_keys( $this->titles );
722 
723  if ( $pageIds === [] ) {
724  return;
725  }
726 
727  $this->resetQueryParams();
728  $this->addTables( 'page_props' );
729  $this->addFields( [ 'pp_page', 'pp_value' ] );
730  $this->addWhereFld( 'pp_page', $pageIds );
731  $this->addWhereFld( 'pp_propname', 'displaytitle' );
732  $res = $this->select( __METHOD__ );
733 
734  foreach ( $res as $row ) {
735  $this->displaytitles[$row->pp_page] = $row->pp_value;
736  }
737  }
738 
746  private function getLinkClasses( ?LinkTarget $context_title = null ) {
747  if ( $this->titles === [] ) {
748  return;
749  }
750  // For compatibility with legacy GetLinkColours hook:
751  // $pagemap maps from page id to title (as prefixed db key)
752  // $classes maps from title (prefixed db key) to a space-separated
753  // list of link classes ("link colours").
754  // The hook should not modify $pagemap, and should only append to
755  // $classes (being careful to maintain space separation).
756  $classes = [];
757  $pagemap = [];
758  foreach ( $this->titles as $pageId => $page ) {
759  $pdbk = $this->titleFormatter->getPrefixedDBkey( $page );
760  $pagemap[$pageId] = $pdbk;
761  $classes[$pdbk] = isset( $this->pageIsRedir[$pageId] ) && $this->pageIsRedir[$pageId] ? 'mw-redirect' : '';
762  }
763  // legacy hook requires a real Title, not a LinkTarget
764  $context_title = $this->titleFactory->newFromLinkTarget(
765  $context_title ?? $this->titleFactory->newMainPage()
766  );
767  $this->getHookRunner()->onGetLinkColours(
768  $pagemap, $classes, $context_title
769  );
770 
771  // This API class expects the class list to be:
772  // (a) indexed by pageid, not title, and
773  // (b) a proper array of strings (possibly zero-length),
774  // not a single space-separated string (possibly the empty string)
775  $this->linkClasses = [];
776  foreach ( $this->titles as $pageId => $page ) {
777  $pdbk = $this->titleFormatter->getPrefixedDBkey( $page );
778  $this->linkClasses[$pageId] = preg_split(
779  '/\s+/', $classes[$pdbk] ?? '', -1, PREG_SPLIT_NO_EMPTY
780  );
781  }
782  }
783 
784  private function getVariantTitles() {
785  if ( $this->titles === [] ) {
786  return;
787  }
788  $this->variantTitles = [];
789  foreach ( $this->titles as $pageId => $page ) {
790  $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
791  ? $this->getAllVariants( $this->displaytitles[$pageId] )
792  : $this->getAllVariants( $this->titleFormatter->getText( $page ), $page->getNamespace() );
793  }
794  }
795 
796  private function getAllVariants( $text, $ns = NS_MAIN ) {
797  $result = [];
798  foreach ( $this->languageConverter->getVariants() as $variant ) {
799  $convertTitle = $this->languageConverter->autoConvert( $text, $variant );
800  if ( $ns !== NS_MAIN ) {
801  $convertNs = $this->languageConverter->convertNamespace( $ns, $variant );
802  $convertTitle = $convertNs . ':' . $convertTitle;
803  }
804  $result[$variant] = $convertTitle;
805  }
806  return $result;
807  }
808 
813  private function getWatchedInfo() {
814  $user = $this->getUser();
815 
816  if ( !$user->isRegistered() || count( $this->everything ) == 0
817  || !$this->getAuthority()->isAllowed( 'viewmywatchlist' )
818  ) {
819  return;
820  }
821 
822  $this->watched = [];
823  $this->watchlistExpiries = [];
824  $this->notificationtimestamps = [];
825 
827  $items = $this->watchedItemStore->loadWatchedItemsBatch( $user, $this->everything );
828 
829  foreach ( $items as $item ) {
830  $nsId = $item->getTarget()->getNamespace();
831  $dbKey = $item->getTarget()->getDBkey();
832 
833  if ( $this->fld_watched ) {
834  $this->watched[$nsId][$dbKey] = true;
835 
836  $expiry = $item->getExpiry( TS_ISO_8601 );
837  if ( $expiry ) {
838  $this->watchlistExpiries[$nsId][$dbKey] = $expiry;
839  }
840  }
841 
842  if ( $this->fld_notificationtimestamp ) {
843  $this->notificationtimestamps[$nsId][$dbKey] = $item->getNotificationTimestamp();
844  }
845  }
846  }
847 
851  private function getWatcherInfo() {
852  if ( count( $this->everything ) == 0 ) {
853  return;
854  }
855 
856  $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
857  $unwatchedPageThreshold =
858  $this->getConfig()->get( MainConfigNames::UnwatchedPageThreshold );
859  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
860  return;
861  }
862 
863  $this->showZeroWatchers = $canUnwatchedpages;
864 
865  $countOptions = [];
866  if ( !$canUnwatchedpages ) {
867  $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
868  }
869 
870  $this->watchers = $this->watchedItemStore->countWatchersMultiple(
871  $this->everything,
872  $countOptions
873  );
874  }
875 
882  private function getVisitingWatcherInfo() {
883  $config = $this->getConfig();
884  $db = $this->getDB();
885 
886  $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
887  $unwatchedPageThreshold = $config->get( MainConfigNames::UnwatchedPageThreshold );
888  if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
889  return;
890  }
891 
892  $this->showZeroWatchers = $canUnwatchedpages;
893 
894  $titlesWithThresholds = [];
895  if ( $this->titles ) {
896  $lb = $this->linkBatchFactory->newLinkBatch( $this->titles );
897 
898  // Fetch last edit timestamps for pages
899  $this->resetQueryParams();
900  $this->addTables( [ 'page', 'revision' ] );
901  $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
902  $this->addWhere( [
903  'page_latest = rev_id',
904  $lb->constructSet( 'page', $db ),
905  ] );
906  $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
907  $timestampRes = $this->select( __METHOD__ );
908 
909  $age = $config->get( MainConfigNames::WatchersMaxAge );
910  $timestamps = [];
911  foreach ( $timestampRes as $row ) {
912  $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
913  $timestamps[$row->page_namespace][$row->page_title] = (int)$revTimestamp - $age;
914  }
915  $titlesWithThresholds = array_map(
916  static function ( PageReference $target ) use ( $timestamps ) {
917  return [
918  $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
919  ];
920  },
921  $this->titles
922  );
923  }
924 
925  if ( $this->missing ) {
926  $titlesWithThresholds = array_merge(
927  $titlesWithThresholds,
928  array_map(
929  static function ( PageReference $target ) {
930  return [ $target, null ];
931  },
932  $this->missing
933  )
934  );
935  }
936  $this->visitingwatchers = $this->watchedItemStore->countVisitingWatchersMultiple(
937  $titlesWithThresholds,
938  !$canUnwatchedpages ? $unwatchedPageThreshold : null
939  );
940  }
941 
942  public function getCacheMode( $params ) {
943  // Other props depend on something about the current user
944  $publicProps = [
945  'protection',
946  'talkid',
947  'subjectid',
948  'associatedpage',
949  'url',
950  'preload',
951  'displaytitle',
952  'varianttitles',
953  ];
954  if ( array_diff( (array)$params['prop'], $publicProps ) ) {
955  return 'private';
956  }
957 
958  // testactions also depends on the current user
959  if ( $params['testactions'] ) {
960  return 'private';
961  }
962 
963  return 'public';
964  }
965 
966  public function getAllowedParams() {
967  return [
968  'prop' => [
969  ParamValidator::PARAM_ISMULTI => true,
970  ParamValidator::PARAM_TYPE => [
971  'protection',
972  'talkid',
973  'watched', # private
974  'watchers', # private
975  'visitingwatchers', # private
976  'notificationtimestamp', # private
977  'subjectid',
978  'associatedpage',
979  'url',
980  'readable', # private
981  'preload',
982  'preloadcontent', # private: checks current user's permissions
983  'editintro', # private: checks current user's permissions
984  'displaytitle',
985  'varianttitles',
986  'linkclasses', # private: stub length (and possibly hook colors)
987  // If you add more properties here, please consider whether they
988  // need to be added to getCacheMode()
989  ],
991  EnumDef::PARAM_DEPRECATED_VALUES => [
992  'readable' => true, // Since 1.32
993  'preload' => true, // Since 1.41
994  ],
995  ],
996  'linkcontext' => [
997  ParamValidator::PARAM_TYPE => 'title',
998  ParamValidator::PARAM_DEFAULT => $this->titleFactory->newMainPage()->getPrefixedText(),
999  TitleDef::PARAM_RETURN_OBJECT => true,
1000  ],
1001  'testactions' => [
1002  ParamValidator::PARAM_TYPE => 'string',
1003  ParamValidator::PARAM_ISMULTI => true,
1004  ],
1005  'testactionsdetail' => [
1006  ParamValidator::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
1007  ParamValidator::PARAM_DEFAULT => 'boolean',
1009  ],
1010  'testactionsautocreate' => false,
1011  'preloadcustom' => [
1012  // This should be a valid and existing page title, but we don't want to validate it here,
1013  // because it's usually someone else's fault. It could emit a warning in the future.
1014  ParamValidator::PARAM_TYPE => 'string',
1015  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'preloadcontentonly' ] ],
1016  ],
1017  'preloadparams' => [
1018  ParamValidator::PARAM_ISMULTI => true,
1019  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'preloadcontentonly' ] ],
1020  ],
1021  'preloadnewsection' => [
1022  ParamValidator::PARAM_TYPE => 'boolean',
1023  ParamValidator::PARAM_DEFAULT => false,
1024  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'preloadcontentonly' ] ],
1025  ],
1026  'editintrostyle' => [
1027  ParamValidator::PARAM_TYPE => [ 'lessframes', 'moreframes' ],
1028  ParamValidator::PARAM_DEFAULT => 'moreframes',
1029  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'editintroonly' ] ],
1030  ],
1031  'editintroskip' => [
1032  ParamValidator::PARAM_TYPE => 'string',
1033  ParamValidator::PARAM_ISMULTI => true,
1034  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'editintroonly' ] ],
1035  ],
1036  'editintrocustom' => [
1037  // This should be a valid and existing page title, but we don't want to validate it here,
1038  // because it's usually someone else's fault. It could emit a warning in the future.
1039  ParamValidator::PARAM_TYPE => 'string',
1040  ApiBase::PARAM_HELP_MSG_INFO => [ [ 'editintroonly' ] ],
1041  ],
1042  'continue' => [
1043  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
1044  ],
1045  ];
1046  }
1047 
1048  protected function getExamplesMessages() {
1049  $title = Title::newMainPage()->getPrefixedText();
1050  $mp = rawurlencode( $title );
1051 
1052  return [
1053  "action=query&prop=info&titles={$mp}"
1054  => 'apihelp-query+info-example-simple',
1055  "action=query&prop=info&inprop=protection&titles={$mp}"
1056  => 'apihelp-query+info-example-protection',
1057  ];
1058  }
1059 
1060  public function getHelpUrls() {
1061  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
1062  }
1063 }
const PROTO_CANONICAL
Definition: Defines.php:197
const NS_FILE
Definition: Defines.php:70
const PROTO_CURRENT
Definition: Defines.php:196
const NS_MAIN
Definition: Defines.php:64
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1515
addBlockInfoToStatus(StatusValue $status, Authority $user=null)
Add block info to block messages in a Status.
Definition: ApiBase.php:1333
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition: ApiBase.php:537
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition: ApiBase.php:1745
getMain()
Get the main module.
Definition: ApiBase.php:546
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition: ApiBase.php:186
getErrorFormatter()
Definition: ApiBase.php:678
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition: ApiBase.php:1706
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition: ApiBase.php:209
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition: ApiBase.php:240
getResult()
Get the result object.
Definition: ApiBase.php:667
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:807
const LIMIT_SML1
Slow query, standard limit.
Definition: ApiBase.php:238
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:169
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:752
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.
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.
__construct(ApiQuery $queryModule, $moduleName, Language $contentLanguage, LinkBatchFactory $linkBatchFactory, NamespaceInfo $namespaceInfo, TitleFactory $titleFactory, TitleFormatter $titleFormatter, WatchedItemStore $watchedItemStore, LanguageConverterFactory $languageConverterFactory, RestrictionStore $restrictionStore, LinksMigration $linksMigration, TempUserCreator $tempUserCreator, IntroMessageBuilder $introMessageBuilder, PreloadedContentBuilder $preloadedContentBuilder, RevisionLookup $revisionLookup, UrlUtils $urlUtils)
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:43
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Definition: ApiResult.php:716
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
Definition: ApiResult.php:467
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Definition: ApiResult.php:1199
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Base class for language-specific code.
Definition: Language.php:61
Provides the intro messages (edit notices and others) to be displayed before an edit form.
Provides the initial content of the edit box displayed in an edit form when creating a new page or a ...
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.
Represents the target of a wiki link.
Definition: TitleValue.php:44
Represents a title within MediaWiki.
Definition: Title.php:76
Service for temporary user creation.
A service to expand, parse, and otherwise manipulate URLs.
Definition: UrlUtils.php:17
Storage layer class for WatchedItems.
Service for formatting and validating API parameters.
Type definition for enumeration types.
Definition: EnumDef.php:32
The shared interface for all language converters.
Represents the target of a wiki link.
Definition: LinkTarget.php:30
Interface for objects (potentially) representing an editable wiki page.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getNamespace()
Returns the page's namespace number.
getDBkey()
Get the page title in DB key form.
Service for looking up page revisions.
A title formatter service for MediaWiki.
Interface for localizing messages in MediaWiki.
$content
Definition: router.php:76