MediaWiki master
ApiQueryInfo.php
Go to the documentation of this file.
1<?php
47
54
55 private ILanguageConverter $languageConverter;
56 private LinkBatchFactory $linkBatchFactory;
57 private NamespaceInfo $namespaceInfo;
58 private TitleFactory $titleFactory;
59 private TitleFormatter $titleFormatter;
60 private WatchedItemStore $watchedItemStore;
61 private RestrictionStore $restrictionStore;
62 private LinksMigration $linksMigration;
63 private TempUserCreator $tempUserCreator;
64 private UserFactory $userFactory;
65 private IntroMessageBuilder $introMessageBuilder;
66 private PreloadedContentBuilder $preloadedContentBuilder;
67 private RevisionLookup $revisionLookup;
68 private UrlUtils $urlUtils;
69
70 private bool $fld_protection = false;
71 private bool $fld_talkid = false;
72 private bool $fld_subjectid = false;
73 private bool $fld_url = false;
74 private bool $fld_readable = false;
75 private bool $fld_watched = false;
76 private bool $fld_watchers = false;
77 private bool $fld_visitingwatchers = false;
78 private bool $fld_notificationtimestamp = false;
79 private bool $fld_preload = false;
80 private bool $fld_preloadcontent = false;
81 private bool $fld_editintro = false;
82 private bool $fld_displaytitle = false;
83 private bool $fld_varianttitles = false;
84
89 private $fld_linkclasses = false;
90
94 private $fld_associatedpage = false;
95
96 private $params;
97
99 private $titles;
101 private $missing;
103 private $everything;
104
109 private $pageIsRedir;
114 private $pageIsNew;
119 private $pageTouched;
124 private $pageLatest;
129 private $pageLength;
130
131 private $protections;
132 private $restrictionTypes;
133 private $watched;
134 private $watchers;
135 private $visitingwatchers;
136 private $notificationtimestamps;
137 private $talkids;
138 private $subjectids;
139 private $displaytitles;
140 private $variantTitles;
141
146 private $watchlistExpiries;
147
152 private $linkClasses;
153
154 private $showZeroWatchers = false;
155
156 private $countTestedActions = 0;
157
177 public function __construct(
178 ApiQuery $queryModule,
179 $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 =
910 $this->getConfig()->get( MainConfigNames::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}
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1540
addBlockInfoToStatus(StatusValue $status, Authority $user=null)
Add block info to block messages in a Status.
Definition ApiBase.php:1361
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:551
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1770
getMain()
Get the main module.
Definition ApiBase.php:560
const PARAM_HELP_MSG_INFO
(array) Specify additional information tags for the parameter.
Definition ApiBase.php:189
getErrorFormatter()
Definition ApiBase.php:692
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1731
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:212
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:243
getResult()
Get the result object.
Definition ApiBase.php:681
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:821
const LIMIT_SML1
Slow query, standard limit.
Definition ApiBase.php:241
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:172
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:766
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.
__construct(ApiQuery $queryModule, $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)
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.
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.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Base class for language-specific code.
Definition Language.php:66
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 ...
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.
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:79
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.
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.