MediaWiki  1.27.2
ApiQueryInfo.php
Go to the documentation of this file.
1 <?php
27 
33 class ApiQueryInfo extends ApiQueryBase {
34 
35  private $fld_protection = false, $fld_talkid = false,
36  $fld_subjectid = false, $fld_url = false,
37  $fld_readable = false, $fld_watched = false,
40  $fld_preload = false, $fld_displaytitle = false;
41 
42  private $params;
43 
45  private $titles;
47  private $missing;
49  private $everything;
50 
53 
56  private $showZeroWatchers = false;
57 
58  private $tokenFunctions;
59 
60  private $countTestedActions = 0;
61 
62  public function __construct( ApiQuery $query, $moduleName ) {
63  parent::__construct( $query, $moduleName, 'in' );
64  }
65 
70  public function requestExtraData( $pageSet ) {
71  $pageSet->requestField( 'page_restrictions' );
72  // If the pageset is resolving redirects we won't get page_is_redirect.
73  // But we can't know for sure until the pageset is executed (revids may
74  // turn it off), so request it unconditionally.
75  $pageSet->requestField( 'page_is_redirect' );
76  $pageSet->requestField( 'page_is_new' );
77  $config = $this->getConfig();
78  $pageSet->requestField( 'page_touched' );
79  $pageSet->requestField( 'page_latest' );
80  $pageSet->requestField( 'page_len' );
81  if ( $config->get( 'ContentHandlerUseDB' ) ) {
82  $pageSet->requestField( 'page_content_model' );
83  }
84  if ( $config->get( 'PageLanguageUseDB' ) ) {
85  $pageSet->requestField( 'page_lang' );
86  }
87  }
88 
96  protected function getTokenFunctions() {
97  // Don't call the hooks twice
98  if ( isset( $this->tokenFunctions ) ) {
99  return $this->tokenFunctions;
100  }
101 
102  // If we're in a mode that breaks the same-origin policy, no tokens can
103  // be obtained
104  if ( $this->lacksSameOriginSecurity() ) {
105  return [];
106  }
107 
108  $this->tokenFunctions = [
109  'edit' => [ 'ApiQueryInfo', 'getEditToken' ],
110  'delete' => [ 'ApiQueryInfo', 'getDeleteToken' ],
111  'protect' => [ 'ApiQueryInfo', 'getProtectToken' ],
112  'move' => [ 'ApiQueryInfo', 'getMoveToken' ],
113  'block' => [ 'ApiQueryInfo', 'getBlockToken' ],
114  'unblock' => [ 'ApiQueryInfo', 'getUnblockToken' ],
115  'email' => [ 'ApiQueryInfo', 'getEmailToken' ],
116  'import' => [ 'ApiQueryInfo', 'getImportToken' ],
117  'watch' => [ 'ApiQueryInfo', 'getWatchToken' ],
118  ];
119  Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
120 
121  return $this->tokenFunctions;
122  }
123 
124  static protected $cachedTokens = [];
125 
129  public static function resetTokenCache() {
131  }
132 
136  public static function getEditToken( $pageid, $title ) {
137  // We could check for $title->userCan('edit') here,
138  // but that's too expensive for this purpose
139  // and would break caching
140  global $wgUser;
141  if ( !$wgUser->isAllowed( 'edit' ) ) {
142  return false;
143  }
144 
145  // The token is always the same, let's exploit that
146  if ( !isset( ApiQueryInfo::$cachedTokens['edit'] ) ) {
147  ApiQueryInfo::$cachedTokens['edit'] = $wgUser->getEditToken();
148  }
149 
150  return ApiQueryInfo::$cachedTokens['edit'];
151  }
152 
156  public static function getDeleteToken( $pageid, $title ) {
157  global $wgUser;
158  if ( !$wgUser->isAllowed( 'delete' ) ) {
159  return false;
160  }
161 
162  // The token is always the same, let's exploit that
163  if ( !isset( ApiQueryInfo::$cachedTokens['delete'] ) ) {
164  ApiQueryInfo::$cachedTokens['delete'] = $wgUser->getEditToken();
165  }
166 
167  return ApiQueryInfo::$cachedTokens['delete'];
168  }
169 
173  public static function getProtectToken( $pageid, $title ) {
174  global $wgUser;
175  if ( !$wgUser->isAllowed( 'protect' ) ) {
176  return false;
177  }
178 
179  // The token is always the same, let's exploit that
180  if ( !isset( ApiQueryInfo::$cachedTokens['protect'] ) ) {
181  ApiQueryInfo::$cachedTokens['protect'] = $wgUser->getEditToken();
182  }
183 
184  return ApiQueryInfo::$cachedTokens['protect'];
185  }
186 
190  public static function getMoveToken( $pageid, $title ) {
191  global $wgUser;
192  if ( !$wgUser->isAllowed( 'move' ) ) {
193  return false;
194  }
195 
196  // The token is always the same, let's exploit that
197  if ( !isset( ApiQueryInfo::$cachedTokens['move'] ) ) {
198  ApiQueryInfo::$cachedTokens['move'] = $wgUser->getEditToken();
199  }
200 
201  return ApiQueryInfo::$cachedTokens['move'];
202  }
203 
207  public static function getBlockToken( $pageid, $title ) {
208  global $wgUser;
209  if ( !$wgUser->isAllowed( 'block' ) ) {
210  return false;
211  }
212 
213  // The token is always the same, let's exploit that
214  if ( !isset( ApiQueryInfo::$cachedTokens['block'] ) ) {
215  ApiQueryInfo::$cachedTokens['block'] = $wgUser->getEditToken();
216  }
217 
218  return ApiQueryInfo::$cachedTokens['block'];
219  }
220 
224  public static function getUnblockToken( $pageid, $title ) {
225  // Currently, this is exactly the same as the block token
226  return self::getBlockToken( $pageid, $title );
227  }
228 
232  public static function getEmailToken( $pageid, $title ) {
233  global $wgUser;
234  if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailuser() ) {
235  return false;
236  }
237 
238  // The token is always the same, let's exploit that
239  if ( !isset( ApiQueryInfo::$cachedTokens['email'] ) ) {
240  ApiQueryInfo::$cachedTokens['email'] = $wgUser->getEditToken();
241  }
242 
243  return ApiQueryInfo::$cachedTokens['email'];
244  }
245 
249  public static function getImportToken( $pageid, $title ) {
250  global $wgUser;
251  if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) {
252  return false;
253  }
254 
255  // The token is always the same, let's exploit that
256  if ( !isset( ApiQueryInfo::$cachedTokens['import'] ) ) {
257  ApiQueryInfo::$cachedTokens['import'] = $wgUser->getEditToken();
258  }
259 
260  return ApiQueryInfo::$cachedTokens['import'];
261  }
262 
266  public static function getWatchToken( $pageid, $title ) {
267  global $wgUser;
268  if ( !$wgUser->isLoggedIn() ) {
269  return false;
270  }
271 
272  // The token is always the same, let's exploit that
273  if ( !isset( ApiQueryInfo::$cachedTokens['watch'] ) ) {
274  ApiQueryInfo::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' );
275  }
276 
277  return ApiQueryInfo::$cachedTokens['watch'];
278  }
279 
283  public static function getOptionsToken( $pageid, $title ) {
284  global $wgUser;
285  if ( !$wgUser->isLoggedIn() ) {
286  return false;
287  }
288 
289  // The token is always the same, let's exploit that
290  if ( !isset( ApiQueryInfo::$cachedTokens['options'] ) ) {
291  ApiQueryInfo::$cachedTokens['options'] = $wgUser->getEditToken();
292  }
293 
294  return ApiQueryInfo::$cachedTokens['options'];
295  }
296 
297  public function execute() {
298  $this->params = $this->extractRequestParams();
299  if ( !is_null( $this->params['prop'] ) ) {
300  $prop = array_flip( $this->params['prop'] );
301  $this->fld_protection = isset( $prop['protection'] );
302  $this->fld_watched = isset( $prop['watched'] );
303  $this->fld_watchers = isset( $prop['watchers'] );
304  $this->fld_visitingwatchers = isset( $prop['visitingwatchers'] );
305  $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
306  $this->fld_talkid = isset( $prop['talkid'] );
307  $this->fld_subjectid = isset( $prop['subjectid'] );
308  $this->fld_url = isset( $prop['url'] );
309  $this->fld_readable = isset( $prop['readable'] );
310  $this->fld_preload = isset( $prop['preload'] );
311  $this->fld_displaytitle = isset( $prop['displaytitle'] );
312  }
313 
314  $pageSet = $this->getPageSet();
315  $this->titles = $pageSet->getGoodTitles();
316  $this->missing = $pageSet->getMissingTitles();
317  $this->everything = $this->titles + $this->missing;
318  $result = $this->getResult();
319 
320  uasort( $this->everything, [ 'Title', 'compare' ] );
321  if ( !is_null( $this->params['continue'] ) ) {
322  // Throw away any titles we're gonna skip so they don't
323  // clutter queries
324  $cont = explode( '|', $this->params['continue'] );
325  $this->dieContinueUsageIf( count( $cont ) != 2 );
326  $conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
327  foreach ( $this->everything as $pageid => $title ) {
328  if ( Title::compare( $title, $conttitle ) >= 0 ) {
329  break;
330  }
331  unset( $this->titles[$pageid] );
332  unset( $this->missing[$pageid] );
333  unset( $this->everything[$pageid] );
334  }
335  }
336 
337  $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
338  // when resolving redirects, no page will have this field
339  $this->pageIsRedir = !$pageSet->isResolvingRedirects()
340  ? $pageSet->getCustomField( 'page_is_redirect' )
341  : [];
342  $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
343 
344  $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
345  $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
346  $this->pageLength = $pageSet->getCustomField( 'page_len' );
347 
348  // Get protection info if requested
349  if ( $this->fld_protection ) {
350  $this->getProtectionInfo();
351  }
352 
353  if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
354  $this->getWatchedInfo();
355  }
356 
357  if ( $this->fld_watchers ) {
358  $this->getWatcherInfo();
359  }
360 
361  if ( $this->fld_visitingwatchers ) {
362  $this->getVisitingWatcherInfo();
363  }
364 
365  // Run the talkid/subjectid query if requested
366  if ( $this->fld_talkid || $this->fld_subjectid ) {
367  $this->getTSIDs();
368  }
369 
370  if ( $this->fld_displaytitle ) {
371  $this->getDisplayTitle();
372  }
373 
375  foreach ( $this->everything as $pageid => $title ) {
376  $pageInfo = $this->extractPageInfo( $pageid, $title );
377  $fit = $pageInfo !== null && $result->addValue( [
378  'query',
379  'pages'
380  ], $pageid, $pageInfo );
381  if ( !$fit ) {
382  $this->setContinueEnumParameter( 'continue',
383  $title->getNamespace() . '|' .
384  $title->getText() );
385  break;
386  }
387  }
388  }
389 
396  private function extractPageInfo( $pageid, $title ) {
397  $pageInfo = [];
398  // $title->exists() needs pageid, which is not set for all title objects
399  $titleExists = $pageid > 0;
400  $ns = $title->getNamespace();
401  $dbkey = $title->getDBkey();
402 
403  $pageInfo['contentmodel'] = $title->getContentModel();
404 
405  $pageLanguage = $title->getPageLanguage();
406  $pageInfo['pagelanguage'] = $pageLanguage->getCode();
407  $pageInfo['pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
408  $pageInfo['pagelanguagedir'] = $pageLanguage->getDir();
409 
410  if ( $titleExists ) {
411  $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
412  $pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] );
413  $pageInfo['length'] = intval( $this->pageLength[$pageid] );
414 
415  if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
416  $pageInfo['redirect'] = true;
417  }
418  if ( $this->pageIsNew[$pageid] ) {
419  $pageInfo['new'] = true;
420  }
421  }
422 
423  if ( !is_null( $this->params['token'] ) ) {
425  $pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
426  foreach ( $this->params['token'] as $t ) {
427  $val = call_user_func( $tokenFunctions[$t], $pageid, $title );
428  if ( $val === false ) {
429  $this->setWarning( "Action '$t' is not allowed for the current user" );
430  } else {
431  $pageInfo[$t . 'token'] = $val;
432  }
433  }
434  }
435 
436  if ( $this->fld_protection ) {
437  $pageInfo['protection'] = [];
438  if ( isset( $this->protections[$ns][$dbkey] ) ) {
439  $pageInfo['protection'] =
440  $this->protections[$ns][$dbkey];
441  }
442  ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
443 
444  $pageInfo['restrictiontypes'] = [];
445  if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
446  $pageInfo['restrictiontypes'] =
447  $this->restrictionTypes[$ns][$dbkey];
448  }
449  ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
450  }
451 
452  if ( $this->fld_watched && $this->watched !== null ) {
453  $pageInfo['watched'] = $this->watched[$ns][$dbkey];
454  }
455 
456  if ( $this->fld_watchers ) {
457  if ( $this->watchers !== null && $this->watchers[$ns][$dbkey] !== 0 ) {
458  $pageInfo['watchers'] = $this->watchers[$ns][$dbkey];
459  } elseif ( $this->showZeroWatchers ) {
460  $pageInfo['watchers'] = 0;
461  }
462  }
463 
464  if ( $this->fld_visitingwatchers ) {
465  if ( $this->visitingwatchers !== null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
466  $pageInfo['visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
467  } elseif ( $this->showZeroWatchers ) {
468  $pageInfo['visitingwatchers'] = 0;
469  }
470  }
471 
472  if ( $this->fld_notificationtimestamp ) {
473  $pageInfo['notificationtimestamp'] = '';
474  if ( $this->notificationtimestamps[$ns][$dbkey] ) {
475  $pageInfo['notificationtimestamp'] =
476  wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
477  }
478  }
479 
480  if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
481  $pageInfo['talkid'] = $this->talkids[$ns][$dbkey];
482  }
483 
484  if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
485  $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey];
486  }
487 
488  if ( $this->fld_url ) {
489  $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
490  $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT );
491  $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL );
492  }
493  if ( $this->fld_readable ) {
494  $pageInfo['readable'] = $title->userCan( 'read', $this->getUser() );
495  }
496 
497  if ( $this->fld_preload ) {
498  if ( $titleExists ) {
499  $pageInfo['preload'] = '';
500  } else {
501  $text = null;
502  Hooks::run( 'EditFormPreloadText', [ &$text, &$title ] );
503 
504  $pageInfo['preload'] = $text;
505  }
506  }
507 
508  if ( $this->fld_displaytitle ) {
509  if ( isset( $this->displaytitles[$pageid] ) ) {
510  $pageInfo['displaytitle'] = $this->displaytitles[$pageid];
511  } else {
512  $pageInfo['displaytitle'] = $title->getPrefixedText();
513  }
514  }
515 
516  if ( $this->params['testactions'] ) {
517  $limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML1 : self::LIMIT_SML2;
518  if ( $this->countTestedActions >= $limit ) {
519  return null; // force a continuation
520  }
521 
522  $user = $this->getUser();
523  $pageInfo['actions'] = [];
524  foreach ( $this->params['testactions'] as $action ) {
525  $this->countTestedActions++;
526  $pageInfo['actions'][$action] = $title->userCan( $action, $user );
527  }
528  }
529 
530  return $pageInfo;
531  }
532 
536  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 ) {
552  $title = $this->titles[$row->pr_page];
553  $a = [
554  'type' => $row->pr_type,
555  'level' => $row->pr_level,
556  'expiry' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 )
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' => $wgContLang->formatExpiry( $row->pt_expiry, TS_ISO_8601 )
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' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 ),
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' => $wgContLang->formatExpiry( $row->pr_expiry, TS_ISO_8601 ),
678  'source' => $source->getPrefixedText()
679  ];
680  }
681  }
682  }
683 
688  private function getTSIDs() {
689  $getTitles = $this->talkids = $this->subjectids = [];
690 
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 
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 
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 
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 = WatchedItemStore::getDefaultInstance()->countWatchersMultiple(
804  $this->everything,
805  $countOptions
806  );
807  }
808 
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  },
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  },
867  )
868  );
869  }
870 
871  $this->visitingwatchers = WatchedItemStore::getDefaultInstance()->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,
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  ],
923  ],
924  'testactions' => [
925  ApiBase::PARAM_TYPE => 'string',
926  ApiBase::PARAM_ISMULTI => true,
927  ],
928  'token' => [
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/API:Info';
950  }
951 }
select($method, $extraQuery=[])
Execute a SELECT query based on the values in the internal arrays.
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below...
Definition: ApiBase.php:88
getDB()
Get the Query database connection (read-only)
the array() calling protocol came about after MediaWiki 1.4rc1.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1418
getResult()
Get the result object.
Definition: ApiBase.php:584
addWhereFld($field, $value)
Equivalent to addWhere(array($field => $value))
static getImportToken($pageid, $title)
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
getMain()
Get the main module.
Definition: ApiBase.php:480
Title[] $missing
static isTalk($index)
Is the given namespace a talk namespace?
Definition: MWNamespace.php:97
This is a base class for all Query modules.
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition: ApiBase.php:512
extractRequestParams($parseLimit=true)
Using getAllowedParams(), this function makes an array of the values provided by the user...
Definition: ApiBase.php:685
$source
const PROTO_CURRENT
Definition: Defines.php:264
getPageSet()
Get the PageSet object to work on.
getCacheMode($params)
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:157
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
wfExpandUrl($url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
addWhere($value)
Add a set of WHERE clauses to the internal array.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:618
getNamespace()
Get the namespace index.
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page.Return false to stop further processing of the tag $reader:XMLReader object &$pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1796
static $cachedTokens
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:31
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static getOptionsToken($pageid, $title)
A query module to show basic page information.
static getMoveToken($pageid, $title)
const TS_ISO_8601
ISO 8601 format with no timezone: 1986-02-09T20:00:00Z.
$res
Definition: database.txt:21
getWatchedInfo()
Get information about watched status and put it in $this->watched and $this->notificationtimestamps.
getConfig()
Get the Config object.
static getUnblockToken($pageid, $title)
addOption($name, $value=null)
Add an option such as LIMIT or USE INDEX.
requestExtraData($pageSet)
getDBkey()
Get the main part with underscores.
static makeTitleSafe($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:548
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:912
getTSIDs()
Get talk page IDs (if requested) and subject page IDs (if requested) and put them in $talkids and $su...
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
const NS_FILE
Definition: Defines.php:75
__construct(ApiQuery $query, $moduleName)
This is the main query class.
Definition: ApiQuery.php:38
setWarning($warning)
Set warning section for this module.
Definition: ApiBase.php:1495
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
dieContinueUsageIf($condition)
Die with the $prefix.
Definition: ApiBase.php:2181
static getProtectToken($pageid, $title)
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter...
Definition: ApiBase.php:125
static getDeleteToken($pageid, $title)
static getEmailToken($pageid, $title)
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
Title[] $everything
getWatcherInfo()
Get the count of watchers and put it in $this->watchers.
static getBlockToken($pageid, $title)
Title[] $titles
getTokenFunctions()
Get an array mapping token names to their handler functions.
static getTalk($index)
Get the talk namespace index for a given namespace.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
const PROTO_CANONICAL
Definition: Defines.php:265
getProtectionInfo()
Get information about protections and put it in $protections.
addFields($value)
Add a set of fields to select to the internal array.
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition: ApiBase.php:53
setContinueEnumParameter($paramName, $paramValue)
Set a query-continue value.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition: hooks.txt:1004
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition: design.txt:56
static getWatchToken($pageid, $title)
const PARAM_DEPRECATED
(boolean) Is the parameter deprecated (will show a warning)?
Definition: ApiBase.php:106
static compare($a, $b)
Callback for usort() to do title sorts by (namespace, title)
Definition: Title.php:780
extractPageInfo($pageid, $title)
Get a result array with information about a title.
const TS_UNIX
Unix time - the number of seconds since 1970-01-01 00:00:00 UTC.
getVisitingWatcherInfo()
Get the count of watchers who have visited recent edits and put it in $this->visitingwatchers.
static getEditToken($pageid, $title)
static getSubject($index)
Get the subject namespace index for a given namespace Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
static resetTokenCache()
getUser()
Get the User object.
addTables($tables, $alias=null)
Add a set of tables to the internal array.
static & makeTitle($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:524
resetQueryParams()
Blank the internal arrays with query parameters.
$wgUser
Definition: Setup.php:794