MediaWiki REL1_39
ApiQueryInfo.php
Go to the documentation of this file.
1<?php
32
39
41 private $languageConverter;
43 private $linkBatchFactory;
45 private $namespaceInfo;
47 private $titleFactory;
49 private $titleFormatter;
51 private $watchedItemStore;
53 private $restrictionStore;
55 private $linksMigration;
56
57 private $fld_protection = false, $fld_talkid = false,
58 $fld_subjectid = false, $fld_url = false,
59 $fld_readable = false, $fld_watched = false,
60 $fld_watchers = false, $fld_visitingwatchers = false,
61 $fld_notificationtimestamp = false,
62 $fld_preload = false, $fld_displaytitle = false, $fld_varianttitles = false;
63
68 private $fld_linkclasses = false;
69
73 private $fld_associatedpage = false;
74
75 private $params;
76
78 private $titles;
80 private $missing;
82 private $everything;
83
84 private $pageIsRedir, $pageIsNew, $pageTouched,
85 $pageLatest, $pageLength;
86
87 private $protections, $restrictionTypes, $watched, $watchers, $visitingwatchers,
88 $notificationtimestamps, $talkids, $subjectids, $displaytitles, $variantTitles;
89
94 private $watchlistExpiries;
95
100 private $linkClasses;
101
102 private $showZeroWatchers = false;
103
104 private $countTestedActions = 0;
105
119 public function __construct(
120 ApiQuery $queryModule,
121 $moduleName,
122 Language $contentLanguage,
123 LinkBatchFactory $linkBatchFactory,
124 NamespaceInfo $namespaceInfo,
125 TitleFactory $titleFactory,
126 TitleFormatter $titleFormatter,
127 WatchedItemStore $watchedItemStore,
128 LanguageConverterFactory $languageConverterFactory,
129 RestrictionStore $restrictionStore,
130 LinksMigration $linksMigration
131 ) {
132 parent::__construct( $queryModule, $moduleName, 'in' );
133 $this->languageConverter = $languageConverterFactory->getLanguageConverter( $contentLanguage );
134 $this->linkBatchFactory = $linkBatchFactory;
135 $this->namespaceInfo = $namespaceInfo;
136 $this->titleFactory = $titleFactory;
137 $this->titleFormatter = $titleFormatter;
138 $this->watchedItemStore = $watchedItemStore;
139 $this->restrictionStore = $restrictionStore;
140 $this->linksMigration = $linksMigration;
141 }
142
147 public function requestExtraData( $pageSet ) {
148 // If the pageset is resolving redirects we won't get page_is_redirect.
149 // But we can't know for sure until the pageset is executed (revids may
150 // turn it off), so request it unconditionally.
151 $pageSet->requestField( 'page_is_redirect' );
152 $pageSet->requestField( 'page_is_new' );
153 $config = $this->getConfig();
154 $pageSet->requestField( 'page_touched' );
155 $pageSet->requestField( 'page_latest' );
156 $pageSet->requestField( 'page_len' );
157 $pageSet->requestField( 'page_content_model' );
158 if ( $config->get( MainConfigNames::PageLanguageUseDB ) ) {
159 $pageSet->requestField( 'page_lang' );
160 }
161 }
162
163 public function execute() {
164 $this->params = $this->extractRequestParams();
165 if ( $this->params['prop'] !== null ) {
166 $prop = array_fill_keys( $this->params['prop'], true );
167 $this->fld_protection = isset( $prop['protection'] );
168 $this->fld_watched = isset( $prop['watched'] );
169 $this->fld_watchers = isset( $prop['watchers'] );
170 $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
171 $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
172 $this->fld_talkid = isset( $prop['talkid'] );
173 $this->fld_subjectid = isset( $prop['subjectid'] );
174 $this->fld_url = isset( $prop['url'] );
175 $this->fld_readable = isset( $prop['readable'] );
176 $this->fld_preload = isset( $prop['preload'] );
177 $this->fld_displaytitle = isset( $prop['displaytitle'] );
178 $this->fld_varianttitles = isset( $prop['varianttitles'] );
179 $this->fld_linkclasses = isset( $prop['linkclasses'] );
180 $this->fld_associatedpage = isset( $prop['associatedpage'] );
181 }
182
183 $pageSet = $this->getPageSet();
184 $this->titles = $pageSet->getGoodTitles();
185 $this->missing = $pageSet->getMissingTitles();
186 $this->everything = $this->titles + $this->missing;
187 $result = $this->getResult();
188
189 uasort( $this->everything, [ Title::class, 'compare' ] );
190 if ( $this->params['continue'] !== null ) {
191 // Throw away any titles we're gonna skip so they don't
192 // clutter queries
193 $cont = explode( '|', $this->params['continue'] );
194 $this->dieContinueUsageIf( count( $cont ) != 2 );
195 $conttitle = $this->titleFactory->makeTitleSafe( (int)$cont[0], $cont[1] );
196 $this->dieContinueUsageIf( !$conttitle );
197 foreach ( $this->everything as $pageid => $title ) {
198 if ( Title::compare( $title, $conttitle ) >= 0 ) {
199 break;
200 }
201 unset( $this->titles[$pageid] );
202 unset( $this->missing[$pageid] );
203 unset( $this->everything[$pageid] );
204 }
205 }
206
207 // when resolving redirects, no page will have this field
208 $this->pageIsRedir = !$pageSet->isResolvingRedirects()
209 ? $pageSet->getCustomField( 'page_is_redirect' )
210 : [];
211 $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
212
213 $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
214 $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
215 $this->pageLength = $pageSet->getCustomField( 'page_len' );
216
217 // Get protection info if requested
218 if ( $this->fld_protection ) {
219 $this->getProtectionInfo();
220 }
221
222 if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
223 $this->getWatchedInfo();
224 }
225
226 if ( $this->fld_watchers ) {
227 $this->getWatcherInfo();
228 }
229
230 if ( $this->fld_visitingwatchers ) {
231 $this->getVisitingWatcherInfo();
232 }
233
234 // Run the talkid/subjectid query if requested
235 if ( $this->fld_talkid || $this->fld_subjectid ) {
236 $this->getTSIDs();
237 }
238
239 if ( $this->fld_displaytitle ) {
240 $this->getDisplayTitle();
241 }
242
243 if ( $this->fld_varianttitles ) {
244 $this->getVariantTitles();
245 }
246
247 if ( $this->fld_linkclasses ) {
248 $this->getLinkClasses( $this->params['linkcontext'] );
249 }
250
252 foreach ( $this->everything as $pageid => $title ) {
253 $pageInfo = $this->extractPageInfo( $pageid, $title );
254 $fit = $pageInfo !== null && $result->addValue( [
255 'query',
256 'pages'
257 ], $pageid, $pageInfo );
258 if ( !$fit ) {
259 $this->setContinueEnumParameter( 'continue',
260 $title->getNamespace() . '|' .
261 $title->getText() );
262 break;
263 }
264 }
265 }
266
273 private function extractPageInfo( $pageid, $title ) {
274 $pageInfo = [];
275 // $title->exists() needs pageid, which is not set for all title objects
276 $titleExists = $pageid > 0;
277 $ns = $title->getNamespace();
278 $dbkey = $title->getDBkey();
279
280 $pageInfo['contentmodel'] = $title->getContentModel();
281
282 $pageLanguage = $title->getPageLanguage();
283 $pageInfo['pagelanguage'] = $pageLanguage->getCode();
284 $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
285 $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
286
287 if ( $titleExists ) {
288 $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
289 $pageInfo['lastrevid'] = (int)$this->pageLatest[$pageid];
290 $pageInfo['length'] = (int)$this->pageLength[$pageid];
291
292 if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
293 $pageInfo['redirect'] = true;
294 }
295 if ( $this->pageIsNew[$pageid] ) {
296 $pageInfo['new'] = true;
297 }
298 }
299
300 if ( $this->fld_protection ) {
301 $pageInfo['protection'] = [];
302 if ( isset( $this->protections[$ns][$dbkey] ) ) {
303 $pageInfo['protection'] =
304 $this->protections[$ns][$dbkey];
305 }
306 ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
307
308 $pageInfo['restrictiontypes'] = [];
309 if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
310 $pageInfo['restrictiontypes'] =
311 $this->restrictionTypes[$ns][$dbkey];
312 }
313 ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
314 }
315
316 if ( $this->fld_watched ) {
317 $pageInfo['watched'] = false;
318
319 if ( isset( $this->watched[$ns][$dbkey] ) ) {
320 $pageInfo['watched'] = $this->watched[$ns][$dbkey];
321 }
322
323 if ( isset( $this->watchlistExpiries[$ns][$dbkey] ) ) {
324 $pageInfo['watchlistexpiry'] = $this->watchlistExpiries[$ns][$dbkey];
325 }
326 }
327
328 if ( $this->fld_watchers ) {
329 if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
330 $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
331 } elseif ( $this->showZeroWatchers ) {
332 $pageInfo['watchers'] = 0;
333 }
334 }
335
336 if ( $this->fld_visitingwatchers ) {
337 if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
338 $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
339 } elseif ( $this->showZeroWatchers ) {
340 $pageInfo['visitingwatchers'] = 0;
341 }
342 }
343
344 if ( $this->fld_notificationtimestamp ) {
345 $pageInfo['notificationtimestamp'] = '';
346 if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
347 $pageInfo['notificationtimestamp'] =
348 wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
349 }
350 }
351
352 if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
353 $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
354 }
355
356 if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
357 $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
358 }
359
360 if ( $this->fld_associatedpage && $ns >= NS_MAIN ) {
361 $pageInfo['associatedpage'] = $this->titleFormatter->getPrefixedText(
362 $this->namespaceInfo->getAssociatedPage( $title )
363 );
364 }
365
366 if ( $this->fld_url ) {
367 $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
368 $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
369 $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
370 }
371 if ( $this->fld_readable ) {
372 $pageInfo['readable'] = $this->getAuthority()->definitelyCan( 'read', $title );
373 }
374
375 if ( $this->fld_preload ) {
376 if ( $titleExists ) {
377 $pageInfo['preload'] = '';
378 } else {
379 $text = null;
380 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
381 $this->getHookRunner()->onEditFormPreloadText( $text, $title );
382
383 $pageInfo['preload'] = $text;
384 }
385 }
386
387 if ( $this->fld_displaytitle ) {
388 $pageInfo['displaytitle'] = $this->displaytitles[$pageid] ??
389 htmlspecialchars( $title->getPrefixedText(), ENT_NOQUOTES );
390 }
391
392 if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
393 $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
394 }
395
396 if ( $this->fld_linkclasses && isset( $this->linkClasses[$pageid] ) ) {
397 $pageInfo['linkclasses'] = $this->linkClasses[$pageid];
398 }
399
400 if ( $this->params['testactions'] ) {
401 $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
402 if ( $this->countTestedActions >= $limit ) {
403 return null; // force a continuation
404 }
405
406 $detailLevel = $this->params['testactionsdetail'];
407 $errorFormatter = $this->getErrorFormatter();
408 if ( $errorFormatter->getFormat() === 'bc' ) {
409 // Eew, no. Use a more modern format here.
410 $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
411 }
412
413 $pageInfo['actions'] = [];
414 foreach ( $this->params['testactions'] as $action ) {
415 $this->countTestedActions++;
416
417 if ( $detailLevel === 'boolean' ) {
418 $pageInfo['actions'][$action] = $this->getAuthority()->authorizeRead( $action, $title );
419 } else {
420 $status = new PermissionStatus();
421 if ( $detailLevel === 'quick' ) {
422 $this->getAuthority()->probablyCan( $action, $title, $status );
423 } else {
424 $this->getAuthority()->definitelyCan( $action, $title, $status );
425 }
426 $this->addBlockInfoToStatus( $status );
427 $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $status );
428 }
429 }
430 }
431
432 return $pageInfo;
433 }
434
438 private function getProtectionInfo() {
439 $this->protections = [];
440 $db = $this->getDB();
441
442 // Get normal protections for existing titles
443 if ( count( $this->titles ) ) {
444 $this->resetQueryParams();
445 $this->addTables( 'page_restrictions' );
446 $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
447 'pr_expiry', 'pr_cascade' ] );
448 $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
449
450 $res = $this->select( __METHOD__ );
451 foreach ( $res as $row ) {
453 $title = $this->titles[$row->pr_page];
454 $a = [
455 'type' => $row->pr_type,
456 'level' => $row->pr_level,
457 'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
458 ];
459 if ( $row->pr_cascade ) {
460 $a['cascade'] = true;
461 }
462 $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
463 }
464 }
465
466 // Get protections for missing titles
467 if ( count( $this->missing ) ) {
468 $this->resetQueryParams();
469 $lb = $this->linkBatchFactory->newLinkBatch( $this->missing );
470 $this->addTables( 'protected_titles' );
471 $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
472 $this->addWhere( $lb->constructSet( 'pt', $db ) );
473 $res = $this->select( __METHOD__ );
474 foreach ( $res as $row ) {
475 $this->protections[$row->pt_namespace][$row->pt_title][] = [
476 'type' => 'create',
477 'level' => $row->pt_create_perm,
478 'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
479 ];
480 }
481 }
482
483 // Separate good and missing titles into files and other pages
484 // and populate $this->restrictionTypes
485 $images = $others = [];
486 foreach ( $this->everything as $title ) {
487 if ( $title->getNamespace() === NS_FILE ) {
488 $images[] = $title->getDBkey();
489 } else {
490 $others[] = $title;
491 }
492 // Applicable protection types
493 $this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
494 array_values( $this->restrictionStore->listApplicableRestrictionTypes( $title ) );
495 }
496
497 list( $blNamespace, $blTitle ) = $this->linksMigration->getTitleFields( 'templatelinks' );
498 $queryInfo = $this->linksMigration->getQueryInfo( 'templatelinks' );
499
500 if ( count( $others ) ) {
501 // Non-images: check templatelinks
502 $lb = $this->linkBatchFactory->newLinkBatch( $others );
503 $this->resetQueryParams();
504 $this->addTables( array_merge( [ 'page_restrictions', 'page' ], $queryInfo['tables'] ) );
505 // templatelinks must use PRIMARY index and not the tl_target_id.
506 $this->addOption( 'USE INDEX', [ 'templatelinks' => 'PRIMARY' ] );
507 $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
508 'page_title', 'page_namespace',
509 $blNamespace, $blTitle ] );
510 $this->addWhere( $lb->constructSet( 'tl', $db ) );
511 $this->addWhere( 'pr_page = page_id' );
512 $this->addWhere( 'pr_page = tl_from' );
513 $this->addWhereFld( 'pr_cascade', 1 );
514 $this->addJoinConds( $queryInfo['joins'] );
515
516 $res = $this->select( __METHOD__ );
517 foreach ( $res as $row ) {
518 $this->protections[$row->$blNamespace][$row->$blTitle][] = [
519 'type' => $row->pr_type,
520 'level' => $row->pr_level,
521 'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
522 'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
523 ];
524 }
525 }
526
527 if ( count( $images ) ) {
528 // Images: check imagelinks
529 $this->resetQueryParams();
530 $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
531 $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
532 'page_title', 'page_namespace', 'il_to' ] );
533 $this->addWhere( 'pr_page = page_id' );
534 $this->addWhere( 'pr_page = il_from' );
535 $this->addWhereFld( 'pr_cascade', 1 );
536 $this->addWhereFld( 'il_to', $images );
537
538 $res = $this->select( __METHOD__ );
539 foreach ( $res as $row ) {
540 $this->protections[NS_FILE][$row->il_to][] = [
541 'type' => $row->pr_type,
542 'level' => $row->pr_level,
543 'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
544 'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
545 ];
546 }
547 }
548 }
549
554 private function getTSIDs() {
555 $getTitles = $this->talkids = $this->subjectids = [];
556 $nsInfo = $this->namespaceInfo;
557
559 foreach ( $this->everything as $t ) {
560 if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
561 if ( $this->fld_subjectid ) {
562 $getTitles[] = $t->getSubjectPage();
563 }
564 } elseif ( $this->fld_talkid ) {
565 $getTitles[] = $t->getTalkPage();
566 }
567 }
568 if ( $getTitles === [] ) {
569 return;
570 }
571
572 $db = $this->getDB();
573
574 // Construct a custom WHERE clause that matches
575 // all titles in $getTitles
576 $lb = $this->linkBatchFactory->newLinkBatch( $getTitles );
577 $this->resetQueryParams();
578 $this->addTables( 'page' );
579 $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
580 $this->addWhere( $lb->constructSet( 'page', $db ) );
581 $res = $this->select( __METHOD__ );
582 foreach ( $res as $row ) {
583 if ( $nsInfo->isTalk( $row->page_namespace ) ) {
584 $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
585 (int)( $row->page_id );
586 } else {
587 $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
588 (int)( $row->page_id );
589 }
590 }
591 }
592
593 private function getDisplayTitle() {
594 $this->displaytitles = [];
595
596 $pageIds = array_keys( $this->titles );
597
598 if ( $pageIds === [] ) {
599 return;
600 }
601
602 $this->resetQueryParams();
603 $this->addTables( 'page_props' );
604 $this->addFields( [ 'pp_page', 'pp_value' ] );
605 $this->addWhereFld( 'pp_page', $pageIds );
606 $this->addWhereFld( 'pp_propname', 'displaytitle' );
607 $res = $this->select( __METHOD__ );
608
609 foreach ( $res as $row ) {
610 $this->displaytitles[$row->pp_page] = $row->pp_value;
611 }
612 }
613
621 private function getLinkClasses( ?LinkTarget $context_title = null ) {
622 if ( $this->titles === [] ) {
623 return;
624 }
625 // For compatibility with legacy GetLinkColours hook:
626 // $pagemap maps from page id to title (as prefixed db key)
627 // $classes maps from title (prefixed db key) to a space-separated
628 // list of link classes ("link colours").
629 // The hook should not modify $pagemap, and should only append to
630 // $classes (being careful to maintain space separation).
631 $classes = [];
632 $pagemap = [];
633 foreach ( $this->titles as $pageId => $title ) {
634 $pdbk = $title->getPrefixedDBkey();
635 $pagemap[$pageId] = $pdbk;
636 $classes[$pdbk] = $title->isRedirect() ? 'mw-redirect' : '';
637 }
638 // legacy hook requires a real Title, not a LinkTarget
639 $context_title = $this->titleFactory->newFromLinkTarget(
640 $context_title ?? $this->titleFactory->newMainPage()
641 );
642 $this->getHookRunner()->onGetLinkColours(
643 $pagemap, $classes, $context_title
644 );
645
646 // This API class expects the class list to be:
647 // (a) indexed by pageid, not title, and
648 // (b) a proper array of strings (possibly zero-length),
649 // not a single space-separated string (possibly the empty string)
650 $this->linkClasses = [];
651 foreach ( $this->titles as $pageId => $title ) {
652 $pdbk = $title->getPrefixedDBkey();
653 $this->linkClasses[$pageId] = preg_split(
654 '/\s+/', $classes[$pdbk] ?? '', -1, PREG_SPLIT_NO_EMPTY
655 );
656 }
657 }
658
659 private function getVariantTitles() {
660 if ( $this->titles === [] ) {
661 return;
662 }
663 $this->variantTitles = [];
664 foreach ( $this->titles as $pageId => $t ) {
665 $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
666 ? $this->getAllVariants( $this->displaytitles[$pageId] )
667 : $this->getAllVariants( $t->getText(), $t->getNamespace() );
668 }
669 }
670
671 private function getAllVariants( $text, $ns = NS_MAIN ) {
672 $result = [];
673 foreach ( $this->languageConverter->getVariants() as $variant ) {
674 $convertTitle = $this->languageConverter->autoConvert( $text, $variant );
675 if ( $ns !== NS_MAIN ) {
676 $convertNs = $this->languageConverter->convertNamespace( $ns, $variant );
677 $convertTitle = $convertNs . ':' . $convertTitle;
678 }
679 $result[$variant] = $convertTitle;
680 }
681 return $result;
682 }
683
688 private function getWatchedInfo() {
689 $user = $this->getUser();
690
691 if ( !$user->isRegistered() || count( $this->everything ) == 0
692 || !$this->getAuthority()->isAllowed( 'viewmywatchlist' )
693 ) {
694 return;
695 }
696
697 $this->watched = [];
698 $this->watchlistExpiries = [];
699 $this->notificationtimestamps = [];
700
702 $items = $this->watchedItemStore->loadWatchedItemsBatch( $user, $this->everything );
703
704 foreach ( $items as $item ) {
705 $nsId = $item->getTarget()->getNamespace();
706 $dbKey = $item->getTarget()->getDBkey();
707
708 if ( $this->fld_watched ) {
709 $this->watched[$nsId][$dbKey] = true;
710
711 $expiry = $item->getExpiry( TS_ISO_8601 );
712 if ( $expiry ) {
713 $this->watchlistExpiries[$nsId][$dbKey] = $expiry;
714 }
715 }
716
717 if ( $this->fld_notificationtimestamp ) {
718 $this->notificationtimestamps[$nsId][$dbKey] = $item->getNotificationTimestamp();
719 }
720 }
721 }
722
726 private function getWatcherInfo() {
727 if ( count( $this->everything ) == 0 ) {
728 return;
729 }
730
731 $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
732 $unwatchedPageThreshold =
733 $this->getConfig()->get( MainConfigNames::UnwatchedPageThreshold );
734 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
735 return;
736 }
737
738 $this->showZeroWatchers = $canUnwatchedpages;
739
740 $countOptions = [];
741 if ( !$canUnwatchedpages ) {
742 $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
743 }
744
745 $this->watchers = $this->watchedItemStore->countWatchersMultiple(
746 $this->everything,
747 $countOptions
748 );
749 }
750
757 private function getVisitingWatcherInfo() {
758 $config = $this->getConfig();
759 $db = $this->getDB();
760
761 $canUnwatchedpages = $this->getAuthority()->isAllowed( 'unwatchedpages' );
762 $unwatchedPageThreshold = $config->get( MainConfigNames::UnwatchedPageThreshold );
763 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
764 return;
765 }
766
767 $this->showZeroWatchers = $canUnwatchedpages;
768
769 $titlesWithThresholds = [];
770 if ( $this->titles ) {
771 $lb = $this->linkBatchFactory->newLinkBatch( $this->titles );
772
773 // Fetch last edit timestamps for pages
774 $this->resetQueryParams();
775 $this->addTables( [ 'page', 'revision' ] );
776 $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
777 $this->addWhere( [
778 'page_latest = rev_id',
779 $lb->constructSet( 'page', $db ),
780 ] );
781 $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
782 $timestampRes = $this->select( __METHOD__ );
783
784 $age = $config->get( MainConfigNames::WatchersMaxAge );
785 $timestamps = [];
786 foreach ( $timestampRes as $row ) {
787 $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
788 $timestamps[$row->page_namespace][$row->page_title] = (int)$revTimestamp - $age;
789 }
790 $titlesWithThresholds = array_map(
791 static function ( LinkTarget $target ) use ( $timestamps ) {
792 return [
793 $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
794 ];
795 },
796 $this->titles
797 );
798 }
799
800 if ( $this->missing ) {
801 $titlesWithThresholds = array_merge(
802 $titlesWithThresholds,
803 array_map(
804 static function ( LinkTarget $target ) {
805 return [ $target, null ];
806 },
807 $this->missing
808 )
809 );
810 }
811 $this->visitingwatchers = $this->watchedItemStore->countVisitingWatchersMultiple(
812 $titlesWithThresholds,
813 !$canUnwatchedpages ? $unwatchedPageThreshold : null
814 );
815 }
816
817 public function getCacheMode( $params ) {
818 // Other props depend on something about the current user
819 $publicProps = [
820 'protection',
821 'talkid',
822 'subjectid',
823 'associatedpage',
824 'url',
825 'preload',
826 'displaytitle',
827 'varianttitles',
828 ];
829 if ( array_diff( (array)$params['prop'], $publicProps ) ) {
830 return 'private';
831 }
832
833 // testactions also depends on the current user
834 if ( $params['testactions'] ) {
835 return 'private';
836 }
837
838 return 'public';
839 }
840
841 public function getAllowedParams() {
842 return [
843 'prop' => [
844 ParamValidator::PARAM_ISMULTI => true,
845 ParamValidator::PARAM_TYPE => [
846 'protection',
847 'talkid',
848 'watched', # private
849 'watchers', # private
850 'visitingwatchers', # private
851 'notificationtimestamp', # private
852 'subjectid',
853 'associatedpage',
854 'url',
855 'readable', # private
856 'preload',
857 'displaytitle',
858 'varianttitles',
859 'linkclasses', # private: stub length (and possibly hook colors)
860 // If you add more properties here, please consider whether they
861 // need to be added to getCacheMode()
862 ],
864 EnumDef::PARAM_DEPRECATED_VALUES => [
865 'readable' => true, // Since 1.32
866 ],
867 ],
868 'linkcontext' => [
869 ParamValidator::PARAM_TYPE => 'title',
870 ParamValidator::PARAM_DEFAULT => $this->titleFactory->newMainPage()->getPrefixedText(),
871 TitleDef::PARAM_RETURN_OBJECT => true,
872 ],
873 'testactions' => [
874 ParamValidator::PARAM_TYPE => 'string',
875 ParamValidator::PARAM_ISMULTI => true,
876 ],
877 'testactionsdetail' => [
878 ParamValidator::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
879 ParamValidator::PARAM_DEFAULT => 'boolean',
881 ],
882 'continue' => [
883 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
884 ],
885 ];
886 }
887
888 protected function getExamplesMessages() {
889 $title = Title::newMainPage()->getPrefixedText();
890 $mp = rawurlencode( $title );
891
892 return [
893 "action=query&prop=info&titles={$mp}"
894 => 'apihelp-query+info-example-simple',
895 "action=query&prop=info&inprop=protection&titles={$mp}"
896 => 'apihelp-query+info-example-protection',
897 ];
898 }
899
900 public function getHelpUrls() {
901 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
902 }
903}
const PROTO_CANONICAL
Definition Defines.php:199
const NS_FILE
Definition Defines.php:70
const PROTO_CURRENT
Definition Defines.php:198
const NS_MAIN
Definition Defines.php:64
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
addBlockInfoToStatus(StatusValue $status, Authority $user=null)
Add block info to block messages in a Status.
Definition ApiBase.php:1276
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1643
getMain()
Get the main module.
Definition ApiBase.php:514
getErrorFormatter()
Definition ApiBase.php:640
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition ApiBase.php:196
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:227
getResult()
Get the result object.
Definition ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:765
const LIMIT_SML1
Slow query, standard limit.
Definition ApiBase.php:225
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:163
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:711
This is a base class for all Query modules.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
resetQueryParams()
Blank the internal arrays with query parameters.
addFields( $value)
Add a set of fields to select to the internal array.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
getDB()
Get the Query database connection (read-only)
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
getPageSet()
Get the PageSet object to work on.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
A query module to show basic page information.
getExamplesMessages()
Returns usage examples for this module.
__construct(ApiQuery $queryModule, $moduleName, Language $contentLanguage, LinkBatchFactory $linkBatchFactory, NamespaceInfo $namespaceInfo, TitleFactory $titleFactory, TitleFormatter $titleFormatter, WatchedItemStore $watchedItemStore, LanguageConverterFactory $languageConverterFactory, RestrictionStore $restrictionStore, LinksMigration $linksMigration)
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
requestExtraData( $pageSet)
getHelpUrls()
Return links to more detailed help pages about the module.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getCacheMode( $params)
Get the cache mode for the data generated by this module.
This is the main query class.
Definition ApiQuery.php:41
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Base class for language-specific code.
Definition Language.php:53
An interface for creating language converters.
getLanguageConverter( $language=null)
Provide a LanguageConverter for given language.
Service for compat reading of links tables.
A class containing constants representing the names of configuration variables.
Type definition for page titles.
Definition TitleDef.php:22
A StatusValue for permission errors.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:49
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.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
A title formatter service for MediaWiki.