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 $text = '';
443 if ( !$pageExists ) {
444 $this->getHookRunner()->onEditFormPreloadText( $text, $title );
445 }
446 $pageInfo['preload'] = $text;
447 }
448
449 if ( $this->fld_preloadcontent ) {
450 $newSection = $this->params['preloadnewsection'];
451 // Preloaded content is not supported for already existing pages or sections.
452 // The actual page/section content should be shown for editing (from prop=revisions API).
453 if ( !$pageExists || $newSection ) {
454 $content = $this->preloadedContentBuilder->getPreloadedContent(
455 $title->toPageIdentity(),
456 $this->getAuthority(),
457 $this->params['preloadcustom'],
458 $this->params['preloadparams'] ?? [],
459 $newSection ? 'new' : null
460 );
461 $defaultContent = $newSection ? null :
462 $this->preloadedContentBuilder->getDefaultContent( $title->toPageIdentity() );
463 $contentIsDefault = $defaultContent ? $content->equals( $defaultContent ) : $content->isEmpty();
464 // Adapted from ApiQueryRevisionsBase::extractAllSlotInfo.
465 // The preloaded content fills the main slot.
466 $pageInfo['preloadcontent']['contentmodel'] = $content->getModel();
467 $pageInfo['preloadcontent']['contentformat'] = $content->getDefaultFormat();
468 ApiResult::setContentValue( $pageInfo['preloadcontent'], 'content', $content->serialize() );
469 // If the preloaded content generated from these parameters is the same as
470 // the default page content, the user should be discouraged from saving the page
471 // (e.g. by disabling the save button until changes are made, or displaying a warning).
472 $pageInfo['preloadisdefault'] = $contentIsDefault;
473 }
474 }
475
476 if ( $this->fld_editintro ) {
477 // Use $page as the context page in every processed message (T300184)
478 $localizerWithPage = new class( $this, $page ) implements MessageLocalizer {
479 private MessageLocalizer $base;
480 private PageReference $page;
481
482 public function __construct( MessageLocalizer $base, PageReference $page ) {
483 $this->base = $base;
484 $this->page = $page;
485 }
486
490 public function msg( $key, ...$params ) {
491 return $this->base->msg( $key, ...$params )->page( $this->page );
492 }
493 };
494
495 $styleParamMap = [
496 'lessframes' => IntroMessageBuilder::LESS_FRAMES,
497 'moreframes' => IntroMessageBuilder::MORE_FRAMES,
498 ];
499 // If we got here, there is exactly one page and revision in the query
500 $revId = array_key_first( $this->getPageSet()->getLiveRevisionIDs() );
501 $revRecord = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
502
503 $messages = $this->introMessageBuilder->getIntroMessages(
504 $styleParamMap[ $this->params['editintrostyle'] ],
505 $this->params['editintroskip'] ?? [],
506 $localizerWithPage,
507 $title->toPageIdentity(),
508 $revRecord,
509 $this->getAuthority(),
510 $this->params['editintrocustom'],
511 // Maybe expose these as parameters in the future, but for now it doesn't seem worth it:
512 null,
513 false
514 );
515 ApiResult::setIndexedTagName( $messages, 'ei' );
516 ApiResult::setArrayType( $messages, 'kvp', 'key' );
517
518 $pageInfo['editintro'] = $messages;
519 }
520
521 if ( $this->fld_displaytitle ) {
522 $pageInfo['displaytitle'] = $this->displaytitles[$pageid] ??
523 htmlspecialchars( $this->titleFormatter->getPrefixedText( $page ), ENT_NOQUOTES );
524 }
525
526 if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
527 $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
528 }
529
530 if ( $this->fld_linkclasses && isset( $this->linkClasses[$pageid] ) ) {
531 $pageInfo['linkclasses'] = $this->linkClasses[$pageid];
532 }
533
534 if ( $this->params['testactions'] ) {
535 $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
536 if ( $this->countTestedActions >= $limit ) {
537 return null; // force a continuation
538 }
539
540 $detailLevel = $this->params['testactionsdetail'];
541 $errorFormatter = $this->getErrorFormatter();
542 if ( $errorFormatter->getFormat() === 'bc' ) {
543 // Eew, no. Use a more modern format here.
544 $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
545 }
546
547 $pageInfo['actions'] = [];
548 if ( $this->params['testactionsautocreate'] ) {
549 $pageInfo['wouldautocreate'] = [];
550 }
551
552 foreach ( $this->params['testactions'] as $action ) {
553 $this->countTestedActions++;
554
555 $shouldAutoCreate = $this->tempUserCreator->shouldAutoCreate( $this->getUser(), $action );
556
557 if ( $shouldAutoCreate ) {
558 $authority = $this->userFactory->newTempPlaceholder();
559 } else {
560 $authority = $this->getAuthority();
561 }
562
563 if ( $detailLevel === 'boolean' ) {
564 $pageInfo['actions'][$action] = $authority->definitelyCan( $action, $page );
565 } else {
566 $status = new PermissionStatus();
567 if ( $detailLevel === 'quick' ) {
568 $authority->probablyCan( $action, $page, $status );
569 } else {
570 $authority->definitelyCan( $action, $page, $status );
571 }
572
573 $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $status );
574 }
575
576 if ( $this->params['testactionsautocreate'] ) {
577 $pageInfo['wouldautocreate'][$action] = $shouldAutoCreate;
578 }
579 }
580 }
581
582 return $pageInfo;
583 }
584
588 private function getProtectionInfo() {
589 $this->protections = [];
590
591 // Get normal protections for existing titles
592 if ( count( $this->titles ) ) {
593 $this->resetQueryParams();
594 $this->addTables( 'page_restrictions' );
595 $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
596 'pr_expiry', 'pr_cascade' ] );
597 $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
598
599 $res = $this->select( __METHOD__ );
600 foreach ( $res as $row ) {
602 $page = $this->titles[$row->pr_page];
603 $a = [
604 'type' => $row->pr_type,
605 'level' => $row->pr_level,
606 'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
607 ];
608 if ( $row->pr_cascade ) {
609 $a['cascade'] = true;
610 }
611 $this->protections[$page->getNamespace()][$page->getDBkey()][] = $a;
612 }
613 }
614
615 // Get protections for missing titles
616 if ( count( $this->missing ) ) {
617 $this->resetQueryParams();
618 $lb = $this->linkBatchFactory->newLinkBatch( $this->missing );
619 $this->addTables( 'protected_titles' );
620 $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
621 $this->addWhere( $lb->constructSet( 'pt', $this->getDB() ) );
622 $res = $this->select( __METHOD__ );
623 foreach ( $res as $row ) {
624 $this->protections[$row->pt_namespace][$row->pt_title][] = [
625 'type' => 'create',
626 'level' => $row->pt_create_perm,
627 'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
628 ];
629 }
630 }
631
632 // Separate good and missing titles into files and other pages
633 // and populate $this->restrictionTypes
634 $images = $others = [];
635 foreach ( $this->everything as $page ) {
636 if ( $page->getNamespace() === NS_FILE ) {
637 $images[] = $page->getDBkey();
638 } else {
639 $others[] = $page;
640 }
641 // Applicable protection types
642 $this->restrictionTypes[$page->getNamespace()][$page->getDBkey()] =
643 array_values( $this->restrictionStore->listApplicableRestrictionTypes( $page ) );
644 }
645
646 if ( count( $others ) ) {
647 $this->resetQueryParams();
648 $this->addTables( [ 'page_restrictions', 'page' ] );
649 $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
650 'page_title', 'page_namespace', 'page_id' ] );
651 $this->addWhere( 'pr_page = page_id' );
652 $this->addWhereFld( 'pr_cascade', 1 );
653
654 $res = $this->select( __METHOD__ );
655
656 $protectedPages = [];
657 foreach ( $res as $row ) {
658 $protectedPages[$row->page_id] = [
659 'type' => $row->pr_type,
660 'level' => $row->pr_level,
661 'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
662 'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
663 ];
664 }
665
666 if ( $protectedPages ) {
667 $this->setVirtualDomain( TemplateLinksTable::VIRTUAL_DOMAIN );
668
669 $lb = $this->linkBatchFactory->newLinkBatch( $others );
670
671 $queryInfo = $this->linksMigration->getQueryInfo( 'templatelinks' );
672 $res = $this->getDB()->newSelectQueryBuilder()
673 ->select( [ 'tl_from', 'lt_namespace', 'lt_title' ] )
674 ->tables( $queryInfo['tables'] )
675 ->joinConds( $queryInfo['joins'] )
676 ->where( [ 'tl_from' => array_keys( $protectedPages ) ] )
677 ->andWhere( $lb->constructSet( 'tl', $this->getDB() ) )
678 ->useIndex( [ 'templatelinks' => 'PRIMARY' ] )
679 ->caller( __METHOD__ )
680 ->fetchResultSet();
681
682 foreach ( $res as $row ) {
683 $protection = $protectedPages[$row->tl_from];
684 $this->protections[$row->lt_namespace][$row->lt_title][] = $protection;
685 }
686
687 $this->resetVirtualDomain();
688 }
689 }
690
691 if ( count( $images ) ) {
692 $this->resetQueryParams();
693 $this->addTables( [ 'page_restrictions', 'page' ] );
694 $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
695 'page_title', 'page_namespace', 'page_id' ] );
696 $this->addWhere( 'pr_page = page_id' );
697 $this->addWhereFld( 'pr_cascade', 1 );
698
699 $res = $this->select( __METHOD__ );
700
701 $protectedPages = [];
702 foreach ( $res as $row ) {
703 $protectedPages[$row->page_id] = [
704 'type' => $row->pr_type,
705 'level' => $row->pr_level,
706 'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
707 'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
708 ];
709 }
710
711 if ( $protectedPages ) {
712 $this->setVirtualDomain( ImageLinksTable::VIRTUAL_DOMAIN );
713
714 $queryInfo = $this->linksMigration->getQueryInfo( 'imagelinks' );
715
716 $res = $this->getDB()->newSelectQueryBuilder()
717 ->fields( [ 'il_from', 'lt_title' ] )
718 ->tables( $queryInfo['tables'] )
719 ->where( [ 'il_from' => array_keys( $protectedPages ) ] )
720 ->joinConds( $queryInfo['joins'] )
721 ->andWhere( [ 'lt_title' => $images, 'lt_namespace' => NS_FILE ] )
722 // Prevent RDBMS from picking the il_target_id index here, which can cause
723 // massive table scans in the case of linking to a highly used image.
724 ->useIndex( [ 'imagelinks' => 'PRIMARY' ] )
725 ->caller( __METHOD__ )
726 ->fetchResultSet();
727
728 foreach ( $res as $row ) {
729 $protection = $protectedPages[$row->il_from];
730 $this->protections[NS_FILE][$row->lt_title][] = $protection;
731 }
732
733 $this->resetVirtualDomain();
734 }
735 }
736 }
737
742 private function getTSIDs() {
743 $getTitles = $this->talkids = $this->subjectids = [];
744 $nsInfo = $this->namespaceInfo;
745
747 foreach ( $this->everything as $page ) {
748 if ( $nsInfo->isTalk( $page->getNamespace() ) ) {
749 if ( $this->fld_subjectid ) {
750 $getTitles[] = $nsInfo->getSubjectPage( TitleValue::newFromPage( $page ) );
751 }
752 } elseif ( $this->fld_talkid ) {
753 $getTitles[] = $nsInfo->getTalkPage( TitleValue::newFromPage( $page ) );
754 }
755 }
756 if ( $getTitles === [] ) {
757 return;
758 }
759
760 $db = $this->getDB();
761
762 // Construct a custom WHERE clause that matches
763 // all titles in $getTitles
764 $lb = $this->linkBatchFactory->newLinkBatch( $getTitles );
765 $this->resetQueryParams();
766 $this->addTables( 'page' );
767 $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
768 $this->addWhere( $lb->constructSet( 'page', $db ) );
769 $res = $this->select( __METHOD__ );
770 foreach ( $res as $row ) {
771 if ( $nsInfo->isTalk( $row->page_namespace ) ) {
772 $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
773 (int)( $row->page_id );
774 } else {
775 $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
776 (int)( $row->page_id );
777 }
778 }
779 }
780
781 private function getDisplayTitle() {
782 $this->displaytitles = [];
783
784 $pageIds = array_keys( $this->titles );
785
786 if ( $pageIds === [] ) {
787 return;
788 }
789
790 $this->resetQueryParams();
791 $this->addTables( 'page_props' );
792 $this->addFields( [ 'pp_page', 'pp_value' ] );
793 $this->addWhereFld( 'pp_page', $pageIds );
794 $this->addWhereFld( 'pp_propname', 'displaytitle' );
795 $res = $this->select( __METHOD__ );
796
797 foreach ( $res as $row ) {
798 $this->displaytitles[$row->pp_page] = $row->pp_value;
799 }
800 }
801
812 private function getLinkClasses( ?LinkTarget $context_title = null, bool $default_link_caption = false ) {
813 if ( $this->titles === [] ) {
814 return;
815 }
816 // For compatibility with legacy GetLinkColours hook:
817 // $pagemap maps from page id to title (as prefixed db key)
818 // $classes maps from title (prefixed db key) to a space-separated
819 // list of link classes ("link colours").
820 // The hook should not modify $pagemap, and should only append to
821 // $classes (being careful to maintain space separation).
822 $classes = [];
823 $pagemap = [];
824 foreach ( $this->titles as $pageId => $page ) {
825 $pdbk = $this->titleFormatter->getPrefixedDBkey( $page );
826 $pagemap[$pageId] = $pdbk;
827 $classes[$pdbk] = $this->linkRenderer->getLinkClasses( $page, $default_link_caption );
828 }
829 // legacy hook requires a real Title, not a LinkTarget
830 $context_title = $this->titleFactory->newFromLinkTarget(
831 $context_title ?? $this->titleFactory->newMainPage()
832 );
833 $this->getHookRunner()->onGetLinkColours(
834 $pagemap, $classes, $context_title
835 );
836
837 // This API class expects the class list to be:
838 // (a) indexed by pageid, not title, and
839 // (b) a proper array of strings (possibly zero-length),
840 // not a single space-separated string (possibly the empty string)
841 $this->linkClasses = [];
842 foreach ( $this->titles as $pageId => $page ) {
843 $pdbk = $this->titleFormatter->getPrefixedDBkey( $page );
844 $this->linkClasses[$pageId] = preg_split(
845 '/\s+/', $classes[$pdbk] ?? '', -1, PREG_SPLIT_NO_EMPTY
846 );
847 }
848 }
849
850 private function getVariantTitles() {
851 if ( $this->titles === [] ) {
852 return;
853 }
854 $this->variantTitles = [];
855 foreach ( $this->titles as $pageId => $page ) {
856 $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
857 ? $this->getAllVariants( $this->displaytitles[$pageId] )
858 : $this->getAllVariants( $this->titleFormatter->getText( $page ), $page->getNamespace() );
859 }
860 }
861
862 private function getAllVariants( string $text, int $ns = NS_MAIN ): array {
863 $result = [];
864 foreach ( $this->languageConverter->getVariants() as $variant ) {
865 $convertTitle = $this->languageConverter->autoConvert( $text, $variant );
866 if ( $ns !== NS_MAIN ) {
867 $convertNs = $this->languageConverter->convertNamespace( $ns, $variant );
868 $convertTitle = $convertNs . ':' . $convertTitle;
869 }
870 $result[$variant] = $convertTitle;
871 }
872 return $result;
873 }
874
879 private function getWatchedInfo() {
880 $user = $this->getUser();
881
882 if ( !$user->isRegistered() || count( $this->everything ) == 0
883 || !$this->getAuthority()->isAllowed( 'viewmywatchlist' )
884 ) {
885 return;
886 }
887
888 $this->watched = [];
889 $this->watchlistExpiries = [];
890 $this->watchlistLabels = [];
891 $this->notificationtimestamps = [];
892
893 $items = $this->watchedItemStore->loadWatchedItemsBatch( $user, $this->everything );
894
895 foreach ( $items as $item ) {
896 $nsId = $item->getTarget()->getNamespace();
897 $dbKey = $item->getTarget()->getDBkey();
898
899 if ( $this->fld_watched ) {
900 $this->watched[$nsId][$dbKey] = true;
901
902 $expiry = $item->getExpiry( TS::ISO_8601 );
903 if ( $expiry ) {
904 $this->watchlistExpiries[$nsId][$dbKey] = $expiry;
905 }
906 }
907
908 if ( $this->fld_watchlistlabels ) {
909 $labels = $item->getLabels();
910 if ( $labels ) {
911 $labelData = [];
912 foreach ( $labels as $label ) {
913 $labelData[] = [
914 'id' => $label->getId(),
915 'name' => $label->getName(),
916 ];
917 }
918 $this->watchlistLabels[$nsId][$dbKey] = $labelData;
919 }
920 }
921
922 if ( $this->fld_notificationtimestamp ) {
923 $this->notificationtimestamps[$nsId][$dbKey] = $item->getNotificationTimestamp();
924 }
925 }
926 }
927
931 private function getWatcherInfo() {
932 if ( count( $this->everything ) == 0 ) {
933 return;
934 }
935
936 $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
937 $unwatchedPageThreshold =
939 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
940 return;
941 }
942
943 $this->showZeroWatchers = $canUnwatchedpages;
944
945 $countOptions = [];
946 if ( !$canUnwatchedpages ) {
947 $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
948 }
949
950 $this->watchers = $this->watchedItemStore->countWatchersMultiple(
951 $this->everything,
952 $countOptions
953 );
954 }
955
962 private function getVisitingWatcherInfo() {
963 $config = $this->getConfig();
964 $db = $this->getDB();
965
966 $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
967 $unwatchedPageThreshold = $config->get( MainConfigNames::UnwatchedPageThreshold );
968 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
969 return;
970 }
971
972 $this->showZeroWatchers = $canUnwatchedpages;
973
974 $titlesWithThresholds = [];
975 if ( $this->titles ) {
976 $lb = $this->linkBatchFactory->newLinkBatch( $this->titles );
977
978 // Fetch last edit timestamps for pages
979 $this->resetQueryParams();
980 $this->addTables( [ 'page', 'revision' ] );
981 $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
982 $this->addWhere( [
983 'page_latest = rev_id',
984 $lb->constructSet( 'page', $db ),
985 ] );
986 $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
987 $timestampRes = $this->select( __METHOD__ );
988
989 $age = $config->get( MainConfigNames::WatchersMaxAge );
990 $timestamps = [];
991 foreach ( $timestampRes as $row ) {
992 $revTimestamp = wfTimestamp( TS::UNIX, (int)$row->rev_timestamp );
993 $timestamps[$row->page_namespace][$row->page_title] = (int)$revTimestamp - $age;
994 }
995 $titlesWithThresholds = array_map(
996 static function ( PageReference $target ) use ( $timestamps ) {
997 return [
998 $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
999 ];
1000 },
1001 $this->titles
1002 );
1003 }
1004
1005 if ( $this->missing ) {
1006 $titlesWithThresholds = array_merge(
1007 $titlesWithThresholds,
1008 array_map(
1009 static function ( PageReference $target ) {
1010 return [ $target, null ];
1011 },
1012 $this->missing
1013 )
1014 );
1015 }
1016 $this->visitingwatchers = $this->watchedItemStore->countVisitingWatchersMultiple(
1017 $titlesWithThresholds,
1018 !$canUnwatchedpages ? $unwatchedPageThreshold : null
1019 );
1020 }
1021
1023 public function getCacheMode( $params ) {
1024 // Other props depend on something about the current user
1025 $publicProps = [
1026 'protection',
1027 'talkid',
1028 'subjectid',
1029 'associatedpage',
1030 'url',
1031 'preload',
1032 'displaytitle',
1033 'varianttitles',
1034 ];
1035 if ( array_diff( (array)$params['prop'], $publicProps ) ) {
1036 return 'private';
1037 }
1038
1039 // testactions also depends on the current user
1040 if ( $params['testactions'] ) {
1041 return 'private';
1042 }
1043
1044 return 'public';
1045 }
1046
1048 public function getAllowedParams() {
1049 return [
1050 'prop' => [
1051 ParamValidator::PARAM_ISMULTI => true,
1052 ParamValidator::PARAM_TYPE => [
1053 'protection',
1054 'talkid',
1055 'watched', # private
1056 'watchlistlabels', # private
1057 'watchers', # private
1058 'visitingwatchers', # private
1059 'notificationtimestamp', # private
1060 'subjectid',
1061 'associatedpage',
1062 'url',
1063 'readable', # private
1064 'preload',
1065 'preloadcontent', # private: checks current user's permissions
1066 'editintro', # private: checks current user's permissions
1067 'displaytitle',
1068 'varianttitles',
1069 'linkclasses', # private: stub length (and possibly hook colors)
1070 // If you add more properties here, please consider whether they
1071 // need to be added to getCacheMode()
1072 ],
1074 EnumDef::PARAM_DEPRECATED_VALUES => [
1075 'readable' => true, // Since 1.32
1076 'preload' => true, // Since 1.41
1077 ],
1078 ],
1079 'linkcontext' => [
1080 ParamValidator::PARAM_TYPE => 'title',
1081 ParamValidator::PARAM_DEFAULT => $this->titleFactory->newMainPage()->getPrefixedText(),
1082 TitleDef::PARAM_RETURN_OBJECT => true,
1083 ],
1084 'defaultlinkcaption' => [
1085 ParamValidator::PARAM_TYPE => 'boolean',
1086 ParamValidator::PARAM_DEFAULT => false,
1087 ],
1088 'testactions' => [
1089 ParamValidator::PARAM_TYPE => 'string',
1090 ParamValidator::PARAM_ISMULTI => true,
1091 ],
1092 'testactionsdetail' => [
1093 ParamValidator::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
1094 ParamValidator::PARAM_DEFAULT => 'boolean',
1096 ],
1097 'testactionsautocreate' => false,
1098 'preloadcustom' => [
1099 // This should be a valid and existing page title, but we don't want to validate it here,
1100 // because it's usually someone else's fault. It could emit a warning in the future.
1101 ParamValidator::PARAM_TYPE => 'string',
1102 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'preloadcontentonly' ] ],
1103 ],
1104 'preloadparams' => [
1105 ParamValidator::PARAM_ISMULTI => true,
1106 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'preloadcontentonly' ] ],
1107 ],
1108 'preloadnewsection' => [
1109 ParamValidator::PARAM_TYPE => 'boolean',
1110 ParamValidator::PARAM_DEFAULT => false,
1111 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'preloadcontentonly' ] ],
1112 ],
1113 'editintrostyle' => [
1114 ParamValidator::PARAM_TYPE => [ 'lessframes', 'moreframes' ],
1115 ParamValidator::PARAM_DEFAULT => 'moreframes',
1116 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'editintroonly' ] ],
1117 ],
1118 'editintroskip' => [
1119 ParamValidator::PARAM_TYPE => 'string',
1120 ParamValidator::PARAM_ISMULTI => true,
1121 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'editintroonly' ] ],
1122 ],
1123 'editintrocustom' => [
1124 // This should be a valid and existing page title, but we don't want to validate it here,
1125 // because it's usually someone else's fault. It could emit a warning in the future.
1126 ParamValidator::PARAM_TYPE => 'string',
1127 ApiBase::PARAM_HELP_MSG_INFO => [ [ 'editintroonly' ] ],
1128 ],
1129 'continue' => [
1130 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
1131 ],
1132 ];
1133 }
1134
1136 protected function getExamplesMessages() {
1137 $title = Title::newMainPage()->getPrefixedText();
1138 $mp = rawurlencode( $title );
1139
1140 return [
1141 "action=query&prop=info&titles={$mp}"
1142 => 'apihelp-query+info-example-simple',
1143 "action=query&prop=info&inprop=protection&titles={$mp}"
1144 => 'apihelp-query+info-example-protection',
1145 ];
1146 }
1147
1149 public function getHelpUrls() {
1150 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
1151 }
1152}
1153
1155class_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:1522
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:566
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:781
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:575
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1746
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1707
getResult()
Get the result object.
Definition ApiBase.php:696
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:837
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)