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