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