MediaWiki  master
OutputPage.php
Go to the documentation of this file.
1 <?php
27 use Wikimedia\RelPath;
28 use Wikimedia\WrappedString;
29 use Wikimedia\WrappedStringList;
30 
46 class OutputPage extends ContextSource {
48  protected $mMetatags = [];
49 
51  protected $mLinktags = [];
52 
54  protected $mCanonicalUrl = false;
55 
59  private $mPageTitle = '';
60 
68  private $displayTitle;
69 
71  private $cacheIsFinal = false;
72 
77  public $mBodytext = '';
78 
80  private $mHTMLtitle = '';
81 
86  private $mIsArticle = false;
87 
89  private $mIsArticleRelated = true;
90 
92  private $mHasCopyright = false;
93 
98  private $mPrintable = false;
99 
104  private $mSubtitle = [];
105 
107  public $mRedirect = '';
108 
110  protected $mStatusCode;
111 
116  protected $mLastModified = '';
117 
119  protected $mCategoryLinks = [];
120 
122  protected $mCategories = [
123  'hidden' => [],
124  'normal' => [],
125  ];
126 
128  protected $mIndicators = [];
129 
131  private $mLanguageLinks = [];
132 
139  private $mScripts = '';
140 
142  protected $mInlineStyles = '';
143 
148  public $mPageLinkTitle = '';
149 
151  protected $mHeadItems = [];
152 
154  protected $mAdditionalBodyClasses = [];
155 
157  protected $mModules = [];
158 
160  protected $mModuleStyles = [];
161 
163  protected $mResourceLoader;
164 
166  private $rlClient;
167 
169  private $rlClientContext;
170 
172  private $rlExemptStyleModules;
173 
175  protected $mJsConfigVars = [];
176 
178  protected $mTemplateIds = [];
179 
181  protected $mImageTimeKeys = [];
182 
184  public $mRedirectCode = '';
185 
186  protected $mFeedLinksAppendQuery = null;
187 
193  protected $mAllowedModules = [
194  ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
195  ];
196 
198  protected $mDoNothing = false;
199 
200  // Parser related.
201 
203  protected $mContainsNewMagic = 0;
204 
209  protected $mParserOptions = null;
210 
216  private $mFeedLinks = [];
217 
218  // Gwicke work on squid caching? Roughly from 2003.
219  protected $mEnableClientCache = true;
220 
222  private $mArticleBodyOnly = false;
223 
225  protected $mNewSectionLink = false;
226 
228  protected $mHideNewSectionLink = false;
229 
235  public $mNoGallery = false;
236 
238  protected $mCdnMaxage = 0;
240  protected $mCdnMaxageLimit = INF;
241 
247  protected $mPreventClickjacking = true;
248 
250  private $mRevisionId = null;
251 
253  private $mRevisionTimestamp = null;
254 
256  protected $mFileVersion = null;
257 
266  protected $styles = [];
267 
268  private $mIndexPolicy = 'index';
269  private $mFollowPolicy = 'follow';
270 
276  private $mVaryHeader = [
277  'Accept-Encoding' => null,
278  ];
279 
286  private $mRedirectedFrom = null;
287 
291  private $mProperties = [];
292 
296  private $mTarget = null;
297 
301  private $mEnableTOC = false;
302 
306  private $copyrightUrl;
307 
309  private $limitReportJSData = [];
310 
312  private $contentOverrides = [];
313 
315  private $contentOverrideCallbacks = [];
316 
320  private $mLinkHeader = [];
321 
325  private $CSP;
326 
330  private static $cacheVaryCookies = null;
331 
338  public function __construct( IContextSource $context ) {
339  $this->setContext( $context );
340  $this->CSP = new ContentSecurityPolicy(
341  $context->getRequest()->response(),
342  $context->getConfig()
343  );
344  }
345 
352  public function redirect( $url, $responsecode = '302' ) {
353  # Strip newlines as a paranoia check for header injection in PHP<5.1.2
354  $this->mRedirect = str_replace( "\n", '', $url );
355  $this->mRedirectCode = (string)$responsecode;
356  }
357 
363  public function getRedirect() {
364  return $this->mRedirect;
365  }
366 
375  public function setCopyrightUrl( $url ) {
376  $this->copyrightUrl = $url;
377  }
378 
384  public function setStatusCode( $statusCode ) {
385  $this->mStatusCode = $statusCode;
386  }
387 
395  public function addMeta( $name, $val ) {
396  $this->mMetatags[] = [ $name, $val ];
397  }
398 
405  public function getMetaTags() {
406  return $this->mMetatags;
407  }
408 
416  public function addLink( array $linkarr ) {
417  $this->mLinktags[] = $linkarr;
418  }
419 
426  public function getLinkTags() {
427  return $this->mLinktags;
428  }
429 
435  public function setCanonicalUrl( $url ) {
436  $this->mCanonicalUrl = $url;
437  }
438 
446  public function getCanonicalUrl() {
447  return $this->mCanonicalUrl;
448  }
449 
457  public function addScript( $script ) {
458  $this->mScripts .= $script;
459  }
460 
469  public function addScriptFile( $file, $unused = null ) {
470  $this->addScript( Html::linkedScript( $file, $this->CSP->getNonce() ) );
471  }
472 
479  public function addInlineScript( $script ) {
480  $this->mScripts .= Html::inlineScript( "\n$script\n", $this->CSP->getNonce() ) . "\n";
481  }
482 
491  protected function filterModules( array $modules, $position = null,
492  $type = ResourceLoaderModule::TYPE_COMBINED
493  ) {
494  $resourceLoader = $this->getResourceLoader();
495  $filteredModules = [];
496  foreach ( $modules as $val ) {
497  $module = $resourceLoader->getModule( $val );
498  if ( $module instanceof ResourceLoaderModule
499  && $module->getOrigin() <= $this->getAllowedModules( $type )
500  ) {
501  if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
502  $this->warnModuleTargetFilter( $module->getName() );
503  continue;
504  }
505  $filteredModules[] = $val;
506  }
507  }
508  return $filteredModules;
509  }
510 
511  private function warnModuleTargetFilter( $moduleName ) {
512  static $warnings = [];
513  if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
514  return;
515  }
516  $warnings[$this->mTarget][$moduleName] = true;
517  $this->getResourceLoader()->getLogger()->debug(
518  'Module "{module}" not loadable on target "{target}".',
519  [
520  'module' => $moduleName,
521  'target' => $this->mTarget,
522  ]
523  );
524  }
525 
535  public function getModules( $filter = false, $position = null, $param = 'mModules',
536  $type = ResourceLoaderModule::TYPE_COMBINED
537  ) {
538  $modules = array_values( array_unique( $this->$param ) );
539  return $filter
540  ? $this->filterModules( $modules, null, $type )
541  : $modules;
542  }
543 
549  public function addModules( $modules ) {
550  $this->mModules = array_merge( $this->mModules, (array)$modules );
551  }
552 
560  public function getModuleStyles( $filter = false, $position = null ) {
561  return $this->getModules( $filter, null, 'mModuleStyles',
562  ResourceLoaderModule::TYPE_STYLES
563  );
564  }
565 
575  public function addModuleStyles( $modules ) {
576  $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
577  }
578 
582  public function getTarget() {
583  return $this->mTarget;
584  }
585 
591  public function setTarget( $target ) {
592  $this->mTarget = $target;
593  }
594 
602  public function addContentOverride( LinkTarget $target, Content $content ) {
603  if ( !$this->contentOverrides ) {
604  // Register a callback for $this->contentOverrides on the first call
605  $this->addContentOverrideCallback( function ( LinkTarget $target ) {
606  $key = $target->getNamespace() . ':' . $target->getDBkey();
607  return $this->contentOverrides[$key] ?? null;
608  } );
609  }
610 
611  $key = $target->getNamespace() . ':' . $target->getDBkey();
612  $this->contentOverrides[$key] = $content;
613  }
614 
622  public function addContentOverrideCallback( callable $callback ) {
623  $this->contentOverrideCallbacks[] = $callback;
624  }
625 
631  public function getHeadItemsArray() {
632  return $this->mHeadItems;
633  }
634 
647  public function addHeadItem( $name, $value ) {
648  $this->mHeadItems[$name] = $value;
649  }
650 
657  public function addHeadItems( $values ) {
658  $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
659  }
660 
667  public function hasHeadItem( $name ) {
668  return isset( $this->mHeadItems[$name] );
669  }
670 
677  public function addBodyClasses( $classes ) {
678  $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes );
679  }
680 
688  public function setArticleBodyOnly( $only ) {
689  $this->mArticleBodyOnly = $only;
690  }
691 
697  public function getArticleBodyOnly() {
698  return $this->mArticleBodyOnly;
699  }
700 
708  public function setProperty( $name, $value ) {
709  $this->mProperties[$name] = $value;
710  }
711 
719  public function getProperty( $name ) {
720  return $this->mProperties[$name] ?? null;
721  }
722 
734  public function checkLastModified( $timestamp ) {
735  if ( !$timestamp || $timestamp == '19700101000000' ) {
736  wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
737  return false;
738  }
739  $config = $this->getConfig();
740  if ( !$config->get( 'CachePages' ) ) {
741  wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
742  return false;
743  }
744 
745  $timestamp = wfTimestamp( TS_MW, $timestamp );
746  $modifiedTimes = [
747  'page' => $timestamp,
748  'user' => $this->getUser()->getTouched(),
749  'epoch' => $config->get( 'CacheEpoch' )
750  ];
751  if ( $config->get( 'UseCdn' ) ) {
752  $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, $this->getCdnCacheEpoch(
753  time(),
754  $config->get( 'CdnMaxAge' )
755  ) );
756  }
757  Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
758 
759  $maxModified = max( $modifiedTimes );
760  $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
761 
762  $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
763  if ( $clientHeader === false ) {
764  wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
765  return false;
766  }
767 
768  # IE sends sizes after the date like this:
769  # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
770  # this breaks strtotime().
771  $clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
772 
773  Wikimedia\suppressWarnings(); // E_STRICT system time warnings
774  $clientHeaderTime = strtotime( $clientHeader );
775  Wikimedia\restoreWarnings();
776  if ( !$clientHeaderTime ) {
777  wfDebug( __METHOD__
778  . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
779  return false;
780  }
781  $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
782 
783  # Make debug info
784  $info = '';
785  foreach ( $modifiedTimes as $name => $value ) {
786  if ( $info !== '' ) {
787  $info .= ', ';
788  }
789  $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
790  }
791 
792  wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
793  wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
794  wfDebug( __METHOD__ . ": effective Last-Modified: " .
795  wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
796  if ( $clientHeaderTime < $maxModified ) {
797  wfDebug( __METHOD__ . ": STALE, $info", 'private' );
798  return false;
799  }
800 
801  # Not modified
802  # Give a 304 Not Modified response code and disable body output
803  wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
804  ini_set( 'zlib.output_compression', 0 );
805  $this->getRequest()->response()->statusHeader( 304 );
806  $this->sendCacheControl();
807  $this->disable();
808 
809  // Don't output a compressed blob when using ob_gzhandler;
810  // it's technically against HTTP spec and seems to confuse
811  // Firefox when the response gets split over two packets.
813 
814  return true;
815  }
816 
822  private function getCdnCacheEpoch( $reqTime, $maxAge ) {
823  // Ensure Last-Modified is never more than $wgCdnMaxAge in the past,
824  // because even if the wiki page content hasn't changed since, static
825  // resources may have changed (skin HTML, interface messages, urls, etc.)
826  // and must roll-over in a timely manner (T46570)
827  return $reqTime - $maxAge;
828  }
829 
836  public function setLastModified( $timestamp ) {
837  $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
838  }
839 
848  public function setRobotPolicy( $policy ) {
849  $policy = Article::formatRobotPolicy( $policy );
850 
851  if ( isset( $policy['index'] ) ) {
852  $this->setIndexPolicy( $policy['index'] );
853  }
854  if ( isset( $policy['follow'] ) ) {
855  $this->setFollowPolicy( $policy['follow'] );
856  }
857  }
858 
865  public function getRobotPolicy() {
866  return "{$this->mIndexPolicy},{$this->mFollowPolicy}";
867  }
868 
876  public function setIndexPolicy( $policy ) {
877  $policy = trim( $policy );
878  if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
879  $this->mIndexPolicy = $policy;
880  }
881  }
882 
888  public function getIndexPolicy() {
889  return $this->mIndexPolicy;
890  }
891 
899  public function setFollowPolicy( $policy ) {
900  $policy = trim( $policy );
901  if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
902  $this->mFollowPolicy = $policy;
903  }
904  }
905 
911  public function getFollowPolicy() {
912  return $this->mFollowPolicy;
913  }
914 
921  public function setHTMLTitle( $name ) {
922  if ( $name instanceof Message ) {
923  $this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
924  } else {
925  $this->mHTMLtitle = $name;
926  }
927  }
928 
934  public function getHTMLTitle() {
935  return $this->mHTMLtitle;
936  }
937 
943  public function setRedirectedFrom( $t ) {
944  $this->mRedirectedFrom = $t;
945  }
946 
959  public function setPageTitle( $name ) {
960  if ( $name instanceof Message ) {
961  $name = $name->setContext( $this->getContext() )->text();
962  }
963 
964  # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
965  # but leave "<i>foobar</i>" alone
966  $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
967  $this->mPageTitle = $nameWithTags;
968 
969  # change "<i>foo&amp;bar</i>" to "foo&bar"
970  $this->setHTMLTitle(
971  $this->msg( 'pagetitle' )->plaintextParams( Sanitizer::stripAllTags( $nameWithTags ) )
972  ->inContentLanguage()
973  );
974  }
975 
981  public function getPageTitle() {
982  return $this->mPageTitle;
983  }
984 
992  public function setDisplayTitle( $html ) {
993  $this->displayTitle = $html;
994  }
995 
1004  public function getDisplayTitle() {
1005  $html = $this->displayTitle;
1006  if ( $html === null ) {
1007  $html = $this->getTitle()->getPrefixedText();
1008  }
1009 
1010  return Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $html ) );
1011  }
1012 
1019  public function getUnprefixedDisplayTitle() {
1020  $text = $this->getDisplayTitle();
1021  $nsPrefix = $this->getTitle()->getNsText() . ':';
1022  $prefix = preg_quote( $nsPrefix, '/' );
1023 
1024  return preg_replace( "/^$prefix/i", '', $text );
1025  }
1026 
1032  public function setTitle( Title $t ) {
1033  // @phan-suppress-next-next-line PhanUndeclaredMethod
1034  // @fixme Not all implementations of IContextSource have this method!
1035  $this->getContext()->setTitle( $t );
1036  }
1037 
1043  public function setSubtitle( $str ) {
1044  $this->clearSubtitle();
1045  $this->addSubtitle( $str );
1046  }
1047 
1053  public function addSubtitle( $str ) {
1054  if ( $str instanceof Message ) {
1055  $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1056  } else {
1057  $this->mSubtitle[] = $str;
1058  }
1059  }
1060 
1069  public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1070  if ( $title->isRedirect() ) {
1071  $query['redirect'] = 'no';
1072  }
1073  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1074  return wfMessage( 'backlinksubtitle' )
1075  ->rawParams( $linkRenderer->makeLink( $title, null, [], $query ) );
1076  }
1077 
1084  public function addBacklinkSubtitle( Title $title, $query = [] ) {
1085  $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1086  }
1087 
1091  public function clearSubtitle() {
1092  $this->mSubtitle = [];
1093  }
1094 
1100  public function getSubtitle() {
1101  return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1102  }
1103 
1108  public function setPrintable() {
1109  $this->mPrintable = true;
1110  }
1111 
1117  public function isPrintable() {
1118  return $this->mPrintable;
1119  }
1120 
1124  public function disable() {
1125  $this->mDoNothing = true;
1126  }
1127 
1133  public function isDisabled() {
1134  return $this->mDoNothing;
1135  }
1136 
1142  public function showNewSectionLink() {
1143  return $this->mNewSectionLink;
1144  }
1145 
1151  public function forceHideNewSectionLink() {
1152  return $this->mHideNewSectionLink;
1153  }
1154 
1163  public function setSyndicated( $show = true ) {
1164  if ( $show ) {
1165  $this->setFeedAppendQuery( false );
1166  } else {
1167  $this->mFeedLinks = [];
1168  }
1169  }
1170 
1177  protected function getAdvertisedFeedTypes() {
1178  if ( $this->getConfig()->get( 'Feed' ) ) {
1179  return $this->getConfig()->get( 'AdvertisedFeedTypes' );
1180  } else {
1181  return [];
1182  }
1183  }
1184 
1194  public function setFeedAppendQuery( $val ) {
1195  $this->mFeedLinks = [];
1196 
1197  foreach ( $this->getAdvertisedFeedTypes() as $type ) {
1198  $query = "feed=$type";
1199  if ( is_string( $val ) ) {
1200  $query .= '&' . $val;
1201  }
1202  $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1203  }
1204  }
1205 
1212  public function addFeedLink( $format, $href ) {
1213  if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) {
1214  $this->mFeedLinks[$format] = $href;
1215  }
1216  }
1217 
1222  public function isSyndicated() {
1223  return count( $this->mFeedLinks ) > 0;
1224  }
1225 
1230  public function getSyndicationLinks() {
1231  return $this->mFeedLinks;
1232  }
1233 
1239  public function getFeedAppendQuery() {
1240  return $this->mFeedLinksAppendQuery;
1241  }
1242 
1250  public function setArticleFlag( $newVal ) {
1251  $this->mIsArticle = $newVal;
1252  if ( $newVal ) {
1253  $this->mIsArticleRelated = $newVal;
1254  }
1255  }
1256 
1263  public function isArticle() {
1264  return $this->mIsArticle;
1265  }
1266 
1273  public function setArticleRelated( $newVal ) {
1274  $this->mIsArticleRelated = $newVal;
1275  if ( !$newVal ) {
1276  $this->mIsArticle = false;
1277  }
1278  }
1279 
1285  public function isArticleRelated() {
1286  return $this->mIsArticleRelated;
1287  }
1288 
1294  public function setCopyright( $hasCopyright ) {
1295  $this->mHasCopyright = $hasCopyright;
1296  }
1297 
1307  public function showsCopyright() {
1308  return $this->isArticle() || $this->mHasCopyright;
1309  }
1310 
1317  public function addLanguageLinks( array $newLinkArray ) {
1318  $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1319  }
1320 
1327  public function setLanguageLinks( array $newLinkArray ) {
1328  $this->mLanguageLinks = $newLinkArray;
1329  }
1330 
1336  public function getLanguageLinks() {
1337  return $this->mLanguageLinks;
1338  }
1339 
1345  public function addCategoryLinks( array $categories ) {
1346  if ( !$categories ) {
1347  return;
1348  }
1349 
1350  $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1351 
1352  # Set all the values to 'normal'.
1353  $categories = array_fill_keys( array_keys( $categories ), 'normal' );
1354 
1355  # Mark hidden categories
1356  foreach ( $res as $row ) {
1357  if ( isset( $row->pp_value ) ) {
1358  $categories[$row->page_title] = 'hidden';
1359  }
1360  }
1361 
1362  // Avoid PHP 7.1 warning of passing $this by reference
1363  $outputPage = $this;
1364  # Add the remaining categories to the skin
1365  if ( Hooks::run(
1366  'OutputPageMakeCategoryLinks',
1367  [ &$outputPage, $categories, &$this->mCategoryLinks ] )
1368  ) {
1369  $services = MediaWikiServices::getInstance();
1370  $linkRenderer = $services->getLinkRenderer();
1371  foreach ( $categories as $category => $type ) {
1372  // array keys will cast numeric category names to ints, so cast back to string
1373  $category = (string)$category;
1374  $origcategory = $category;
1375  $title = Title::makeTitleSafe( NS_CATEGORY, $category );
1376  if ( !$title ) {
1377  continue;
1378  }
1379  $services->getContentLanguage()->findVariantLink( $category, $title, true );
1380  if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1381  continue;
1382  }
1383  $text = $services->getContentLanguage()->convertHtml( $title->getText() );
1384  $this->mCategories[$type][] = $title->getText();
1385  $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) );
1386  }
1387  }
1388  }
1389 
1394  protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1395  # Add the links to a LinkBatch
1396  $arr = [ NS_CATEGORY => $categories ];
1397  $lb = new LinkBatch;
1398  $lb->setArray( $arr );
1399 
1400  # Fetch existence plus the hiddencat property
1401  $dbr = wfGetDB( DB_REPLICA );
1402  $fields = array_merge(
1404  [ 'page_namespace', 'page_title', 'pp_value' ]
1405  );
1406 
1407  $res = $dbr->select( [ 'page', 'page_props' ],
1408  $fields,
1409  $lb->constructSet( 'page', $dbr ),
1410  __METHOD__,
1411  [],
1412  [ 'page_props' => [ 'LEFT JOIN', [
1413  'pp_propname' => 'hiddencat',
1414  'pp_page = page_id'
1415  ] ] ]
1416  );
1417 
1418  # Add the results to the link cache
1419  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1420  $lb->addResultToCache( $linkCache, $res );
1421 
1422  return $res;
1423  }
1424 
1430  public function setCategoryLinks( array $categories ) {
1431  $this->mCategoryLinks = [];
1432  $this->addCategoryLinks( $categories );
1433  }
1434 
1443  public function getCategoryLinks() {
1444  return $this->mCategoryLinks;
1445  }
1446 
1456  public function getCategories( $type = 'all' ) {
1457  if ( $type === 'all' ) {
1458  $allCategories = [];
1459  foreach ( $this->mCategories as $categories ) {
1460  $allCategories = array_merge( $allCategories, $categories );
1461  }
1462  return $allCategories;
1463  }
1464  if ( !isset( $this->mCategories[$type] ) ) {
1465  throw new InvalidArgumentException( 'Invalid category type given: ' . $type );
1466  }
1467  return $this->mCategories[$type];
1468  }
1469 
1479  public function setIndicators( array $indicators ) {
1480  $this->mIndicators = $indicators + $this->mIndicators;
1481  // Keep ordered by key
1482  ksort( $this->mIndicators );
1483  }
1484 
1493  public function getIndicators() {
1494  return $this->mIndicators;
1495  }
1496 
1505  public function addHelpLink( $to, $overrideBaseUrl = false ) {
1506  $this->addModuleStyles( 'mediawiki.helplink' );
1507  $text = $this->msg( 'helppage-top-gethelp' )->escaped();
1508 
1509  if ( $overrideBaseUrl ) {
1510  $helpUrl = $to;
1511  } else {
1512  $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1513  $helpUrl = "https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1514  }
1515 
1516  $link = Html::rawElement(
1517  'a',
1518  [
1519  'href' => $helpUrl,
1520  'target' => '_blank',
1521  'class' => 'mw-helplink',
1522  ],
1523  $text
1524  );
1525 
1526  $this->setIndicators( [ 'mw-helplink' => $link ] );
1527  }
1528 
1537  public function disallowUserJs() {
1538  $this->reduceAllowedModules(
1539  ResourceLoaderModule::TYPE_SCRIPTS,
1540  ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1541  );
1542 
1543  // Site-wide styles are controlled by a config setting, see T73621
1544  // for background on why. User styles are never allowed.
1545  if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1546  $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1547  } else {
1548  $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1549  }
1550  $this->reduceAllowedModules(
1551  ResourceLoaderModule::TYPE_STYLES,
1552  $styleOrigin
1553  );
1554  }
1555 
1562  public function getAllowedModules( $type ) {
1563  if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1564  return min( array_values( $this->mAllowedModules ) );
1565  } else {
1566  return $this->mAllowedModules[$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1567  }
1568  }
1569 
1579  public function reduceAllowedModules( $type, $level ) {
1580  $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1581  }
1582 
1588  public function prependHTML( $text ) {
1589  $this->mBodytext = $text . $this->mBodytext;
1590  }
1591 
1597  public function addHTML( $text ) {
1598  $this->mBodytext .= $text;
1599  }
1600 
1610  public function addElement( $element, array $attribs = [], $contents = '' ) {
1611  $this->addHTML( Html::element( $element, $attribs, $contents ) );
1612  }
1613 
1617  public function clearHTML() {
1618  $this->mBodytext = '';
1619  }
1620 
1626  public function getHTML() {
1627  return $this->mBodytext;
1628  }
1629 
1636  public function parserOptions() {
1637  if ( !$this->mParserOptions ) {
1638  if ( !$this->getUser()->isSafeToLoad() ) {
1639  // $wgUser isn't unstubbable yet, so don't try to get a
1640  // ParserOptions for it. And don't cache this ParserOptions
1641  // either.
1643  $po->setAllowUnsafeRawHtml( false );
1644  $po->isBogus = true;
1645  return $po;
1646  }
1647 
1648  $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1649  $this->mParserOptions->setAllowUnsafeRawHtml( false );
1650  }
1651 
1652  return $this->mParserOptions;
1653  }
1654 
1662  public function setRevisionId( $revid ) {
1663  $val = $revid === null ? null : intval( $revid );
1664  return wfSetVar( $this->mRevisionId, $val, true );
1665  }
1666 
1672  public function getRevisionId() {
1673  return $this->mRevisionId;
1674  }
1675 
1682  public function isRevisionCurrent() {
1683  return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
1684  }
1685 
1693  public function setRevisionTimestamp( $timestamp ) {
1694  return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
1695  }
1696 
1703  public function getRevisionTimestamp() {
1704  return $this->mRevisionTimestamp;
1705  }
1706 
1713  public function setFileVersion( $file ) {
1714  $val = null;
1715  if ( $file instanceof File && $file->exists() ) {
1716  $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1717  }
1718  return wfSetVar( $this->mFileVersion, $val, true );
1719  }
1720 
1726  public function getFileVersion() {
1727  return $this->mFileVersion;
1728  }
1729 
1736  public function getTemplateIds() {
1737  return $this->mTemplateIds;
1738  }
1739 
1746  public function getFileSearchOptions() {
1747  return $this->mImageTimeKeys;
1748  }
1749 
1766  public function addWikiTextAsInterface(
1767  $text, $linestart = true, Title $title = null
1768  ) {
1769  if ( $title === null ) {
1770  $title = $this->getTitle();
1771  }
1772  if ( !$title ) {
1773  throw new MWException( 'Title is null' );
1774  }
1775  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true );
1776  }
1777 
1791  public function wrapWikiTextAsInterface(
1792  $wrapperClass, $text
1793  ) {
1794  $this->addWikiTextTitleInternal(
1795  $text, $this->getTitle(),
1796  /*linestart*/true, /*interface*/true,
1797  $wrapperClass
1798  );
1799  }
1800 
1816  public function addWikiTextAsContent(
1817  $text, $linestart = true, Title $title = null
1818  ) {
1819  if ( $title === null ) {
1820  $title = $this->getTitle();
1821  }
1822  if ( !$title ) {
1823  throw new MWException( 'Title is null' );
1824  }
1825  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false );
1826  }
1827 
1840  private function addWikiTextTitleInternal(
1841  $text, Title $title, $linestart, $interface, $wrapperClass = null
1842  ) {
1843  $parserOutput = $this->parseInternal(
1844  $text, $title, $linestart, $interface
1845  );
1846 
1847  $this->addParserOutput( $parserOutput, [
1848  'enableSectionEditLinks' => false,
1849  'wrapperDivClass' => $wrapperClass ?? '',
1850  ] );
1851  }
1852 
1861  public function addParserOutputMetadata( ParserOutput $parserOutput ) {
1862  $this->mLanguageLinks =
1863  array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() );
1864  $this->addCategoryLinks( $parserOutput->getCategories() );
1865  $this->setIndicators( $parserOutput->getIndicators() );
1866  $this->mNewSectionLink = $parserOutput->getNewSection();
1867  $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1868 
1869  if ( !$parserOutput->isCacheable() ) {
1870  $this->enableClientCache( false );
1871  }
1872  $this->mNoGallery = $parserOutput->getNoGallery();
1873  $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1874  $this->addModules( $parserOutput->getModules() );
1875  $this->addModuleStyles( $parserOutput->getModuleStyles() );
1876  $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1877  $this->mPreventClickjacking = $this->mPreventClickjacking
1878  || $parserOutput->preventClickjacking();
1879  $scriptSrcs = $parserOutput->getExtraCSPScriptSrcs();
1880  foreach ( $scriptSrcs as $src ) {
1881  $this->getCSP()->addScriptSrc( $src );
1882  }
1883  $defaultSrcs = $parserOutput->getExtraCSPDefaultSrcs();
1884  foreach ( $defaultSrcs as $src ) {
1885  $this->getCSP()->addDefaultSrc( $src );
1886  }
1887  $styleSrcs = $parserOutput->getExtraCSPStyleSrcs();
1888  foreach ( $styleSrcs as $src ) {
1889  $this->getCSP()->addStyleSrc( $src );
1890  }
1891 
1892  // If $wgImagePreconnect is true, and if the output contains
1893  // images, give the user-agent a hint about foreign repos from
1894  // which those images may be served. See T123582.
1895  //
1896  // TODO: We don't have an easy way to know from which remote(s)
1897  // the image(s) will be served. For now, we only hint the first
1898  // valid one.
1899  if ( $this->getConfig()->get( 'ImagePreconnect' ) && count( $parserOutput->getImages() ) ) {
1900  $preconnect = [];
1901  $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
1902  $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$preconnect ) {
1903  $preconnect[] = wfParseUrl( $repo->getZoneUrl( 'thumb' ) )['host'];
1904  } );
1905  $preconnect[] = wfParseUrl( $repoGroup->getLocalRepo()->getZoneUrl( 'thumb' ) )['host'];
1906  foreach ( $preconnect as $host ) {
1907  if ( $host ) {
1908  $this->addLink( [ 'rel' => 'preconnect', 'href' => '//' . $host ] );
1909  break;
1910  }
1911  }
1912  }
1913 
1914  // Template versioning...
1915  foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1916  if ( isset( $this->mTemplateIds[$ns] ) ) {
1917  $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1918  } else {
1919  $this->mTemplateIds[$ns] = $dbks;
1920  }
1921  }
1922  // File versioning...
1923  foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1924  $this->mImageTimeKeys[$dbk] = $data;
1925  }
1926 
1927  // Hooks registered in the object
1928  $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1929  foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1930  list( $hookName, $data ) = $hookInfo;
1931  if ( isset( $parserOutputHooks[$hookName] ) ) {
1932  $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1933  }
1934  }
1935 
1936  // Enable OOUI if requested via ParserOutput
1937  if ( $parserOutput->getEnableOOUI() ) {
1938  $this->enableOOUI();
1939  }
1940 
1941  // Include parser limit report
1942  if ( !$this->limitReportJSData ) {
1943  $this->limitReportJSData = $parserOutput->getLimitReportJSData();
1944  }
1945 
1946  // Link flags are ignored for now, but may in the future be
1947  // used to mark individual language links.
1948  $linkFlags = [];
1949  // Avoid PHP 7.1 warning of passing $this by reference
1950  $outputPage = $this;
1951  Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1952  Hooks::runWithoutAbort( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] );
1953 
1954  // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
1955  // so that extensions may modify ParserOutput to toggle TOC.
1956  // This cannot be moved to addParserOutputText because that is not
1957  // called by EditPage for Preview.
1958  if ( $parserOutput->getTOCHTML() ) {
1959  $this->mEnableTOC = true;
1960  }
1961  }
1962 
1971  public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) {
1972  $this->addParserOutputText( $parserOutput, $poOptions );
1973 
1974  $this->addModules( $parserOutput->getModules() );
1975  $this->addModuleStyles( $parserOutput->getModuleStyles() );
1976 
1977  $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1978  }
1979 
1987  public function addParserOutputText( ParserOutput $parserOutput, $poOptions = [] ) {
1988  $text = $parserOutput->getText( $poOptions );
1989  // Avoid PHP 7.1 warning of passing $this by reference
1990  $outputPage = $this;
1991  Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] );
1992  $this->addHTML( $text );
1993  }
1994 
2001  public function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) {
2002  $this->addParserOutputMetadata( $parserOutput );
2003  $this->addParserOutputText( $parserOutput, $poOptions );
2004  }
2005 
2011  public function addTemplate( &$template ) {
2012  $this->addHTML( $template->getHTML() );
2013  }
2014 
2026  public function parseAsContent( $text, $linestart = true ) {
2027  return $this->parseInternal(
2028  $text, $this->getTitle(), $linestart, /*interface*/false
2029  )->getText( [
2030  'enableSectionEditLinks' => false,
2031  'wrapperDivClass' => ''
2032  ] );
2033  }
2034 
2047  public function parseAsInterface( $text, $linestart = true ) {
2048  return $this->parseInternal(
2049  $text, $this->getTitle(), $linestart, /*interface*/true
2050  )->getText( [
2051  'enableSectionEditLinks' => false,
2052  'wrapperDivClass' => ''
2053  ] );
2054  }
2055 
2070  public function parseInlineAsInterface( $text, $linestart = true ) {
2071  return Parser::stripOuterParagraph(
2072  $this->parseAsInterface( $text, $linestart )
2073  );
2074  }
2075 
2088  private function parseInternal( $text, $title, $linestart, $interface ) {
2089  if ( $title === null ) {
2090  throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
2091  }
2092 
2093  $popts = $this->parserOptions();
2094  $oldTidy = $popts->setTidy( true );
2095  $oldInterface = $popts->setInterfaceMessage( (bool)$interface );
2096 
2097  $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
2098  $text, $title, $popts,
2099  $linestart, true, $this->mRevisionId
2100  );
2101 
2102  $popts->setTidy( $oldTidy );
2103  $popts->setInterfaceMessage( $oldInterface );
2104 
2105  return $parserOutput;
2106  }
2107 
2113  public function setCdnMaxage( $maxage ) {
2114  $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2115  }
2116 
2126  public function lowerCdnMaxage( $maxage ) {
2127  $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2128  $this->setCdnMaxage( $this->mCdnMaxage );
2129  }
2130 
2143  public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2144  $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
2145  $maxTTL = $maxTTL ?: $this->getConfig()->get( 'CdnMaxAge' );
2146 
2147  if ( $mtime === null || $mtime === false ) {
2148  return; // entity does not exist
2149  }
2150 
2151  $age = MWTimestamp::time() - (int)wfTimestamp( TS_UNIX, $mtime );
2152  $adaptiveTTL = max( 0.9 * $age, $minTTL );
2153  $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2154 
2155  $this->lowerCdnMaxage( (int)$adaptiveTTL );
2156  }
2157 
2165  public function enableClientCache( $state ) {
2166  return wfSetVar( $this->mEnableClientCache, $state );
2167  }
2168 
2175  public function couldBePublicCached() {
2176  if ( !$this->cacheIsFinal ) {
2177  // - The entry point handles its own caching and/or doesn't use OutputPage.
2178  // (such as load.php, AjaxDispatcher, or MediaWiki\Rest\EntryPoint).
2179  //
2180  // - Or, we haven't finished processing the main part of the request yet
2181  // (e.g. Action::show, SpecialPage::execute), and the state may still
2182  // change via enableClientCache().
2183  return true;
2184  }
2185  // e.g. various error-type pages disable all client caching
2186  return $this->mEnableClientCache;
2187  }
2188 
2198  public function considerCacheSettingsFinal() {
2199  $this->cacheIsFinal = true;
2200  }
2201 
2207  public function getCacheVaryCookies() {
2208  if ( self::$cacheVaryCookies === null ) {
2209  $config = $this->getConfig();
2210  self::$cacheVaryCookies = array_values( array_unique( array_merge(
2211  SessionManager::singleton()->getVaryCookies(),
2212  [
2213  'forceHTTPS',
2214  ],
2215  $config->get( 'CacheVaryCookies' )
2216  ) ) );
2217  Hooks::run( 'GetCacheVaryCookies', [ $this, &self::$cacheVaryCookies ] );
2218  }
2219  return self::$cacheVaryCookies;
2220  }
2221 
2228  public function haveCacheVaryCookies() {
2229  $request = $this->getRequest();
2230  foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2231  if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
2232  wfDebug( __METHOD__ . ": found $cookieName\n" );
2233  return true;
2234  }
2235  }
2236  wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
2237  return false;
2238  }
2239 
2249  public function addVaryHeader( $header, array $option = null ) {
2250  if ( $option !== null && count( $option ) > 0 ) {
2251  wfDeprecated( 'addVaryHeader $option is ignored', '1.34' );
2252  }
2253  if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2254  $this->mVaryHeader[$header] = null;
2255  }
2256  }
2257 
2264  public function getVaryHeader() {
2265  // If we vary on cookies, let's make sure it's always included here too.
2266  if ( $this->getCacheVaryCookies() ) {
2267  $this->addVaryHeader( 'Cookie' );
2268  }
2269 
2270  foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2271  $this->addVaryHeader( $header, $options );
2272  }
2273  return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2274  }
2275 
2281  public function addLinkHeader( $header ) {
2282  $this->mLinkHeader[] = $header;
2283  }
2284 
2290  public function getLinkHeader() {
2291  if ( !$this->mLinkHeader ) {
2292  return false;
2293  }
2294 
2295  return 'Link: ' . implode( ',', $this->mLinkHeader );
2296  }
2297 
2305  private function addAcceptLanguage() {
2306  $title = $this->getTitle();
2307  if ( !$title instanceof Title ) {
2308  return;
2309  }
2310 
2311  $lang = $title->getPageLanguage();
2312  if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2313  $this->addVaryHeader( 'Accept-Language' );
2314  }
2315  }
2316 
2327  public function preventClickjacking( $enable = true ) {
2328  $this->mPreventClickjacking = $enable;
2329  }
2330 
2336  public function allowClickjacking() {
2337  $this->mPreventClickjacking = false;
2338  }
2339 
2346  public function getPreventClickjacking() {
2347  return $this->mPreventClickjacking;
2348  }
2349 
2357  public function getFrameOptions() {
2358  $config = $this->getConfig();
2359  if ( $config->get( 'BreakFrames' ) ) {
2360  return 'DENY';
2361  } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2362  return $config->get( 'EditPageFrameOptions' );
2363  }
2364  return false;
2365  }
2366 
2373  private function getOriginTrials() {
2374  $config = $this->getConfig();
2375 
2376  return $config->get( 'OriginTrials' );
2377  }
2378 
2379  private function getReportTo() {
2380  $config = $this->getConfig();
2381 
2382  $expiry = $config->get( 'ReportToExpiry' );
2383 
2384  if ( !$expiry ) {
2385  return false;
2386  }
2387 
2388  $endpoints = $config->get( 'ReportToEndpoints' );
2389 
2390  if ( !$endpoints ) {
2391  return false;
2392  }
2393 
2394  $output = [ 'max_age' => $expiry, 'endpoints' => [] ];
2395 
2396  foreach ( $endpoints as $endpoint ) {
2397  $output['endpoints'][] = [ 'url' => $endpoint ];
2398  }
2399 
2400  return json_encode( $output, JSON_UNESCAPED_SLASHES );
2401  }
2402 
2403  private function getFeaturePolicyReportOnly() {
2404  $config = $this->getConfig();
2405 
2406  $features = $config->get( 'FeaturePolicyReportOnly' );
2407  return implode( ';', $features );
2408  }
2409 
2413  public function sendCacheControl() {
2414  $response = $this->getRequest()->response();
2415  $config = $this->getConfig();
2416 
2417  $this->addVaryHeader( 'Cookie' );
2418  $this->addAcceptLanguage();
2419 
2420  # don't serve compressed data to clients who can't handle it
2421  # maintain different caches for logged-in users and non-logged in ones
2422  $response->header( $this->getVaryHeader() );
2423 
2424  if ( $this->mEnableClientCache ) {
2425  if (
2426  $config->get( 'UseCdn' ) &&
2427  !$response->hasCookies() &&
2428  // The client might use methods other than cookies to appear logged-in.
2429  // E.g. HTTP headers, or query parameter tokens, OAuth, etc.
2430  !SessionManager::getGlobalSession()->isPersistent() &&
2431  !$this->isPrintable() &&
2432  $this->mCdnMaxage != 0 &&
2433  !$this->haveCacheVaryCookies()
2434  ) {
2435  # We'll purge the proxy cache for anons explicitly, but require end user agents
2436  # to revalidate against the proxy on each visit.
2437  # IMPORTANT! The CDN needs to replace the Cache-Control header with
2438  # Cache-Control: s-maxage=0, must-revalidate, max-age=0
2439  wfDebug( __METHOD__ .
2440  ": local proxy caching; {$this->mLastModified} **", 'private' );
2441  # start with a shorter timeout for initial testing
2442  # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2443  $response->header( "Cache-Control: " .
2444  "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2445  } else {
2446  # We do want clients to cache if they can, but they *must* check for updates
2447  # on revisiting the page, after the max-age period.
2448  wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **", 'private' );
2449 
2450  if ( $response->hasCookies() || SessionManager::getGlobalSession()->isPersistent() ) {
2451  $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2452  $response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2453  } else {
2454  $response->header(
2455  'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $config->get( 'LoggedOutMaxAge' ) ) . ' GMT'
2456  );
2457  $response->header(
2458  "Cache-Control: private, must-revalidate, max-age={$config->get( 'LoggedOutMaxAge' )}"
2459  );
2460  }
2461  }
2462  if ( $this->mLastModified ) {
2463  $response->header( "Last-Modified: {$this->mLastModified}" );
2464  }
2465  } else {
2466  wfDebug( __METHOD__ . ": no caching **", 'private' );
2467 
2468  # In general, the absence of a last modified header should be enough to prevent
2469  # the client from using its cache. We send a few other things just to make sure.
2470  $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2471  $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2472  $response->header( 'Pragma: no-cache' );
2473  }
2474  }
2475 
2481  public function loadSkinModules( $sk ) {
2482  foreach ( $sk->getDefaultModules() as $group => $modules ) {
2483  if ( $group === 'styles' ) {
2484  foreach ( $modules as $key => $moduleMembers ) {
2485  $this->addModuleStyles( $moduleMembers );
2486  }
2487  } else {
2488  $this->addModules( $modules );
2489  }
2490  }
2491  }
2492 
2503  public function output( $return = false ) {
2504  if ( $this->mDoNothing ) {
2505  return $return ? '' : null;
2506  }
2507 
2508  $response = $this->getRequest()->response();
2509  $config = $this->getConfig();
2510 
2511  if ( $this->mRedirect != '' ) {
2512  # Standards require redirect URLs to be absolute
2513  $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
2514 
2515  $redirect = $this->mRedirect;
2516  $code = $this->mRedirectCode;
2517 
2518  if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2519  if ( $code == '301' || $code == '303' ) {
2520  if ( !$config->get( 'DebugRedirects' ) ) {
2521  $response->statusHeader( $code );
2522  }
2523  $this->mLastModified = wfTimestamp( TS_RFC2822 );
2524  }
2525  if ( $config->get( 'VaryOnXFP' ) ) {
2526  $this->addVaryHeader( 'X-Forwarded-Proto' );
2527  }
2528  $this->sendCacheControl();
2529 
2530  $response->header( "Content-Type: text/html; charset=utf-8" );
2531  if ( $config->get( 'DebugRedirects' ) ) {
2532  $url = htmlspecialchars( $redirect );
2533  print "<!DOCTYPE html>\n<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2534  print "<p>Location: <a href=\"$url\">$url</a></p>\n";
2535  print "</body>\n</html>\n";
2536  } else {
2537  $response->header( 'Location: ' . $redirect );
2538  }
2539  }
2540 
2541  return $return ? '' : null;
2542  } elseif ( $this->mStatusCode ) {
2543  $response->statusHeader( $this->mStatusCode );
2544  }
2545 
2546  # Buffer output; final headers may depend on later processing
2547  ob_start();
2548 
2549  $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2550  $response->header( 'Content-language: ' .
2551  MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2552 
2553  $linkHeader = $this->getLinkHeader();
2554  if ( $linkHeader ) {
2555  $response->header( $linkHeader );
2556  }
2557 
2558  // Prevent framing, if requested
2559  $frameOptions = $this->getFrameOptions();
2560  if ( $frameOptions ) {
2561  $response->header( "X-Frame-Options: $frameOptions" );
2562  }
2563 
2564  $originTrials = $this->getOriginTrials();
2565  foreach ( $originTrials as $originTrial ) {
2566  $response->header( "Origin-Trial: $originTrial", false );
2567  }
2568 
2569  $reportTo = $this->getReportTo();
2570  if ( $reportTo ) {
2571  $response->header( "Report-To: $reportTo" );
2572  }
2573 
2574  $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
2575  if ( $featurePolicyReportOnly ) {
2576  $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" );
2577  }
2578 
2579  if ( $this->mArticleBodyOnly ) {
2580  $this->CSP->sendHeaders();
2581  echo $this->mBodytext;
2582  } else {
2583  // Enable safe mode if requested (T152169)
2584  if ( $this->getRequest()->getBool( 'safemode' ) ) {
2585  $this->disallowUserJs();
2586  }
2587 
2588  $sk = $this->getSkin();
2589  $this->loadSkinModules( $sk );
2590 
2591  MWDebug::addModules( $this );
2592 
2593  // Avoid PHP 7.1 warning of passing $this by reference
2594  $outputPage = $this;
2595  // Hook that allows last minute changes to the output page, e.g.
2596  // adding of CSS or Javascript by extensions, adding CSP sources.
2597  Hooks::runWithoutAbort( 'BeforePageDisplay', [ &$outputPage, &$sk ] );
2598 
2599  $this->CSP->sendHeaders();
2600 
2601  try {
2602  $sk->outputPage();
2603  } catch ( Exception $e ) {
2604  ob_end_clean(); // bug T129657
2605  throw $e;
2606  }
2607  }
2608 
2609  try {
2610  // This hook allows last minute changes to final overall output by modifying output buffer
2611  Hooks::runWithoutAbort( 'AfterFinalPageOutput', [ $this ] );
2612  } catch ( Exception $e ) {
2613  ob_end_clean(); // bug T129657
2614  throw $e;
2615  }
2616 
2617  $this->sendCacheControl();
2618 
2619  if ( $return ) {
2620  return ob_get_clean();
2621  } else {
2622  ob_end_flush();
2623  return null;
2624  }
2625  }
2626 
2637  public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2638  $this->setPageTitle( $pageTitle );
2639  if ( $htmlTitle !== false ) {
2640  $this->setHTMLTitle( $htmlTitle );
2641  }
2642  $this->setRobotPolicy( 'noindex,nofollow' );
2643  $this->setArticleRelated( false );
2644  $this->enableClientCache( false );
2645  $this->mRedirect = '';
2646  $this->clearSubtitle();
2647  $this->clearHTML();
2648  }
2649 
2662  public function showErrorPage( $title, $msg, $params = [] ) {
2663  if ( !$title instanceof Message ) {
2664  $title = $this->msg( $title );
2665  }
2666 
2667  $this->prepareErrorPage( $title );
2668 
2669  if ( $msg instanceof Message ) {
2670  if ( $params !== [] ) {
2671  trigger_error( 'Argument ignored: $params. The message parameters argument '
2672  . 'is discarded when the $msg argument is a Message object instead of '
2673  . 'a string.', E_USER_NOTICE );
2674  }
2675  $this->addHTML( $msg->parseAsBlock() );
2676  } else {
2677  $this->addWikiMsgArray( $msg, $params );
2678  }
2679 
2680  $this->returnToMain();
2681  }
2682 
2689  public function showPermissionsErrorPage( array $errors, $action = null ) {
2690  $services = MediaWikiServices::getInstance();
2691  $permissionManager = $services->getPermissionManager();
2692  foreach ( $errors as $key => $error ) {
2693  $errors[$key] = (array)$error;
2694  }
2695 
2696  // For some action (read, edit, create and upload), display a "login to do this action"
2697  // error if all of the following conditions are met:
2698  // 1. the user is not logged in
2699  // 2. the only error is insufficient permissions (i.e. no block or something else)
2700  // 3. the error can be avoided simply by logging in
2701 
2702  if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2703  && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2704  && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2705  && ( $permissionManager->groupHasPermission( 'user', $action )
2706  || $permissionManager->groupHasPermission( 'autoconfirmed', $action ) )
2707  ) {
2708  $displayReturnto = null;
2709 
2710  # Due to T34276, if a user does not have read permissions,
2711  # $this->getTitle() will just give Special:Badtitle, which is
2712  # not especially useful as a returnto parameter. Use the title
2713  # from the request instead, if there was one.
2714  $request = $this->getRequest();
2715  $returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2716  if ( $action == 'edit' ) {
2717  $msg = 'whitelistedittext';
2718  $displayReturnto = $returnto;
2719  } elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2720  $msg = 'nocreatetext';
2721  } elseif ( $action == 'upload' ) {
2722  $msg = 'uploadnologintext';
2723  } else { # Read
2724  $msg = 'loginreqpagetext';
2725  $displayReturnto = Title::newMainPage();
2726  }
2727 
2728  $query = [];
2729 
2730  if ( $returnto ) {
2731  $query['returnto'] = $returnto->getPrefixedText();
2732 
2733  if ( !$request->wasPosted() ) {
2734  $returntoquery = $request->getValues();
2735  unset( $returntoquery['title'] );
2736  unset( $returntoquery['returnto'] );
2737  unset( $returntoquery['returntoquery'] );
2738  $query['returntoquery'] = wfArrayToCgi( $returntoquery );
2739  }
2740  }
2741 
2742  $title = SpecialPage::getTitleFor( 'Userlogin' );
2743  $linkRenderer = $services->getLinkRenderer();
2744  $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
2745  $loginLink = $linkRenderer->makeKnownLink(
2746  $title,
2747  $this->msg( 'loginreqlink' )->text(),
2748  [],
2749  $query
2750  );
2751 
2752  $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2753  $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
2754 
2755  # Don't return to a page the user can't read otherwise
2756  # we'll end up in a pointless loop
2757  if ( $displayReturnto && $permissionManager->userCan(
2758  'read', $this->getUser(), $displayReturnto
2759  ) ) {
2760  $this->returnToMain( null, $displayReturnto );
2761  }
2762  } else {
2763  $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2764  $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) );
2765  }
2766  }
2767 
2774  public function versionRequired( $version ) {
2775  $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2776 
2777  $this->addWikiMsg( 'versionrequiredtext', $version );
2778  $this->returnToMain();
2779  }
2780 
2788  public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2789  if ( $action == null ) {
2790  $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2791  } else {
2792  $action_desc = $this->msg( "action-$action" )->plain();
2793  $text = $this->msg(
2794  'permissionserrorstext-withaction',
2795  count( $errors ),
2796  $action_desc
2797  )->plain() . "\n\n";
2798  }
2799 
2800  if ( count( $errors ) > 1 ) {
2801  $text .= '<ul class="permissions-errors">' . "\n";
2802 
2803  foreach ( $errors as $error ) {
2804  $text .= '<li>';
2805  $text .= $this->msg( ...$error )->plain();
2806  $text .= "</li>\n";
2807  }
2808  $text .= '</ul>';
2809  } else {
2810  $text .= "<div class=\"permissions-errors\">\n" .
2811  $this->msg( ...reset( $errors ) )->plain() .
2812  "\n</div>";
2813  }
2814 
2815  return $text;
2816  }
2817 
2827  public function showLagWarning( $lag ) {
2828  $config = $this->getConfig();
2829  if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2830  $lag = floor( $lag ); // floor to avoid nano seconds to display
2831  $message = $lag < $config->get( 'SlaveLagCritical' )
2832  ? 'lag-warn-normal'
2833  : 'lag-warn-high';
2834  // For grep: mw-lag-warn-normal, mw-lag-warn-high
2835  $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2836  $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2837  }
2838  }
2839 
2846  public function showFatalError( $message ) {
2847  $this->prepareErrorPage( $this->msg( 'internalerror' ) );
2848 
2849  $this->addHTML( $message );
2850  }
2851 
2860  public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2861  $linkRenderer = MediaWikiServices::getInstance()
2862  ->getLinkRendererFactory()->createFromLegacyOptions( $options );
2863  $link = $this->msg( 'returnto' )->rawParams(
2864  $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped();
2865  $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2866  }
2867 
2876  public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2877  if ( $returnto == null ) {
2878  $returnto = $this->getRequest()->getText( 'returnto' );
2879  }
2880 
2881  if ( $returntoquery == null ) {
2882  $returntoquery = $this->getRequest()->getText( 'returntoquery' );
2883  }
2884 
2885  if ( $returnto === '' ) {
2886  $returnto = Title::newMainPage();
2887  }
2888 
2889  if ( is_object( $returnto ) ) {
2890  $titleObj = $returnto;
2891  } else {
2892  $titleObj = Title::newFromText( $returnto );
2893  }
2894  // We don't want people to return to external interwiki. That
2895  // might potentially be used as part of a phishing scheme
2896  if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
2897  $titleObj = Title::newMainPage();
2898  }
2899 
2900  $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
2901  }
2902 
2903  private function getRlClientContext() {
2904  if ( !$this->rlClientContext ) {
2905  $query = ResourceLoader::makeLoaderQuery(
2906  [], // modules; not relevant
2907  $this->getLanguage()->getCode(),
2908  $this->getSkin()->getSkinName(),
2909  $this->getUser()->isLoggedIn() ? $this->getUser()->getName() : null,
2910  null, // version; not relevant
2911  ResourceLoader::inDebugMode(),
2912  null, // only; not relevant
2913  $this->isPrintable(),
2914  $this->getRequest()->getBool( 'handheld' )
2915  );
2916  $this->rlClientContext = new ResourceLoaderContext(
2917  $this->getResourceLoader(),
2918  new FauxRequest( $query )
2919  );
2920  if ( $this->contentOverrideCallbacks ) {
2921  $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
2922  $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) {
2923  foreach ( $this->contentOverrideCallbacks as $callback ) {
2924  $content = $callback( $title );
2925  if ( $content !== null ) {
2927  if ( strpos( $text, '</script>' ) !== false ) {
2928  // Proactively replace this so that we can display a message
2929  // to the user, instead of letting it go to Html::inlineScript(),
2930  // where it would be considered a server-side issue.
2931  $titleFormatted = $title->getPrefixedText();
2933  Xml::encodeJsCall( 'mw.log.error', [
2934  "Cannot preview $titleFormatted due to script-closing tag."
2935  ] )
2936  );
2937  }
2938  return $content;
2939  }
2940  }
2941  return null;
2942  } );
2943  }
2944  }
2945  return $this->rlClientContext;
2946  }
2947 
2959  public function getRlClient() {
2960  if ( !$this->rlClient ) {
2961  $context = $this->getRlClientContext();
2962  $rl = $this->getResourceLoader();
2963  $this->addModules( [
2964  'user',
2965  'user.options',
2966  ] );
2967  $this->addModuleStyles( [
2968  'site.styles',
2969  'noscript',
2970  'user.styles',
2971  ] );
2972  $this->getSkin()->setupSkinUserCss( $this );
2973 
2974  // Prepare exempt modules for buildExemptModules()
2975  $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
2976  $exemptStates = [];
2977  $moduleStyles = $this->getModuleStyles( /*filter*/ true );
2978 
2979  // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
2980  // Separate user-specific batch for improved cache-hit ratio.
2981  $userBatch = [ 'user.styles', 'user' ];
2982  $siteBatch = array_diff( $moduleStyles, $userBatch );
2983  $dbr = wfGetDB( DB_REPLICA );
2986 
2987  // Filter out modules handled by buildExemptModules()
2988  $moduleStyles = array_filter( $moduleStyles,
2989  function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
2990  $module = $rl->getModule( $name );
2991  if ( $module ) {
2992  $group = $module->getGroup();
2993  if ( isset( $exemptGroups[$group] ) ) {
2994  $exemptStates[$name] = 'ready';
2995  if ( !$module->isKnownEmpty( $context ) ) {
2996  // E.g. Don't output empty <styles>
2997  $exemptGroups[$group][] = $name;
2998  }
2999  return false;
3000  }
3001  }
3002  return true;
3003  }
3004  );
3005  $this->rlExemptStyleModules = $exemptGroups;
3006 
3007  $rlClient = new ResourceLoaderClientHtml( $context, [
3008  'target' => $this->getTarget(),
3009  'nonce' => $this->CSP->getNonce(),
3010  // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
3011  // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
3012  // modules enqueud for loading on this page is filtered to just those.
3013  // However, to make sure we also apply the restriction to dynamic dependencies and
3014  // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
3015  // StartupModule so that the client-side registry will not contain any restricted
3016  // modules either. (T152169, T185303)
3017  'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
3018  <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
3019  ) ? '1' : null,
3020  ] );
3021  $rlClient->setConfig( $this->getJSVars() );
3022  $rlClient->setModules( $this->getModules( /*filter*/ true ) );
3023  $rlClient->setModuleStyles( $moduleStyles );
3024  $rlClient->setExemptStates( $exemptStates );
3025  $this->rlClient = $rlClient;
3026  }
3027  return $this->rlClient;
3028  }
3029 
3035  public function headElement( Skin $sk, $includeStyle = true ) {
3036  $config = $this->getConfig();
3037  $userdir = $this->getLanguage()->getDir();
3038  $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
3039 
3040  $pieces = [];
3041  $htmlAttribs = Sanitizer::mergeAttributes(
3042  $this->getRlClient()->getDocumentAttributes(),
3044  );
3045  $pieces[] = Html::htmlHeader( $htmlAttribs );
3046  $pieces[] = Html::openElement( 'head' );
3047 
3048  if ( $this->getHTMLTitle() == '' ) {
3049  $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3050  }
3051 
3052  if ( !Html::isXmlMimeType( $config->get( 'MimeType' ) ) ) {
3053  // Add <meta charset="UTF-8">
3054  // This should be before <title> since it defines the charset used by
3055  // text including the text inside <title>.
3056  // The spec recommends defining XHTML5's charset using the XML declaration
3057  // instead of meta.
3058  // Our XML declaration is output by Html::htmlHeader.
3059  // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
3060  // https://html.spec.whatwg.org/multipage/semantics.html#charset
3061  $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
3062  }
3063 
3064  $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
3065  $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null );
3066  $pieces[] = $this->buildExemptModules();
3067  $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
3068  $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
3069 
3070  // This library is intended to run on older browsers that MediaWiki no longer
3071  // supports as Grade A. For these Grade C browsers, we provide an experience
3072  // using only HTML and CSS. But, where standards-compliant browsers are able to
3073  // style unknown HTML elements without issue, old IE ignores these styles.
3074  // The html5shiv library fixes that.
3075  // Use an IE conditional comment to serve the script only to old IE
3076  $shivUrl = $config->get( 'ResourceBasePath' ) . '/resources/lib/html5shiv/html5shiv.js';
3077  $pieces[] = '<!--[if lt IE 9]>' .
3078  Html::linkedScript( $shivUrl, $this->CSP->getNonce() ) .
3079  '<![endif]-->';
3080 
3081  $pieces[] = Html::closeElement( 'head' );
3082 
3083  $bodyClasses = $this->mAdditionalBodyClasses;
3084  $bodyClasses[] = 'mediawiki';
3085 
3086  # Classes for LTR/RTL directionality support
3087  $bodyClasses[] = $userdir;
3088  $bodyClasses[] = "sitedir-$sitedir";
3089 
3090  $underline = $this->getUser()->getOption( 'underline' );
3091  if ( $underline < 2 ) {
3092  // The following classes can be used here:
3093  // * mw-underline-always
3094  // * mw-underline-never
3095  $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' );
3096  }
3097 
3098  if ( $this->getLanguage()->capitalizeAllNouns() ) {
3099  # A <body> class is probably not the best way to do this . . .
3100  $bodyClasses[] = 'capitalize-all-nouns';
3101  }
3102 
3103  // Parser feature migration class
3104  // The idea is that this will eventually be removed, after the wikitext
3105  // which requires it is cleaned up.
3106  $bodyClasses[] = 'mw-hide-empty-elt';
3107 
3108  $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
3109  $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
3110  $bodyClasses[] =
3111  'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
3112 
3113  $bodyAttrs = [];
3114  // While the implode() is not strictly needed, it's used for backwards compatibility
3115  // (this used to be built as a string and hooks likely still expect that).
3116  $bodyAttrs['class'] = implode( ' ', $bodyClasses );
3117 
3118  // Allow skins and extensions to add body attributes they need
3119  $sk->addToBodyAttributes( $this, $bodyAttrs );
3120  Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
3121 
3122  $pieces[] = Html::openElement( 'body', $bodyAttrs );
3123 
3124  return self::combineWrappedStrings( $pieces );
3125  }
3126 
3132  public function getResourceLoader() {
3133  if ( $this->mResourceLoader === null ) {
3134  // Lazy-initialise as needed
3135  $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
3136  }
3137  return $this->mResourceLoader;
3138  }
3139 
3148  public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
3149  // Apply 'target' and 'origin' filters
3150  $modules = $this->filterModules( (array)$modules, null, $only );
3151 
3153  $this->getRlClientContext(),
3154  $modules,
3155  $only,
3156  $extraQuery,
3157  $this->CSP->getNonce()
3158  );
3159  }
3160 
3167  protected static function combineWrappedStrings( array $chunks ) {
3168  // Filter out empty values
3169  $chunks = array_filter( $chunks, 'strlen' );
3170  return WrappedString::join( "\n", $chunks );
3171  }
3172 
3179  public function getBottomScripts() {
3180  $chunks = [];
3181  $chunks[] = $this->getRlClient()->getBodyHtml();
3182 
3183  // Legacy non-ResourceLoader scripts
3184  $chunks[] = $this->mScripts;
3185 
3186  if ( $this->limitReportJSData ) {
3187  $chunks[] = ResourceLoader::makeInlineScript(
3188  ResourceLoader::makeConfigSetScript(
3189  [ 'wgPageParseReport' => $this->limitReportJSData ]
3190  ),
3191  $this->CSP->getNonce()
3192  );
3193  }
3194 
3195  return self::combineWrappedStrings( $chunks );
3196  }
3197 
3204  public function getJsConfigVars() {
3205  return $this->mJsConfigVars;
3206  }
3207 
3214  public function addJsConfigVars( $keys, $value = null ) {
3215  if ( is_array( $keys ) ) {
3216  foreach ( $keys as $key => $value ) {
3217  $this->mJsConfigVars[$key] = $value;
3218  }
3219  return;
3220  }
3221 
3222  $this->mJsConfigVars[$keys] = $value;
3223  }
3224 
3234  public function getJSVars() {
3235  $curRevisionId = 0;
3236  $articleId = 0;
3237  $canonicalSpecialPageName = false; # T23115
3238  $services = MediaWikiServices::getInstance();
3239 
3240  $title = $this->getTitle();
3241  $ns = $title->getNamespace();
3242  $nsInfo = $services->getNamespaceInfo();
3243  $canonicalNamespace = $nsInfo->exists( $ns )
3244  ? $nsInfo->getCanonicalName( $ns )
3245  : $title->getNsText();
3246 
3247  $sk = $this->getSkin();
3248  // Get the relevant title so that AJAX features can use the correct page name
3249  // when making API requests from certain special pages (T36972).
3250  $relevantTitle = $sk->getRelevantTitle();
3251  $relevantUser = $sk->getRelevantUser();
3252 
3253  if ( $ns == NS_SPECIAL ) {
3254  list( $canonicalSpecialPageName, /*...*/ ) =
3255  $services->getSpecialPageFactory()->
3256  resolveAlias( $title->getDBkey() );
3257  } elseif ( $this->canUseWikiPage() ) {
3258  $wikiPage = $this->getWikiPage();
3259  $curRevisionId = $wikiPage->getLatest();
3260  $articleId = $wikiPage->getId();
3261  }
3262 
3263  $lang = $title->getPageViewLanguage();
3264 
3265  // Pre-process information
3266  $separatorTransTable = $lang->separatorTransformTable();
3267  $separatorTransTable = $separatorTransTable ?: [];
3268  $compactSeparatorTransTable = [
3269  implode( "\t", array_keys( $separatorTransTable ) ),
3270  implode( "\t", $separatorTransTable ),
3271  ];
3272  $digitTransTable = $lang->digitTransformTable();
3273  $digitTransTable = $digitTransTable ?: [];
3274  $compactDigitTransTable = [
3275  implode( "\t", array_keys( $digitTransTable ) ),
3276  implode( "\t", $digitTransTable ),
3277  ];
3278 
3279  $user = $this->getUser();
3280 
3281  // Internal variables for MediaWiki core
3282  $vars = [
3283  // @internal For mediawiki.page.startup
3284  'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3285 
3286  // @internal For jquery.tablesorter
3287  'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3288  'wgDigitTransformTable' => $compactDigitTransTable,
3289  'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3290  'wgMonthNames' => $lang->getMonthNamesArray(),
3291 
3292  // @internal For debugging purposes
3293  'wgRequestId' => WebRequest::getRequestId(),
3294 
3295  // @internal For mw.loader
3296  'wgCSPNonce' => $this->CSP->getNonce(),
3297  ];
3298 
3299  // Start of supported and stable config vars (for use by extensions/gadgets).
3300  $vars += [
3301  'wgCanonicalNamespace' => $canonicalNamespace,
3302  'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3303  'wgNamespaceNumber' => $title->getNamespace(),
3304  'wgPageName' => $title->getPrefixedDBkey(),
3305  'wgTitle' => $title->getText(),
3306  'wgCurRevisionId' => $curRevisionId,
3307  'wgRevisionId' => (int)$this->getRevisionId(),
3308  'wgArticleId' => $articleId,
3309  'wgIsArticle' => $this->isArticle(),
3310  'wgIsRedirect' => $title->isRedirect(),
3311  'wgAction' => Action::getActionName( $this->getContext() ),
3312  'wgUserName' => $user->isAnon() ? null : $user->getName(),
3313  'wgUserGroups' => $user->getEffectiveGroups(),
3314  'wgCategories' => $this->getCategories(),
3315  'wgPageContentLanguage' => $lang->getCode(),
3316  'wgPageContentModel' => $title->getContentModel(),
3317  'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3318  'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3319  ];
3320  if ( $user->isLoggedIn() ) {
3321  $vars['wgUserId'] = $user->getId();
3322  $vars['wgUserEditCount'] = $user->getEditCount();
3323  $userReg = $user->getRegistration();
3324  $vars['wgUserRegistration'] = $userReg ? (int)wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3325  // Get the revision ID of the oldest new message on the user's talk
3326  // page. This can be used for constructing new message alerts on
3327  // the client side.
3328  $userNewMsgRevId = $user->getNewMessageRevisionId();
3329  // Only occupy precious space in the <head> when it is non-null (T53640)
3330  // mw.config.get returns null by default.
3331  if ( $userNewMsgRevId ) {
3332  $vars['wgUserNewMsgRevisionId'] = $userNewMsgRevId;
3333  }
3334  }
3335  $contLang = $services->getContentLanguage();
3336  if ( $contLang->hasVariants() ) {
3337  $vars['wgUserVariant'] = $contLang->getPreferredVariant();
3338  }
3339  // Same test as SkinTemplate
3340  $vars['wgIsProbablyEditable'] = $this->userCanEditOrCreate( $user, $title );
3341  $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle &&
3342  $this->userCanEditOrCreate( $user, $relevantTitle );
3343  foreach ( $title->getRestrictionTypes() as $type ) {
3344  // Following keys are set in $vars:
3345  // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
3346  $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3347  }
3348  if ( $title->isMainPage() ) {
3349  $vars['wgIsMainPage'] = true;
3350  }
3351  if ( $relevantUser ) {
3352  $vars['wgRelevantUserName'] = $relevantUser->getName();
3353  }
3354  // End of stable config vars
3355 
3356  if ( $this->mRedirectedFrom ) {
3357  // @internal For skin JS
3358  $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3359  }
3360 
3361  // Allow extensions to add their custom variables to the mw.config map.
3362  // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3363  // page-dependant but site-wide (without state).
3364  // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3365  Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3366 
3367  // Merge in variables from addJsConfigVars last
3368  return array_merge( $vars, $this->getJsConfigVars() );
3369  }
3370 
3380  public function userCanPreview() {
3381  $request = $this->getRequest();
3382  if (
3383  $request->getVal( 'action' ) !== 'submit' ||
3384  !$request->wasPosted()
3385  ) {
3386  return false;
3387  }
3388 
3389  $user = $this->getUser();
3390 
3391  if ( !$user->isLoggedIn() ) {
3392  // Anons have predictable edit tokens
3393  return false;
3394  }
3395  if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3396  return false;
3397  }
3398 
3399  $title = $this->getTitle();
3400  $errors = MediaWikiServices::getInstance()->getPermissionManager()
3401  ->getPermissionErrors( 'edit', $user, $title );
3402  if ( count( $errors ) !== 0 ) {
3403  return false;
3404  }
3405 
3406  return true;
3407  }
3408 
3414  private function userCanEditOrCreate(
3415  User $user,
3417  ) {
3418  $pm = MediaWikiServices::getInstance()->getPermissionManager();
3419  return $pm->quickUserCan( 'edit', $user, $title )
3420  && ( $this->getTitle()->exists() ||
3421  $pm->quickUserCan( 'create', $user, $title ) );
3422  }
3423 
3427  public function getHeadLinksArray() {
3428  $tags = [];
3429  $config = $this->getConfig();
3430 
3431  $canonicalUrl = $this->mCanonicalUrl;
3432 
3433  $tags['meta-generator'] = Html::element( 'meta', [
3434  'name' => 'generator',
3435  'content' => 'MediaWiki ' . MW_VERSION,
3436  ] );
3437 
3438  if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3439  // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
3440  // fallbacks should come before the primary value so we need to reverse the array.
3441  foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
3442  $tags["meta-referrer-$i"] = Html::element( 'meta', [
3443  'name' => 'referrer',
3444  'content' => $policy,
3445  ] );
3446  }
3447  }
3448 
3449  $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3450  if ( $p !== 'index,follow' ) {
3451  // http://www.robotstxt.org/wc/meta-user.html
3452  // Only show if it's different from the default robots policy
3453  $tags['meta-robots'] = Html::element( 'meta', [
3454  'name' => 'robots',
3455  'content' => $p,
3456  ] );
3457  }
3458 
3459  foreach ( $this->mMetatags as $tag ) {
3460  if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) {
3461  $a = 'http-equiv';
3462  $tag[0] = substr( $tag[0], 5 );
3463  } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) {
3464  $a = 'property';
3465  } else {
3466  $a = 'name';
3467  }
3468  $tagName = "meta-{$tag[0]}";
3469  if ( isset( $tags[$tagName] ) ) {
3470  $tagName .= $tag[1];
3471  }
3472  $tags[$tagName] = Html::element( 'meta',
3473  [
3474  $a => $tag[0],
3475  'content' => $tag[1]
3476  ]
3477  );
3478  }
3479 
3480  foreach ( $this->mLinktags as $tag ) {
3481  $tags[] = Html::element( 'link', $tag );
3482  }
3483 
3484  # Universal edit button
3485  if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3486  if ( $this->userCanEditOrCreate( $this->getUser(), $this->getTitle() ) ) {
3487  // Original UniversalEditButton
3488  $msg = $this->msg( 'edit' )->text();
3489  $tags['universal-edit-button'] = Html::element( 'link', [
3490  'rel' => 'alternate',
3491  'type' => 'application/x-wiki',
3492  'title' => $msg,
3493  'href' => $this->getTitle()->getEditURL(),
3494  ] );
3495  // Alternate edit link
3496  $tags['alternative-edit'] = Html::element( 'link', [
3497  'rel' => 'edit',
3498  'title' => $msg,
3499  'href' => $this->getTitle()->getEditURL(),
3500  ] );
3501  }
3502  }
3503 
3504  # Generally the order of the favicon and apple-touch-icon links
3505  # should not matter, but Konqueror (3.5.9 at least) incorrectly
3506  # uses whichever one appears later in the HTML source. Make sure
3507  # apple-touch-icon is specified first to avoid this.
3508  if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3509  $tags['apple-touch-icon'] = Html::element( 'link', [
3510  'rel' => 'apple-touch-icon',
3511  'href' => $config->get( 'AppleTouchIcon' )
3512  ] );
3513  }
3514 
3515  if ( $config->get( 'Favicon' ) !== false ) {
3516  $tags['favicon'] = Html::element( 'link', [
3517  'rel' => 'shortcut icon',
3518  'href' => $config->get( 'Favicon' )
3519  ] );
3520  }
3521 
3522  # OpenSearch description link
3523  $tags['opensearch'] = Html::element( 'link', [
3524  'rel' => 'search',
3525  'type' => 'application/opensearchdescription+xml',
3526  'href' => wfScript( 'opensearch_desc' ),
3527  'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3528  ] );
3529 
3530  # Real Simple Discovery link, provides auto-discovery information
3531  # for the MediaWiki API (and potentially additional custom API
3532  # support such as WordPress or Twitter-compatible APIs for a
3533  # blogging extension, etc)
3534  $tags['rsd'] = Html::element( 'link', [
3535  'rel' => 'EditURI',
3536  'type' => 'application/rsd+xml',
3537  // Output a protocol-relative URL here if $wgServer is protocol-relative.
3538  // Whether RSD accepts relative or protocol-relative URLs is completely
3539  // undocumented, though.
3540  'href' => wfExpandUrl( wfAppendQuery(
3541  wfScript( 'api' ),
3542  [ 'action' => 'rsd' ] ),
3544  ),
3545  ] );
3546 
3547  # Language variants
3548  if ( !$config->get( 'DisableLangConversion' ) ) {
3549  $lang = $this->getTitle()->getPageLanguage();
3550  if ( $lang->hasVariants() ) {
3551  $variants = $lang->getVariants();
3552  foreach ( $variants as $variant ) {
3553  $tags["variant-$variant"] = Html::element( 'link', [
3554  'rel' => 'alternate',
3555  'hreflang' => LanguageCode::bcp47( $variant ),
3556  'href' => $this->getTitle()->getLocalURL(
3557  [ 'variant' => $variant ] )
3558  ]
3559  );
3560  }
3561  # x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3562  $tags["variant-x-default"] = Html::element( 'link', [
3563  'rel' => 'alternate',
3564  'hreflang' => 'x-default',
3565  'href' => $this->getTitle()->getLocalURL() ] );
3566  }
3567  }
3568 
3569  # Copyright
3570  if ( $this->copyrightUrl !== null ) {
3571  $copyright = $this->copyrightUrl;
3572  } else {
3573  $copyright = '';
3574  if ( $config->get( 'RightsPage' ) ) {
3575  $copy = Title::newFromText( $config->get( 'RightsPage' ) );
3576 
3577  if ( $copy ) {
3578  $copyright = $copy->getLocalURL();
3579  }
3580  }
3581 
3582  if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3583  $copyright = $config->get( 'RightsUrl' );
3584  }
3585  }
3586 
3587  if ( $copyright ) {
3588  $tags['copyright'] = Html::element( 'link', [
3589  'rel' => 'license',
3590  'href' => $copyright ]
3591  );
3592  }
3593 
3594  # Feeds
3595  if ( $config->get( 'Feed' ) ) {
3596  $feedLinks = [];
3597 
3598  foreach ( $this->getSyndicationLinks() as $format => $link ) {
3599  # Use the page name for the title. In principle, this could
3600  # lead to issues with having the same name for different feeds
3601  # corresponding to the same page, but we can't avoid that at
3602  # this low a level.
3603 
3604  $feedLinks[] = $this->feedLink(
3605  $format,
3606  $link,
3607  # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3608  $this->msg(
3609  "page-{$format}-feed", $this->getTitle()->getPrefixedText()
3610  )->text()
3611  );
3612  }
3613 
3614  # Recent changes feed should appear on every page (except recentchanges,
3615  # that would be redundant). Put it after the per-page feed to avoid
3616  # changing existing behavior. It's still available, probably via a
3617  # menu in your browser. Some sites might have a different feed they'd
3618  # like to promote instead of the RC feed (maybe like a "Recent New Articles"
3619  # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3620  # If so, use it instead.
3621  $sitename = $config->get( 'Sitename' );
3622  $overrideSiteFeed = $config->get( 'OverrideSiteFeed' );
3623  if ( $overrideSiteFeed ) {
3624  foreach ( $overrideSiteFeed as $type => $feedUrl ) {
3625  // Note, this->feedLink escapes the url.
3626  $feedLinks[] = $this->feedLink(
3627  $type,
3628  $feedUrl,
3629  $this->msg( "site-{$type}-feed", $sitename )->text()
3630  );
3631  }
3632  } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3633  $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3634  foreach ( $this->getAdvertisedFeedTypes() as $format ) {
3635  $feedLinks[] = $this->feedLink(
3636  $format,
3637  $rctitle->getLocalURL( [ 'feed' => $format ] ),
3638  # For grep: 'site-rss-feed', 'site-atom-feed'
3639  $this->msg( "site-{$format}-feed", $sitename )->text()
3640  );
3641  }
3642  }
3643 
3644  # Allow extensions to change the list pf feeds. This hook is primarily for changing,
3645  # manipulating or removing existing feed tags. If you want to add new feeds, you should
3646  # use OutputPage::addFeedLink() instead.
3647  Hooks::run( 'AfterBuildFeedLinks', [ &$feedLinks ] );
3648 
3649  $tags += $feedLinks;
3650  }
3651 
3652  # Canonical URL
3653  if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3654  if ( $canonicalUrl !== false ) {
3655  $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3656  } elseif ( $this->isArticleRelated() ) {
3657  // This affects all requests where "setArticleRelated" is true. This is
3658  // typically all requests that show content (query title, curid, oldid, diff),
3659  // and all wikipage actions (edit, delete, purge, info, history etc.).
3660  // It does not apply to File pages and Special pages.
3661  // 'history' and 'info' actions address page metadata rather than the page
3662  // content itself, so they may not be canonicalized to the view page url.
3663  // TODO: this ought to be better encapsulated in the Action class.
3664  $action = Action::getActionName( $this->getContext() );
3665  if ( in_array( $action, [ 'history', 'info' ] ) ) {
3666  $query = "action={$action}";
3667  } else {
3668  $query = '';
3669  }
3670  $canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3671  } else {
3672  $reqUrl = $this->getRequest()->getRequestURL();
3673  $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3674  }
3675  }
3676  if ( $canonicalUrl !== false ) {
3677  $tags[] = Html::element( 'link', [
3678  'rel' => 'canonical',
3679  'href' => $canonicalUrl
3680  ] );
3681  }
3682 
3683  // Allow extensions to add, remove and/or otherwise manipulate these links
3684  // If you want only to *add* <head> links, please use the addHeadItem()
3685  // (or addHeadItems() for multiple items) method instead.
3686  // This hook is provided as a last resort for extensions to modify these
3687  // links before the output is sent to client.
3688  Hooks::run( 'OutputPageAfterGetHeadLinksArray', [ &$tags, $this ] );
3689 
3690  return $tags;
3691  }
3692 
3701  private function feedLink( $type, $url, $text ) {
3702  return Html::element( 'link', [
3703  'rel' => 'alternate',
3704  'type' => "application/$type+xml",
3705  'title' => $text,
3706  'href' => $url ]
3707  );
3708  }
3709 
3719  public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3720  $options = [];
3721  if ( $media ) {
3722  $options['media'] = $media;
3723  }
3724  if ( $condition ) {
3725  $options['condition'] = $condition;
3726  }
3727  if ( $dir ) {
3728  $options['dir'] = $dir;
3729  }
3730  $this->styles[$style] = $options;
3731  }
3732 
3740  public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3741  if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3742  # If wanted, and the interface is right-to-left, flip the CSS
3743  $style_css = CSSJanus::transform( $style_css, true, false );
3744  }
3745  $this->mInlineStyles .= Html::inlineStyle( $style_css );
3746  }
3747 
3753  protected function buildExemptModules() {
3754  $chunks = [];
3755 
3756  // Requirements:
3757  // - Within modules provided by the software (core, skin, extensions),
3758  // styles from skin stylesheets should be overridden by styles
3759  // from modules dynamically loaded with JavaScript.
3760  // - Styles from site-specific, private, and user modules should override
3761  // both of the above.
3762  //
3763  // The effective order for stylesheets must thus be:
3764  // 1. Page style modules, formatted server-side by ResourceLoaderClientHtml.
3765  // 2. Dynamically-loaded styles, inserted client-side by mw.loader.
3766  // 3. Styles that are site-specific, private or from the user, formatted
3767  // server-side by this function.
3768  //
3769  // The 'ResourceLoaderDynamicStyles' marker helps JavaScript know where
3770  // point #2 is.
3771 
3772  // Add legacy styles added through addStyle()/addInlineStyle() here
3773  $chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3774 
3775  // Things that go after the ResourceLoaderDynamicStyles marker
3776  $append = [];
3777  $separateReq = [ 'site.styles', 'user.styles' ];
3778  foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3779  if ( $moduleNames ) {
3780  $append[] = $this->makeResourceLoaderLink(
3781  array_diff( $moduleNames, $separateReq ),
3782  ResourceLoaderModule::TYPE_STYLES
3783  );
3784 
3785  foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) {
3786  // These require their own dedicated request in order to support "@import"
3787  // syntax, which is incompatible with concatenation. (T147667, T37562)
3788  $append[] = $this->makeResourceLoaderLink( $name,
3789  ResourceLoaderModule::TYPE_STYLES
3790  );
3791  }
3792  }
3793  }
3794  if ( $append ) {
3795  $chunks[] = Html::element(
3796  'meta',
3797  [ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3798  );
3799  $chunks = array_merge( $chunks, $append );
3800  }
3801 
3802  return self::combineWrappedStrings( $chunks );
3803  }
3804 
3808  public function buildCssLinksArray() {
3809  $links = [];
3810 
3811  foreach ( $this->styles as $file => $options ) {
3812  $link = $this->styleLink( $file, $options );
3813  if ( $link ) {
3814  $links[$file] = $link;
3815  }
3816  }
3817  return $links;
3818  }
3819 
3827  protected function styleLink( $style, array $options ) {
3828  if ( isset( $options['dir'] ) && $this->getLanguage()->getDir() != $options['dir'] ) {
3829  return '';
3830  }
3831 
3832  if ( isset( $options['media'] ) ) {
3833  $media = self::transformCssMedia( $options['media'] );
3834  if ( $media === null ) {
3835  return '';
3836  }
3837  } else {
3838  $media = 'all';
3839  }
3840 
3841  if ( substr( $style, 0, 1 ) == '/' ||
3842  substr( $style, 0, 5 ) == 'http:' ||
3843  substr( $style, 0, 6 ) == 'https:' ) {
3844  $url = $style;
3845  } else {
3846  $config = $this->getConfig();
3847  // Append file hash as query parameter
3848  $url = self::transformResourcePath(
3849  $config,
3850  $config->get( 'StylePath' ) . '/' . $style
3851  );
3852  }
3853 
3854  $link = Html::linkedStyle( $url, $media );
3855 
3856  if ( isset( $options['condition'] ) ) {
3857  $condition = htmlspecialchars( $options['condition'] );
3858  $link = "<!--[if $condition]>$link<![endif]-->";
3859  }
3860  return $link;
3861  }
3862 
3884  public static function transformResourcePath( Config $config, $path ) {
3885  global $IP;
3886 
3887  $localDir = $IP;
3888  $remotePathPrefix = $config->get( 'ResourceBasePath' );
3889  if ( $remotePathPrefix === '' ) {
3890  // The configured base path is required to be empty string for
3891  // wikis in the domain root
3892  $remotePath = '/';
3893  } else {
3894  $remotePath = $remotePathPrefix;
3895  }
3896  if ( strpos( $path, $remotePath ) !== 0 || substr( $path, 0, 2 ) === '//' ) {
3897  // - Path is outside wgResourceBasePath, ignore.
3898  // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib.
3899  return $path;
3900  }
3901  // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here.
3902  // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth
3903  // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath)
3904  // which is not expected to be in wgResourceBasePath on CDNs. (T155146)
3905  $uploadPath = $config->get( 'UploadPath' );
3906  if ( strpos( $path, $uploadPath ) === 0 ) {
3907  $localDir = $config->get( 'UploadDirectory' );
3908  $remotePathPrefix = $remotePath = $uploadPath;
3909  }
3910 
3911  $path = RelPath::getRelativePath( $path, $remotePath );
3912  return self::transformFilePath( $remotePathPrefix, $localDir, $path );
3913  }
3914 
3926  public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
3927  $hash = md5_file( "$localPath/$file" );
3928  if ( $hash === false ) {
3929  wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
3930  $hash = '';
3931  }
3932  return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3933  }
3934 
3942  public static function transformCssMedia( $media ) {
3943  global $wgRequest;
3944 
3945  // https://www.w3.org/TR/css3-mediaqueries/#syntax
3946  $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
3947 
3948  // Switch in on-screen display for media testing
3949  $switches = [
3950  'printable' => 'print',
3951  'handheld' => 'handheld',
3952  ];
3953  foreach ( $switches as $switch => $targetMedia ) {
3954  if ( $wgRequest->getBool( $switch ) ) {
3955  if ( $media == $targetMedia ) {
3956  $media = '';
3957  } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3958  /* This regex will not attempt to understand a comma-separated media_query_list
3959  *
3960  * Example supported values for $media:
3961  * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
3962  * Example NOT supported value for $media:
3963  * '3d-glasses, screen, print and resolution > 90dpi'
3964  *
3965  * If it's a print request, we never want any kind of screen stylesheets
3966  * If it's a handheld request (currently the only other choice with a switch),
3967  * we don't want simple 'screen' but we might want screen queries that
3968  * have a max-width or something, so we'll pass all others on and let the
3969  * client do the query.
3970  */
3971  if ( $targetMedia == 'print' || $media == 'screen' ) {
3972  return null;
3973  }
3974  }
3975  }
3976  }
3977 
3978  return $media;
3979  }
3980 
3989  public function addWikiMsg( ...$args ) {
3990  $name = array_shift( $args );
3991  $this->addWikiMsgArray( $name, $args );
3992  }
3993 
4002  public function addWikiMsgArray( $name, $args ) {
4003  $this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
4004  }
4005 
4032  public function wrapWikiMsg( $wrap, ...$msgSpecs ) {
4033  $s = $wrap;
4034  foreach ( $msgSpecs as $n => $spec ) {
4035  if ( is_array( $spec ) ) {
4036  $args = $spec;
4037  $name = array_shift( $args );
4038  } else {
4039  $args = [];
4040  $name = $spec;
4041  }
4042  $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
4043  }
4044  $this->addWikiTextAsInterface( $s );
4045  }
4046 
4052  public function isTOCEnabled() {
4053  return $this->mEnableTOC;
4054  }
4055 
4063  public static function setupOOUI( $skinName = 'default', $dir = 'ltr' ) {
4065  $theme = $themes[$skinName] ?? $themes['default'];
4066  // For example, 'OOUI\WikimediaUITheme'.
4067  $themeClass = "OOUI\\{$theme}Theme";
4068  OOUI\Theme::setSingleton( new $themeClass() );
4069  OOUI\Element::setDefaultDir( $dir );
4070  }
4071 
4078  public function enableOOUI() {
4079  self::setupOOUI(
4080  strtolower( $this->getSkin()->getSkinName() ),
4081  $this->getLanguage()->getDir()
4082  );
4083  $this->addModuleStyles( [
4084  'oojs-ui-core.styles',
4085  'oojs-ui.styles.indicators',
4086  'mediawiki.widgets.styles',
4087  'oojs-ui-core.icons',
4088  ] );
4089  }
4090 
4101  public function getCSPNonce() {
4102  return $this->CSP->getNonce();
4103  }
4104 
4111  public function getCSP() {
4112  return $this->CSP;
4113  }
4114 }
Action\getActionName
static getActionName(IContextSource $context)
Get the action that will be executed, not necessarily the one passed passed through the "action" requ...
Definition: Action.php:138
ParserOutput\getEnableOOUI
getEnableOOUI()
Definition: ParserOutput.php:656
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:63
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:332
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:28
ResourceLoaderClientHtml
Load and configure a ResourceLoader client on an HTML page.
Definition: ResourceLoaderClientHtml.php:30
PROTO_CANONICAL
const PROTO_CANONICAL
Definition: Defines.php:212
ParserOutput
Definition: ParserOutput.php:25
Article\formatRobotPolicy
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition: Article.php:1046
User\getId
getId()
Get the user's ID.
Definition: User.php:2159
User\isAnon
isAnon()
Get whether the user is anonymous.
Definition: User.php:3519
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:35
$response
$response
Definition: opensearch_desc.php:44
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:137
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
User\getEditCount
getEditCount()
Get the user's edit count.
Definition: User.php:3399
wfSetVar
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
Definition: GlobalFunctions.php:1541
ParserOutput\getModules
getModules()
Definition: ParserOutput.php:609
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1806
ParserOutput\getImages
& getImages()
Definition: ParserOutput.php:585
$resourceLoader
$resourceLoader
Definition: load.php:42
User\getNewMessageRevisionId
getNewMessageRevisionId()
Get the revision ID for the last talk page revision viewed by the talk page owner.
Definition: User.php:2367
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:39
ParserOutput\getJsConfigVars
getJsConfigVars()
Definition: ParserOutput.php:621
wfUrlencode
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
Definition: GlobalFunctions.php:309
ParserOptions\newFromAnon
static newFromAnon()
Get a ParserOptions object for an anonymous user.
Definition: ParserOptions.php:999
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
Skin\addToBodyAttributes
addToBodyAttributes( $out, &$bodyAttrs)
This will be called by OutputPage::headElement when it is creating the "<body>" tag,...
Definition: Skin.php:480
ResourceLoaderClientHtml\setExemptStates
setExemptStates(array $states)
Set state of special modules that are handled by the caller manually.
Definition: ResourceLoaderClientHtml.php:109
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1198
$s
$s
Definition: mergeMessageFileList.php:185
SpecialPage\getTitleFor
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:83
ContextSource\canUseWikiPage
canUseWikiPage()
Check whether a WikiPage object can be get with getWikiPage().
Definition: ContextSource.php:91
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1064
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:71
Title\newMainPage
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:657
Message
$res
$res
Definition: testCompression.php:57
ParserOutput\getHeadItems
getHeadItems()
Definition: ParserOutput.php:605
ContextSource\getUser
getUser()
Definition: ContextSource.php:120
ContextSource\getTitle
getTitle()
Definition: ContextSource.php:79
Skin\getHtmlElementAttributes
getHtmlElementAttributes()
Return values for <html> element.
Definition: Skin.php:464
LinkBatch\setArray
setArray( $array)
Set the link list to a given 2-d array First key is the namespace, second is the DB key,...
Definition: LinkBatch.php:147
LinkCache\getSelectFields
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:219
$dbr
$dbr
Definition: testCompression.php:54
ResourceLoaderClientHtml\setConfig
setConfig(array $vars)
Set mw.config variables.
Definition: ResourceLoaderClientHtml.php:78
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:128
wfAppendQuery
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Definition: GlobalFunctions.php:439
ParserOutput\getModuleStyles
getModuleStyles()
Definition: ParserOutput.php:613
User\matchEditToken
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4407
Xml\encodeJsCall
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:679
Config
Interface for configuration instances.
Definition: Config.php:28
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:58
ParserOutput\getExtraCSPDefaultSrcs
getExtraCSPDefaultSrcs()
Get extra Content-Security-Policy 'default-src' directives.
Definition: ParserOutput.php:665
MediaWiki\Linker\LinkTarget\getNamespace
getNamespace()
Get the namespace index.
wfParseUrl
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
Definition: GlobalFunctions.php:793
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:61
ParserOutput\getLimitReportJSData
getLimitReportJSData()
Definition: ParserOutput.php:652
MWException
MediaWiki exception.
Definition: MWException.php:26
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2564
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
ResourceLoaderWikiModule\preloadTitleInfo
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
Definition: ResourceLoaderWikiModule.php:460
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2497
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:29
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:104
$modules
$modules
Definition: HTMLFormElement.php:13
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:211
ContextSource\getSkin
getSkin()
Definition: ContextSource.php:136
$args
if( $line===false) $args
Definition: mcc.php:124
User\getEffectiveGroups
getEffectiveGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition: User.php:3323
wfCgiToArray
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
Definition: GlobalFunctions.php:392
$title
$title
Definition: testCompression.php:38
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
ParserOutput\getIndicators
getIndicators()
Definition: ParserOutput.php:561
ResourceLoaderModule\getOrigin
getOrigin()
Get this module's origin.
Definition: ResourceLoaderModule.php:139
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:55
ParserOutput\getLanguageLinks
& getLanguageLinks()
Definition: ParserOutput.php:541
JavaScriptContent
Content for JavaScript pages.
Definition: JavaScriptContent.php:35
ParserOutput\getTemplateIds
& getTemplateIds()
Definition: ParserOutput.php:581
ParserOutput\getTOCHTML
getTOCHTML()
Definition: ParserOutput.php:637
ParserOutput\getExtraCSPScriptSrcs
getExtraCSPScriptSrcs()
Get extra Content-Security-Policy 'script-src' directives.
Definition: ParserOutput.php:674
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:168
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:621
Skin\getRelevantTitle
getRelevantTitle()
Return the "relevant" title.
Definition: Skin.php:331
ParserOutput\getNewSection
getNewSection()
Definition: ParserOutput.php:767
Hooks\runWithoutAbort
static runWithoutAbort( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:231
$content
$content
Definition: router.php:78
Skin\getPageClasses
getPageClasses( $title)
TODO: document.
Definition: Skin.php:426
wfClearOutputBuffers
wfClearOutputBuffers()
More legible than passing a 'false' parameter to wfResetOutputBuffers():
Definition: GlobalFunctions.php:1685
Skin\getDefaultModules
getDefaultModules()
Defines the ResourceLoader modules that should be added to the skin It is recommended that skins wish...
Definition: Skin.php:169
ParserOptions\newFromContext
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
Definition: ParserOptions.php:1038
$header
$header
Definition: updateCredits.php:41
ParserOutput\getOutputHooks
getOutputHooks()
Definition: ParserOutput.php:625
ParserOutput\getHideNewSection
getHideNewSection()
Definition: ParserOutput.php:763
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:50
DerivativeResourceLoaderContext
A mutable version of ResourceLoaderContext.
Definition: DerivativeResourceLoaderContext.php:33
MediaWiki\Linker\LinkTarget\getDBkey
getDBkey()
Get the main part with underscores.
PROTO_RELATIVE
const PROTO_RELATIVE
Definition: Defines.php:210
ParserOutput\getNoGallery
getNoGallery()
Definition: ParserOutput.php:601
getSkinThemeMap
static getSkinThemeMap()
Return a map of skin names (in lowercase) to OOUI theme names, defining which theme a given skin shou...
Definition: ResourceLoaderOOUIModule.php:76
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
$context
$context
Definition: load.php:43
Content
Base interface for content objects.
Definition: Content.php:34
ResourceLoaderClientHtml\makeLoad
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicitly load or embed modules on a page.
Definition: ResourceLoaderClientHtml.php:391
User\getRegistration
getRegistration()
Get the timestamp of account creation.
Definition: User.php:4686
ParserOutput\getFileSearchOptions
& getFileSearchOptions()
Definition: ParserOutput.php:589
Title
Represents a title within MediaWiki.
Definition: Title.php:42
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:36
ContentHandler\getContentText
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
Definition: ContentHandler.php:85
ResourceLoaderClientHtml\setModules
setModules(array $modules)
Ensure one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:89
User\isLoggedIn
isLoggedIn()
Get whether the user is logged in.
Definition: User.php:3511
ResourceLoaderClientHtml\setModuleStyles
setModuleStyles(array $modules)
Ensure the styles of one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:98
WebRequest\getRequestId
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:330
$path
$path
Definition: NoLocalSettings.php:25
ParserOutput\getCategories
& getCategories()
Definition: ParserOutput.php:553
LanguageCode\bcp47
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Definition: LanguageCode.php:175
Skin\getRelevantUser
getRelevantUser()
Return the "relevant" user.
Definition: Skin.php:352
$keys
$keys
Definition: testCompression.php:72
MWDebug\addModules
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition: MWDebug.php:120
CacheTime\isCacheable
isCacheable()
Definition: CacheTime.php:155
ParserOutput\getText
getText( $options=[])
Get the output HTML.
Definition: ParserOutput.php:340
$t
$t
Definition: testCompression.php:74
Skin\outputPage
outputPage()
Outputs the HTML generated by other functions.
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:641
Skin\getSkinName
getSkinName()
Definition: Skin.php:148
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:38
$IP
$IP
Definition: WebStart.php:49
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:54
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2188
ContentSecurityPolicy
Definition: ContentSecurityPolicy.php:30
ParserOutput\getExtraCSPStyleSrcs
getExtraCSPStyleSrcs()
Get extra Content-Security-Policy 'style-src' directives.
Definition: ParserOutput.php:683
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:491
ParserOutput\preventClickjacking
preventClickjacking( $flag=null)
Get or set the prevent-clickjacking flag.
Definition: ParserOutput.php:1335
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
Definition: GlobalFunctions.php:347
$type
$type
Definition: testCompression.php:52