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