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