]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiQueryInfo.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / api / ApiQueryInfo.php
1 <?php
2 /**
3  *
4  *
5  * Created on Sep 25, 2006
6  *
7  * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with this program; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22  * http://www.gnu.org/copyleft/gpl.html
23  *
24  * @file
25  */
26 use MediaWiki\MediaWikiServices;
27 use MediaWiki\Linker\LinkTarget;
28
29 /**
30  * A query module to show basic page information.
31  *
32  * @ingroup API
33  */
34 class ApiQueryInfo extends ApiQueryBase {
35
36         private $fld_protection = false, $fld_talkid = false,
37                 $fld_subjectid = false, $fld_url = false,
38                 $fld_readable = false, $fld_watched = false,
39                 $fld_watchers = false, $fld_visitingwatchers = false,
40                 $fld_notificationtimestamp = false,
41                 $fld_preload = false, $fld_displaytitle = false;
42
43         private $params;
44
45         /** @var Title[] */
46         private $titles;
47         /** @var Title[] */
48         private $missing;
49         /** @var Title[] */
50         private $everything;
51
52         private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched,
53                 $pageLatest, $pageLength;
54
55         private $protections, $restrictionTypes, $watched, $watchers, $visitingwatchers,
56                 $notificationtimestamps, $talkids, $subjectids, $displaytitles;
57         private $showZeroWatchers = false;
58
59         private $tokenFunctions;
60
61         private $countTestedActions = 0;
62
63         public function __construct( ApiQuery $query, $moduleName ) {
64                 parent::__construct( $query, $moduleName, 'in' );
65         }
66
67         /**
68          * @param ApiPageSet $pageSet
69          * @return void
70          */
71         public function requestExtraData( $pageSet ) {
72                 $pageSet->requestField( 'page_restrictions' );
73                 // If the pageset is resolving redirects we won't get page_is_redirect.
74                 // But we can't know for sure until the pageset is executed (revids may
75                 // turn it off), so request it unconditionally.
76                 $pageSet->requestField( 'page_is_redirect' );
77                 $pageSet->requestField( 'page_is_new' );
78                 $config = $this->getConfig();
79                 $pageSet->requestField( 'page_touched' );
80                 $pageSet->requestField( 'page_latest' );
81                 $pageSet->requestField( 'page_len' );
82                 if ( $config->get( 'ContentHandlerUseDB' ) ) {
83                         $pageSet->requestField( 'page_content_model' );
84                 }
85                 if ( $config->get( 'PageLanguageUseDB' ) ) {
86                         $pageSet->requestField( 'page_lang' );
87                 }
88         }
89
90         /**
91          * Get an array mapping token names to their handler functions.
92          * The prototype for a token function is func($pageid, $title)
93          * it should return a token or false (permission denied)
94          * @deprecated since 1.24
95          * @return array [ tokenname => function ]
96          */
97         protected function getTokenFunctions() {
98                 // Don't call the hooks twice
99                 if ( isset( $this->tokenFunctions ) ) {
100                         return $this->tokenFunctions;
101                 }
102
103                 // If we're in a mode that breaks the same-origin policy, no tokens can
104                 // be obtained
105                 if ( $this->lacksSameOriginSecurity() ) {
106                         return [];
107                 }
108
109                 $this->tokenFunctions = [
110                         'edit' => [ 'ApiQueryInfo', 'getEditToken' ],
111                         'delete' => [ 'ApiQueryInfo', 'getDeleteToken' ],
112                         'protect' => [ 'ApiQueryInfo', 'getProtectToken' ],
113                         'move' => [ 'ApiQueryInfo', 'getMoveToken' ],
114                         'block' => [ 'ApiQueryInfo', 'getBlockToken' ],
115                         'unblock' => [ 'ApiQueryInfo', 'getUnblockToken' ],
116                         'email' => [ 'ApiQueryInfo', 'getEmailToken' ],
117                         'import' => [ 'ApiQueryInfo', 'getImportToken' ],
118                         'watch' => [ 'ApiQueryInfo', 'getWatchToken' ],
119                 ];
120                 Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
121
122                 return $this->tokenFunctions;
123         }
124
125         static protected $cachedTokens = [];
126
127         /**
128          * @deprecated since 1.24
129          */
130         public static function resetTokenCache() {
131                 self::$cachedTokens = [];
132         }
133
134         /**
135          * @deprecated since 1.24
136          */
137         public static function getEditToken( $pageid, $title ) {
138                 // We could check for $title->userCan('edit') here,
139                 // but that's too expensive for this purpose
140                 // and would break caching
141                 global $wgUser;
142                 if ( !$wgUser->isAllowed( 'edit' ) ) {
143                         return false;
144                 }
145
146                 // The token is always the same, let's exploit that
147                 if ( !isset( self::$cachedTokens['edit'] ) ) {
148                         self::$cachedTokens['edit'] = $wgUser->getEditToken();
149                 }
150
151                 return self::$cachedTokens['edit'];
152         }
153
154         /**
155          * @deprecated since 1.24
156          */
157         public static function getDeleteToken( $pageid, $title ) {
158                 global $wgUser;
159                 if ( !$wgUser->isAllowed( 'delete' ) ) {
160                         return false;
161                 }
162
163                 // The token is always the same, let's exploit that
164                 if ( !isset( self::$cachedTokens['delete'] ) ) {
165                         self::$cachedTokens['delete'] = $wgUser->getEditToken();
166                 }
167
168                 return self::$cachedTokens['delete'];
169         }
170
171         /**
172          * @deprecated since 1.24
173          */
174         public static function getProtectToken( $pageid, $title ) {
175                 global $wgUser;
176                 if ( !$wgUser->isAllowed( '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
188         /**
189          * @deprecated since 1.24
190          */
191         public static function getMoveToken( $pageid, $title ) {
192                 global $wgUser;
193                 if ( !$wgUser->isAllowed( 'move' ) ) {
194                         return false;
195                 }
196
197                 // The token is always the same, let's exploit that
198                 if ( !isset( self::$cachedTokens['move'] ) ) {
199                         self::$cachedTokens['move'] = $wgUser->getEditToken();
200                 }
201
202                 return self::$cachedTokens['move'];
203         }
204
205         /**
206          * @deprecated since 1.24
207          */
208         public static function getBlockToken( $pageid, $title ) {
209                 global $wgUser;
210                 if ( !$wgUser->isAllowed( 'block' ) ) {
211                         return false;
212                 }
213
214                 // The token is always the same, let's exploit that
215                 if ( !isset( self::$cachedTokens['block'] ) ) {
216                         self::$cachedTokens['block'] = $wgUser->getEditToken();
217                 }
218
219                 return self::$cachedTokens['block'];
220         }
221
222         /**
223          * @deprecated since 1.24
224          */
225         public static function getUnblockToken( $pageid, $title ) {
226                 // Currently, this is exactly the same as the block token
227                 return self::getBlockToken( $pageid, $title );
228         }
229
230         /**
231          * @deprecated since 1.24
232          */
233         public static function getEmailToken( $pageid, $title ) {
234                 global $wgUser;
235                 if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailuser() ) {
236                         return false;
237                 }
238
239                 // The token is always the same, let's exploit that
240                 if ( !isset( self::$cachedTokens['email'] ) ) {
241                         self::$cachedTokens['email'] = $wgUser->getEditToken();
242                 }
243
244                 return self::$cachedTokens['email'];
245         }
246
247         /**
248          * @deprecated since 1.24
249          */
250         public static function getImportToken( $pageid, $title ) {
251                 global $wgUser;
252                 if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) {
253                         return false;
254                 }
255
256                 // The token is always the same, let's exploit that
257                 if ( !isset( self::$cachedTokens['import'] ) ) {
258                         self::$cachedTokens['import'] = $wgUser->getEditToken();
259                 }
260
261                 return self::$cachedTokens['import'];
262         }
263
264         /**
265          * @deprecated since 1.24
266          */
267         public static function getWatchToken( $pageid, $title ) {
268                 global $wgUser;
269                 if ( !$wgUser->isLoggedIn() ) {
270                         return false;
271                 }
272
273                 // The token is always the same, let's exploit that
274                 if ( !isset( self::$cachedTokens['watch'] ) ) {
275                         self::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
276                 }
277
278                 return self::$cachedTokens['watch'];
279         }
280
281         /**
282          * @deprecated since 1.24
283          */
284         public static function getOptionsToken( $pageid, $title ) {
285                 global $wgUser;
286                 if ( !$wgUser->isLoggedIn() ) {
287                         return false;
288                 }
289
290                 // The token is always the same, let's exploit that
291                 if ( !isset( self::$cachedTokens['options'] ) ) {
292                         self::$cachedTokens['options'] = $wgUser->getEditToken();
293                 }
294
295                 return self::$cachedTokens['options'];
296         }
297
298         public function execute() {
299                 $this->params = $this->extractRequestParams();
300                 if ( !is_null( $this->params['prop'] ) ) {
301                         $prop = array_flip( $this->params['prop'] );
302                         $this->fld_protection = isset( $prop['protection'] );
303                         $this->fld_watched = isset( $prop['watched'] );
304                         $this->fld_watchers = isset( $prop['watchers'] );
305                         $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
306                         $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
307                         $this->fld_talkid = isset( $prop['talkid'] );
308                         $this->fld_subjectid = isset( $prop['subjectid'] );
309                         $this->fld_url = isset( $prop['url'] );
310                         $this->fld_readable = isset( $prop['readable'] );
311                         $this->fld_preload = isset( $prop['preload'] );
312                         $this->fld_displaytitle = isset( $prop['displaytitle'] );
313                 }
314
315                 $pageSet = $this->getPageSet();
316                 $this->titles = $pageSet->getGoodTitles();
317                 $this->missing = $pageSet->getMissingTitles();
318                 $this->everything = $this->titles + $this->missing;
319                 $result = $this->getResult();
320
321                 uasort( $this->everything, [ 'Title', 'compare' ] );
322                 if ( !is_null( $this->params['continue'] ) ) {
323                         // Throw away any titles we're gonna skip so they don't
324                         // clutter queries
325                         $cont = explode( '|', $this->params['continue'] );
326                         $this->dieContinueUsageIf( count( $cont ) != 2 );
327                         $conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
328                         foreach ( $this->everything as $pageid => $title ) {
329                                 if ( Title::compare( $title, $conttitle ) >= 0 ) {
330                                         break;
331                                 }
332                                 unset( $this->titles[$pageid] );
333                                 unset( $this->missing[$pageid] );
334                                 unset( $this->everything[$pageid] );
335                         }
336                 }
337
338                 $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
339                 // when resolving redirects, no page will have this field
340                 $this->pageIsRedir = !$pageSet->isResolvingRedirects()
341                         ? $pageSet->getCustomField( 'page_is_redirect' )
342                         : [];
343                 $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
344
345                 $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
346                 $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
347                 $this->pageLength = $pageSet->getCustomField( 'page_len' );
348
349                 // Get protection info if requested
350                 if ( $this->fld_protection ) {
351                         $this->getProtectionInfo();
352                 }
353
354                 if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
355                         $this->getWatchedInfo();
356                 }
357
358                 if ( $this->fld_watchers ) {
359                         $this->getWatcherInfo();
360                 }
361
362                 if ( $this->fld_visitingwatchers ) {
363                         $this->getVisitingWatcherInfo();
364                 }
365
366                 // Run the talkid/subjectid query if requested
367                 if ( $this->fld_talkid || $this->fld_subjectid ) {
368                         $this->getTSIDs();
369                 }
370
371                 if ( $this->fld_displaytitle ) {
372                         $this->getDisplayTitle();
373                 }
374
375                 /** @var Title $title */
376                 foreach ( $this->everything as $pageid => $title ) {
377                         $pageInfo = $this->extractPageInfo( $pageid, $title );
378                         $fit = $pageInfo !== null && $result->addValue( [
379                                 'query',
380                                 'pages'
381                         ], $pageid, $pageInfo );
382                         if ( !$fit ) {
383                                 $this->setContinueEnumParameter( 'continue',
384                                         $title->getNamespace() . '|' .
385                                         $title->getText() );
386                                 break;
387                         }
388                 }
389         }
390
391         /**
392          * Get a result array with information about a title
393          * @param int $pageid Page ID (negative for missing titles)
394          * @param Title $title
395          * @return array|null
396          */
397         private function extractPageInfo( $pageid, $title ) {
398                 $pageInfo = [];
399                 // $title->exists() needs pageid, which is not set for all title objects
400                 $titleExists = $pageid > 0;
401                 $ns = $title->getNamespace();
402                 $dbkey = $title->getDBkey();
403
404                 $pageInfo['contentmodel'] = $title->getContentModel();
405
406                 $pageLanguage = $title->getPageLanguage();
407                 $pageInfo['pagelanguage'] = $pageLanguage->getCode();
408                 $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
409                 $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
410
411                 if ( $titleExists ) {
412                         $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
413                         $pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] );
414                         $pageInfo['length'] = intval( $this->pageLength[$pageid] );
415
416                         if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
417                                 $pageInfo['redirect'] = true;
418                         }
419                         if ( $this->pageIsNew[$pageid] ) {
420                                 $pageInfo['new'] = true;
421                         }
422                 }
423
424                 if ( !is_null( $this->params['token'] ) ) {
425                         $tokenFunctions = $this->getTokenFunctions();
426                         $pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
427                         foreach ( $this->params['token'] as $t ) {
428                                 $val = call_user_func( $tokenFunctions[$t], $pageid, $title );
429                                 if ( $val === false ) {
430                                         $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
431                                 } else {
432                                         $pageInfo[$t . 'token'] = $val;
433                                 }
434                         }
435                 }
436
437                 if ( $this->fld_protection ) {
438                         $pageInfo['protection'] = [];
439                         if ( isset( $this->protections[$ns][$dbkey] ) ) {
440                                 $pageInfo['protection'] =
441                                         $this->protections[$ns][$dbkey];
442                         }
443                         ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
444
445                         $pageInfo['restrictiontypes'] = [];
446                         if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
447                                 $pageInfo['restrictiontypes'] =
448                                         $this->restrictionTypes[$ns][$dbkey];
449                         }
450                         ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
451                 }
452
453                 if ( $this->fld_watched && $this->watched !== null ) {
454                         $pageInfo['watched'] = $this->watched[$ns][$dbkey];
455                 }
456
457                 if ( $this->fld_watchers ) {
458                         if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
459                                 $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
460                         } elseif ( $this->showZeroWatchers ) {
461                                 $pageInfo['watchers'] = 0;
462                         }
463                 }
464
465                 if ( $this->fld_visitingwatchers ) {
466                         if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
467                                 $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
468                         } elseif ( $this->showZeroWatchers ) {
469                                 $pageInfo['visitingwatchers'] = 0;
470                         }
471                 }
472
473                 if ( $this->fld_notificationtimestamp ) {
474                         $pageInfo['notificationtimestamp'] = '';
475                         if ( $this->notificationtimestamps[$ns][$dbkey] ) {
476                                 $pageInfo['notificationtimestamp'] =
477                                         wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
478                         }
479                 }
480
481                 if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
482                         $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
483                 }
484
485                 if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
486                         $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
487                 }
488
489                 if ( $this->fld_url ) {
490                         $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
491                         $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
492                         $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
493                 }
494                 if ( $this->fld_readable ) {
495                         $pageInfo['readable'] = $title->userCan( 'read', $this->getUser() );
496                 }
497
498                 if ( $this->fld_preload ) {
499                         if ( $titleExists ) {
500                                 $pageInfo['preload'] = '';
501                         } else {
502                                 $text = null;
503                                 Hooks::run( 'EditFormPreloadText', [ &$text, &$title ] );
504
505                                 $pageInfo['preload'] = $text;
506                         }
507                 }
508
509                 if ( $this->fld_displaytitle ) {
510                         if ( isset( $this->displaytitles[$pageid] ) ) {
511                                 $pageInfo['displaytitle'] = $this->displaytitles[$pageid];
512                         } else {
513                                 $pageInfo['displaytitle'] = $title->getPrefixedText();
514                         }
515                 }
516
517                 if ( $this->params['testactions'] ) {
518                         $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML1 : self::LIMIT_SML2;
519                         if ( $this->countTestedActions >= $limit ) {
520                                 return null; // force a continuation
521                         }
522
523                         $user = $this->getUser();
524                         $pageInfo['actions'] = [];
525                         foreach ( $this->params['testactions'] as $action ) {
526                                 $this->countTestedActions++;
527                                 $pageInfo['actions'][$action] = $title->userCan( $action, $user );
528                         }
529                 }
530
531                 return $pageInfo;
532         }
533
534         /**
535          * Get information about protections and put it in $protections
536          */
537         private function getProtectionInfo() {
538                 $this->protections = [];
539                 $db = $this->getDB();
540
541                 // Get normal protections for existing titles
542                 if ( count( $this->titles ) ) {
543                         $this->resetQueryParams();
544                         $this->addTables( 'page_restrictions' );
545                         $this->addFields( [ 'pr_page', 'pr_type', 'pr_level',
546                                 'pr_expiry', 'pr_cascade' ] );
547                         $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
548
549                         $res = $this->select( __METHOD__ );
550                         foreach ( $res as $row ) {
551                                 /** @var Title $title */
552                                 $title = $this->titles[$row->pr_page];
553                                 $a = [
554                                         'type' => $row->pr_type,
555                                         'level' => $row->pr_level,
556                                         'expiry' => ApiResult::formatExpiry( $row->pr_expiry )
557                                 ];
558                                 if ( $row->pr_cascade ) {
559                                         $a['cascade'] = true;
560                                 }
561                                 $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a;
562                         }
563                         // Also check old restrictions
564                         foreach ( $this->titles as $pageId => $title ) {
565                                 if ( $this->pageRestrictions[$pageId] ) {
566                                         $namespace = $title->getNamespace();
567                                         $dbKey = $title->getDBkey();
568                                         $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) );
569                                         foreach ( $restrictions as $restrict ) {
570                                                 $temp = explode( '=', trim( $restrict ) );
571                                                 if ( count( $temp ) == 1 ) {
572                                                         // old old format should be treated as edit/move restriction
573                                                         $restriction = trim( $temp[0] );
574
575                                                         if ( $restriction == '' ) {
576                                                                 continue;
577                                                         }
578                                                         $this->protections[$namespace][$dbKey][] = [
579                                                                 'type' => 'edit',
580                                                                 'level' => $restriction,
581                                                                 'expiry' => 'infinity',
582                                                         ];
583                                                         $this->protections[$namespace][$dbKey][] = [
584                                                                 'type' => 'move',
585                                                                 'level' => $restriction,
586                                                                 'expiry' => 'infinity',
587                                                         ];
588                                                 } else {
589                                                         $restriction = trim( $temp[1] );
590                                                         if ( $restriction == '' ) {
591                                                                 continue;
592                                                         }
593                                                         $this->protections[$namespace][$dbKey][] = [
594                                                                 'type' => $temp[0],
595                                                                 'level' => $restriction,
596                                                                 'expiry' => 'infinity',
597                                                         ];
598                                                 }
599                                         }
600                                 }
601                         }
602                 }
603
604                 // Get protections for missing titles
605                 if ( count( $this->missing ) ) {
606                         $this->resetQueryParams();
607                         $lb = new LinkBatch( $this->missing );
608                         $this->addTables( 'protected_titles' );
609                         $this->addFields( [ 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ] );
610                         $this->addWhere( $lb->constructSet( 'pt', $db ) );
611                         $res = $this->select( __METHOD__ );
612                         foreach ( $res as $row ) {
613                                 $this->protections[$row->pt_namespace][$row->pt_title][] = [
614                                         'type' => 'create',
615                                         'level' => $row->pt_create_perm,
616                                         'expiry' => ApiResult::formatExpiry( $row->pt_expiry )
617                                 ];
618                         }
619                 }
620
621                 // Separate good and missing titles into files and other pages
622                 // and populate $this->restrictionTypes
623                 $images = $others = [];
624                 foreach ( $this->everything as $title ) {
625                         if ( $title->getNamespace() == NS_FILE ) {
626                                 $images[] = $title->getDBkey();
627                         } else {
628                                 $others[] = $title;
629                         }
630                         // Applicable protection types
631                         $this->restrictionTypes[$title->getNamespace()][$title->getDBkey()] =
632                                 array_values( $title->getRestrictionTypes() );
633                 }
634
635                 if ( count( $others ) ) {
636                         // Non-images: check templatelinks
637                         $lb = new LinkBatch( $others );
638                         $this->resetQueryParams();
639                         $this->addTables( [ 'page_restrictions', 'page', 'templatelinks' ] );
640                         $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
641                                 'page_title', 'page_namespace',
642                                 'tl_title', 'tl_namespace' ] );
643                         $this->addWhere( $lb->constructSet( 'tl', $db ) );
644                         $this->addWhere( 'pr_page = page_id' );
645                         $this->addWhere( 'pr_page = tl_from' );
646                         $this->addWhereFld( 'pr_cascade', 1 );
647
648                         $res = $this->select( __METHOD__ );
649                         foreach ( $res as $row ) {
650                                 $source = Title::makeTitle( $row->page_namespace, $row->page_title );
651                                 $this->protections[$row->tl_namespace][$row->tl_title][] = [
652                                         'type' => $row->pr_type,
653                                         'level' => $row->pr_level,
654                                         'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
655                                         'source' => $source->getPrefixedText()
656                                 ];
657                         }
658                 }
659
660                 if ( count( $images ) ) {
661                         // Images: check imagelinks
662                         $this->resetQueryParams();
663                         $this->addTables( [ 'page_restrictions', 'page', 'imagelinks' ] );
664                         $this->addFields( [ 'pr_type', 'pr_level', 'pr_expiry',
665                                 'page_title', 'page_namespace', 'il_to' ] );
666                         $this->addWhere( 'pr_page = page_id' );
667                         $this->addWhere( 'pr_page = il_from' );
668                         $this->addWhereFld( 'pr_cascade', 1 );
669                         $this->addWhereFld( 'il_to', $images );
670
671                         $res = $this->select( __METHOD__ );
672                         foreach ( $res as $row ) {
673                                 $source = Title::makeTitle( $row->page_namespace, $row->page_title );
674                                 $this->protections[NS_FILE][$row->il_to][] = [
675                                         'type' => $row->pr_type,
676                                         'level' => $row->pr_level,
677                                         'expiry' => ApiResult::formatExpiry( $row->pr_expiry ),
678                                         'source' => $source->getPrefixedText()
679                                 ];
680                         }
681                 }
682         }
683
684         /**
685          * Get talk page IDs (if requested) and subject page IDs (if requested)
686          * and put them in $talkids and $subjectids
687          */
688         private function getTSIDs() {
689                 $getTitles = $this->talkids = $this->subjectids = [];
690
691                 /** @var Title $t */
692                 foreach ( $this->everything as $t ) {
693                         if ( MWNamespace::isTalk( $t->getNamespace() ) ) {
694                                 if ( $this->fld_subjectid ) {
695                                         $getTitles[] = $t->getSubjectPage();
696                                 }
697                         } elseif ( $this->fld_talkid ) {
698                                 $getTitles[] = $t->getTalkPage();
699                         }
700                 }
701                 if ( !count( $getTitles ) ) {
702                         return;
703                 }
704
705                 $db = $this->getDB();
706
707                 // Construct a custom WHERE clause that matches
708                 // all titles in $getTitles
709                 $lb = new LinkBatch( $getTitles );
710                 $this->resetQueryParams();
711                 $this->addTables( 'page' );
712                 $this->addFields( [ 'page_title', 'page_namespace', 'page_id' ] );
713                 $this->addWhere( $lb->constructSet( 'page', $db ) );
714                 $res = $this->select( __METHOD__ );
715                 foreach ( $res as $row ) {
716                         if ( MWNamespace::isTalk( $row->page_namespace ) ) {
717                                 $this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] =
718                                         intval( $row->page_id );
719                         } else {
720                                 $this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] =
721                                         intval( $row->page_id );
722                         }
723                 }
724         }
725
726         private function getDisplayTitle() {
727                 $this->displaytitles = [];
728
729                 $pageIds = array_keys( $this->titles );
730
731                 if ( !count( $pageIds ) ) {
732                         return;
733                 }
734
735                 $this->resetQueryParams();
736                 $this->addTables( 'page_props' );
737                 $this->addFields( [ 'pp_page', 'pp_value' ] );
738                 $this->addWhereFld( 'pp_page', $pageIds );
739                 $this->addWhereFld( 'pp_propname', 'displaytitle' );
740                 $res = $this->select( __METHOD__ );
741
742                 foreach ( $res as $row ) {
743                         $this->displaytitles[$row->pp_page] = $row->pp_value;
744                 }
745         }
746
747         /**
748          * Get information about watched status and put it in $this->watched
749          * and $this->notificationtimestamps
750          */
751         private function getWatchedInfo() {
752                 $user = $this->getUser();
753
754                 if ( $user->isAnon() || count( $this->everything ) == 0
755                         || !$user->isAllowed( 'viewmywatchlist' )
756                 ) {
757                         return;
758                 }
759
760                 $this->watched = [];
761                 $this->notificationtimestamps = [];
762
763                 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
764                 $timestamps = $store->getNotificationTimestampsBatch( $user, $this->everything );
765
766                 if ( $this->fld_watched ) {
767                         foreach ( $timestamps as $namespaceId => $dbKeys ) {
768                                 $this->watched[$namespaceId] = array_map(
769                                         function ( $x ) {
770                                                 return $x !== false;
771                                         },
772                                         $dbKeys
773                                 );
774                         }
775                 }
776                 if ( $this->fld_notificationtimestamp ) {
777                         $this->notificationtimestamps = $timestamps;
778                 }
779         }
780
781         /**
782          * Get the count of watchers and put it in $this->watchers
783          */
784         private function getWatcherInfo() {
785                 if ( count( $this->everything ) == 0 ) {
786                         return;
787                 }
788
789                 $user = $this->getUser();
790                 $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
791                 $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
792                 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
793                         return;
794                 }
795
796                 $this->showZeroWatchers = $canUnwatchedpages;
797
798                 $countOptions = [];
799                 if ( !$canUnwatchedpages ) {
800                         $countOptions['minimumWatchers'] = $unwatchedPageThreshold;
801                 }
802
803                 $this->watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchersMultiple(
804                         $this->everything,
805                         $countOptions
806                 );
807         }
808
809         /**
810          * Get the count of watchers who have visited recent edits and put it in
811          * $this->visitingwatchers
812          *
813          * Based on InfoAction::pageCounts
814          */
815         private function getVisitingWatcherInfo() {
816                 $config = $this->getConfig();
817                 $user = $this->getUser();
818                 $db = $this->getDB();
819
820                 $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' );
821                 $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' );
822                 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
823                         return;
824                 }
825
826                 $this->showZeroWatchers = $canUnwatchedpages;
827
828                 $titlesWithThresholds = [];
829                 if ( $this->titles ) {
830                         $lb = new LinkBatch( $this->titles );
831
832                         // Fetch last edit timestamps for pages
833                         $this->resetQueryParams();
834                         $this->addTables( [ 'page', 'revision' ] );
835                         $this->addFields( [ 'page_namespace', 'page_title', 'rev_timestamp' ] );
836                         $this->addWhere( [
837                                 'page_latest = rev_id',
838                                 $lb->constructSet( 'page', $db ),
839                         ] );
840                         $this->addOption( 'GROUP BY', [ 'page_namespace', 'page_title' ] );
841                         $timestampRes = $this->select( __METHOD__ );
842
843                         $age = $config->get( 'WatchersMaxAge' );
844                         $timestamps = [];
845                         foreach ( $timestampRes as $row ) {
846                                 $revTimestamp = wfTimestamp( TS_UNIX, (int)$row->rev_timestamp );
847                                 $timestamps[$row->page_namespace][$row->page_title] = $revTimestamp - $age;
848                         }
849                         $titlesWithThresholds = array_map(
850                                 function ( LinkTarget $target ) use ( $timestamps ) {
851                                         return [
852                                                 $target, $timestamps[$target->getNamespace()][$target->getDBkey()]
853                                         ];
854                                 },
855                                 $this->titles
856                         );
857                 }
858
859                 if ( $this->missing ) {
860                         $titlesWithThresholds = array_merge(
861                                 $titlesWithThresholds,
862                                 array_map(
863                                         function ( LinkTarget $target ) {
864                                                 return [ $target, null ];
865                                         },
866                                         $this->missing
867                                 )
868                         );
869                 }
870                 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
871                 $this->visitingwatchers = $store->countVisitingWatchersMultiple(
872                         $titlesWithThresholds,
873                         !$canUnwatchedpages ? $unwatchedPageThreshold : null
874                 );
875         }
876
877         public function getCacheMode( $params ) {
878                 // Other props depend on something about the current user
879                 $publicProps = [
880                         'protection',
881                         'talkid',
882                         'subjectid',
883                         'url',
884                         'preload',
885                         'displaytitle',
886                 ];
887                 if ( array_diff( (array)$params['prop'], $publicProps ) ) {
888                         return 'private';
889                 }
890
891                 // testactions also depends on the current user
892                 if ( $params['testactions'] ) {
893                         return 'private';
894                 }
895
896                 if ( !is_null( $params['token'] ) ) {
897                         return 'private';
898                 }
899
900                 return 'public';
901         }
902
903         public function getAllowedParams() {
904                 return [
905                         'prop' => [
906                                 ApiBase::PARAM_ISMULTI => true,
907                                 ApiBase::PARAM_TYPE => [
908                                         'protection',
909                                         'talkid',
910                                         'watched', # private
911                                         'watchers', # private
912                                         'visitingwatchers', # private
913                                         'notificationtimestamp', # private
914                                         'subjectid',
915                                         'url',
916                                         'readable', # private
917                                         'preload',
918                                         'displaytitle',
919                                         // If you add more properties here, please consider whether they
920                                         // need to be added to getCacheMode()
921                                 ],
922                                 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
923                         ],
924                         'testactions' => [
925                                 ApiBase::PARAM_TYPE => 'string',
926                                 ApiBase::PARAM_ISMULTI => true,
927                         ],
928                         'token' => [
929                                 ApiBase::PARAM_DEPRECATED => true,
930                                 ApiBase::PARAM_ISMULTI => true,
931                                 ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() )
932                         ],
933                         'continue' => [
934                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
935                         ],
936                 ];
937         }
938
939         protected function getExamplesMessages() {
940                 return [
941                         'action=query&prop=info&titles=Main%20Page'
942                                 => 'apihelp-query+info-example-simple',
943                         'action=query&prop=info&inprop=protection&titles=Main%20Page'
944                                 => 'apihelp-query+info-example-protection',
945                 ];
946         }
947
948         public function getHelpUrls() {
949                 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';
950         }
951 }