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