27 use Wikimedia\RelPath;
28 use Wikimedia\WrappedString;
29 use Wikimedia\WrappedStringList;
48 protected $mMetatags = [];
51 protected $mLinktags = [];
54 protected $mCanonicalUrl =
false;
59 private $mPageTitle =
'';
68 private $displayTitle;
74 public $mBodytext =
'';
77 private $mHTMLtitle =
'';
83 private $mIsArticle =
false;
86 private $mIsArticleRelated =
true;
89 private $mHasCopyright =
false;
95 private $mPrintable =
false;
101 private $mSubtitle = [];
104 public $mRedirect =
'';
107 protected $mStatusCode;
113 protected $mLastModified =
'';
116 protected $mCategoryLinks = [];
119 protected $mCategories = [
125 protected $mIndicators = [];
128 private $mLanguageLinks = [];
136 private $mScripts =
'';
139 protected $mInlineStyles =
'';
145 public $mPageLinkTitle =
'';
148 protected $mHeadItems = [];
151 protected $mAdditionalBodyClasses = [];
154 protected $mModules = [];
157 protected $mModuleStyles = [];
160 protected $mResourceLoader;
166 private $rlClientContext;
169 private $rlExemptStyleModules;
172 protected $mJsConfigVars = [];
175 protected $mTemplateIds = [];
178 protected $mImageTimeKeys = [];
181 public $mRedirectCode =
'';
183 protected $mFeedLinksAppendQuery =
null;
190 protected $mAllowedModules = [
191 ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
195 protected $mDoNothing =
false;
200 protected $mContainsNewMagic = 0;
206 protected $mParserOptions =
null;
213 private $mFeedLinks = [];
216 protected $mEnableClientCache =
true;
219 private $mArticleBodyOnly =
false;
222 protected $mNewSectionLink =
false;
225 protected $mHideNewSectionLink =
false;
232 public $mNoGallery =
false;
235 protected $mCdnMaxage = 0;
237 protected $mCdnMaxageLimit = INF;
244 protected $mPreventClickjacking =
true;
247 private $mRevisionId =
null;
250 private $mRevisionTimestamp =
null;
253 protected $mFileVersion =
null;
263 protected $styles = [];
265 private $mIndexPolicy =
'index';
266 private $mFollowPolicy =
'follow';
273 private $mVaryHeader = [
274 'Accept-Encoding' =>
null,
283 private $mRedirectedFrom =
null;
288 private $mProperties = [];
293 private $mTarget =
null;
298 private $mEnableTOC =
false;
303 private $copyrightUrl;
306 private $limitReportJSData = [];
309 private $contentOverrides = [];
312 private $contentOverrideCallbacks = [];
317 private $mLinkHeader = [];
327 private static $cacheVaryCookies =
null;
345 public function redirect( $url, $responsecode =
'302' ) {
346 # Strip newlines as a paranoia check for header injection in PHP<5.1.2
347 $this->mRedirect = str_replace(
"\n",
'', $url );
348 $this->mRedirectCode = $responsecode;
356 public function getRedirect() {
357 return $this->mRedirect;
368 public function setCopyrightUrl( $url ) {
369 $this->copyrightUrl = $url;
377 public function setStatusCode( $statusCode ) {
378 $this->mStatusCode = $statusCode;
388 public function addMeta( $name, $val ) {
389 $this->mMetatags[] = [ $name, $val ];
398 public function getMetaTags() {
399 return $this->mMetatags;
409 public function addLink( array $linkarr ) {
410 $this->mLinktags[] = $linkarr;
419 public function getLinkTags() {
420 return $this->mLinktags;
428 public function setCanonicalUrl( $url ) {
429 $this->mCanonicalUrl = $url;
439 public function getCanonicalUrl() {
440 return $this->mCanonicalUrl;
450 public function addScript( $script ) {
451 $this->mScripts .= $script;
462 public function addScriptFile(
$file, $unused =
null ) {
463 $this->addScript( Html::linkedScript(
$file, $this->getCSPNonce() ) );
472 public function addInlineScript( $script ) {
473 $this->mScripts .= Html::inlineScript(
"\n$script\n", $this->getCSPNonce() ) .
"\n";
484 protected function filterModules( array
$modules, $position =
null,
485 $type = ResourceLoaderModule::TYPE_COMBINED
488 $filteredModules = [];
494 if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
495 $this->warnModuleTargetFilter( $module->getName() );
498 $filteredModules[] = $val;
501 return $filteredModules;
504 private function warnModuleTargetFilter( $moduleName ) {
505 static $warnings = [];
506 if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
509 $warnings[$this->mTarget][$moduleName] =
true;
510 $this->getResourceLoader()->getLogger()->debug(
511 'Module "{module}" not loadable on target "{target}".',
513 'module' => $moduleName,
514 'target' => $this->mTarget,
528 public function getModules(
$filter =
false, $position =
null, $param =
'mModules',
529 $type = ResourceLoaderModule::TYPE_COMBINED
531 $modules = array_values( array_unique( $this->$param ) );
542 public function addModules(
$modules ) {
543 $this->mModules = array_merge( $this->mModules, (array)
$modules );
553 public function getModuleStyles(
$filter =
false, $position =
null ) {
554 return $this->getModules(
$filter,
null,
'mModuleStyles',
555 ResourceLoaderModule::TYPE_STYLES
568 public function addModuleStyles(
$modules ) {
569 $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)
$modules );
575 public function getTarget() {
576 return $this->mTarget;
584 public function setTarget( $target ) {
585 $this->mTarget = $target;
596 if ( !$this->contentOverrides ) {
598 $this->addContentOverrideCallback(
function (
LinkTarget $target ) {
600 return $this->contentOverrides[$key] ??
null;
605 $this->contentOverrides[$key] =
$content;
615 public function addContentOverrideCallback( callable $callback ) {
616 $this->contentOverrideCallbacks[] = $callback;
624 public function getHeadItemsArray() {
625 return $this->mHeadItems;
640 public function addHeadItem( $name, $value ) {
641 $this->mHeadItems[$name] = $value;
650 public function addHeadItems( $values ) {
651 $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
660 public function hasHeadItem( $name ) {
661 return isset( $this->mHeadItems[$name] );
670 public function addBodyClasses( $classes ) {
671 $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes );
681 public function setArticleBodyOnly( $only ) {
682 $this->mArticleBodyOnly = $only;
690 public function getArticleBodyOnly() {
691 return $this->mArticleBodyOnly;
701 public function setProperty( $name, $value ) {
702 $this->mProperties[$name] = $value;
712 public function getProperty( $name ) {
713 return $this->mProperties[$name] ??
null;
727 public function checkLastModified( $timestamp ) {
728 if ( !$timestamp || $timestamp ==
'19700101000000' ) {
729 wfDebug( __METHOD__ .
": CACHE DISABLED, NO TIMESTAMP\n" );
733 if ( !$config->
get(
'CachePages' ) ) {
734 wfDebug( __METHOD__ .
": CACHE DISABLED\n" );
740 'page' => $timestamp,
741 'user' => $this->
getUser()->getTouched(),
742 'epoch' => $config->
get(
'CacheEpoch' )
744 if ( $config->
get(
'UseCdn' ) ) {
745 $modifiedTimes[
'sepoch'] =
wfTimestamp( TS_MW, $this->getCdnCacheEpoch(
747 $config->
get(
'CdnMaxAge' )
750 Hooks::run(
'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
752 $maxModified = max( $modifiedTimes );
753 $this->mLastModified =
wfTimestamp( TS_RFC2822, $maxModified );
755 $clientHeader = $this->
getRequest()->getHeader(
'If-Modified-Since' );
756 if ( $clientHeader ===
false ) {
757 wfDebug( __METHOD__ .
": client did not send If-Modified-Since header",
'private' );
761 # IE sends sizes after the date like this:
762 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
763 # this breaks strtotime().
764 $clientHeader = preg_replace(
'/;.*$/',
'', $clientHeader );
766 Wikimedia\suppressWarnings();
767 $clientHeaderTime = strtotime( $clientHeader );
768 Wikimedia\restoreWarnings();
769 if ( !$clientHeaderTime ) {
771 .
": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
774 $clientHeaderTime =
wfTimestamp( TS_MW, $clientHeaderTime );
778 foreach ( $modifiedTimes as $name => $value ) {
779 if ( $info !==
'' ) {
782 $info .=
"$name=" .
wfTimestamp( TS_ISO_8601, $value );
785 wfDebug( __METHOD__ .
": client sent If-Modified-Since: " .
786 wfTimestamp( TS_ISO_8601, $clientHeaderTime ),
'private' );
787 wfDebug( __METHOD__ .
": effective Last-Modified: " .
788 wfTimestamp( TS_ISO_8601, $maxModified ),
'private' );
789 if ( $clientHeaderTime < $maxModified ) {
790 wfDebug( __METHOD__ .
": STALE, $info",
'private' );
795 # Give a 304 Not Modified response code and disable body output
796 wfDebug( __METHOD__ .
": NOT MODIFIED, $info",
'private' );
797 ini_set(
'zlib.output_compression', 0 );
798 $this->
getRequest()->response()->statusHeader( 304 );
799 $this->sendCacheControl();
815 private function getCdnCacheEpoch( $reqTime, $maxAge ) {
820 return $reqTime - $maxAge;
829 public function setLastModified( $timestamp ) {
830 $this->mLastModified =
wfTimestamp( TS_RFC2822, $timestamp );
841 public function setRobotPolicy( $policy ) {
844 if ( isset( $policy[
'index'] ) ) {
845 $this->setIndexPolicy( $policy[
'index'] );
847 if ( isset( $policy[
'follow'] ) ) {
848 $this->setFollowPolicy( $policy[
'follow'] );
859 public function setIndexPolicy( $policy ) {
860 $policy = trim( $policy );
861 if ( in_array( $policy, [
'index',
'noindex' ] ) ) {
862 $this->mIndexPolicy = $policy;
873 public function setFollowPolicy( $policy ) {
874 $policy = trim( $policy );
875 if ( in_array( $policy, [
'follow',
'nofollow' ] ) ) {
876 $this->mFollowPolicy = $policy;
886 public function setHTMLTitle( $name ) {
887 if ( $name instanceof
Message ) {
888 $this->mHTMLtitle = $name->setContext( $this->
getContext() )->text();
890 $this->mHTMLtitle = $name;
899 public function getHTMLTitle() {
900 return $this->mHTMLtitle;
908 public function setRedirectedFrom(
$t ) {
909 $this->mRedirectedFrom =
$t;
924 public function setPageTitle( $name ) {
925 if ( $name instanceof
Message ) {
926 $name = $name->setContext( $this->
getContext() )->text();
929 # change "<script>foo&bar</script>" to "<script>foo&bar</script>"
930 # but leave "<i>foobar</i>" alone
931 $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
932 $this->mPageTitle = $nameWithTags;
934 # change "<i>foo&bar</i>" to "foo&bar"
936 $this->
msg(
'pagetitle' )->plaintextParams( Sanitizer::stripAllTags( $nameWithTags ) )
937 ->inContentLanguage()
946 public function getPageTitle() {
947 return $this->mPageTitle;
957 public function setDisplayTitle( $html ) {
958 $this->displayTitle = $html;
969 public function getDisplayTitle() {
970 $html = $this->displayTitle;
971 if ( $html ===
null ) {
972 $html = $this->
getTitle()->getPrefixedText();
975 return Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $html ) );
984 public function getUnprefixedDisplayTitle() {
985 $text = $this->getDisplayTitle();
986 $nsPrefix = $this->
getTitle()->getNsText() .
':';
987 $prefix = preg_quote( $nsPrefix,
'/' );
989 return preg_replace(
"/^$prefix/i",
'', $text );
997 public function setTitle(
Title $t ) {
1008 public function setSubtitle( $str ) {
1009 $this->clearSubtitle();
1010 $this->addSubtitle( $str );
1018 public function addSubtitle( $str ) {
1019 if ( $str instanceof
Message ) {
1020 $this->mSubtitle[] = $str->setContext( $this->
getContext() )->parse();
1022 $this->mSubtitle[] = $str;
1034 public static function buildBacklinkSubtitle(
Title $title, $query = [] ) {
1035 if (
$title->isRedirect() ) {
1036 $query[
'redirect'] =
'no';
1038 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1040 ->rawParams( $linkRenderer->makeLink(
$title,
null, [], $query ) );
1049 public function addBacklinkSubtitle(
Title $title, $query = [] ) {
1050 $this->addSubtitle( self::buildBacklinkSubtitle(
$title, $query ) );
1056 public function clearSubtitle() {
1057 $this->mSubtitle = [];
1065 public function getSubtitle() {
1066 return implode(
"<br />\n\t\t\t\t", $this->mSubtitle );
1073 public function setPrintable() {
1074 $this->mPrintable =
true;
1082 public function isPrintable() {
1083 return $this->mPrintable;
1089 public function disable() {
1090 $this->mDoNothing =
true;
1098 public function isDisabled() {
1099 return $this->mDoNothing;
1107 public function showNewSectionLink() {
1108 return $this->mNewSectionLink;
1116 public function forceHideNewSectionLink() {
1117 return $this->mHideNewSectionLink;
1128 public function setSyndicated( $show =
true ) {
1130 $this->setFeedAppendQuery(
false );
1132 $this->mFeedLinks = [];
1142 protected function getAdvertisedFeedTypes() {
1143 if ( $this->
getConfig()->
get(
'Feed' ) ) {
1144 return $this->
getConfig()->get(
'AdvertisedFeedTypes' );
1159 public function setFeedAppendQuery( $val ) {
1160 $this->mFeedLinks = [];
1162 foreach ( $this->getAdvertisedFeedTypes() as
$type ) {
1163 $query =
"feed=$type";
1164 if ( is_string( $val ) ) {
1165 $query .=
'&' . $val;
1167 $this->mFeedLinks[
$type] = $this->
getTitle()->getLocalURL( $query );
1177 public function addFeedLink( $format, $href ) {
1178 if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) {
1179 $this->mFeedLinks[$format] = $href;
1187 public function isSyndicated() {
1188 return count( $this->mFeedLinks ) > 0;
1195 public function getSyndicationLinks() {
1196 return $this->mFeedLinks;
1204 public function getFeedAppendQuery() {
1205 return $this->mFeedLinksAppendQuery;
1215 public function setArticleFlag( $newVal ) {
1216 $this->mIsArticle = $newVal;
1218 $this->mIsArticleRelated = $newVal;
1228 public function isArticle() {
1229 return $this->mIsArticle;
1238 public function setArticleRelated( $newVal ) {
1239 $this->mIsArticleRelated = $newVal;
1241 $this->mIsArticle =
false;
1250 public function isArticleRelated() {
1251 return $this->mIsArticleRelated;
1259 public function setCopyright( $hasCopyright ) {
1260 $this->mHasCopyright = $hasCopyright;
1272 public function showsCopyright() {
1273 return $this->isArticle() || $this->mHasCopyright;
1282 public function addLanguageLinks( array $newLinkArray ) {
1283 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1292 public function setLanguageLinks( array $newLinkArray ) {
1293 $this->mLanguageLinks = $newLinkArray;
1301 public function getLanguageLinks() {
1302 return $this->mLanguageLinks;
1310 public function addCategoryLinks( array $categories ) {
1311 if ( !$categories ) {
1315 $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1317 # Set all the values to 'normal'.
1318 $categories = array_fill_keys( array_keys( $categories ),
'normal' );
1320 # Mark hidden categories
1321 foreach (
$res as $row ) {
1322 if ( isset( $row->pp_value ) ) {
1323 $categories[$row->page_title] =
'hidden';
1328 $outputPage = $this;
1329 # Add the remaining categories to the skin
1331 'OutputPageMakeCategoryLinks',
1332 [ &$outputPage, $categories, &$this->mCategoryLinks ] )
1334 $services = MediaWikiServices::getInstance();
1335 $linkRenderer = $services->getLinkRenderer();
1336 foreach ( $categories as $category =>
$type ) {
1338 $category = (string)$category;
1339 $origcategory = $category;
1344 $services->getContentLanguage()->findVariantLink( $category,
$title,
true );
1345 if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1348 $text = $services->getContentLanguage()->convertHtml(
$title->getText() );
1359 protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1360 # Add the links to a LinkBatch
1365 # Fetch existence plus the hiddencat property
1367 $fields = array_merge(
1369 [
'page_namespace',
'page_title',
'pp_value' ]
1372 $res =
$dbr->select( [
'page',
'page_props' ],
1374 $lb->constructSet(
'page',
$dbr ),
1377 [
'page_props' => [
'LEFT JOIN', [
1378 'pp_propname' =>
'hiddencat',
1383 # Add the results to the link cache
1384 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1385 $lb->addResultToCache( $linkCache,
$res );
1395 public function setCategoryLinks( array $categories ) {
1396 $this->mCategoryLinks = [];
1397 $this->addCategoryLinks( $categories );
1408 public function getCategoryLinks() {
1409 return $this->mCategoryLinks;
1421 public function getCategories(
$type =
'all' ) {
1422 if (
$type ===
'all' ) {
1423 $allCategories = [];
1424 foreach ( $this->mCategories as $categories ) {
1425 $allCategories = array_merge( $allCategories, $categories );
1427 return $allCategories;
1429 if ( !isset( $this->mCategories[
$type] ) ) {
1430 throw new InvalidArgumentException(
'Invalid category type given: ' .
$type );
1432 return $this->mCategories[
$type];
1444 public function setIndicators( array $indicators ) {
1445 $this->mIndicators = $indicators + $this->mIndicators;
1447 ksort( $this->mIndicators );
1458 public function getIndicators() {
1459 return $this->mIndicators;
1470 public function addHelpLink( $to, $overrideBaseUrl =
false ) {
1471 $this->addModuleStyles(
'mediawiki.helplink' );
1472 $text = $this->
msg(
'helppage-top-gethelp' )->escaped();
1474 if ( $overrideBaseUrl ) {
1477 $toUrlencoded =
wfUrlencode( str_replace(
' ',
'_', $to ) );
1478 $helpUrl =
"https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1481 $link = Html::rawElement(
1485 'target' =>
'_blank',
1486 'class' =>
'mw-helplink',
1491 $this->setIndicators( [
'mw-helplink' => $link ] );
1502 public function disallowUserJs() {
1503 $this->reduceAllowedModules(
1504 ResourceLoaderModule::TYPE_SCRIPTS,
1505 ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1510 if ( $this->
getConfig()->
get(
'AllowSiteCSSOnRestrictedPages' ) ) {
1511 $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1513 $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1515 $this->reduceAllowedModules(
1516 ResourceLoaderModule::TYPE_STYLES,
1527 public function getAllowedModules(
$type ) {
1528 if (
$type == ResourceLoaderModule::TYPE_COMBINED ) {
1529 return min( array_values( $this->mAllowedModules ) );
1531 return $this->mAllowedModules[
$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1544 public function reduceAllowedModules(
$type, $level ) {
1545 $this->mAllowedModules[
$type] = min( $this->getAllowedModules(
$type ), $level );
1553 public function prependHTML( $text ) {
1554 $this->mBodytext = $text . $this->mBodytext;
1562 public function addHTML( $text ) {
1563 $this->mBodytext .= $text;
1575 public function addElement( $element, array $attribs = [], $contents =
'' ) {
1576 $this->addHTML( Html::element( $element, $attribs, $contents ) );
1582 public function clearHTML() {
1583 $this->mBodytext =
'';
1591 public function getHTML() {
1592 return $this->mBodytext;
1603 public function parserOptions( $options =
null ) {
1604 if ( $options !==
null ) {
1605 wfDeprecated( __METHOD__ .
' with non-null $options',
'1.31' );
1608 if ( $options !==
null && !empty( $options->isBogus ) ) {
1612 $anonPO->setAllowUnsafeRawHtml(
false );
1613 if ( !$options->matches( $anonPO ) ) {
1615 $options->isBogus =
false;
1619 if ( !$this->mParserOptions ) {
1620 if ( !$this->
getUser()->isSafeToLoad() ) {
1625 $po->setAllowUnsafeRawHtml(
false );
1626 $po->isBogus =
true;
1627 if ( $options !==
null ) {
1628 $this->mParserOptions = empty( $options->isBogus ) ? $options :
null;
1634 $this->mParserOptions->setAllowUnsafeRawHtml(
false );
1637 if ( $options !==
null && !empty( $options->isBogus ) ) {
1640 return wfSetVar( $this->mParserOptions,
null,
true );
1642 return wfSetVar( $this->mParserOptions, $options );
1653 public function setRevisionId( $revid ) {
1654 $val = is_null( $revid ) ? null : intval( $revid );
1655 return wfSetVar( $this->mRevisionId, $val,
true );
1663 public function getRevisionId() {
1664 return $this->mRevisionId;
1673 public function isRevisionCurrent() {
1674 return $this->mRevisionId == 0 || $this->mRevisionId == $this->
getTitle()->getLatestRevID();
1684 public function setRevisionTimestamp( $timestamp ) {
1685 return wfSetVar( $this->mRevisionTimestamp, $timestamp,
true );
1694 public function getRevisionTimestamp() {
1695 return $this->mRevisionTimestamp;
1704 public function setFileVersion(
$file ) {
1707 $val = [
'time' =>
$file->getTimestamp(),
'sha1' =>
$file->getSha1() ];
1709 return wfSetVar( $this->mFileVersion, $val,
true );
1717 public function getFileVersion() {
1718 return $this->mFileVersion;
1727 public function getTemplateIds() {
1728 return $this->mTemplateIds;
1737 public function getFileSearchOptions() {
1738 return $this->mImageTimeKeys;
1757 public function addWikiTextAsInterface(
1766 $this->addWikiTextTitleInternal( $text,
$title, $linestart,
true );
1782 public function wrapWikiTextAsInterface(
1783 $wrapperClass, $text
1785 $this->addWikiTextTitleInternal(
1807 public function addWikiTextAsContent(
1816 $this->addWikiTextTitleInternal( $text,
$title, $linestart,
false );
1831 private function addWikiTextTitleInternal(
1832 $text,
Title $title, $linestart, $interface, $wrapperClass =
null
1834 $parserOutput = $this->parseInternal(
1835 $text,
$title, $linestart,
true, $interface,
null
1838 $this->addParserOutput( $parserOutput, [
1839 'enableSectionEditLinks' =>
false,
1840 'wrapperDivClass' => $wrapperClass ??
'',
1852 public function addParserOutputMetadata(
ParserOutput $parserOutput ) {
1853 $this->mLanguageLinks =
1861 $this->enableClientCache(
false );
1864 $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->
getHeadItems() );
1865 $this->addModules( $parserOutput->
getModules() );
1868 $this->mPreventClickjacking = $this->mPreventClickjacking
1872 foreach ( (array)$parserOutput->
getTemplateIds() as $ns => $dbks ) {
1873 if ( isset( $this->mTemplateIds[$ns] ) ) {
1874 $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1876 $this->mTemplateIds[$ns] = $dbks;
1881 $this->mImageTimeKeys[$dbk] = $data;
1885 $parserOutputHooks = $this->
getConfig()->get(
'ParserOutputHooks' );
1887 list( $hookName, $data ) = $hookInfo;
1888 if ( isset( $parserOutputHooks[$hookName] ) ) {
1889 $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1895 $this->enableOOUI();
1899 if ( !$this->limitReportJSData ) {
1907 $outputPage = $this;
1908 Hooks::run(
'LanguageLinks', [ $this->
getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1916 $this->mEnableTOC =
true;
1928 public function addParserOutputContent(
ParserOutput $parserOutput, $poOptions = [] ) {
1929 $this->addParserOutputText( $parserOutput, $poOptions );
1931 $this->addModules( $parserOutput->
getModules() );
1944 public function addParserOutputText(
ParserOutput $parserOutput, $poOptions = [] ) {
1945 $text = $parserOutput->
getText( $poOptions );
1947 $outputPage = $this;
1949 $this->addHTML( $text );
1958 public function addParserOutput(
ParserOutput $parserOutput, $poOptions = [] ) {
1959 $this->addParserOutputMetadata( $parserOutput );
1960 $this->addParserOutputText( $parserOutput, $poOptions );
1968 public function addTemplate( &$template ) {
1969 $this->addHTML( $template->getHTML() );
1990 public function parse( $text, $linestart =
true, $interface =
false, $language =
null ) {
1992 return $this->parseInternal(
1993 $text, $this->
getTitle(), $linestart,
false, $interface, $language
1995 'enableSectionEditLinks' =>
false,
2010 public function parseAsContent( $text, $linestart =
true ) {
2011 return $this->parseInternal(
2012 $text, $this->
getTitle(), $linestart,
true,
false,
null
2014 'enableSectionEditLinks' =>
false,
2015 'wrapperDivClass' =>
''
2031 public function parseAsInterface( $text, $linestart =
true ) {
2032 return $this->parseInternal(
2033 $text, $this->
getTitle(), $linestart,
true,
true,
null
2035 'enableSectionEditLinks' =>
false,
2036 'wrapperDivClass' =>
''
2054 public function parseInlineAsInterface( $text, $linestart =
true ) {
2055 return Parser::stripOuterParagraph(
2056 $this->parseAsInterface( $text, $linestart )
2073 public function parseInline( $text, $linestart =
true, $interface =
false ) {
2075 $parsed = $this->parseInternal(
2076 $text, $this->
getTitle(), $linestart,
false, $interface,
null
2078 'enableSectionEditLinks' =>
false,
2079 'wrapperDivClass' =>
'',
2081 return Parser::stripOuterParagraph( $parsed );
2098 private function parseInternal( $text,
$title, $linestart, $tidy, $interface, $language ) {
2099 if ( is_null(
$title ) ) {
2100 throw new MWException(
'Empty $mTitle in ' . __METHOD__ );
2103 $popts = $this->parserOptions();
2104 $oldTidy = $popts->setTidy( $tidy );
2105 $oldInterface = $popts->setInterfaceMessage( (
bool)$interface );
2107 if ( $language !==
null ) {
2108 $oldLang = $popts->setTargetLanguage( $language );
2111 $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
2113 $linestart,
true, $this->mRevisionId
2116 $popts->setTidy( $oldTidy );
2117 $popts->setInterfaceMessage( $oldInterface );
2119 if ( $language !==
null ) {
2120 $popts->setTargetLanguage( $oldLang );
2123 return $parserOutput;
2131 public function setCdnMaxage( $maxage ) {
2132 $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2144 public function lowerCdnMaxage( $maxage ) {
2145 $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2146 $this->setCdnMaxage( $this->mCdnMaxage );
2161 public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2163 $maxTTL = $maxTTL ?: $this->
getConfig()->get(
'CdnMaxAge' );
2165 if ( $mtime ===
null || $mtime ===
false ) {
2169 $age = MWTimestamp::time() - (int)
wfTimestamp( TS_UNIX, $mtime );
2170 $adaptiveTTL = max( 0.9 * $age, $minTTL );
2171 $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2173 $this->lowerCdnMaxage( (
int)$adaptiveTTL );
2183 public function enableClientCache( $state ) {
2184 return wfSetVar( $this->mEnableClientCache, $state );
2192 public function getCacheVaryCookies() {
2193 if ( self::$cacheVaryCookies ===
null ) {
2195 self::$cacheVaryCookies = array_values( array_unique( array_merge(
2196 SessionManager::singleton()->getVaryCookies(),
2200 $config->
get(
'CacheVaryCookies' )
2202 Hooks::run(
'GetCacheVaryCookies', [ $this, &self::$cacheVaryCookies ] );
2204 return self::$cacheVaryCookies;
2213 public function haveCacheVaryCookies() {
2215 foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2216 if ( $request->getCookie( $cookieName,
'',
'' ) !==
'' ) {
2217 wfDebug( __METHOD__ .
": found $cookieName\n" );
2221 wfDebug( __METHOD__ .
": no cache-varying cookies found\n" );
2234 public function addVaryHeader(
$header, array $option =
null ) {
2235 if ( $option !==
null && count( $option ) > 0 ) {
2236 wfDeprecated(
'addVaryHeader $option is ignored',
'1.34' );
2238 if ( !array_key_exists(
$header, $this->mVaryHeader ) ) {
2239 $this->mVaryHeader[
$header] =
null;
2249 public function getVaryHeader() {
2251 if ( $this->getCacheVaryCookies() ) {
2252 $this->addVaryHeader(
'Cookie' );
2255 foreach ( SessionManager::singleton()->getVaryHeaders() as
$header => $options ) {
2256 $this->addVaryHeader(
$header, $options );
2258 return 'Vary: ' . implode(
', ', array_keys( $this->mVaryHeader ) );
2266 public function addLinkHeader(
$header ) {
2267 $this->mLinkHeader[] =
$header;
2275 public function getLinkHeader() {
2276 if ( !$this->mLinkHeader ) {
2280 return 'Link: ' . implode(
',', $this->mLinkHeader );
2290 private function addAcceptLanguage() {
2292 if ( !$title instanceof
Title ) {
2297 if ( !$this->
getRequest()->getCheck(
'variant' ) &&
$lang->hasVariants() ) {
2298 $this->addVaryHeader(
'Accept-Language' );
2312 public function preventClickjacking( $enable =
true ) {
2313 $this->mPreventClickjacking = $enable;
2321 public function allowClickjacking() {
2322 $this->mPreventClickjacking =
false;
2331 public function getPreventClickjacking() {
2332 return $this->mPreventClickjacking;
2342 public function getFrameOptions() {
2344 if ( $config->
get(
'BreakFrames' ) ) {
2346 } elseif ( $this->mPreventClickjacking && $config->
get(
'EditPageFrameOptions' ) ) {
2347 return $config->
get(
'EditPageFrameOptions' );
2358 private function getOriginTrials() {
2361 return $config->
get(
'OriginTrials' );
2364 private function getReportTo() {
2367 $expiry = $config->
get(
'ReportToExpiry' );
2373 $endpoints = $config->
get(
'ReportToEndpoints' );
2375 if ( !$endpoints ) {
2379 $output = [
'max_age' => $expiry,
'endpoints' => [] ];
2381 foreach ( $endpoints as $endpoint ) {
2382 $output[
'endpoints'][] = [
'url' => $endpoint ];
2385 return json_encode(
$output, JSON_UNESCAPED_SLASHES );
2388 private function getFeaturePolicyReportOnly() {
2391 $features = $config->
get(
'FeaturePolicyReportOnly' );
2392 return implode(
';', $features );
2398 public function sendCacheControl() {
2402 $this->addVaryHeader(
'Cookie' );
2403 $this->addAcceptLanguage();
2405 # don't serve compressed data to clients who can't handle it
2406 # maintain different caches for logged-in users and non-logged in ones
2407 $response->header( $this->getVaryHeader() );
2409 if ( $this->mEnableClientCache ) {
2411 $config->
get(
'UseCdn' ) &&
2413 !SessionManager::getGlobalSession()->isPersistent() &&
2414 !$this->isPrintable() &&
2415 $this->mCdnMaxage != 0 &&
2416 !$this->haveCacheVaryCookies()
2418 # We'll purge the proxy cache for anons explicitly, but require end user agents
2419 # to revalidate against the proxy on each visit.
2420 # IMPORTANT! The CDN needs to replace the Cache-Control header with
2421 # Cache-Control: s-maxage=0, must-revalidate, max-age=0
2423 ": local proxy caching; {$this->mLastModified} **",
'private' );
2424 # start with a shorter timeout for initial testing
2425 # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2427 "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2429 # We do want clients to cache if they can, but they *must* check for updates
2430 # on revisiting the page.
2431 wfDebug( __METHOD__ .
": private caching; {$this->mLastModified} **",
'private' );
2432 $response->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
2433 $response->header(
"Cache-Control: private, must-revalidate, max-age=0" );
2435 if ( $this->mLastModified ) {
2436 $response->header(
"Last-Modified: {$this->mLastModified}" );
2439 wfDebug( __METHOD__ .
": no caching **",
'private' );
2441 # In general, the absence of a last modified header should be enough to prevent
2442 # the client from using its cache. We send a few other things just to make sure.
2443 $response->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
2444 $response->header(
'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2445 $response->header(
'Pragma: no-cache' );
2454 public function loadSkinModules( $sk ) {
2456 if ( $group ===
'styles' ) {
2457 foreach (
$modules as $key => $moduleMembers ) {
2458 $this->addModuleStyles( $moduleMembers );
2476 public function output( $return =
false ) {
2477 if ( $this->mDoNothing ) {
2478 return $return ?
'' :
null;
2484 if ( $this->mRedirect !=
'' ) {
2485 # Standards require redirect URLs to be absolute
2488 $redirect = $this->mRedirect;
2489 $code = $this->mRedirectCode;
2491 if (
Hooks::run(
"BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2492 if ( $code ==
'301' || $code ==
'303' ) {
2493 if ( !$config->
get(
'DebugRedirects' ) ) {
2498 if ( $config->
get(
'VaryOnXFP' ) ) {
2499 $this->addVaryHeader(
'X-Forwarded-Proto' );
2501 $this->sendCacheControl();
2503 $response->header(
"Content-Type: text/html; charset=utf-8" );
2504 if ( $config->
get(
'DebugRedirects' ) ) {
2505 $url = htmlspecialchars( $redirect );
2506 print
"<!DOCTYPE html>\n<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2507 print
"<p>Location: <a href=\"$url\">$url</a></p>\n";
2508 print
"</body>\n</html>\n";
2510 $response->header(
'Location: ' . $redirect );
2514 return $return ?
'' :
null;
2515 } elseif ( $this->mStatusCode ) {
2516 $response->statusHeader( $this->mStatusCode );
2519 # Buffer output; final headers may depend on later processing
2522 $response->header(
'Content-type: ' . $config->
get(
'MimeType' ) .
'; charset=UTF-8' );
2523 $response->header(
'Content-language: ' .
2524 MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2526 $linkHeader = $this->getLinkHeader();
2527 if ( $linkHeader ) {
2532 $frameOptions = $this->getFrameOptions();
2533 if ( $frameOptions ) {
2534 $response->header(
"X-Frame-Options: $frameOptions" );
2537 $originTrials = $this->getOriginTrials();
2538 foreach ( $originTrials as $originTrial ) {
2539 $response->header(
"Origin-Trial: $originTrial",
false );
2542 $reportTo = $this->getReportTo();
2544 $response->header(
"Report-To: $reportTo" );
2547 $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
2548 if ( $featurePolicyReportOnly ) {
2549 $response->header(
"Feature-Policy-Report-Only: $featurePolicyReportOnly" );
2554 if ( $this->mArticleBodyOnly ) {
2555 echo $this->mBodytext;
2558 if ( $this->
getRequest()->getBool(
'safemode' ) ) {
2559 $this->disallowUserJs();
2563 $this->loadSkinModules( $sk );
2568 $outputPage = $this;
2575 }
catch ( Exception $e ) {
2584 }
catch ( Exception $e ) {
2589 $this->sendCacheControl();
2592 return ob_get_clean();
2609 public function prepareErrorPage( $pageTitle, $htmlTitle =
false ) {
2610 $this->setPageTitle( $pageTitle );
2611 if ( $htmlTitle !==
false ) {
2612 $this->setHTMLTitle( $htmlTitle );
2614 $this->setRobotPolicy(
'noindex,nofollow' );
2615 $this->setArticleRelated(
false );
2616 $this->enableClientCache(
false );
2617 $this->mRedirect =
'';
2618 $this->clearSubtitle();
2634 public function showErrorPage(
$title, $msg, $params = [] ) {
2639 $this->prepareErrorPage(
$title );
2641 if ( $msg instanceof
Message ) {
2642 if ( $params !== [] ) {
2643 trigger_error(
'Argument ignored: $params. The message parameters argument '
2644 .
'is discarded when the $msg argument is a Message object instead of '
2645 .
'a string.', E_USER_NOTICE );
2647 $this->addHTML( $msg->parseAsBlock() );
2649 $this->addWikiMsgArray( $msg, $params );
2652 $this->returnToMain();
2661 public function showPermissionsErrorPage( array $errors, $action =
null ) {
2662 $services = MediaWikiServices::getInstance();
2663 $permissionManager = $services->getPermissionManager();
2664 foreach ( $errors as $key => $error ) {
2665 $errors[$key] = (array)$error;
2674 if ( in_array( $action, [
'read',
'edit',
'createpage',
'createtalk',
'upload' ] )
2675 && $this->
getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2676 && ( $errors[0][0] ==
'badaccess-groups' || $errors[0][0] ==
'badaccess-group0' )
2677 && ( $permissionManager->groupHasPermission(
'user', $action )
2678 || $permissionManager->groupHasPermission(
'autoconfirmed', $action ) )
2680 $displayReturnto =
null;
2682 # Due to T34276, if a user does not have read permissions,
2683 # $this->getTitle() will just give Special:Badtitle, which is
2684 # not especially useful as a returnto parameter. Use the title
2685 # from the request instead, if there was one.
2688 if ( $action ==
'edit' ) {
2689 $msg =
'whitelistedittext';
2690 $displayReturnto = $returnto;
2691 } elseif ( $action ==
'createpage' || $action ==
'createtalk' ) {
2692 $msg =
'nocreatetext';
2693 } elseif ( $action ==
'upload' ) {
2694 $msg =
'uploadnologintext';
2696 $msg =
'loginreqpagetext';
2703 $query[
'returnto'] = $returnto->getPrefixedText();
2705 if ( !$request->wasPosted() ) {
2706 $returntoquery = $request->getValues();
2707 unset( $returntoquery[
'title'] );
2708 unset( $returntoquery[
'returnto'] );
2709 unset( $returntoquery[
'returntoquery'] );
2710 $query[
'returntoquery'] =
wfArrayToCgi( $returntoquery );
2715 $linkRenderer = $services->getLinkRenderer();
2717 $loginLink = $linkRenderer->makeKnownLink(
2719 $this->
msg(
'loginreqlink' )->text(),
2724 $this->prepareErrorPage( $this->
msg(
'loginreqtitle' ) );
2725 $this->addHTML( $this->
msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
2727 # Don't return to a page the user can't read otherwise
2728 # we'll end up in a pointless loop
2729 if ( $displayReturnto && $permissionManager->userCan(
2730 'read', $this->getUser(), $displayReturnto
2732 $this->returnToMain(
null, $displayReturnto );
2735 $this->prepareErrorPage( $this->
msg(
'permissionserrors' ) );
2736 $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) );
2746 public function versionRequired( $version ) {
2747 $this->prepareErrorPage( $this->
msg(
'versionrequired', $version ) );
2749 $this->addWikiMsg(
'versionrequiredtext', $version );
2750 $this->returnToMain();
2760 public function formatPermissionsErrorMessage( array $errors, $action =
null ) {
2761 if ( $action ==
null ) {
2762 $text = $this->
msg(
'permissionserrorstext', count( $errors ) )->plain() .
"\n\n";
2764 $action_desc = $this->
msg(
"action-$action" )->plain();
2766 'permissionserrorstext-withaction',
2769 )->plain() .
"\n\n";
2772 if ( count( $errors ) > 1 ) {
2773 $text .=
'<ul class="permissions-errors">' .
"\n";
2775 foreach ( $errors as $error ) {
2777 $text .= $this->
msg( ...$error )->plain();
2782 $text .=
"<div class=\"permissions-errors\">\n" .
2783 $this->
msg( ...reset( $errors ) )->plain() .
2799 public function showLagWarning( $lag ) {
2801 if ( $lag >= $config->
get(
'SlaveLagWarning' ) ) {
2802 $lag = floor( $lag );
2803 $message = $lag < $config->
get(
'SlaveLagCritical' )
2806 $wrap = Html::rawElement(
'div', [
'class' =>
"mw-{$message}" ],
"\n$1\n" );
2807 $this->wrapWikiMsg(
"$wrap\n", [ $message, $this->
getLanguage()->formatNum( $lag ) ] );
2817 public function showFatalError( $message ) {
2818 $this->prepareErrorPage( $this->
msg(
'internalerror' ) );
2820 $this->addHTML( $message );
2831 public function addReturnTo(
$title, array $query = [], $text =
null, $options = [] ) {
2832 $linkRenderer = MediaWikiServices::getInstance()
2833 ->getLinkRendererFactory()->createFromLegacyOptions( $options );
2834 $link = $this->
msg(
'returnto' )->rawParams(
2835 $linkRenderer->makeLink(
$title, $text, [], $query ) )->escaped();
2836 $this->addHTML(
"<p id=\"mw-returnto\">{$link}</p>\n" );
2847 public function returnToMain( $unused =
null, $returnto =
null, $returntoquery =
null ) {
2848 if ( $returnto ==
null ) {
2849 $returnto = $this->
getRequest()->getText(
'returnto' );
2852 if ( $returntoquery ==
null ) {
2853 $returntoquery = $this->
getRequest()->getText(
'returntoquery' );
2856 if ( $returnto ===
'' ) {
2860 if ( is_object( $returnto ) ) {
2861 $titleObj = $returnto;
2867 if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
2871 $this->addReturnTo( $titleObj,
wfCgiToArray( $returntoquery ) );
2874 private function getRlClientContext() {
2875 if ( !$this->rlClientContext ) {
2876 $query = ResourceLoader::makeLoaderQuery(
2879 $this->
getSkin()->getSkinName(),
2880 $this->
getUser()->isLoggedIn() ? $this->
getUser()->getName() :
null,
2882 ResourceLoader::inDebugMode(),
2884 $this->isPrintable(),
2888 $this->getResourceLoader(),
2891 if ( $this->contentOverrideCallbacks ) {
2893 $this->rlClientContext->setContentOverrideCallback(
function (
Title $title ) {
2894 foreach ( $this->contentOverrideCallbacks as $callback ) {
2898 if ( strpos( $text,
'</script>' ) !==
false ) {
2902 $titleFormatted =
$title->getPrefixedText();
2905 "Cannot preview $titleFormatted due to script-closing tag."
2916 return $this->rlClientContext;
2930 public function getRlClient() {
2931 if ( !$this->rlClient ) {
2932 $context = $this->getRlClientContext();
2933 $rl = $this->getResourceLoader();
2934 $this->addModules( [
2939 $this->addModuleStyles( [
2944 $this->
getSkin()->setupSkinUserCss( $this );
2947 $exemptGroups = [
'site' => [],
'noscript' => [],
'private' => [],
'user' => [] ];
2949 $moduleStyles = $this->getModuleStyles(
true );
2953 $userBatch = [
'user.styles',
'user' ];
2954 $siteBatch = array_diff( $moduleStyles, $userBatch );
2960 $moduleStyles = array_filter( $moduleStyles,
2961 function ( $name ) use ( $rl,
$context, &$exemptGroups, &$exemptStates ) {
2962 $module = $rl->getModule( $name );
2964 $group = $module->getGroup();
2965 if ( isset( $exemptGroups[$group] ) ) {
2966 $exemptStates[$name] =
'ready';
2967 if ( !$module->isKnownEmpty(
$context ) ) {
2969 $exemptGroups[$group][] = $name;
2977 $this->rlExemptStyleModules = $exemptGroups;
2980 'target' => $this->getTarget(),
2981 'nonce' => $this->getCSPNonce(),
2989 'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
2990 <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
2993 $rlClient->
setConfig( $this->getJSVars() );
2994 $rlClient->
setModules( $this->getModules(
true ) );
2997 $this->rlClient = $rlClient;
2999 return $this->rlClient;
3007 public function headElement(
Skin $sk, $includeStyle =
true ) {
3010 $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
3013 $htmlAttribs = Sanitizer::mergeAttributes(
3014 $this->getRlClient()->getDocumentAttributes(),
3017 $pieces[] = Html::htmlHeader( $htmlAttribs );
3018 $pieces[] = Html::openElement(
'head' );
3020 if ( $this->getHTMLTitle() ==
'' ) {
3021 $this->setHTMLTitle( $this->
msg(
'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3024 if ( !Html::isXmlMimeType( $config->
get(
'MimeType' ) ) ) {
3033 $pieces[] = Html::element(
'meta', [
'charset' =>
'UTF-8' ] );
3036 $pieces[] = Html::element(
'title',
null, $this->getHTMLTitle() );
3037 $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs[
'class'] ??
null );
3038 $pieces[] = $this->buildExemptModules();
3039 $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
3040 $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
3048 $shivUrl = $config->
get(
'ResourceBasePath' ) .
'/resources/lib/html5shiv/html5shiv.js';
3049 $pieces[] =
'<!--[if lt IE 9]>' .
3050 Html::linkedScript( $shivUrl, $this->getCSPNonce() ) .
3053 $pieces[] = Html::closeElement(
'head' );
3055 $bodyClasses = $this->mAdditionalBodyClasses;
3056 $bodyClasses[] =
'mediawiki';
3058 # Classes for LTR/RTL directionality support
3059 $bodyClasses[] = $userdir;
3060 $bodyClasses[] =
"sitedir-$sitedir";
3062 $underline = $this->
getUser()->getOption(
'underline' );
3063 if ( $underline < 2 ) {
3067 $bodyClasses[] =
'mw-underline-' . ( $underline ?
'always' :
'never' );
3070 if ( $this->
getLanguage()->capitalizeAllNouns() ) {
3071 # A <body> class is probably not the best way to do this . . .
3072 $bodyClasses[] =
'capitalize-all-nouns';
3078 $bodyClasses[] =
'mw-hide-empty-elt';
3081 $bodyClasses[] =
'skin-' . Sanitizer::escapeClass( $sk->
getSkinName() );
3088 $bodyAttrs[
'class'] = implode(
' ', $bodyClasses );
3092 Hooks::run(
'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
3094 $pieces[] = Html::openElement(
'body', $bodyAttrs );
3096 return self::combineWrappedStrings( $pieces );
3104 public function getResourceLoader() {
3105 if ( is_null( $this->mResourceLoader ) ) {
3107 $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
3109 return $this->mResourceLoader;
3120 public function makeResourceLoaderLink(
$modules, $only, array $extraQuery = [] ) {
3125 $this->getRlClientContext(),
3129 $this->getCSPNonce()
3139 protected static function combineWrappedStrings( array $chunks ) {
3141 $chunks = array_filter( $chunks,
'strlen' );
3142 return WrappedString::join(
"\n", $chunks );
3151 public function getBottomScripts() {
3153 $chunks[] = $this->getRlClient()->getBodyHtml();
3156 $chunks[] = $this->mScripts;
3158 if ( $this->limitReportJSData ) {
3159 $chunks[] = ResourceLoader::makeInlineScript(
3160 ResourceLoader::makeConfigSetScript(
3161 [
'wgPageParseReport' => $this->limitReportJSData ]
3163 $this->getCSPNonce()
3167 return self::combineWrappedStrings( $chunks );
3176 public function getJsConfigVars() {
3177 return $this->mJsConfigVars;
3186 public function addJsConfigVars(
$keys, $value =
null ) {
3187 if ( is_array(
$keys ) ) {
3188 foreach (
$keys as $key => $value ) {
3189 $this->mJsConfigVars[$key] = $value;
3194 $this->mJsConfigVars[
$keys] = $value;
3206 public function getJSVars() {
3209 $canonicalSpecialPageName =
false; # T23115
3210 $services = MediaWikiServices::getInstance();
3213 $ns =
$title->getNamespace();
3214 $nsInfo = $services->getNamespaceInfo();
3215 $canonicalNamespace = $nsInfo->exists( $ns )
3216 ? $nsInfo->getCanonicalName( $ns )
3226 list( $canonicalSpecialPageName, ) =
3227 $services->getSpecialPageFactory()->
3228 resolveAlias(
$title->getDBkey() );
3231 $curRevisionId = $wikiPage->getLatest();
3232 $articleId = $wikiPage->getId();
3238 $separatorTransTable =
$lang->separatorTransformTable();
3239 $separatorTransTable = $separatorTransTable ?: [];
3240 $compactSeparatorTransTable = [
3241 implode(
"\t", array_keys( $separatorTransTable ) ),
3242 implode(
"\t", $separatorTransTable ),
3244 $digitTransTable =
$lang->digitTransformTable();
3245 $digitTransTable = $digitTransTable ?: [];
3246 $compactDigitTransTable = [
3247 implode(
"\t", array_keys( $digitTransTable ) ),
3248 implode(
"\t", $digitTransTable ),
3254 'wgCanonicalNamespace' => $canonicalNamespace,
3255 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3256 'wgNamespaceNumber' =>
$title->getNamespace(),
3257 'wgPageName' =>
$title->getPrefixedDBkey(),
3258 'wgTitle' =>
$title->getText(),
3259 'wgCurRevisionId' => $curRevisionId,
3260 'wgRevisionId' => (int)$this->getRevisionId(),
3261 'wgArticleId' => $articleId,
3262 'wgIsArticle' => $this->isArticle(),
3263 'wgIsRedirect' =>
$title->isRedirect(),
3267 'wgCategories' => $this->getCategories(),
3268 'wgBreakFrames' => $this->getFrameOptions() ==
'DENY',
3269 'wgPageContentLanguage' =>
$lang->getCode(),
3270 'wgPageContentModel' =>
$title->getContentModel(),
3271 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3272 'wgDigitTransformTable' => $compactDigitTransTable,
3273 'wgDefaultDateFormat' =>
$lang->getDefaultDateFormat(),
3274 'wgMonthNames' =>
$lang->getMonthNamesArray(),
3275 'wgMonthNamesShort' =>
$lang->getMonthAbbreviationsArray(),
3276 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3277 'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3279 'wgCSPNonce' => $this->getCSPNonce(),
3283 $vars[
'wgUserId'] = $user->
getId();
3286 $vars[
'wgUserRegistration'] = $userReg ? (int)
wfTimestamp( TS_UNIX, $userReg ) * 1000 :
null;
3293 $contLang = $services->getContentLanguage();
3294 if ( $contLang->hasVariants() ) {
3295 $vars[
'wgUserVariant'] = $contLang->getPreferredVariant();
3298 $vars[
'wgIsProbablyEditable'] = $this->userCanEditOrCreate( $user,
$title );
3300 $vars[
'wgRelevantPageIsProbablyEditable'] = $relevantTitle &&
3301 $this->userCanEditOrCreate( $user, $relevantTitle );
3303 foreach (
$title->getRestrictionTypes() as
$type ) {
3306 $vars[
'wgRestriction' . ucfirst(
$type )] =
$title->getRestrictions(
$type );
3309 if (
$title->isMainPage() ) {
3310 $vars[
'wgIsMainPage'] =
true;
3313 if ( $this->mRedirectedFrom ) {
3314 $vars[
'wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3317 if ( $relevantUser ) {
3318 $vars[
'wgRelevantUserName'] = $relevantUser->getName();
3325 Hooks::run(
'MakeGlobalVariablesScript', [ &$vars, $this ] );
3328 return array_merge( $vars, $this->getJsConfigVars() );
3340 public function userCanPreview() {
3343 $request->getVal(
'action' ) !==
'submit' ||
3344 !$request->wasPosted()
3355 if ( !$user->
matchEditToken( $request->getVal(
'wpEditToken' ) ) ) {
3360 $errors =
$title->getUserPermissionsErrors(
'edit', $user );
3361 if ( count( $errors ) !== 0 ) {
3373 private function userCanEditOrCreate(
3377 $pm = MediaWikiServices::getInstance()->getPermissionManager();
3378 return $pm->quickUserCan(
'edit', $user,
$title )
3379 && ( $this->
getTitle()->exists() ||
3380 $pm->quickUserCan(
'create', $user,
$title ) );
3386 public function getHeadLinksArray() {
3392 $canonicalUrl = $this->mCanonicalUrl;
3394 $tags[
'meta-generator'] = Html::element(
'meta', [
3395 'name' =>
'generator',
3396 'content' =>
"MediaWiki $wgVersion",
3399 if ( $config->
get(
'ReferrerPolicy' ) !== false ) {
3402 foreach ( array_reverse( (array)$config->
get(
'ReferrerPolicy' ) ) as $i => $policy ) {
3403 $tags[
"meta-referrer-$i"] = Html::element(
'meta', [
3404 'name' =>
'referrer',
3405 'content' => $policy,
3410 $p =
"{$this->mIndexPolicy},{$this->mFollowPolicy}";
3411 if ( $p !==
'index,follow' ) {
3414 $tags[
'meta-robots'] = Html::element(
'meta', [
3420 foreach ( $this->mMetatags as $tag ) {
3421 if ( strncasecmp( $tag[0],
'http:', 5 ) === 0 ) {
3423 $tag[0] = substr( $tag[0], 5 );
3424 } elseif ( strncasecmp( $tag[0],
'og:', 3 ) === 0 ) {
3429 $tagName =
"meta-{$tag[0]}";
3430 if ( isset( $tags[$tagName] ) ) {
3431 $tagName .= $tag[1];
3433 $tags[$tagName] = Html::element(
'meta',
3436 'content' => $tag[1]
3441 foreach ( $this->mLinktags as $tag ) {
3442 $tags[] = Html::element(
'link', $tag );
3445 # Universal edit button
3446 if ( $config->
get(
'UniversalEditButton' ) && $this->isArticleRelated() ) {
3447 if ( $this->userCanEditOrCreate( $this->
getUser(), $this->
getTitle() ) ) {
3449 $msg = $this->
msg(
'edit' )->text();
3450 $tags[
'universal-edit-button'] = Html::element(
'link', [
3451 'rel' =>
'alternate',
3452 'type' =>
'application/x-wiki',
3454 'href' => $this->
getTitle()->getEditURL(),
3457 $tags[
'alternative-edit'] = Html::element(
'link', [
3460 'href' => $this->
getTitle()->getEditURL(),
3465 # Generally the order of the favicon and apple-touch-icon links
3466 # should not matter, but Konqueror (3.5.9 at least) incorrectly
3467 # uses whichever one appears later in the HTML source. Make sure
3468 # apple-touch-icon is specified first to avoid this.
3469 if ( $config->
get(
'AppleTouchIcon' ) !== false ) {
3470 $tags[
'apple-touch-icon'] = Html::element(
'link', [
3471 'rel' =>
'apple-touch-icon',
3472 'href' => $config->
get(
'AppleTouchIcon' )
3476 if ( $config->
get(
'Favicon' ) !== false ) {
3477 $tags[
'favicon'] = Html::element(
'link', [
3478 'rel' =>
'shortcut icon',
3479 'href' => $config->
get(
'Favicon' )
3483 # OpenSearch description link
3484 $tags[
'opensearch'] = Html::element(
'link', [
3486 'type' =>
'application/opensearchdescription+xml',
3487 'href' =>
wfScript(
'opensearch_desc' ),
3488 'title' => $this->
msg(
'opensearch-desc' )->inContentLanguage()->text(),
3491 # Real Simple Discovery link, provides auto-discovery information
3492 # for the MediaWiki API (and potentially additional custom API
3493 # support such as WordPress or Twitter-compatible APIs for a
3494 # blogging extension, etc)
3495 $tags[
'rsd'] = Html::element(
'link', [
3497 'type' =>
'application/rsd+xml',
3503 [
'action' =>
'rsd' ] ),
3509 if ( !$config->
get(
'DisableLangConversion' ) ) {
3511 if (
$lang->hasVariants() ) {
3512 $variants =
$lang->getVariants();
3513 foreach ( $variants as $variant ) {
3514 $tags[
"variant-$variant"] = Html::element(
'link', [
3515 'rel' =>
'alternate',
3517 'href' => $this->
getTitle()->getLocalURL(
3518 [
'variant' => $variant ] )
3522 # x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3523 $tags[
"variant-x-default"] = Html::element(
'link', [
3524 'rel' =>
'alternate',
3525 'hreflang' =>
'x-default',
3526 'href' => $this->
getTitle()->getLocalURL() ] );
3531 if ( $this->copyrightUrl !==
null ) {
3532 $copyright = $this->copyrightUrl;
3535 if ( $config->
get(
'RightsPage' ) ) {
3539 $copyright = $copy->getLocalURL();
3543 if ( !$copyright && $config->
get(
'RightsUrl' ) ) {
3544 $copyright = $config->
get(
'RightsUrl' );
3549 $tags[
'copyright'] = Html::element(
'link', [
3551 'href' => $copyright ]
3556 if ( $config->
get(
'Feed' ) ) {
3559 foreach ( $this->getSyndicationLinks() as $format => $link ) {
3560 # Use the page name for the title. In principle, this could
3561 # lead to issues with having the same name for different feeds
3562 # corresponding to the same page, but we can't avoid that at
3565 $feedLinks[] = $this->feedLink(
3568 # Used messages:
'page-rss-feed' and
'page-atom-feed' (
for an easier grep)
3570 "page-{$format}-feed", $this->
getTitle()->getPrefixedText()
3575 # Recent changes feed should appear on every page (except recentchanges,
3576 # that would be redundant). Put it after the per-page feed to avoid
3577 # changing existing behavior. It's still available, probably via a
3578 # menu in your browser. Some sites might have a different feed they'd
3579 # like to promote instead of the RC feed (maybe like a "Recent New Articles"
3580 # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3581 # If so, use it instead.
3582 $sitename = $config->
get(
'Sitename' );
3583 $overrideSiteFeed = $config->
get(
'OverrideSiteFeed' );
3584 if ( $overrideSiteFeed ) {
3585 foreach ( $overrideSiteFeed as
$type => $feedUrl ) {
3587 $feedLinks[] = $this->feedLink(
3590 $this->
msg(
"site-{$type}-feed", $sitename )->text()
3593 } elseif ( !$this->
getTitle()->isSpecial(
'Recentchanges' ) ) {
3595 foreach ( $this->getAdvertisedFeedTypes() as $format ) {
3596 $feedLinks[] = $this->feedLink(
3598 $rctitle->getLocalURL( [
'feed' => $format ] ),
3599 # For grep: 'site-rss-feed', 'site-atom-feed'
3600 $this->
msg(
"site-{$format}-feed", $sitename )->text()
3605 # Allow extensions to change the list pf feeds. This hook is primarily for changing,
3606 # manipulating or removing existing feed tags. If you want to add new feeds, you should
3607 # use OutputPage::addFeedLink() instead.
3608 Hooks::run(
'AfterBuildFeedLinks', [ &$feedLinks ] );
3610 $tags += $feedLinks;
3614 if ( $config->
get(
'EnableCanonicalServerLink' ) ) {
3615 if ( $canonicalUrl !==
false ) {
3617 } elseif ( $this->isArticleRelated() ) {
3626 if ( in_array( $action, [
'history',
'info' ] ) ) {
3627 $query =
"action={$action}";
3631 $canonicalUrl = $this->
getTitle()->getCanonicalURL( $query );
3633 $reqUrl = $this->
getRequest()->getRequestURL();
3637 if ( $canonicalUrl !==
false ) {
3638 $tags[] = Html::element(
'link', [
3639 'rel' =>
'canonical',
3640 'href' => $canonicalUrl
3649 Hooks::run(
'OutputPageAfterGetHeadLinksArray', [ &$tags, $this ] );
3662 private function feedLink(
$type, $url, $text ) {
3663 return Html::element(
'link', [
3664 'rel' =>
'alternate',
3665 'type' =>
"application/$type+xml",
3680 public function addStyle( $style, $media =
'', $condition =
'', $dir =
'' ) {
3683 $options[
'media'] = $media;
3686 $options[
'condition'] = $condition;
3689 $options[
'dir'] = $dir;
3691 $this->styles[$style] = $options;
3701 public function addInlineStyle( $style_css, $flip =
'noflip' ) {
3702 if ( $flip ===
'flip' && $this->
getLanguage()->isRTL() ) {
3703 # If wanted, and the interface is right-to-left, flip the CSS
3704 $style_css = CSSJanus::transform( $style_css,
true,
false );
3706 $this->mInlineStyles .= Html::inlineStyle( $style_css );
3714 protected function buildExemptModules() {
3734 $chunks[] = implode(
'', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3738 $separateReq = [
'site.styles',
'user.styles' ];
3739 foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3740 if ( $moduleNames ) {
3741 $append[] = $this->makeResourceLoaderLink(
3742 array_diff( $moduleNames, $separateReq ),
3743 ResourceLoaderModule::TYPE_STYLES
3746 foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) {
3749 $append[] = $this->makeResourceLoaderLink( $name,
3750 ResourceLoaderModule::TYPE_STYLES
3756 $chunks[] = Html::element(
3758 [
'name' =>
'ResourceLoaderDynamicStyles',
'content' =>
'' ]
3760 $chunks = array_merge( $chunks, $append );
3763 return self::combineWrappedStrings( $chunks );
3769 public function buildCssLinksArray() {
3772 foreach ( $this->styles as
$file => $options ) {
3773 $link = $this->styleLink(
$file, $options );
3775 $links[
$file] = $link;
3788 protected function styleLink( $style, array $options ) {
3789 if ( isset( $options[
'dir'] ) && $this->
getLanguage()->getDir() != $options[
'dir'] ) {
3793 if ( isset( $options[
'media'] ) ) {
3794 $media = self::transformCssMedia( $options[
'media'] );
3795 if ( is_null( $media ) ) {
3802 if ( substr( $style, 0, 1 ) ==
'/' ||
3803 substr( $style, 0, 5 ) ==
'http:' ||
3804 substr( $style, 0, 6 ) ==
'https:' ) {
3809 $url = self::transformResourcePath(
3811 $config->
get(
'StylePath' ) .
'/' . $style
3815 $link = Html::linkedStyle( $url, $media );
3817 if ( isset( $options[
'condition'] ) ) {
3818 $condition = htmlspecialchars( $options[
'condition'] );
3819 $link =
"<!--[if $condition]>$link<![endif]-->";
3845 public static function transformResourcePath(
Config $config,
$path ) {
3849 $remotePathPrefix = $config->
get(
'ResourceBasePath' );
3850 if ( $remotePathPrefix ===
'' ) {
3855 $remotePath = $remotePathPrefix;
3857 if ( strpos(
$path, $remotePath ) !== 0 || substr(
$path, 0, 2 ) ===
'//' ) {
3866 $uploadPath = $config->
get(
'UploadPath' );
3867 if ( strpos(
$path, $uploadPath ) === 0 ) {
3868 $localDir = $config->
get(
'UploadDirectory' );
3869 $remotePathPrefix = $remotePath = $uploadPath;
3872 $path = RelPath::getRelativePath(
$path, $remotePath );
3873 return self::transformFilePath( $remotePathPrefix, $localDir,
$path );
3887 public static function transformFilePath( $remotePathPrefix, $localPath,
$file ) {
3888 $hash = md5_file(
"$localPath/$file" );
3889 if ( $hash ===
false ) {
3890 wfLogWarning( __METHOD__ .
": Failed to hash $localPath/$file" );
3893 return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3903 public static function transformCssMedia( $media ) {
3907 $screenMediaQueryRegex =
'/^(?:only\s+)?screen\b/i';
3911 'printable' =>
'print',
3912 'handheld' =>
'handheld',
3914 foreach ( $switches as $switch => $targetMedia ) {
3916 if ( $media == $targetMedia ) {
3918 } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3932 if ( $targetMedia ==
'print' || $media ==
'screen' ) {
3948 public function addWikiMsg( ) {
3949 $args = func_get_args();
3950 $name = array_shift(
$args );
3951 $this->addWikiMsgArray( $name,
$args );
3962 public function addWikiMsgArray( $name,
$args ) {
3963 $this->addHTML( $this->
msg( $name,
$args )->parseAsBlock() );
3991 public function wrapWikiMsg( $wrap ) {
3992 $msgSpecs = func_get_args();
3993 array_shift( $msgSpecs );
3994 $msgSpecs = array_values( $msgSpecs );
3996 foreach ( $msgSpecs as $n => $spec ) {
3997 if ( is_array( $spec ) ) {
3999 $name = array_shift(
$args );
4004 $s = str_replace(
'$' . ( $n + 1 ), $this->
msg( $name,
$args )->plain(),
$s );
4006 $this->addWikiTextAsInterface(
$s );
4014 public function isTOCEnabled() {
4015 return $this->mEnableTOC;
4025 public static function setupOOUI( $skinName =
'default', $dir =
'ltr' ) {
4027 $theme = $themes[$skinName] ?? $themes[
'default'];
4029 $themeClass =
"OOUI\\{$theme}Theme";
4030 OOUI\Theme::setSingleton(
new $themeClass() );
4031 OOUI\Element::setDefaultDir( $dir );
4040 public function enableOOUI() {
4042 strtolower( $this->
getSkin()->getSkinName() ),
4045 $this->addModuleStyles( [
4046 'oojs-ui-core.styles',
4047 'oojs-ui.styles.indicators',
4048 'mediawiki.widgets.styles',
4049 'oojs-ui-core.icons',
4062 public function getCSPNonce() {
4066 if ( $this->CSPNonce ===
null ) {
4069 $rand = random_bytes( 15 );
4070 $this->CSPNonce = base64_encode( $rand );
4072 return $this->CSPNonce;