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