MediaWiki REL1_34
ApiQueryInfo.php
Go to the documentation of this file.
1<?php
24
31
32 private $fld_protection = false, $fld_talkid = false,
33 $fld_subjectid = false, $fld_url = false,
34 $fld_readable = false, $fld_watched = false,
38
39 private $params;
40
42 private $titles;
44 private $missing;
46 private $everything;
47
50
53 private $showZeroWatchers = false;
54
56
58
59 public function __construct( ApiQuery $query, $moduleName ) {
60 parent::__construct( $query, $moduleName, 'in' );
61 }
62
67 public function requestExtraData( $pageSet ) {
68 $pageSet->requestField( 'page_restrictions' );
69 // If the pageset is resolving redirects we won't get page_is_redirect.
70 // But we can't know for sure until the pageset is executed (revids may
71 // turn it off), so request it unconditionally.
72 $pageSet->requestField( 'page_is_redirect' );
73 $pageSet->requestField( 'page_is_new' );
74 $config = $this->getConfig();
75 $pageSet->requestField( 'page_touched' );
76 $pageSet->requestField( 'page_latest' );
77 $pageSet->requestField( 'page_len' );
78 if ( $config->get( 'ContentHandlerUseDB' ) ) {
79 $pageSet->requestField( 'page_content_model' );
80 }
81 if ( $config->get( 'PageLanguageUseDB' ) ) {
82 $pageSet->requestField( 'page_lang' );
83 }
84 }
85
93 protected function getTokenFunctions() {
94 // Don't call the hooks twice
95 if ( isset( $this->tokenFunctions ) ) {
97 }
98
99 // If we're in a mode that breaks the same-origin policy, no tokens can
100 // be obtained
101 if ( $this->lacksSameOriginSecurity() ) {
102 return [];
103 }
104
105 $this->tokenFunctions = [
106 'edit' => [ self::class, 'getEditToken' ],
107 'delete' => [ self::class, 'getDeleteToken' ],
108 'protect' => [ self::class, 'getProtectToken' ],
109 'move' => [ self::class, 'getMoveToken' ],
110 'block' => [ self::class, 'getBlockToken' ],
111 'unblock' => [ self::class, 'getUnblockToken' ],
112 'email' => [ self::class, 'getEmailToken' ],
113 'import' => [ self::class, 'getImportToken' ],
114 'watch' => [ self::class, 'getWatchToken' ],
115 ];
116 Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
117
119 }
120
122 protected static $cachedTokens = [];
123
127 public static function resetTokenCache() {
128 self::$cachedTokens = [];
129 }
130
134 public static function getEditToken( $pageid, $title ) {
135 // We could check for $title->userCan('edit') here,
136 // but that's too expensive for this purpose
137 // and would break caching
138 global $wgUser;
139 if ( !MediaWikiServices::getInstance()->getPermissionManager()
140 ->userHasRight( $wgUser, 'edit' ) ) {
141 return false;
142 }
143
144 // The token is always the same, let's exploit that
145 if ( !isset( self::$cachedTokens['edit'] ) ) {
146 self::$cachedTokens['edit'] = $wgUser->getEditToken();
147 }
148
149 return self::$cachedTokens['edit'];
150 }
151
155 public static function getDeleteToken( $pageid, $title ) {
156 global $wgUser;
157 if ( !MediaWikiServices::getInstance()->getPermissionManager()
158 ->userHasRight( $wgUser, 'delete' ) ) {
159 return false;
160 }
161
162 // The token is always the same, let's exploit that
163 if ( !isset( self::$cachedTokens['delete'] ) ) {
164 self::$cachedTokens['delete'] = $wgUser->getEditToken();
165 }
166
167 return self::$cachedTokens['delete'];
168 }
169
173 public static function getProtectToken( $pageid, $title ) {
174 global $wgUser;
175 if ( !MediaWikiServices::getInstance()->getPermissionManager()
176 ->userHasRight( $wgUser, 'protect' ) ) {
177 return false;
178 }
179
180 // The token is always the same, let's exploit that
181 if ( !isset( self::$cachedTokens['protect'] ) ) {
182 self::$cachedTokens['protect'] = $wgUser->getEditToken();
183 }
184
185 return self::$cachedTokens['protect'];
186 }
187
191 public static function getMoveToken( $pageid, $title ) {
192 global $wgUser;
193 if ( !MediaWikiServices::getInstance()->getPermissionManager()
194 ->userHasRight( $wgUser, 'move' ) ) {
195 return false;
196 }
197
198 // The token is always the same, let's exploit that
199 if ( !isset( self::$cachedTokens['move'] ) ) {
200 self::$cachedTokens['move'] = $wgUser->getEditToken();
201 }
202
203 return self::$cachedTokens['move'];
204 }
205
209 public static function getBlockToken( $pageid, $title ) {
210 global $wgUser;
211 if ( !MediaWikiServices::getInstance()->getPermissionManager()
212 ->userHasRight( $wgUser, 'block' ) ) {
213 return false;
214 }
215
216 // The token is always the same, let's exploit that
217 if ( !isset( self::$cachedTokens['block'] ) ) {
218 self::$cachedTokens['block'] = $wgUser->getEditToken();
219 }
220
221 return self::$cachedTokens['block'];
222 }
223
227 public static function getUnblockToken( $pageid, $title ) {
228 // Currently, this is exactly the same as the block token
229 return self::getBlockToken( $pageid, $title );
230 }
231
235 public static function getEmailToken( $pageid, $title ) {
236 global $wgUser;
237 if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailuser() ) {
238 return false;
239 }
240
241 // The token is always the same, let's exploit that
242 if ( !isset( self::$cachedTokens['email'] ) ) {
243 self::$cachedTokens['email'] = $wgUser->getEditToken();
244 }
245
246 return self::$cachedTokens['email'];
247 }
248
252 public static function getImportToken( $pageid, $title ) {
253 global $wgUser;
254 if ( !MediaWikiServices::getInstance()
256 ->userHasAnyRight( $wgUser, 'import', 'importupload' ) ) {
257 return false;
258 }
259
260 // The token is always the same, let's exploit that
261 if ( !isset( self::$cachedTokens['import'] ) ) {
262 self::$cachedTokens['import'] = $wgUser->getEditToken();
263 }
264
265 return self::$cachedTokens['import'];
266 }
267
271 public static function getWatchToken( $pageid, $title ) {
272 global $wgUser;
273 if ( !$wgUser->isLoggedIn() ) {
274 return false;
275 }
276
277 // The token is always the same, let's exploit that
278 if ( !isset( self::$cachedTokens['watch'] ) ) {
279 self::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
280 }
281
282 return self::$cachedTokens['watch'];
283 }
284
288 public static function getOptionsToken( $pageid, $title ) {
289 global $wgUser;
290 if ( !$wgUser->isLoggedIn() ) {
291 return false;
292 }
293
294 // The token is always the same, let's exploit that
295 if ( !isset( self::$cachedTokens['options'] ) ) {
296 self::$cachedTokens['options'] = $wgUser->getEditToken();
297 }
298
299 return self::$cachedTokens['options'];
300 }
301
302 public function execute() {
303 $this->params = $this->extractRequestParams();
304 if ( !is_null( $this->params['prop'] ) ) {
305 $prop = array_flip( $this->params['prop'] );
306 $this->fld_protection = isset( $prop['protection'] );
307 $this->fld_watched = isset( $prop['watched'] );
308 $this->fld_watchers = isset( $prop['watchers'] );
309 $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
310 $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
311 $this->fld_talkid = isset( $prop['talkid'] );
312 $this->fld_subjectid = isset( $prop['subjectid'] );
313 $this->fld_url = isset( $prop['url'] );
314 $this->fld_readable = isset( $prop['readable'] );
315 $this->fld_preload = isset( $prop['preload'] );
316 $this->fld_displaytitle = isset( $prop['displaytitle'] );
317 $this->fld_varianttitles = isset( $prop['varianttitles'] );
318 }
319
320 $pageSet = $this->getPageSet();
321 $this->titles = $pageSet->getGoodTitles();
322 $this->missing = $pageSet->getMissingTitles();
323 $this->everything = $this->titles + $this->missing;
324 $result = $this->getResult();
325
326 uasort( $this->everything, [ Title::class, 'compare' ] );
327 if ( !is_null( $this->params['continue'] ) ) {
328 // Throw away any titles we're gonna skip so they don't
329 // clutter queries
330 $cont = explode( '|', $this->params['continue'] );
331 $this->dieContinueUsageIf( count( $cont ) != 2 );
332 $conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
333 foreach ( $this->everything as $pageid => $title ) {
334 if ( Title::compare( $title, $conttitle ) >= 0 ) {
335 break;
336 }
337 unset( $this->titles[$pageid] );
338 unset( $this->missing[$pageid] );
339 unset( $this->everything[$pageid] );
340 }
341 }
342
343 $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
344 // when resolving redirects, no page will have this field
345 $this->pageIsRedir = !$pageSet->isResolvingRedirects()
346 ? $pageSet->getCustomField( 'page_is_redirect' )
347 : [];
348 $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
349
350 $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
351 $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
352 $this->pageLength = $pageSet->getCustomField( 'page_len' );
353
354 // Get protection info if requested
355 if ( $this->fld_protection ) {
356 $this->getProtectionInfo();
357 }
358
359 if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
360 $this->getWatchedInfo();
361 }
362
363 if ( $this->fld_watchers ) {
364 $this->getWatcherInfo();
365 }
366
367 if ( $this->fld_visitingwatchers ) {
368 $this->getVisitingWatcherInfo();
369 }
370
371 // Run the talkid/subjectid query if requested
372 if ( $this->fld_talkid || $this->fld_subjectid ) {
373 $this->getTSIDs();
374 }
375
376 if ( $this->fld_displaytitle ) {
377 $this->getDisplayTitle();
378 }
379
380 if ( $this->fld_varianttitles ) {
381 $this->getVariantTitles();
382 }
383
385 foreach ( $this->everything as $pageid => $title ) {
386 $pageInfo = $this->extractPageInfo( $pageid, $title );
387 $fit = $pageInfo !== null && $result->addValue( [
388 'query',
389 'pages'
390 ], $pageid, $pageInfo );
391 if ( !$fit ) {
392 $this->setContinueEnumParameter( 'continue',
393 $title->getNamespace() . '|' .
394 $title->getText() );
395 break;
396 }
397 }
398 }
399
406 private function extractPageInfo( $pageid, $title ) {
407 $pageInfo = [];
408 // $title->exists() needs pageid, which is not set for all title objects
409 $titleExists = $pageid > 0;
410 $ns = $title->getNamespace();
411 $dbkey = $title->getDBkey();
412
413 $pageInfo['contentmodel'] = $title->getContentModel();
414
415 $pageLanguage = $title->getPageLanguage();
416 $pageInfo['pagelanguage'] = $pageLanguage->getCode();
417 $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
418 $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
419
420 $user = $this->getUser();
421
422 if ( $titleExists ) {
423 $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
424 $pageInfo['lastrevid'] = (int)$this->pageLatest[$pageid];
425 $pageInfo['length'] = (int)$this->pageLength[$pageid];
426
427 if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
428 $pageInfo['redirect'] = true;
429 }
430 if ( $this->pageIsNew[$pageid] ) {
431 $pageInfo['new'] = true;
432 }
433 }
434
435 if ( !is_null( $this->params['token'] ) ) {
437 $pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
438 foreach ( $this->params['token'] as $t ) {
439 $val = call_user_func( $tokenFunctions[$t], $pageid, $title );
440 if ( $val === false ) {
441 $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
442 } else {
443 $pageInfo[$t . 'token'] = $val;
444 }
445 }
446 }
447
448 if ( $this->fld_protection ) {
449 $pageInfo['protection'] = [];
450 if ( isset( $this->protections[$ns][$dbkey] ) ) {
451 $pageInfo['protection'] =
452 $this->protections[$ns][$dbkey];
453 }
454 ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
455
456 $pageInfo['restrictiontypes'] = [];
457 if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
458 $pageInfo['restrictiontypes'] =
459 $this->restrictionTypes[$ns][$dbkey];
460 }
461 ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
462 }
463
464 if ( $this->fld_watched && $this->watched !== null ) {
465 $pageInfo['watched'] = $this->watched[$ns][$dbkey];
466 }
467
468 if ( $this->fld_watchers ) {
469 if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
470 $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
471 } elseif ( $this->showZeroWatchers ) {
472 $pageInfo['watchers'] = 0;
473 }
474 }
475
476 if ( $this->fld_visitingwatchers ) {
477 if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
478 $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
479 } elseif ( $this->showZeroWatchers ) {
480 $pageInfo['visitingwatchers'] = 0;
481 }
482 }
483
484 if ( $this->fld_notificationtimestamp ) {
485 $pageInfo['notificationtimestamp'] = '';
486 if ( $this->notificationtimestamps[$ns][$dbkey] ) {
487 $pageInfo['notificationtimestamp'] =
488 wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
489 }
490 }
491
492 if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
493 $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
494 }
495
496 if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
497 $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
498 }
499
500 if ( $this->fld_url ) {
501 $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
502 $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
503 $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
504 }
505 if ( $this->fld_readable ) {
506 $pageInfo['readable'] = $this->getPermissionManager()->userCan(
507 'read', $user, $title
508 );
509 }
510
511 if ( $this->fld_preload ) {
512 if ( $titleExists ) {
513 $pageInfo['preload'] = '';
514 } else {
515 $text = null;
516 Hooks::run( 'EditFormPreloadText', [ &$text, &$title ] );
517
518 $pageInfo['preload'] = $text;
519 }
520 }
521
522 if ( $this->fld_displaytitle ) {
523 if ( isset( $this->displaytitles[$pageid] ) ) {
524 $pageInfo['displaytitle'] = $this->displaytitles[$pageid];
525 } else {
526 $pageInfo['displaytitle'] = $title->getPrefixedText();
527 }
528 }
529
530 if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
531 $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
532 }
533
534 if ( $this->params['testactions'] ) {
535 $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
536 if ( $this->countTestedActions >= $limit ) {
537 return null; // force a continuation
538 }
539
540 $detailLevel = $this->params['testactionsdetail'];
541 $rigor = $detailLevel === 'quick' ? 'quick' : 'secure';
542 $errorFormatter = $this->getErrorFormatter();
543 if ( $errorFormatter->getFormat() === 'bc' ) {
544 // Eew, no. Use a more modern format here.
545 $errorFormatter = $errorFormatter->newWithFormat( 'plaintext' );
546 }
547
548 $user = $this->getUser();
549 $pageInfo['actions'] = [];
550 foreach ( $this->params['testactions'] as $action ) {
551 $this->countTestedActions++;
552
553 if ( $detailLevel === 'boolean' ) {
554 $pageInfo['actions'][$action] = $this->getPermissionManager()->userCan(
555 $action, $user, $title
556 );
557 } else {
558 $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $this->errorArrayToStatus(
559 $this->getPermissionManager()->getPermissionErrors(
560 $action, $user, $title, $rigor
561 ),
562 $user
563 ) );
564 }
565 }
566 }
567
568 return $pageInfo;
569 }
570
574 private function getProtectionInfo() {
575 $this->protections = [];
576 $db = $this->getDB();
577
578 // Get normal protections for existing titles
579 if ( count( $this->titles ) ) {
580 $this->resetQueryParams();
581 $this->addTables( 'page_restrictions' );
582 $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
583 'pr_expiry', 'pr_cascade' ] );
584 $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
585
586 $res = $this->select( __METHOD__ );
587 foreach ( $res as $row ) {
589 $title = $this->titles[$row->pr_page];
590 $a = [
591 'type' => $row->pr_type,
592 'level' => $row->pr_level,
593 'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
594 ];
595 if ( $row->pr_cascade ) {
596 $a['cascade'] = true;
597 }
598 $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
599 }
600 // Also check old restrictions
601 foreach ( $this->titles as $pageId => $title ) {
602 if ( $this->pageRestrictions[$pageId] ) {
603 $namespace = $title->getNamespace();
604 $dbKey = $title->getDBkey();
605 $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
606 foreach ( $restrictions as $restrict ) {
607 $temp = explode( '=', trim( $restrict ) );
608 if ( count( $temp ) == 1 ) {
609 // old old format should be treated as edit/move restriction
610 $restriction = trim( $temp[0] );
611
612 if ( $restriction == '' ) {
613 continue;
614 }
615 $this->protections[$namespace][$dbKey][] = [
616 'type' => 'edit',
617 'level' => $restriction,
618 'expiry' => 'infinity',
619 ];
620 $this->protections[$namespace][$dbKey][] = [
621 'type' => 'move',
622 'level' => $restriction,
623 'expiry' => 'infinity',
624 ];
625 } else {
626 $restriction = trim( $temp[1] );
627 if ( $restriction == '' ) {
628 continue;
629 }
630 $this->protections[$namespace][$dbKey][] = [
631 'type' => $temp[0],
632 'level' => $restriction,
633 'expiry' => 'infinity',
634 ];
635 }
636 }
637 }
638 }
639 }
640
641 // Get protections for missing titles
642 if ( count( $this->missing ) ) {
643 $this->resetQueryParams();
644 $lb = new LinkBatch( $this->missing );
645 $this->addTables( 'protected_titles' );
646 $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
647 $this->addWhere( $lb->constructSet( 'pt', $db ) );
648 $res = $this->select( __METHOD__ );
649 foreach ( $res as $row ) {
650 $this->protections[$row->pt_namespace][$row->pt_title][] = [
651 'type' => 'create',
652 'level' => $row->pt_create_perm,
653 'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
654 ];
655 }
656 }
657
658 // Separate good and missing titles into files and other pages
659 // and populate $this->restrictionTypes
660 $images = $others = [];
661 foreach ( $this->everything as $title ) {
662 if ( $title->getNamespace() == NS_FILE ) {
663 $images[] = $title->getDBkey();
664 } else {
665 $others[] = $title;
666 }
667 // Applicable protection types
668 $this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
669 array_values( $title->getRestrictionTypes() );
670 }
671
672 if ( count( $others ) ) {
673 // Non-images: check templatelinks
674 $lb = new LinkBatch( $others );
675 $this->resetQueryParams();
676 $this->addTables( [ 'page_restrictions', 'page', 'templatelinks' ] );
677 $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
678 'page_title', 'page_namespace',
679 'tl_title', 'tl_namespace' ] );
680 $this->addWhere( $lb->constructSet( 'tl', $db ) );
681 $this->addWhere( 'pr_page = page_id' );
682 $this->addWhere( 'pr_page = tl_from' );
683 $this->addWhereFld( 'pr_cascade', 1 );
684
685 $res = $this->select( __METHOD__ );
686 foreach ( $res as $row ) {
687 $source = Title::makeTitle( $row->page_namespace, $row->page_title );
688 $this->protections[$row->tl_namespace][$row->tl_title][] = [
689 'type' => $row->pr_type,
690 'level' => $row->pr_level,
691 'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
692 'source' => $source->getPrefixedText()
693 ];
694 }
695 }
696
697 if ( count( $images ) ) {
698 // Images: check imagelinks
699 $this->resetQueryParams();
700 $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
701 $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
702 'page_title', 'page_namespace', 'il_to' ] );
703 $this->addWhere( 'pr_page = page_id' );
704 $this->addWhere( 'pr_page = il_from' );
705 $this->addWhereFld( 'pr_cascade', 1 );
706 $this->addWhereFld( 'il_to', $images );
707
708 $res = $this->select( __METHOD__ );
709 foreach ( $res as $row ) {
710 $source = Title::makeTitle( $row->page_namespace, $row->page_title );
711 $this->protections[NS_FILE][$row->il_to][] = [
712 'type' => $row->pr_type,
713 'level' => $row->pr_level,
714 'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
715 'source' => $source->getPrefixedText()
716 ];
717 }
718 }
719 }
720
725 private function getTSIDs() {
726 $getTitles = $this->talkids = $this->subjectids = [];
727 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
728
730 foreach ( $this->everything as $t ) {
731 if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
732 if ( $this->fld_subjectid ) {
733 $getTitles[] = $t->getSubjectPage();
734 }
735 } elseif ( $this->fld_talkid ) {
736 $getTitles[] = $t->getTalkPage();
737 }
738 }
739 if ( $getTitles === [] ) {
740 return;
741 }
742
743 $db = $this->getDB();
744
745 // Construct a custom WHERE clause that matches
746 // all titles in $getTitles
747 $lb = new LinkBatch( $getTitles );
748 $this->resetQueryParams();
749 $this->addTables( 'page' );
750 $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
751 $this->addWhere( $lb->constructSet( 'page', $db ) );
752 $res = $this->select( __METHOD__ );
753 foreach ( $res as $row ) {
754 if ( $nsInfo->isTalk( $row->page_namespace ) ) {
755 $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
756 (int)( $row->page_id );
757 } else {
758 $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
759 (int)( $row->page_id );
760 }
761 }
762 }
763
764 private function getDisplayTitle() {
765 $this->displaytitles = [];
766
767 $pageIds = array_keys( $this->titles );
768
769 if ( $pageIds === [] ) {
770 return;
771 }
772
773 $this->resetQueryParams();
774 $this->addTables( 'page_props' );
775 $this->addFields( [ 'pp_page', 'pp_value' ] );
776 $this->addWhereFld( 'pp_page', $pageIds );
777 $this->addWhereFld( 'pp_propname', 'displaytitle' );
778 $res = $this->select( __METHOD__ );
779
780 foreach ( $res as $row ) {
781 $this->displaytitles[$row->pp_page] = $row->pp_value;
782 }
783 }
784
785 private function getVariantTitles() {
786 if ( $this->titles === [] ) {
787 return;
788 }
789 $this->variantTitles = [];
790 foreach ( $this->titles as $pageId => $t ) {
791 $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
792 ? $this->getAllVariants( $this->displaytitles[$pageId] )
793 : $this->getAllVariants( $t->getText(), $t->getNamespace() );
794 }
795 }
796
797 private function getAllVariants( $text, $ns = NS_MAIN ) {
798 $result = [];
799 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
800 foreach ( $contLang->getVariants() as $variant ) {
801 $convertTitle = $contLang->autoConvert( $text, $variant );
802 if ( $ns !== NS_MAIN ) {
803 $convertNs = $contLang->convertNamespace( $ns, $variant );
804 $convertTitle = $convertNs . ':' . $convertTitle;
805 }
806 $result[$variant] = $convertTitle;
807 }
808 return $result;
809 }
810
815 private function getWatchedInfo() {
816 $user = $this->getUser();
817
818 if ( $user->isAnon() || count( $this->everything ) == 0
819 || !$this->getPermissionManager()->userHasRight( $user, 'viewmywatchlist' )
820 ) {
821 return;
822 }
823
824 $this->watched = [];
825 $this->notificationtimestamps = [];
826
827 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
828 $timestamps = $store->getNotificationTimestampsBatch( $user, $this->everything );
829
830 if ( $this->fld_watched ) {
831 foreach ( $timestamps as $namespaceId => $dbKeys ) {
832 $this->watched[$namespaceId] = array_map(
833 function ( $x ) {
834 return $x !== false;
835 },
836 $dbKeys
837 );
838 }
839 }
840 if ( $this->fld_notificationtimestamp ) {
841 $this->notificationtimestamps = $timestamps;
842 }
843 }
844
848 private function getWatcherInfo() {
849 if ( count( $this->everything ) == 0 ) {
850 return;
851 }
852
853 $user = $this->getUser();
854 $canUnwatchedpages = $this->getPermissionManager()->userHasRight( $user, 'unwatchedpages' );
855 $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
856 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
857 return;
858 }
859
860 $this->showZeroWatchers = $canUnwatchedpages;
861
862 $countOptions = [];
863 if ( !$canUnwatchedpages ) {
864 $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
865 }
866
867 $this->watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchersMultiple(
868 $this->everything,
869 $countOptions
870 );
871 }
872
879 private function getVisitingWatcherInfo() {
880 $config = $this->getConfig();
881 $user = $this->getUser();
882 $db = $this->getDB();
883
884 $canUnwatchedpages = $this->getPermissionManager()->userHasRight( $user, 'unwatchedpages' );
885 $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
886 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
887 return;
888 }
889
890 $this->showZeroWatchers = $canUnwatchedpages;
891
892 $titlesWithThresholds = [];
893 if ( $this->titles ) {
894 $lb = new LinkBatch( $this->titles );
895
896 // Fetch last edit timestamps for pages
897 $this->resetQueryParams();
898 $this->addTables( [ 'page', 'revision' ] );
899 $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
900 $this->addWhere( [
901 'page_latest = rev_id',
902 $lb->constructSet( 'page', $db ),
903 ] );
904 $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
905 $timestampRes = $this->select( __METHOD__ );
906
907 $age = $config->get( 'WatchersMaxAge' );
908 $timestamps = [];
909 foreach ( $timestampRes as $row ) {
910 $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
911 $timestamps[$row->page_namespace][$row->page_title] = $revTimestamp - $age;
912 }
913 $titlesWithThresholds = array_map(
914 function ( LinkTarget $target ) use ( $timestamps ) {
915 return [
916 $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
917 ];
918 },
920 );
921 }
922
923 if ( $this->missing ) {
924 $titlesWithThresholds = array_merge(
925 $titlesWithThresholds,
926 array_map(
927 function ( LinkTarget $target ) {
928 return [ $target, null ];
929 },
931 )
932 );
933 }
934 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
935 $this->visitingwatchers = $store->countVisitingWatchersMultiple(
936 $titlesWithThresholds,
937 !$canUnwatchedpages ? $unwatchedPageThreshold : null
938 );
939 }
940
941 public function getCacheMode( $params ) {
942 // Other props depend on something about the current user
943 $publicProps = [
944 'protection',
945 'talkid',
946 'subjectid',
947 'url',
948 'preload',
949 'displaytitle',
950 'varianttitles',
951 ];
952 if ( array_diff( (array)$params['prop'], $publicProps ) ) {
953 return 'private';
954 }
955
956 // testactions also depends on the current user
957 if ( $params['testactions'] ) {
958 return 'private';
959 }
960
961 if ( !is_null( $params['token'] ) ) {
962 return 'private';
963 }
964
965 return 'public';
966 }
967
968 public function getAllowedParams() {
969 return [
970 'prop' => [
973 'protection',
974 'talkid',
975 'watched', # private
976 'watchers', # private
977 'visitingwatchers', # private
978 'notificationtimestamp', # private
979 'subjectid',
980 'url',
981 'readable', # private
982 'preload',
983 'displaytitle',
984 'varianttitles',
985 // If you add more properties here, please consider whether they
986 // need to be added to getCacheMode()
987 ],
990 'readable' => true, // Since 1.32
991 ],
992 ],
993 'testactions' => [
994 ApiBase::PARAM_TYPE => 'string',
996 ],
997 'testactionsdetail' => [
998 ApiBase::PARAM_TYPE => [ 'boolean', 'full', 'quick' ],
999 ApiBase::PARAM_DFLT => 'boolean',
1001 ],
1002 'token' => [
1004 ApiBase::PARAM_ISMULTI => true,
1005 ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() )
1006 ],
1007 'continue' => [
1008 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
1009 ],
1010 ];
1011 }
1012
1013 protected function getExamplesMessages() {
1014 return [
1015 'action=query&prop=info&titles=Main%20Page'
1016 => 'apihelp-query+info-example-simple',
1017 'action=query&prop=info&inprop=protection&titles=Main%20Page'
1018 => 'apihelp-query+info-example-protection',
1019 ];
1020 }
1021
1022 public function getHelpUrls() {
1023 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
1024 }
1025}
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.
const PARAM_DEPRECATED
(boolean) Is the parameter deprecated (will show a warning)?
Definition ApiBase.php:112
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:2208
const PARAM_DEPRECATED_VALUES
(array) When PARAM_TYPE is an array, this indicates which of the values are deprecated.
Definition ApiBase.php:209
getMain()
Get the main module.
Definition ApiBase.php:536
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition ApiBase.php:94
getErrorFormatter()
Get the error formatter.
Definition ApiBase.php:654
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition ApiBase.php:55
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:710
errorArrayToStatus(array $errors, User $user=null)
Turn an array of message keys or key+param arrays into a Status.
Definition ApiBase.php:1825
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:164
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:265
getResult()
Get the result object.
Definition ApiBase.php:640
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:761
const LIMIT_SML1
Slow query, standard limit.
Definition ApiBase.php:263
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:131
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1933
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition ApiBase.php:58
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition ApiBase.php:568
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.
static resetTokenCache()
getTokenFunctions()
Get an array mapping token names to their handler functions.
getVisitingWatcherInfo()
Get the count of watchers who have visited recent edits and put it in $this->visitingwatchers.
getExamplesMessages()
Returns usage examples for this module.
static getUnblockToken( $pageid, $title)
static getWatchToken( $pageid, $title)
static getDeleteToken( $pageid, $title)
getAllVariants( $text, $ns=NS_MAIN)
Title[] $titles
Title[] $everything
static getEditToken( $pageid, $title)
static string[] $cachedTokens
Title[] $missing
static getMoveToken( $pageid, $title)
static getEmailToken( $pageid, $title)
static getProtectToken( $pageid, $title)
getWatchedInfo()
Get information about watched status and put it in $this->watched and $this->notificationtimestamps.
__construct(ApiQuery $query, $moduleName)
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
static getOptionsToken( $pageid, $title)
getProtectionInfo()
Get information about protections and put it in $protections.
static getBlockToken( $pageid, $title)
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.
static getImportToken( $pageid, $title)
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
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.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
MediaWikiServices is the service locator for the application scope of MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:42
const PROTO_CANONICAL
Definition Defines.php:212
const NS_FILE
Definition Defines.php:75
const PROTO_CURRENT
Definition Defines.php:211
const NS_MAIN
Definition Defines.php:69
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
$source