MediaWiki  1.28.1
InfoAction.php
Go to the documentation of this file.
1 <?php
26 
32 class InfoAction extends FormlessAction {
33  const VERSION = 1;
34 
40  public function getName() {
41  return 'info';
42  }
43 
49  public function requiresUnblock() {
50  return false;
51  }
52 
58  public function requiresWrite() {
59  return false;
60  }
61 
69  public static function invalidateCache( Title $title, $revid = null ) {
70  if ( !$revid ) {
71  $revision = Revision::newFromTitle( $title, 0, Revision::READ_LATEST );
72  $revid = $revision ? $revision->getId() : null;
73  }
74  if ( $revid !== null ) {
75  $key = self::getCacheKey( $title, $revid );
76  ObjectCache::getMainWANInstance()->delete( $key );
77  }
78  }
79 
85  public function onView() {
86  $content = '';
87 
88  // Validate revision
89  $oldid = $this->page->getOldID();
90  if ( $oldid ) {
91  $revision = $this->page->getRevisionFetched();
92 
93  // Revision is missing
94  if ( $revision === null ) {
95  return $this->msg( 'missing-revision', $oldid )->parse();
96  }
97 
98  // Revision is not current
99  if ( !$revision->isCurrent() ) {
100  return $this->msg( 'pageinfo-not-current' )->plain();
101  }
102  }
103 
104  // Page header
105  if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) {
106  $content .= $this->msg( 'pageinfo-header' )->parse();
107  }
108 
109  // Hide "This page is a member of # hidden categories" explanation
110  $content .= Html::element( 'style', [],
111  '.mw-hiddenCategoriesExplanation { display: none; }' ) . "\n";
112 
113  // Hide "Templates used on this page" explanation
114  $content .= Html::element( 'style', [],
115  '.mw-templatesUsedExplanation { display: none; }' ) . "\n";
116 
117  // Get page information
118  $pageInfo = $this->pageInfo();
119 
120  // Allow extensions to add additional information
121  Hooks::run( 'InfoAction', [ $this->getContext(), &$pageInfo ] );
122 
123  // Render page information
124  foreach ( $pageInfo as $header => $infoTable ) {
125  // Messages:
126  // pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
127  // pageinfo-header-properties, pageinfo-category-info
128  $content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() ) . "\n";
129  $table = "\n";
130  foreach ( $infoTable as $infoRow ) {
131  $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
132  $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
133  $id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
134  $table = $this->addRow( $table, $name, $value, $id ) . "\n";
135  }
136  $content = $this->addTable( $content, $table ) . "\n";
137  }
138 
139  // Page footer
140  if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
141  $content .= $this->msg( 'pageinfo-footer' )->parse();
142  }
143 
144  return $content;
145  }
146 
153  protected function makeHeader( $header ) {
154  $spanAttribs = [ 'class' => 'mw-headline', 'id' => Sanitizer::escapeId( $header ) ];
155 
156  return Html::rawElement( 'h2', [], Html::element( 'span', $spanAttribs, $header ) );
157  }
158 
168  protected function addRow( $table, $name, $value, $id ) {
169  return $table .
171  'tr',
172  $id === null ? [] : [ 'id' => 'mw-' . $id ],
173  Html::rawElement( 'td', [ 'style' => 'vertical-align: top;' ], $name ) .
174  Html::rawElement( 'td', [], $value )
175  );
176  }
177 
185  protected function addTable( $content, $table ) {
186  return $content . Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ],
187  $table );
188  }
189 
197  protected function pageInfo() {
199 
200  $user = $this->getUser();
201  $lang = $this->getLanguage();
202  $title = $this->getTitle();
203  $id = $title->getArticleID();
204  $config = $this->context->getConfig();
205  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
206 
207  $pageCounts = $this->pageCounts( $this->page );
208 
209  $pageProperties = [];
210  $props = PageProps::getInstance()->getAllProperties( $title );
211  if ( isset( $props[$id] ) ) {
212  $pageProperties = $props[$id];
213  }
214 
215  // Basic information
216  $pageInfo = [];
217  $pageInfo['header-basic'] = [];
218 
219  // Display title
220  $displayTitle = $title->getPrefixedText();
221  if ( isset( $pageProperties['displaytitle'] ) ) {
222  $displayTitle = $pageProperties['displaytitle'];
223  }
224 
225  $pageInfo['header-basic'][] = [
226  $this->msg( 'pageinfo-display-title' ), $displayTitle
227  ];
228 
229  // Is it a redirect? If so, where to?
230  if ( $title->isRedirect() ) {
231  $pageInfo['header-basic'][] = [
232  $this->msg( 'pageinfo-redirectsto' ),
233  Linker::link( $this->page->getRedirectTarget() ) .
234  $this->msg( 'word-separator' )->escaped() .
235  $this->msg( 'parentheses' )->rawParams( Linker::link(
236  $this->page->getRedirectTarget(),
237  $this->msg( 'pageinfo-redirectsto-info' )->escaped(),
238  [],
239  [ 'action' => 'info' ]
240  ) )->escaped()
241  ];
242  }
243 
244  // Default sort key
245  $sortKey = $title->getCategorySortkey();
246  if ( isset( $pageProperties['defaultsort'] ) ) {
247  $sortKey = $pageProperties['defaultsort'];
248  }
249 
250  $sortKey = htmlspecialchars( $sortKey );
251  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
252 
253  // Page length (in bytes)
254  $pageInfo['header-basic'][] = [
255  $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() )
256  ];
257 
258  // Page ID (number not localised, as it's a database ID)
259  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
260 
261  // Language in which the page content is (supposed to be) written
262  $pageLang = $title->getPageLanguage()->getCode();
263 
264  if ( $config->get( 'PageLanguageUseDB' )
265  && $this->getTitle()->userCan( 'pagelang', $this->getUser() )
266  ) {
267  // Link to Special:PageLanguage with pre-filled page title if user has permissions
268  $titleObj = SpecialPage::getTitleFor( 'PageLanguage', $title->getPrefixedText() );
269  $langDisp = Linker::link(
270  $titleObj,
271  $this->msg( 'pageinfo-language' )->escaped()
272  );
273  } else {
274  // Display just the message
275  $langDisp = $this->msg( 'pageinfo-language' )->escaped();
276  }
277 
278  $pageInfo['header-basic'][] = [ $langDisp,
279  Language::fetchLanguageName( $pageLang, $lang->getCode() )
280  . ' ' . $this->msg( 'parentheses', $pageLang )->escaped() ];
281 
282  // Content model of the page
283  $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
284  // If the user can change it, add a link to Special:ChangeContentModel
285  if ( $title->quickUserCan( 'editcontentmodel' ) ) {
286  $modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
287  SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
288  $this->msg( 'pageinfo-content-model-change' )->text()
289  ) )->escaped();
290  }
291 
292  $pageInfo['header-basic'][] = [
293  $this->msg( 'pageinfo-content-model' ),
294  $modelHtml
295  ];
296 
297  if ( $title->inNamespace( NS_USER ) ) {
298  $pageUser = User::newFromName( $title->getRootText() );
299  if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
300  $pageInfo['header-basic'][] = [
301  $this->msg( 'pageinfo-user-id' ),
302  $pageUser->getId()
303  ];
304  }
305  }
306 
307  // Search engine status
308  $pOutput = new ParserOutput();
309  if ( isset( $pageProperties['noindex'] ) ) {
310  $pOutput->setIndexPolicy( 'noindex' );
311  }
312  if ( isset( $pageProperties['index'] ) ) {
313  $pOutput->setIndexPolicy( 'index' );
314  }
315 
316  // Use robot policy logic
317  $policy = $this->page->getRobotPolicy( 'view', $pOutput );
318  $pageInfo['header-basic'][] = [
319  // Messages: pageinfo-robot-index, pageinfo-robot-noindex
320  $this->msg( 'pageinfo-robot-policy' ),
321  $this->msg( "pageinfo-robot-${policy['index']}" )
322  ];
323 
324  $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
325  if (
326  $user->isAllowed( 'unwatchedpages' ) ||
327  ( $unwatchedPageThreshold !== false &&
328  $pageCounts['watchers'] >= $unwatchedPageThreshold )
329  ) {
330  // Number of page watchers
331  $pageInfo['header-basic'][] = [
332  $this->msg( 'pageinfo-watchers' ),
333  $lang->formatNum( $pageCounts['watchers'] )
334  ];
335  if (
336  $config->get( 'ShowUpdatedMarker' ) &&
337  isset( $pageCounts['visitingWatchers'] )
338  ) {
339  $minToDisclose = $config->get( 'UnwatchedPageSecret' );
340  if ( $pageCounts['visitingWatchers'] > $minToDisclose ||
341  $user->isAllowed( 'unwatchedpages' ) ) {
342  $pageInfo['header-basic'][] = [
343  $this->msg( 'pageinfo-visiting-watchers' ),
344  $lang->formatNum( $pageCounts['visitingWatchers'] )
345  ];
346  } else {
347  $pageInfo['header-basic'][] = [
348  $this->msg( 'pageinfo-visiting-watchers' ),
349  $this->msg( 'pageinfo-few-visiting-watchers' )
350  ];
351  }
352  }
353  } elseif ( $unwatchedPageThreshold !== false ) {
354  $pageInfo['header-basic'][] = [
355  $this->msg( 'pageinfo-watchers' ),
356  $this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
357  ];
358  }
359 
360  // Redirects to this page
361  $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
362  $pageInfo['header-basic'][] = [
363  Linker::link(
364  $whatLinksHere,
365  $this->msg( 'pageinfo-redirects-name' )->escaped(),
366  [],
367  [
368  'hidelinks' => 1,
369  'hidetrans' => 1,
370  'hideimages' => $title->getNamespace() == NS_FILE
371  ]
372  ),
373  $this->msg( 'pageinfo-redirects-value' )
374  ->numParams( count( $title->getRedirectsHere() ) )
375  ];
376 
377  // Is it counted as a content page?
378  if ( $this->page->isCountable() ) {
379  $pageInfo['header-basic'][] = [
380  $this->msg( 'pageinfo-contentpage' ),
381  $this->msg( 'pageinfo-contentpage-yes' )
382  ];
383  }
384 
385  // Subpages of this page, if subpages are enabled for the current NS
386  if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
387  $prefixIndex = SpecialPage::getTitleFor(
388  'Prefixindex', $title->getPrefixedText() . '/' );
389  $pageInfo['header-basic'][] = [
390  Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ),
391  $this->msg( 'pageinfo-subpages-value' )
392  ->numParams(
393  $pageCounts['subpages']['total'],
394  $pageCounts['subpages']['redirects'],
395  $pageCounts['subpages']['nonredirects'] )
396  ];
397  }
398 
399  if ( $title->inNamespace( NS_CATEGORY ) ) {
400  $category = Category::newFromTitle( $title );
401 
402  // $allCount is the total number of cat members,
403  // not the count of how many members are normal pages.
404  $allCount = (int)$category->getPageCount();
405  $subcatCount = (int)$category->getSubcatCount();
406  $fileCount = (int)$category->getFileCount();
407  $pagesCount = $allCount - $subcatCount - $fileCount;
408 
409  $pageInfo['category-info'] = [
410  [
411  $this->msg( 'pageinfo-category-total' ),
412  $lang->formatNum( $allCount )
413  ],
414  [
415  $this->msg( 'pageinfo-category-pages' ),
416  $lang->formatNum( $pagesCount )
417  ],
418  [
419  $this->msg( 'pageinfo-category-subcats' ),
420  $lang->formatNum( $subcatCount )
421  ],
422  [
423  $this->msg( 'pageinfo-category-files' ),
424  $lang->formatNum( $fileCount )
425  ]
426  ];
427  }
428 
429  // Page protection
430  $pageInfo['header-restrictions'] = [];
431 
432  // Is this page affected by the cascading protection of something which includes it?
433  if ( $title->isCascadeProtected() ) {
434  $cascadingFrom = '';
435  $sources = $title->getCascadeProtectionSources()[0];
436 
437  foreach ( $sources as $sourceTitle ) {
438  $cascadingFrom .= Html::rawElement(
439  'li', [], Linker::linkKnown( $sourceTitle ) );
440  }
441 
442  $cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
443  $pageInfo['header-restrictions'][] = [
444  $this->msg( 'pageinfo-protect-cascading-from' ),
445  $cascadingFrom
446  ];
447  }
448 
449  // Is out protection set to cascade to other pages?
450  if ( $title->areRestrictionsCascading() ) {
451  $pageInfo['header-restrictions'][] = [
452  $this->msg( 'pageinfo-protect-cascading' ),
453  $this->msg( 'pageinfo-protect-cascading-yes' )
454  ];
455  }
456 
457  // Page protection
458  foreach ( $title->getRestrictionTypes() as $restrictionType ) {
459  $protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
460 
461  if ( $protectionLevel == '' ) {
462  // Allow all users
463  $message = $this->msg( 'protect-default' )->escaped();
464  } else {
465  // Administrators only
466  // Messages: protect-level-autoconfirmed, protect-level-sysop
467  $message = $this->msg( "protect-level-$protectionLevel" );
468  if ( $message->isDisabled() ) {
469  // Require "$1" permission
470  $message = $this->msg( "protect-fallback", $protectionLevel )->parse();
471  } else {
472  $message = $message->escaped();
473  }
474  }
475  $expiry = $title->getRestrictionExpiry( $restrictionType );
476  $formattedexpiry = $this->msg( 'parentheses',
477  $this->getLanguage()->formatExpiry( $expiry ) )->escaped();
478  $message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
479 
480  // Messages: restriction-edit, restriction-move, restriction-create,
481  // restriction-upload
482  $pageInfo['header-restrictions'][] = [
483  $this->msg( "restriction-$restrictionType" ), $message
484  ];
485  }
486 
487  if ( !$this->page->exists() ) {
488  return $pageInfo;
489  }
490 
491  // Edit history
492  $pageInfo['header-edits'] = [];
493 
494  $firstRev = $this->page->getOldestRevision();
495  $lastRev = $this->page->getRevision();
496  $batch = new LinkBatch;
497 
498  if ( $firstRev ) {
499  $firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER );
500  if ( $firstRevUser !== '' ) {
501  $firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser );
502  $batch->addObj( $firstRevUserTitle );
503  $batch->addObj( $firstRevUserTitle->getTalkPage() );
504  }
505  }
506 
507  if ( $lastRev ) {
508  $lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER );
509  if ( $lastRevUser !== '' ) {
510  $lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser );
511  $batch->addObj( $lastRevUserTitle );
512  $batch->addObj( $lastRevUserTitle->getTalkPage() );
513  }
514  }
515 
516  $batch->execute();
517 
518  if ( $firstRev ) {
519  // Page creator
520  $pageInfo['header-edits'][] = [
521  $this->msg( 'pageinfo-firstuser' ),
522  Linker::revUserTools( $firstRev )
523  ];
524 
525  // Date of page creation
526  $pageInfo['header-edits'][] = [
527  $this->msg( 'pageinfo-firsttime' ),
529  $title,
530  htmlspecialchars( $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ) ),
531  [],
532  [ 'oldid' => $firstRev->getId() ]
533  )
534  ];
535  }
536 
537  if ( $lastRev ) {
538  // Latest editor
539  $pageInfo['header-edits'][] = [
540  $this->msg( 'pageinfo-lastuser' ),
541  Linker::revUserTools( $lastRev )
542  ];
543 
544  // Date of latest edit
545  $pageInfo['header-edits'][] = [
546  $this->msg( 'pageinfo-lasttime' ),
548  $title,
549  htmlspecialchars(
550  $lang->userTimeAndDate( $this->page->getTimestamp(), $user )
551  ),
552  [],
553  [ 'oldid' => $this->page->getLatest() ]
554  )
555  ];
556  }
557 
558  // Total number of edits
559  $pageInfo['header-edits'][] = [
560  $this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] )
561  ];
562 
563  // Total number of distinct authors
564  if ( $pageCounts['authors'] > 0 ) {
565  $pageInfo['header-edits'][] = [
566  $this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] )
567  ];
568  }
569 
570  // Recent number of edits (within past 30 days)
571  $pageInfo['header-edits'][] = [
572  $this->msg( 'pageinfo-recent-edits',
573  $lang->formatDuration( $config->get( 'RCMaxAge' ) ) ),
574  $lang->formatNum( $pageCounts['recent_edits'] )
575  ];
576 
577  // Recent number of distinct authors
578  $pageInfo['header-edits'][] = [
579  $this->msg( 'pageinfo-recent-authors' ),
580  $lang->formatNum( $pageCounts['recent_authors'] )
581  ];
582 
583  // Array of MagicWord objects
585 
586  // Array of magic word IDs
587  $wordIDs = $magicWords->names;
588 
589  // Array of IDs => localized magic words
590  $localizedWords = $wgContLang->getMagicWords();
591 
592  $listItems = [];
593  foreach ( $pageProperties as $property => $value ) {
594  if ( in_array( $property, $wordIDs ) ) {
595  $listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
596  }
597  }
598 
599  $localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
600  $hiddenCategories = $this->page->getHiddenCategories();
601 
602  if (
603  count( $listItems ) > 0 ||
604  count( $hiddenCategories ) > 0 ||
605  $pageCounts['transclusion']['from'] > 0 ||
606  $pageCounts['transclusion']['to'] > 0
607  ) {
608  $options = [ 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ];
609  $transcludedTemplates = $title->getTemplateLinksFrom( $options );
610  if ( $config->get( 'MiserMode' ) ) {
611  $transcludedTargets = [];
612  } else {
613  $transcludedTargets = $title->getTemplateLinksTo( $options );
614  }
615 
616  // Page properties
617  $pageInfo['header-properties'] = [];
618 
619  // Magic words
620  if ( count( $listItems ) > 0 ) {
621  $pageInfo['header-properties'][] = [
622  $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
623  $localizedList
624  ];
625  }
626 
627  // Hidden categories
628  if ( count( $hiddenCategories ) > 0 ) {
629  $pageInfo['header-properties'][] = [
630  $this->msg( 'pageinfo-hidden-categories' )
631  ->numParams( count( $hiddenCategories ) ),
632  Linker::formatHiddenCategories( $hiddenCategories )
633  ];
634  }
635 
636  // Transcluded templates
637  if ( $pageCounts['transclusion']['from'] > 0 ) {
638  if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
639  $more = $this->msg( 'morenotlisted' )->escaped();
640  } else {
641  $more = null;
642  }
643 
644  $templateListFormatter = new TemplatesOnThisPageFormatter(
645  $this->getContext(),
647  );
648 
649  $pageInfo['header-properties'][] = [
650  $this->msg( 'pageinfo-templates' )
651  ->numParams( $pageCounts['transclusion']['from'] ),
652  $templateListFormatter->format( $transcludedTemplates, false, $more )
653  ];
654  }
655 
656  if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) {
657  if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
658  $more = Linker::link(
659  $whatLinksHere,
660  $this->msg( 'moredotdotdot' )->escaped(),
661  [],
662  [ 'hidelinks' => 1, 'hideredirs' => 1 ]
663  );
664  } else {
665  $more = null;
666  }
667 
668  $templateListFormatter = new TemplatesOnThisPageFormatter(
669  $this->getContext(),
671  );
672 
673  $pageInfo['header-properties'][] = [
674  $this->msg( 'pageinfo-transclusions' )
675  ->numParams( $pageCounts['transclusion']['to'] ),
676  $templateListFormatter->format( $transcludedTargets, false, $more )
677  ];
678  }
679  }
680 
681  return $pageInfo;
682  }
683 
690  protected function pageCounts( Page $page ) {
691  $fname = __METHOD__;
692  $config = $this->context->getConfig();
693 
694  return ObjectCache::getMainWANInstance()->getWithSetCallback(
695  self::getCacheKey( $page->getTitle(), $page->getLatest() ),
697  function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
698  $title = $page->getTitle();
699  $id = $title->getArticleID();
700 
701  $dbr = wfGetDB( DB_REPLICA );
702  $dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
703  $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
704 
705  $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
706 
707  $result = [];
708  $result['watchers'] = $watchedItemStore->countWatchers( $title );
709 
710  if ( $config->get( 'ShowUpdatedMarker' ) ) {
711  $updated = wfTimestamp( TS_UNIX, $page->getTimestamp() );
712  $result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
713  $title,
714  $updated - $config->get( 'WatchersMaxAge' )
715  );
716  }
717 
718  // Total number of edits
719  $edits = (int)$dbr->selectField(
720  'revision',
721  'COUNT(*)',
722  [ 'rev_page' => $id ],
723  $fname
724  );
725  $result['edits'] = $edits;
726 
727  // Total number of distinct authors
728  if ( $config->get( 'MiserMode' ) ) {
729  $result['authors'] = 0;
730  } else {
731  $result['authors'] = (int)$dbr->selectField(
732  'revision',
733  'COUNT(DISTINCT rev_user_text)',
734  [ 'rev_page' => $id ],
735  $fname
736  );
737  }
738 
739  // "Recent" threshold defined by RCMaxAge setting
740  $threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) );
741 
742  // Recent number of edits
743  $edits = (int)$dbr->selectField(
744  'revision',
745  'COUNT(rev_page)',
746  [
747  'rev_page' => $id,
748  "rev_timestamp >= " . $dbr->addQuotes( $threshold )
749  ],
750  $fname
751  );
752  $result['recent_edits'] = $edits;
753 
754  // Recent number of distinct authors
755  $result['recent_authors'] = (int)$dbr->selectField(
756  'revision',
757  'COUNT(DISTINCT rev_user_text)',
758  [
759  'rev_page' => $id,
760  "rev_timestamp >= " . $dbr->addQuotes( $threshold )
761  ],
762  $fname
763  );
764 
765  // Subpages (if enabled)
766  if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
767  $conds = [ 'page_namespace' => $title->getNamespace() ];
768  $conds[] = 'page_title ' .
769  $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
770 
771  // Subpages of this page (redirects)
772  $conds['page_is_redirect'] = 1;
773  $result['subpages']['redirects'] = (int)$dbr->selectField(
774  'page',
775  'COUNT(page_id)',
776  $conds,
777  $fname
778  );
779 
780  // Subpages of this page (non-redirects)
781  $conds['page_is_redirect'] = 0;
782  $result['subpages']['nonredirects'] = (int)$dbr->selectField(
783  'page',
784  'COUNT(page_id)',
785  $conds,
786  $fname
787  );
788 
789  // Subpages of this page (total)
790  $result['subpages']['total'] = $result['subpages']['redirects']
791  + $result['subpages']['nonredirects'];
792  }
793 
794  // Counts for the number of transclusion links (to/from)
795  if ( $config->get( 'MiserMode' ) ) {
796  $result['transclusion']['to'] = 0;
797  } else {
798  $result['transclusion']['to'] = (int)$dbr->selectField(
799  'templatelinks',
800  'COUNT(tl_from)',
801  [
802  'tl_namespace' => $title->getNamespace(),
803  'tl_title' => $title->getDBkey()
804  ],
805  $fname
806  );
807  }
808 
809  $result['transclusion']['from'] = (int)$dbr->selectField(
810  'templatelinks',
811  'COUNT(*)',
812  [ 'tl_from' => $title->getArticleID() ],
813  $fname
814  );
815 
816  return $result;
817  }
818  );
819  }
820 
826  protected function getPageTitle() {
827  return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
828  }
829 
834  protected function getContributors() {
835  $contributors = $this->page->getContributors();
836  $real_names = [];
837  $user_names = [];
838  $anon_ips = [];
839 
840  # Sift for real versus user names
841 
842  foreach ( $contributors as $user ) {
843  $page = $user->isAnon()
844  ? SpecialPage::getTitleFor( 'Contributions', $user->getName() )
845  : $user->getUserPage();
846 
847  $hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' );
848  if ( $user->getId() == 0 ) {
849  $anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
850  } elseif ( !in_array( 'realname', $hiddenPrefs ) && $user->getRealName() ) {
851  $real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) );
852  } else {
853  $user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
854  }
855  }
856 
857  $lang = $this->getLanguage();
858 
859  $real = $lang->listToText( $real_names );
860 
861  # "ThisSite user(s) A, B and C"
862  if ( count( $user_names ) ) {
863  $user = $this->msg( 'siteusers' )
864  ->rawParams( $lang->listToText( $user_names ) )
865  ->params( count( $user_names ) )->escaped();
866  } else {
867  $user = false;
868  }
869 
870  if ( count( $anon_ips ) ) {
871  $anon = $this->msg( 'anonusers' )
872  ->rawParams( $lang->listToText( $anon_ips ) )
873  ->params( count( $anon_ips ) )->escaped();
874  } else {
875  $anon = false;
876  }
877 
878  # This is the big list, all mooshed together. We sift for blank strings
879  $fulllist = [];
880  foreach ( [ $real, $user, $anon ] as $s ) {
881  if ( $s !== '' ) {
882  array_push( $fulllist, $s );
883  }
884  }
885 
886  $count = count( $fulllist );
887 
888  # "Based on work by ..."
889  return $count
890  ? $this->msg( 'othercontribs' )->rawParams(
891  $lang->listToText( $fulllist ) )->params( $count )->escaped()
892  : '';
893  }
894 
900  protected function getDescription() {
901  return '';
902  }
903 
909  protected static function getCacheKey( Title $title, $revId ) {
910  return wfMemcKey( 'infoaction', md5( $title->getPrefixedText() ), $revId, self::VERSION );
911  }
912 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:525
const FOR_THIS_USER
Definition: Revision.php:93
static getMainWANInstance()
Get the main WAN cache object.
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
const VERSION
Definition: InfoAction.php:33
$property
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:246
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static getTitleFor($name, $subpage=false, $fragment= '')
Get a localised Title object for a specified special page name If you don't need a full Title object...
Definition: SpecialPage.php:82
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing after in associative array form before processing starts Return false to skip default processing and return $ret $linkRenderer
Definition: hooks.txt:1936
Handles formatting for the "templates used on this page" lists.
static getCacheSetOptions(IDatabase $db1)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:3039
static rawElement($element, $attribs=[], $contents= '')
Returns an HTML element in a string.
Definition: Html.php:209
if(!isset($args[0])) $lang
pageInfo()
Returns page information in an easily-manipulated format.
Definition: InfoAction.php:197
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:217
$value
onView()
Shows page information on GET request.
Definition: InfoAction.php:85
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 $revId
Definition: hooks.txt:1046
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 MediaWikiServices
Definition: injection.txt:23
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition: InfoAction.php:49
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1455
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:236
$contributors
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target...
Definition: Revision.php:128
makeHeader($header)
Creates a header that can be added to the output.
Definition: InfoAction.php:153
pageCounts(Page $page)
Returns page counts that would be too "expensive" to retrieve by normal means.
Definition: InfoAction.php:690
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':DEPRECATED!Use HtmlPageLinkRendererBegin instead.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:1934
const TS_UNIX
Unix time - the number of seconds since 1970-01-01 00:00:00 UTC.
Definition: defines.php:6
$batch
Definition: linkcache.txt:23
static getDoubleUnderscoreArray()
Get a MagicWordArray of double-underscore entities.
Definition: MagicWord.php:307
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:32
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static getLocalizedName($name, Language $lang=null)
Returns the localized name for a given content model.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:24
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:69
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 $options
Definition: hooks.txt:1046
static newFromTitle($title)
Factory function.
Definition: Category.php:140
const NS_CATEGORY
Definition: Defines.php:70
static getTitleValueFor($name, $subpage=false, $fragment= '')
Get a localised TitleValue object for a specified special page name.
Definition: SpecialPage.php:97
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:953
static linkKnown($target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:255
static hasSubpages($index)
Does the namespace allow subpages?
getContext()
Get the IContextSource in use here.
Definition: Action.php:178
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:62
Displays information about a page.
Definition: InfoAction.php:32
$header
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
static fetchLanguageName($code, $inLanguage=null, $include= 'all')
Definition: Language.php:888
$page
Page on which we're performing the action.
Definition: Action.php:44
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
static link($target, $html=null, $customAttribs=[], $query=[], $options=[])
This function returns an HTML link to the given target.
Definition: Linker.php:203
static escapeId($id, $options=[])
Given a value, escape it so that it can be used in an id attribute and return it. ...
Definition: Sanitizer.php:1170
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: InfoAction.php:58
An action which just does something, without showing a form first.
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
static getInstance()
Definition: PageProps.php:67
getDescription()
Returns the description that goes below the "

" tag.

Definition: InfoAction.php:900
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined...
Definition: Setup.php:36
addRow($table, $name, $value, $id)
Adds a row to a table that will be added to the content.
Definition: InfoAction.php:168
static formatHiddenCategories($hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:1970
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 $content
Definition: hooks.txt:1046
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
$count
wfMemcKey()
Make a cache key for the local wiki.
getName()
Returns the name of the action this object responds to.
Definition: InfoAction.php:40
getPageTitle()
Returns the name that goes in the "

" page title.

Definition: InfoAction.php:826
const DB_REPLICA
Definition: defines.php:22
getContributors()
Get a list of contributors of $article.
Definition: InfoAction.php:834
msg()
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:256
static getCacheKey(Title $title, $revId)
Definition: InfoAction.php:909
static revUserTools($rev, $isPublic=false)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1141
static element($element, $attribs=[], $contents= '')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:229
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk page
Definition: hooks.txt:2491
static makeTitle($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:511
addTable($content, $table)
Adds a table to the content that will be added to the output.
Definition: InfoAction.php:185
magicword txt Magic Words are some phrases used in the wikitext They are used for two that looks like templates but that don t accept any parameter *Parser functions(like{{fullurl:...}},{{#special:...}}) $magicWords['en']
Definition: magicword.txt:33
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:300