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