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