MediaWiki REL1_34
OutputPage.php
Go to the documentation of this file.
1<?php
27use Wikimedia\RelPath;
28use Wikimedia\WrappedString;
29use Wikimedia\WrappedStringList;
30
48 protected $mMetatags = [];
49
51 protected $mLinktags = [];
52
54 protected $mCanonicalUrl = false;
55
59 private $mPageTitle = '';
60
69
74 public $mBodytext = '';
75
77 private $mHTMLtitle = '';
78
83 private $mIsArticle = false;
84
86 private $mIsArticleRelated = true;
87
89 private $mHasCopyright = false;
90
95 private $mPrintable = false;
96
101 private $mSubtitle = [];
102
104 public $mRedirect = '';
105
107 protected $mStatusCode;
108
113 protected $mLastModified = '';
114
116 protected $mCategoryLinks = [];
117
119 protected $mCategories = [
120 'hidden' => [],
121 'normal' => [],
122 ];
123
125 protected $mIndicators = [];
126
128 private $mLanguageLinks = [];
129
136 private $mScripts = '';
137
139 protected $mInlineStyles = '';
140
145 public $mPageLinkTitle = '';
146
148 protected $mHeadItems = [];
149
152
154 protected $mModules = [];
155
157 protected $mModuleStyles = [];
158
161
163 private $rlClient;
164
167
170
172 protected $mJsConfigVars = [];
173
175 protected $mTemplateIds = [];
176
178 protected $mImageTimeKeys = [];
179
181 public $mRedirectCode = '';
182
183 protected $mFeedLinksAppendQuery = null;
184
190 protected $mAllowedModules = [
191 ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
192 ];
193
195 protected $mDoNothing = false;
196
197 // Parser related.
198
200 protected $mContainsNewMagic = 0;
201
206 protected $mParserOptions = null;
207
213 private $mFeedLinks = [];
214
215 // Gwicke work on squid caching? Roughly from 2003.
216 protected $mEnableClientCache = true;
217
219 private $mArticleBodyOnly = false;
220
222 protected $mNewSectionLink = false;
223
225 protected $mHideNewSectionLink = false;
226
232 public $mNoGallery = false;
233
235 protected $mCdnMaxage = 0;
237 protected $mCdnMaxageLimit = INF;
238
244 protected $mPreventClickjacking = true;
245
247 private $mRevisionId = null;
248
250 private $mRevisionTimestamp = null;
251
253 protected $mFileVersion = null;
254
263 protected $styles = [];
264
265 private $mIndexPolicy = 'index';
266 private $mFollowPolicy = 'follow';
267
273 private $mVaryHeader = [
274 'Accept-Encoding' => null,
275 ];
276
283 private $mRedirectedFrom = null;
284
288 private $mProperties = [];
289
293 private $mTarget = null;
294
298 private $mEnableTOC = false;
299
304
306 private $limitReportJSData = [];
307
309 private $contentOverrides = [];
310
313
317 private $mLinkHeader = [];
318
322 private $CSPNonce;
323
327 private static $cacheVaryCookies = null;
328
336 $this->setContext( $context );
337 }
338
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;
349 }
350
356 public function getRedirect() {
357 return $this->mRedirect;
358 }
359
368 public function setCopyrightUrl( $url ) {
369 $this->copyrightUrl = $url;
370 }
371
377 public function setStatusCode( $statusCode ) {
378 $this->mStatusCode = $statusCode;
379 }
380
388 public function addMeta( $name, $val ) {
389 $this->mMetatags[] = [ $name, $val ];
390 }
391
398 public function getMetaTags() {
399 return $this->mMetatags;
400 }
401
409 public function addLink( array $linkarr ) {
410 $this->mLinktags[] = $linkarr;
411 }
412
419 public function getLinkTags() {
420 return $this->mLinktags;
421 }
422
428 public function setCanonicalUrl( $url ) {
429 $this->mCanonicalUrl = $url;
430 }
431
439 public function getCanonicalUrl() {
440 return $this->mCanonicalUrl;
441 }
442
450 public function addScript( $script ) {
451 $this->mScripts .= $script;
452 }
453
462 public function addScriptFile( $file, $unused = null ) {
463 $this->addScript( Html::linkedScript( $file, $this->getCSPNonce() ) );
464 }
465
472 public function addInlineScript( $script ) {
473 $this->mScripts .= Html::inlineScript( "\n$script\n", $this->getCSPNonce() ) . "\n";
474 }
475
484 protected function filterModules( array $modules, $position = null,
485 $type = ResourceLoaderModule::TYPE_COMBINED
486 ) {
488 $filteredModules = [];
489 foreach ( $modules as $val ) {
490 $module = $resourceLoader->getModule( $val );
491 if ( $module instanceof ResourceLoaderModule
492 && $module->getOrigin() <= $this->getAllowedModules( $type )
493 ) {
494 if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
495 $this->warnModuleTargetFilter( $module->getName() );
496 continue;
497 }
498 $filteredModules[] = $val;
499 }
500 }
501 return $filteredModules;
502 }
503
504 private function warnModuleTargetFilter( $moduleName ) {
505 static $warnings = [];
506 if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
507 return;
508 }
509 $warnings[$this->mTarget][$moduleName] = true;
510 $this->getResourceLoader()->getLogger()->debug(
511 'Module "{module}" not loadable on target "{target}".',
512 [
513 'module' => $moduleName,
514 'target' => $this->mTarget,
515 ]
516 );
517 }
518
528 public function getModules( $filter = false, $position = null, $param = 'mModules',
529 $type = ResourceLoaderModule::TYPE_COMBINED
530 ) {
531 $modules = array_values( array_unique( $this->$param ) );
532 return $filter
533 ? $this->filterModules( $modules, null, $type )
534 : $modules;
535 }
536
542 public function addModules( $modules ) {
543 $this->mModules = array_merge( $this->mModules, (array)$modules );
544 }
545
553 public function getModuleStyles( $filter = false, $position = null ) {
554 return $this->getModules( $filter, null, 'mModuleStyles',
555 ResourceLoaderModule::TYPE_STYLES
556 );
557 }
558
568 public function addModuleStyles( $modules ) {
569 $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
570 }
571
575 public function getTarget() {
576 return $this->mTarget;
577 }
578
584 public function setTarget( $target ) {
585 $this->mTarget = $target;
586 }
587
595 public function addContentOverride( LinkTarget $target, Content $content ) {
596 if ( !$this->contentOverrides ) {
597 // Register a callback for $this->contentOverrides on the first call
598 $this->addContentOverrideCallback( function ( LinkTarget $target ) {
599 $key = $target->getNamespace() . ':' . $target->getDBkey();
600 return $this->contentOverrides[$key] ?? null;
601 } );
602 }
603
604 $key = $target->getNamespace() . ':' . $target->getDBkey();
605 $this->contentOverrides[$key] = $content;
606 }
607
615 public function addContentOverrideCallback( callable $callback ) {
616 $this->contentOverrideCallbacks[] = $callback;
617 }
618
624 public function getHeadItemsArray() {
625 return $this->mHeadItems;
626 }
627
640 public function addHeadItem( $name, $value ) {
641 $this->mHeadItems[$name] = $value;
642 }
643
650 public function addHeadItems( $values ) {
651 $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
652 }
653
660 public function hasHeadItem( $name ) {
661 return isset( $this->mHeadItems[$name] );
662 }
663
670 public function addBodyClasses( $classes ) {
671 $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes );
672 }
673
681 public function setArticleBodyOnly( $only ) {
682 $this->mArticleBodyOnly = $only;
683 }
684
690 public function getArticleBodyOnly() {
691 return $this->mArticleBodyOnly;
692 }
693
701 public function setProperty( $name, $value ) {
702 $this->mProperties[$name] = $value;
703 }
704
712 public function getProperty( $name ) {
713 return $this->mProperties[$name] ?? null;
714 }
715
727 public function checkLastModified( $timestamp ) {
728 if ( !$timestamp || $timestamp == '19700101000000' ) {
729 wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
730 return false;
731 }
732 $config = $this->getConfig();
733 if ( !$config->get( 'CachePages' ) ) {
734 wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
735 return false;
736 }
737
738 $timestamp = wfTimestamp( TS_MW, $timestamp );
739 $modifiedTimes = [
740 'page' => $timestamp,
741 'user' => $this->getUser()->getTouched(),
742 'epoch' => $config->get( 'CacheEpoch' )
743 ];
744 if ( $config->get( 'UseCdn' ) ) {
745 $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, $this->getCdnCacheEpoch(
746 time(),
747 $config->get( 'CdnMaxAge' )
748 ) );
749 }
750 Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
751
752 $maxModified = max( $modifiedTimes );
753 $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
754
755 $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
756 if ( $clientHeader === false ) {
757 wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
758 return false;
759 }
760
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 );
765
766 Wikimedia\suppressWarnings(); // E_STRICT system time warnings
767 $clientHeaderTime = strtotime( $clientHeader );
768 Wikimedia\restoreWarnings();
769 if ( !$clientHeaderTime ) {
770 wfDebug( __METHOD__
771 . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
772 return false;
773 }
774 $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
775
776 # Make debug info
777 $info = '';
778 foreach ( $modifiedTimes as $name => $value ) {
779 if ( $info !== '' ) {
780 $info .= ', ';
781 }
782 $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
783 }
784
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' );
791 return false;
792 }
793
794 # Not modified
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();
800 $this->disable();
801
802 // Don't output a compressed blob when using ob_gzhandler;
803 // it's technically against HTTP spec and seems to confuse
804 // Firefox when the response gets split over two packets.
806
807 return true;
808 }
809
815 private function getCdnCacheEpoch( $reqTime, $maxAge ) {
816 // Ensure Last-Modified is never more than $wgCdnMaxAge in the past,
817 // because even if the wiki page content hasn't changed since, static
818 // resources may have changed (skin HTML, interface messages, urls, etc.)
819 // and must roll-over in a timely manner (T46570)
820 return $reqTime - $maxAge;
821 }
822
829 public function setLastModified( $timestamp ) {
830 $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
831 }
832
841 public function setRobotPolicy( $policy ) {
842 $policy = Article::formatRobotPolicy( $policy );
843
844 if ( isset( $policy['index'] ) ) {
845 $this->setIndexPolicy( $policy['index'] );
846 }
847 if ( isset( $policy['follow'] ) ) {
848 $this->setFollowPolicy( $policy['follow'] );
849 }
850 }
851
859 public function setIndexPolicy( $policy ) {
860 $policy = trim( $policy );
861 if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
862 $this->mIndexPolicy = $policy;
863 }
864 }
865
873 public function setFollowPolicy( $policy ) {
874 $policy = trim( $policy );
875 if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
876 $this->mFollowPolicy = $policy;
877 }
878 }
879
886 public function setHTMLTitle( $name ) {
887 if ( $name instanceof Message ) {
888 $this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
889 } else {
890 $this->mHTMLtitle = $name;
891 }
892 }
893
899 public function getHTMLTitle() {
900 return $this->mHTMLtitle;
901 }
902
908 public function setRedirectedFrom( $t ) {
909 $this->mRedirectedFrom = $t;
910 }
911
924 public function setPageTitle( $name ) {
925 if ( $name instanceof Message ) {
926 $name = $name->setContext( $this->getContext() )->text();
927 }
928
929 # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
930 # but leave "<i>foobar</i>" alone
931 $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
932 $this->mPageTitle = $nameWithTags;
933
934 # change "<i>foo&amp;bar</i>" to "foo&bar"
935 $this->setHTMLTitle(
936 $this->msg( 'pagetitle' )->plaintextParams( Sanitizer::stripAllTags( $nameWithTags ) )
937 ->inContentLanguage()
938 );
939 }
940
946 public function getPageTitle() {
947 return $this->mPageTitle;
948 }
949
957 public function setDisplayTitle( $html ) {
958 $this->displayTitle = $html;
959 }
960
969 public function getDisplayTitle() {
970 $html = $this->displayTitle;
971 if ( $html === null ) {
972 $html = $this->getTitle()->getPrefixedText();
973 }
974
975 return Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $html ) );
976 }
977
984 public function getUnprefixedDisplayTitle() {
985 $text = $this->getDisplayTitle();
986 $nsPrefix = $this->getTitle()->getNsText() . ':';
987 $prefix = preg_quote( $nsPrefix, '/' );
988
989 return preg_replace( "/^$prefix/i", '', $text );
990 }
991
997 public function setTitle( Title $t ) {
998 // @phan-suppress-next-next-line PhanUndeclaredMethod
999 // @fixme Not all implementations of IContextSource have this method!
1000 $this->getContext()->setTitle( $t );
1001 }
1002
1008 public function setSubtitle( $str ) {
1009 $this->clearSubtitle();
1010 $this->addSubtitle( $str );
1011 }
1012
1018 public function addSubtitle( $str ) {
1019 if ( $str instanceof Message ) {
1020 $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1021 } else {
1022 $this->mSubtitle[] = $str;
1023 }
1024 }
1025
1034 public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1035 if ( $title->isRedirect() ) {
1036 $query['redirect'] = 'no';
1037 }
1038 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1039 return wfMessage( 'backlinksubtitle' )
1040 ->rawParams( $linkRenderer->makeLink( $title, null, [], $query ) );
1041 }
1042
1049 public function addBacklinkSubtitle( Title $title, $query = [] ) {
1050 $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1051 }
1052
1056 public function clearSubtitle() {
1057 $this->mSubtitle = [];
1058 }
1059
1065 public function getSubtitle() {
1066 return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1067 }
1068
1073 public function setPrintable() {
1074 $this->mPrintable = true;
1075 }
1076
1082 public function isPrintable() {
1083 return $this->mPrintable;
1084 }
1085
1089 public function disable() {
1090 $this->mDoNothing = true;
1091 }
1092
1098 public function isDisabled() {
1099 return $this->mDoNothing;
1100 }
1101
1107 public function showNewSectionLink() {
1108 return $this->mNewSectionLink;
1109 }
1110
1116 public function forceHideNewSectionLink() {
1117 return $this->mHideNewSectionLink;
1118 }
1119
1128 public function setSyndicated( $show = true ) {
1129 if ( $show ) {
1130 $this->setFeedAppendQuery( false );
1131 } else {
1132 $this->mFeedLinks = [];
1133 }
1134 }
1135
1142 protected function getAdvertisedFeedTypes() {
1143 if ( $this->getConfig()->get( 'Feed' ) ) {
1144 return $this->getConfig()->get( 'AdvertisedFeedTypes' );
1145 } else {
1146 return [];
1147 }
1148 }
1149
1159 public function setFeedAppendQuery( $val ) {
1160 $this->mFeedLinks = [];
1161
1162 foreach ( $this->getAdvertisedFeedTypes() as $type ) {
1163 $query = "feed=$type";
1164 if ( is_string( $val ) ) {
1165 $query .= '&' . $val;
1166 }
1167 $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1168 }
1169 }
1170
1177 public function addFeedLink( $format, $href ) {
1178 if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) {
1179 $this->mFeedLinks[$format] = $href;
1180 }
1181 }
1182
1187 public function isSyndicated() {
1188 return count( $this->mFeedLinks ) > 0;
1189 }
1190
1195 public function getSyndicationLinks() {
1196 return $this->mFeedLinks;
1197 }
1198
1204 public function getFeedAppendQuery() {
1205 return $this->mFeedLinksAppendQuery;
1206 }
1207
1215 public function setArticleFlag( $newVal ) {
1216 $this->mIsArticle = $newVal;
1217 if ( $newVal ) {
1218 $this->mIsArticleRelated = $newVal;
1219 }
1220 }
1221
1228 public function isArticle() {
1229 return $this->mIsArticle;
1230 }
1231
1238 public function setArticleRelated( $newVal ) {
1239 $this->mIsArticleRelated = $newVal;
1240 if ( !$newVal ) {
1241 $this->mIsArticle = false;
1242 }
1243 }
1244
1250 public function isArticleRelated() {
1251 return $this->mIsArticleRelated;
1252 }
1253
1259 public function setCopyright( $hasCopyright ) {
1260 $this->mHasCopyright = $hasCopyright;
1261 }
1262
1272 public function showsCopyright() {
1273 return $this->isArticle() || $this->mHasCopyright;
1274 }
1275
1282 public function addLanguageLinks( array $newLinkArray ) {
1283 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1284 }
1285
1292 public function setLanguageLinks( array $newLinkArray ) {
1293 $this->mLanguageLinks = $newLinkArray;
1294 }
1295
1301 public function getLanguageLinks() {
1302 return $this->mLanguageLinks;
1303 }
1304
1310 public function addCategoryLinks( array $categories ) {
1311 if ( !$categories ) {
1312 return;
1313 }
1314
1315 $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1316
1317 # Set all the values to 'normal'.
1318 $categories = array_fill_keys( array_keys( $categories ), 'normal' );
1319
1320 # Mark hidden categories
1321 foreach ( $res as $row ) {
1322 if ( isset( $row->pp_value ) ) {
1323 $categories[$row->page_title] = 'hidden';
1324 }
1325 }
1326
1327 // Avoid PHP 7.1 warning of passing $this by reference
1328 $outputPage = $this;
1329 # Add the remaining categories to the skin
1330 if ( Hooks::run(
1331 'OutputPageMakeCategoryLinks',
1332 [ &$outputPage, $categories, &$this->mCategoryLinks ] )
1333 ) {
1334 $services = MediaWikiServices::getInstance();
1335 $linkRenderer = $services->getLinkRenderer();
1336 foreach ( $categories as $category => $type ) {
1337 // array keys will cast numeric category names to ints, so cast back to string
1338 $category = (string)$category;
1339 $origcategory = $category;
1340 $title = Title::makeTitleSafe( NS_CATEGORY, $category );
1341 if ( !$title ) {
1342 continue;
1343 }
1344 $services->getContentLanguage()->findVariantLink( $category, $title, true );
1345 if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1346 continue;
1347 }
1348 $text = $services->getContentLanguage()->convertHtml( $title->getText() );
1349 $this->mCategories[$type][] = $title->getText();
1350 $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) );
1351 }
1352 }
1353 }
1354
1359 protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1360 # Add the links to a LinkBatch
1361 $arr = [ NS_CATEGORY => $categories ];
1362 $lb = new LinkBatch;
1363 $lb->setArray( $arr );
1364
1365 # Fetch existence plus the hiddencat property
1366 $dbr = wfGetDB( DB_REPLICA );
1367 $fields = array_merge(
1368 LinkCache::getSelectFields(),
1369 [ 'page_namespace', 'page_title', 'pp_value' ]
1370 );
1371
1372 $res = $dbr->select( [ 'page', 'page_props' ],
1373 $fields,
1374 $lb->constructSet( 'page', $dbr ),
1375 __METHOD__,
1376 [],
1377 [ 'page_props' => [ 'LEFT JOIN', [
1378 'pp_propname' => 'hiddencat',
1379 'pp_page = page_id'
1380 ] ] ]
1381 );
1382
1383 # Add the results to the link cache
1384 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1385 $lb->addResultToCache( $linkCache, $res );
1386
1387 return $res;
1388 }
1389
1395 public function setCategoryLinks( array $categories ) {
1396 $this->mCategoryLinks = [];
1397 $this->addCategoryLinks( $categories );
1398 }
1399
1408 public function getCategoryLinks() {
1409 return $this->mCategoryLinks;
1410 }
1411
1421 public function getCategories( $type = 'all' ) {
1422 if ( $type === 'all' ) {
1423 $allCategories = [];
1424 foreach ( $this->mCategories as $categories ) {
1425 $allCategories = array_merge( $allCategories, $categories );
1426 }
1427 return $allCategories;
1428 }
1429 if ( !isset( $this->mCategories[$type] ) ) {
1430 throw new InvalidArgumentException( 'Invalid category type given: ' . $type );
1431 }
1432 return $this->mCategories[$type];
1433 }
1434
1444 public function setIndicators( array $indicators ) {
1445 $this->mIndicators = $indicators + $this->mIndicators;
1446 // Keep ordered by key
1447 ksort( $this->mIndicators );
1448 }
1449
1458 public function getIndicators() {
1459 return $this->mIndicators;
1460 }
1461
1470 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1471 $this->addModuleStyles( 'mediawiki.helplink' );
1472 $text = $this->msg( 'helppage-top-gethelp' )->escaped();
1473
1474 if ( $overrideBaseUrl ) {
1475 $helpUrl = $to;
1476 } else {
1477 $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1478 $helpUrl = "https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1479 }
1480
1481 $link = Html::rawElement(
1482 'a',
1483 [
1484 'href' => $helpUrl,
1485 'target' => '_blank',
1486 'class' => 'mw-helplink',
1487 ],
1488 $text
1489 );
1490
1491 $this->setIndicators( [ 'mw-helplink' => $link ] );
1492 }
1493
1502 public function disallowUserJs() {
1503 $this->reduceAllowedModules(
1504 ResourceLoaderModule::TYPE_SCRIPTS,
1505 ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1506 );
1507
1508 // Site-wide styles are controlled by a config setting, see T73621
1509 // for background on why. User styles are never allowed.
1510 if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1511 $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1512 } else {
1513 $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1514 }
1515 $this->reduceAllowedModules(
1516 ResourceLoaderModule::TYPE_STYLES,
1517 $styleOrigin
1518 );
1519 }
1520
1527 public function getAllowedModules( $type ) {
1528 if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1529 return min( array_values( $this->mAllowedModules ) );
1530 } else {
1531 return $this->mAllowedModules[$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1532 }
1533 }
1534
1544 public function reduceAllowedModules( $type, $level ) {
1545 $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1546 }
1547
1553 public function prependHTML( $text ) {
1554 $this->mBodytext = $text . $this->mBodytext;
1555 }
1556
1562 public function addHTML( $text ) {
1563 $this->mBodytext .= $text;
1564 }
1565
1575 public function addElement( $element, array $attribs = [], $contents = '' ) {
1576 $this->addHTML( Html::element( $element, $attribs, $contents ) );
1577 }
1578
1582 public function clearHTML() {
1583 $this->mBodytext = '';
1584 }
1585
1591 public function getHTML() {
1592 return $this->mBodytext;
1593 }
1594
1603 public function parserOptions( $options = null ) {
1604 if ( $options !== null ) {
1605 wfDeprecated( __METHOD__ . ' with non-null $options', '1.31' );
1606 }
1607
1608 if ( $options !== null && !empty( $options->isBogus ) ) {
1609 // Someone is trying to set a bogus pre-$wgUser PO. Check if it has
1610 // been changed somehow, and keep it if so.
1611 $anonPO = ParserOptions::newFromAnon();
1612 $anonPO->setAllowUnsafeRawHtml( false );
1613 if ( !$options->matches( $anonPO ) ) {
1614 wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
1615 $options->isBogus = false;
1616 }
1617 }
1618
1619 if ( !$this->mParserOptions ) {
1620 if ( !$this->getUser()->isSafeToLoad() ) {
1621 // $wgUser isn't unstubbable yet, so don't try to get a
1622 // ParserOptions for it. And don't cache this ParserOptions
1623 // either.
1624 $po = ParserOptions::newFromAnon();
1625 $po->setAllowUnsafeRawHtml( false );
1626 $po->isBogus = true;
1627 if ( $options !== null ) {
1628 $this->mParserOptions = empty( $options->isBogus ) ? $options : null;
1629 }
1630 return $po;
1631 }
1632
1633 $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1634 $this->mParserOptions->setAllowUnsafeRawHtml( false );
1635 }
1636
1637 if ( $options !== null && !empty( $options->isBogus ) ) {
1638 // They're trying to restore the bogus pre-$wgUser PO. Do the right
1639 // thing.
1640 return wfSetVar( $this->mParserOptions, null, true );
1641 } else {
1642 return wfSetVar( $this->mParserOptions, $options );
1643 }
1644 }
1645
1653 public function setRevisionId( $revid ) {
1654 $val = is_null( $revid ) ? null : intval( $revid );
1655 return wfSetVar( $this->mRevisionId, $val, true );
1656 }
1657
1663 public function getRevisionId() {
1664 return $this->mRevisionId;
1665 }
1666
1673 public function isRevisionCurrent() {
1674 return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
1675 }
1676
1684 public function setRevisionTimestamp( $timestamp ) {
1685 return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
1686 }
1687
1694 public function getRevisionTimestamp() {
1695 return $this->mRevisionTimestamp;
1696 }
1697
1704 public function setFileVersion( $file ) {
1705 $val = null;
1706 if ( $file instanceof File && $file->exists() ) {
1707 $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1708 }
1709 return wfSetVar( $this->mFileVersion, $val, true );
1710 }
1711
1717 public function getFileVersion() {
1718 return $this->mFileVersion;
1719 }
1720
1727 public function getTemplateIds() {
1728 return $this->mTemplateIds;
1729 }
1730
1737 public function getFileSearchOptions() {
1738 return $this->mImageTimeKeys;
1739 }
1740
1757 public function addWikiTextAsInterface(
1758 $text, $linestart = true, Title $title = null
1759 ) {
1760 if ( $title === null ) {
1761 $title = $this->getTitle();
1762 }
1763 if ( !$title ) {
1764 throw new MWException( 'Title is null' );
1765 }
1766 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true );
1767 }
1768
1783 $wrapperClass, $text
1784 ) {
1786 $text, $this->getTitle(),
1787 /*linestart*/true, /*interface*/true,
1788 $wrapperClass
1789 );
1790 }
1791
1807 public function addWikiTextAsContent(
1808 $text, $linestart = true, Title $title = null
1809 ) {
1810 if ( $title === null ) {
1811 $title = $this->getTitle();
1812 }
1813 if ( !$title ) {
1814 throw new MWException( 'Title is null' );
1815 }
1816 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false );
1817 }
1818
1832 $text, Title $title, $linestart, $interface, $wrapperClass = null
1833 ) {
1834 $parserOutput = $this->parseInternal(
1835 $text, $title, $linestart, true, $interface, /*language*/null
1836 );
1837
1838 $this->addParserOutput( $parserOutput, [
1839 'enableSectionEditLinks' => false,
1840 'wrapperDivClass' => $wrapperClass ?? '',
1841 ] );
1842 }
1843
1852 public function addParserOutputMetadata( ParserOutput $parserOutput ) {
1853 $this->mLanguageLinks =
1854 array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() );
1855 $this->addCategoryLinks( $parserOutput->getCategories() );
1856 $this->setIndicators( $parserOutput->getIndicators() );
1857 $this->mNewSectionLink = $parserOutput->getNewSection();
1858 $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1859
1860 if ( !$parserOutput->isCacheable() ) {
1861 $this->enableClientCache( false );
1862 }
1863 $this->mNoGallery = $parserOutput->getNoGallery();
1864 $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1865 $this->addModules( $parserOutput->getModules() );
1866 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1867 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1868 $this->mPreventClickjacking = $this->mPreventClickjacking
1869 || $parserOutput->preventClickjacking();
1870
1871 // Template versioning...
1872 foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1873 if ( isset( $this->mTemplateIds[$ns] ) ) {
1874 $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1875 } else {
1876 $this->mTemplateIds[$ns] = $dbks;
1877 }
1878 }
1879 // File versioning...
1880 foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1881 $this->mImageTimeKeys[$dbk] = $data;
1882 }
1883
1884 // Hooks registered in the object
1885 $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1886 foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1887 list( $hookName, $data ) = $hookInfo;
1888 if ( isset( $parserOutputHooks[$hookName] ) ) {
1889 $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1890 }
1891 }
1892
1893 // Enable OOUI if requested via ParserOutput
1894 if ( $parserOutput->getEnableOOUI() ) {
1895 $this->enableOOUI();
1896 }
1897
1898 // Include parser limit report
1899 if ( !$this->limitReportJSData ) {
1900 $this->limitReportJSData = $parserOutput->getLimitReportJSData();
1901 }
1902
1903 // Link flags are ignored for now, but may in the future be
1904 // used to mark individual language links.
1905 $linkFlags = [];
1906 // Avoid PHP 7.1 warning of passing $this by reference
1907 $outputPage = $this;
1908 Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1909 Hooks::runWithoutAbort( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] );
1910
1911 // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
1912 // so that extensions may modify ParserOutput to toggle TOC.
1913 // This cannot be moved to addParserOutputText because that is not
1914 // called by EditPage for Preview.
1915 if ( $parserOutput->getTOCHTML() ) {
1916 $this->mEnableTOC = true;
1917 }
1918 }
1919
1928 public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) {
1929 $this->addParserOutputText( $parserOutput, $poOptions );
1930
1931 $this->addModules( $parserOutput->getModules() );
1932 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1933
1934 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1935 }
1936
1944 public function addParserOutputText( ParserOutput $parserOutput, $poOptions = [] ) {
1945 $text = $parserOutput->getText( $poOptions );
1946 // Avoid PHP 7.1 warning of passing $this by reference
1947 $outputPage = $this;
1948 Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] );
1949 $this->addHTML( $text );
1950 }
1951
1958 public function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) {
1959 $this->addParserOutputMetadata( $parserOutput );
1960 $this->addParserOutputText( $parserOutput, $poOptions );
1961 }
1962
1968 public function addTemplate( &$template ) {
1969 $this->addHTML( $template->getHTML() );
1970 }
1971
1990 public function parse( $text, $linestart = true, $interface = false, $language = null ) {
1991 wfDeprecated( __METHOD__, '1.33' );
1992 return $this->parseInternal(
1993 $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, $language
1994 )->getText( [
1995 'enableSectionEditLinks' => false,
1996 ] );
1997 }
1998
2010 public function parseAsContent( $text, $linestart = true ) {
2011 return $this->parseInternal(
2012 $text, $this->getTitle(), $linestart, /*tidy*/true, /*interface*/false, /*language*/null
2013 )->getText( [
2014 'enableSectionEditLinks' => false,
2015 'wrapperDivClass' => ''
2016 ] );
2017 }
2018
2031 public function parseAsInterface( $text, $linestart = true ) {
2032 return $this->parseInternal(
2033 $text, $this->getTitle(), $linestart, /*tidy*/true, /*interface*/true, /*language*/null
2034 )->getText( [
2035 'enableSectionEditLinks' => false,
2036 'wrapperDivClass' => ''
2037 ] );
2038 }
2039
2054 public function parseInlineAsInterface( $text, $linestart = true ) {
2055 return Parser::stripOuterParagraph(
2056 $this->parseAsInterface( $text, $linestart )
2057 );
2058 }
2059
2073 public function parseInline( $text, $linestart = true, $interface = false ) {
2074 wfDeprecated( __METHOD__, '1.33' );
2075 $parsed = $this->parseInternal(
2076 $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, /*language*/null
2077 )->getText( [
2078 'enableSectionEditLinks' => false,
2079 'wrapperDivClass' => '', /* no wrapper div */
2080 ] );
2081 return Parser::stripOuterParagraph( $parsed );
2082 }
2083
2098 private function parseInternal( $text, $title, $linestart, $tidy, $interface, $language ) {
2099 if ( is_null( $title ) ) {
2100 throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
2101 }
2102
2103 $popts = $this->parserOptions();
2104 $oldTidy = $popts->setTidy( $tidy );
2105 $oldInterface = $popts->setInterfaceMessage( (bool)$interface );
2106
2107 if ( $language !== null ) {
2108 $oldLang = $popts->setTargetLanguage( $language );
2109 }
2110
2111 $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
2112 $text, $title, $popts,
2113 $linestart, true, $this->mRevisionId
2114 );
2115
2116 $popts->setTidy( $oldTidy );
2117 $popts->setInterfaceMessage( $oldInterface );
2118
2119 if ( $language !== null ) {
2120 $popts->setTargetLanguage( $oldLang );
2121 }
2122
2123 return $parserOutput;
2124 }
2125
2131 public function setCdnMaxage( $maxage ) {
2132 $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2133 }
2134
2144 public function lowerCdnMaxage( $maxage ) {
2145 $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2146 $this->setCdnMaxage( $this->mCdnMaxage );
2147 }
2148
2161 public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2162 $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
2163 $maxTTL = $maxTTL ?: $this->getConfig()->get( 'CdnMaxAge' );
2164
2165 if ( $mtime === null || $mtime === false ) {
2166 return; // entity does not exist
2167 }
2168
2169 $age = MWTimestamp::time() - (int)wfTimestamp( TS_UNIX, $mtime );
2170 $adaptiveTTL = max( 0.9 * $age, $minTTL );
2171 $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2172
2173 $this->lowerCdnMaxage( (int)$adaptiveTTL );
2174 }
2175
2183 public function enableClientCache( $state ) {
2184 return wfSetVar( $this->mEnableClientCache, $state );
2185 }
2186
2192 public function getCacheVaryCookies() {
2193 if ( self::$cacheVaryCookies === null ) {
2194 $config = $this->getConfig();
2195 self::$cacheVaryCookies = array_values( array_unique( array_merge(
2196 SessionManager::singleton()->getVaryCookies(),
2197 [
2198 'forceHTTPS',
2199 ],
2200 $config->get( 'CacheVaryCookies' )
2201 ) ) );
2202 Hooks::run( 'GetCacheVaryCookies', [ $this, &self::$cacheVaryCookies ] );
2203 }
2204 return self::$cacheVaryCookies;
2205 }
2206
2213 public function haveCacheVaryCookies() {
2214 $request = $this->getRequest();
2215 foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2216 if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
2217 wfDebug( __METHOD__ . ": found $cookieName\n" );
2218 return true;
2219 }
2220 }
2221 wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
2222 return false;
2223 }
2224
2234 public function addVaryHeader( $header, array $option = null ) {
2235 if ( $option !== null && count( $option ) > 0 ) {
2236 wfDeprecated( 'addVaryHeader $option is ignored', '1.34' );
2237 }
2238 if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2239 $this->mVaryHeader[$header] = null;
2240 }
2241 }
2242
2249 public function getVaryHeader() {
2250 // If we vary on cookies, let's make sure it's always included here too.
2251 if ( $this->getCacheVaryCookies() ) {
2252 $this->addVaryHeader( 'Cookie' );
2253 }
2254
2255 foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2256 $this->addVaryHeader( $header, $options );
2257 }
2258 return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2259 }
2260
2266 public function addLinkHeader( $header ) {
2267 $this->mLinkHeader[] = $header;
2268 }
2269
2275 public function getLinkHeader() {
2276 if ( !$this->mLinkHeader ) {
2277 return false;
2278 }
2279
2280 return 'Link: ' . implode( ',', $this->mLinkHeader );
2281 }
2282
2290 private function addAcceptLanguage() {
2291 $title = $this->getTitle();
2292 if ( !$title instanceof Title ) {
2293 return;
2294 }
2295
2296 $lang = $title->getPageLanguage();
2297 if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2298 $this->addVaryHeader( 'Accept-Language' );
2299 }
2300 }
2301
2312 public function preventClickjacking( $enable = true ) {
2313 $this->mPreventClickjacking = $enable;
2314 }
2315
2321 public function allowClickjacking() {
2322 $this->mPreventClickjacking = false;
2323 }
2324
2331 public function getPreventClickjacking() {
2332 return $this->mPreventClickjacking;
2333 }
2334
2342 public function getFrameOptions() {
2343 $config = $this->getConfig();
2344 if ( $config->get( 'BreakFrames' ) ) {
2345 return 'DENY';
2346 } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2347 return $config->get( 'EditPageFrameOptions' );
2348 }
2349 return false;
2350 }
2351
2358 private function getOriginTrials() {
2359 $config = $this->getConfig();
2360
2361 return $config->get( 'OriginTrials' );
2362 }
2363
2364 private function getReportTo() {
2365 $config = $this->getConfig();
2366
2367 $expiry = $config->get( 'ReportToExpiry' );
2368
2369 if ( !$expiry ) {
2370 return false;
2371 }
2372
2373 $endpoints = $config->get( 'ReportToEndpoints' );
2374
2375 if ( !$endpoints ) {
2376 return false;
2377 }
2378
2379 $output = [ 'max_age' => $expiry, 'endpoints' => [] ];
2380
2381 foreach ( $endpoints as $endpoint ) {
2382 $output['endpoints'][] = [ 'url' => $endpoint ];
2383 }
2384
2385 return json_encode( $output, JSON_UNESCAPED_SLASHES );
2386 }
2387
2388 private function getFeaturePolicyReportOnly() {
2389 $config = $this->getConfig();
2390
2391 $features = $config->get( 'FeaturePolicyReportOnly' );
2392 return implode( ';', $features );
2393 }
2394
2398 public function sendCacheControl() {
2399 $response = $this->getRequest()->response();
2400 $config = $this->getConfig();
2401
2402 $this->addVaryHeader( 'Cookie' );
2403 $this->addAcceptLanguage();
2404
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() );
2408
2409 if ( $this->mEnableClientCache ) {
2410 if (
2411 $config->get( 'UseCdn' ) &&
2412 !$response->hasCookies() &&
2413 !SessionManager::getGlobalSession()->isPersistent() &&
2414 !$this->isPrintable() &&
2415 $this->mCdnMaxage != 0 &&
2416 !$this->haveCacheVaryCookies()
2417 ) {
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
2422 wfDebug( __METHOD__ .
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" );
2426 $response->header( "Cache-Control: " .
2427 "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2428 } else {
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" );
2434 }
2435 if ( $this->mLastModified ) {
2436 $response->header( "Last-Modified: {$this->mLastModified}" );
2437 }
2438 } else {
2439 wfDebug( __METHOD__ . ": no caching **", 'private' );
2440
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' );
2446 }
2447 }
2448
2454 public function loadSkinModules( $sk ) {
2455 foreach ( $sk->getDefaultModules() as $group => $modules ) {
2456 if ( $group === 'styles' ) {
2457 foreach ( $modules as $key => $moduleMembers ) {
2458 $this->addModuleStyles( $moduleMembers );
2459 }
2460 } else {
2461 $this->addModules( $modules );
2462 }
2463 }
2464 }
2465
2476 public function output( $return = false ) {
2477 if ( $this->mDoNothing ) {
2478 return $return ? '' : null;
2479 }
2480
2481 $response = $this->getRequest()->response();
2482 $config = $this->getConfig();
2483
2484 if ( $this->mRedirect != '' ) {
2485 # Standards require redirect URLs to be absolute
2486 $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
2487
2488 $redirect = $this->mRedirect;
2489 $code = $this->mRedirectCode;
2490
2491 if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2492 if ( $code == '301' || $code == '303' ) {
2493 if ( !$config->get( 'DebugRedirects' ) ) {
2494 $response->statusHeader( $code );
2495 }
2496 $this->mLastModified = wfTimestamp( TS_RFC2822 );
2497 }
2498 if ( $config->get( 'VaryOnXFP' ) ) {
2499 $this->addVaryHeader( 'X-Forwarded-Proto' );
2500 }
2501 $this->sendCacheControl();
2502
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";
2509 } else {
2510 $response->header( 'Location: ' . $redirect );
2511 }
2512 }
2513
2514 return $return ? '' : null;
2515 } elseif ( $this->mStatusCode ) {
2516 $response->statusHeader( $this->mStatusCode );
2517 }
2518
2519 # Buffer output; final headers may depend on later processing
2520 ob_start();
2521
2522 $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2523 $response->header( 'Content-language: ' .
2524 MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2525
2526 $linkHeader = $this->getLinkHeader();
2527 if ( $linkHeader ) {
2528 $response->header( $linkHeader );
2529 }
2530
2531 // Prevent framing, if requested
2532 $frameOptions = $this->getFrameOptions();
2533 if ( $frameOptions ) {
2534 $response->header( "X-Frame-Options: $frameOptions" );
2535 }
2536
2537 $originTrials = $this->getOriginTrials();
2538 foreach ( $originTrials as $originTrial ) {
2539 $response->header( "Origin-Trial: $originTrial", false );
2540 }
2541
2542 $reportTo = $this->getReportTo();
2543 if ( $reportTo ) {
2544 $response->header( "Report-To: $reportTo" );
2545 }
2546
2547 $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
2548 if ( $featurePolicyReportOnly ) {
2549 $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" );
2550 }
2551
2553
2554 if ( $this->mArticleBodyOnly ) {
2555 echo $this->mBodytext;
2556 } else {
2557 // Enable safe mode if requested (T152169)
2558 if ( $this->getRequest()->getBool( 'safemode' ) ) {
2559 $this->disallowUserJs();
2560 }
2561
2562 $sk = $this->getSkin();
2563 $this->loadSkinModules( $sk );
2564
2565 MWDebug::addModules( $this );
2566
2567 // Avoid PHP 7.1 warning of passing $this by reference
2568 $outputPage = $this;
2569 // Hook that allows last minute changes to the output page, e.g.
2570 // adding of CSS or Javascript by extensions.
2571 Hooks::runWithoutAbort( 'BeforePageDisplay', [ &$outputPage, &$sk ] );
2572
2573 try {
2574 $sk->outputPage();
2575 } catch ( Exception $e ) {
2576 ob_end_clean(); // bug T129657
2577 throw $e;
2578 }
2579 }
2580
2581 try {
2582 // This hook allows last minute changes to final overall output by modifying output buffer
2583 Hooks::runWithoutAbort( 'AfterFinalPageOutput', [ $this ] );
2584 } catch ( Exception $e ) {
2585 ob_end_clean(); // bug T129657
2586 throw $e;
2587 }
2588
2589 $this->sendCacheControl();
2590
2591 if ( $return ) {
2592 return ob_get_clean();
2593 } else {
2594 ob_end_flush();
2595 return null;
2596 }
2597 }
2598
2609 public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2610 $this->setPageTitle( $pageTitle );
2611 if ( $htmlTitle !== false ) {
2612 $this->setHTMLTitle( $htmlTitle );
2613 }
2614 $this->setRobotPolicy( 'noindex,nofollow' );
2615 $this->setArticleRelated( false );
2616 $this->enableClientCache( false );
2617 $this->mRedirect = '';
2618 $this->clearSubtitle();
2619 $this->clearHTML();
2620 }
2621
2634 public function showErrorPage( $title, $msg, $params = [] ) {
2635 if ( !$title instanceof Message ) {
2636 $title = $this->msg( $title );
2637 }
2638
2639 $this->prepareErrorPage( $title );
2640
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 );
2646 }
2647 $this->addHTML( $msg->parseAsBlock() );
2648 } else {
2649 $this->addWikiMsgArray( $msg, $params );
2650 }
2651
2652 $this->returnToMain();
2653 }
2654
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;
2666 }
2667
2668 // For some action (read, edit, create and upload), display a "login to do this action"
2669 // error if all of the following conditions are met:
2670 // 1. the user is not logged in
2671 // 2. the only error is insufficient permissions (i.e. no block or something else)
2672 // 3. the error can be avoided simply by logging in
2673
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 ) )
2679 ) {
2680 $displayReturnto = null;
2681
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.
2686 $request = $this->getRequest();
2687 $returnto = Title::newFromText( $request->getVal( 'title', '' ) );
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';
2695 } else { # Read
2696 $msg = 'loginreqpagetext';
2697 $displayReturnto = Title::newMainPage();
2698 }
2699
2700 $query = [];
2701
2702 if ( $returnto ) {
2703 $query['returnto'] = $returnto->getPrefixedText();
2704
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 );
2711 }
2712 }
2713
2714 $title = SpecialPage::getTitleFor( 'Userlogin' );
2715 $linkRenderer = $services->getLinkRenderer();
2716 $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
2717 $loginLink = $linkRenderer->makeKnownLink(
2718 $title,
2719 $this->msg( 'loginreqlink' )->text(),
2720 [],
2721 $query
2722 );
2723
2724 $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2725 $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
2726
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
2731 ) ) {
2732 $this->returnToMain( null, $displayReturnto );
2733 }
2734 } else {
2735 $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2736 $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) );
2737 }
2738 }
2739
2746 public function versionRequired( $version ) {
2747 $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2748
2749 $this->addWikiMsg( 'versionrequiredtext', $version );
2750 $this->returnToMain();
2751 }
2752
2760 public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2761 if ( $action == null ) {
2762 $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2763 } else {
2764 $action_desc = $this->msg( "action-$action" )->plain();
2765 $text = $this->msg(
2766 'permissionserrorstext-withaction',
2767 count( $errors ),
2768 $action_desc
2769 )->plain() . "\n\n";
2770 }
2771
2772 if ( count( $errors ) > 1 ) {
2773 $text .= '<ul class="permissions-errors">' . "\n";
2774
2775 foreach ( $errors as $error ) {
2776 $text .= '<li>';
2777 $text .= $this->msg( ...$error )->plain();
2778 $text .= "</li>\n";
2779 }
2780 $text .= '</ul>';
2781 } else {
2782 $text .= "<div class=\"permissions-errors\">\n" .
2783 $this->msg( ...reset( $errors ) )->plain() .
2784 "\n</div>";
2785 }
2786
2787 return $text;
2788 }
2789
2799 public function showLagWarning( $lag ) {
2800 $config = $this->getConfig();
2801 if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2802 $lag = floor( $lag ); // floor to avoid nano seconds to display
2803 $message = $lag < $config->get( 'SlaveLagCritical' )
2804 ? 'lag-warn-normal'
2805 : 'lag-warn-high';
2806 $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2807 $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2808 }
2809 }
2810
2817 public function showFatalError( $message ) {
2818 $this->prepareErrorPage( $this->msg( 'internalerror' ) );
2819
2820 $this->addHTML( $message );
2821 }
2822
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" );
2837 }
2838
2847 public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2848 if ( $returnto == null ) {
2849 $returnto = $this->getRequest()->getText( 'returnto' );
2850 }
2851
2852 if ( $returntoquery == null ) {
2853 $returntoquery = $this->getRequest()->getText( 'returntoquery' );
2854 }
2855
2856 if ( $returnto === '' ) {
2857 $returnto = Title::newMainPage();
2858 }
2859
2860 if ( is_object( $returnto ) ) {
2861 $titleObj = $returnto;
2862 } else {
2863 $titleObj = Title::newFromText( $returnto );
2864 }
2865 // We don't want people to return to external interwiki. That
2866 // might potentially be used as part of a phishing scheme
2867 if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
2868 $titleObj = Title::newMainPage();
2869 }
2870
2871 $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
2872 }
2873
2874 private function getRlClientContext() {
2875 if ( !$this->rlClientContext ) {
2876 $query = ResourceLoader::makeLoaderQuery(
2877 [], // modules; not relevant
2878 $this->getLanguage()->getCode(),
2879 $this->getSkin()->getSkinName(),
2880 $this->getUser()->isLoggedIn() ? $this->getUser()->getName() : null,
2881 null, // version; not relevant
2882 ResourceLoader::inDebugMode(),
2883 null, // only; not relevant
2884 $this->isPrintable(),
2885 $this->getRequest()->getBool( 'handheld' )
2886 );
2887 $this->rlClientContext = new ResourceLoaderContext(
2888 $this->getResourceLoader(),
2889 new FauxRequest( $query )
2890 );
2891 if ( $this->contentOverrideCallbacks ) {
2892 $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
2893 $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) {
2894 foreach ( $this->contentOverrideCallbacks as $callback ) {
2895 $content = $callback( $title );
2896 if ( $content !== null ) {
2897 $text = ContentHandler::getContentText( $content );
2898 if ( strpos( $text, '</script>' ) !== false ) {
2899 // Proactively replace this so that we can display a message
2900 // to the user, instead of letting it go to Html::inlineScript(),
2901 // where it would be considered a server-side issue.
2902 $titleFormatted = $title->getPrefixedText();
2904 Xml::encodeJsCall( 'mw.log.error', [
2905 "Cannot preview $titleFormatted due to script-closing tag."
2906 ] )
2907 );
2908 }
2909 return $content;
2910 }
2911 }
2912 return null;
2913 } );
2914 }
2915 }
2916 return $this->rlClientContext;
2917 }
2918
2930 public function getRlClient() {
2931 if ( !$this->rlClient ) {
2932 $context = $this->getRlClientContext();
2933 $rl = $this->getResourceLoader();
2934 $this->addModules( [
2935 'user',
2936 'user.options',
2937 'user.tokens',
2938 ] );
2939 $this->addModuleStyles( [
2940 'site.styles',
2941 'noscript',
2942 'user.styles',
2943 ] );
2944 $this->getSkin()->setupSkinUserCss( $this );
2945
2946 // Prepare exempt modules for buildExemptModules()
2947 $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
2948 $exemptStates = [];
2949 $moduleStyles = $this->getModuleStyles( /*filter*/ true );
2950
2951 // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
2952 // Separate user-specific batch for improved cache-hit ratio.
2953 $userBatch = [ 'user.styles', 'user' ];
2954 $siteBatch = array_diff( $moduleStyles, $userBatch );
2955 $dbr = wfGetDB( DB_REPLICA );
2956 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $siteBatch );
2957 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
2958
2959 // Filter out modules handled by buildExemptModules()
2960 $moduleStyles = array_filter( $moduleStyles,
2961 function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
2962 $module = $rl->getModule( $name );
2963 if ( $module ) {
2964 $group = $module->getGroup();
2965 if ( isset( $exemptGroups[$group] ) ) {
2966 $exemptStates[$name] = 'ready';
2967 if ( !$module->isKnownEmpty( $context ) ) {
2968 // E.g. Don't output empty <styles>
2969 $exemptGroups[$group][] = $name;
2970 }
2971 return false;
2972 }
2973 }
2974 return true;
2975 }
2976 );
2977 $this->rlExemptStyleModules = $exemptGroups;
2978
2980 'target' => $this->getTarget(),
2981 'nonce' => $this->getCSPNonce(),
2982 // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
2983 // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
2984 // modules enqueud for loading on this page is filtered to just those.
2985 // However, to make sure we also apply the restriction to dynamic dependencies and
2986 // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
2987 // StartupModule so that the client-side registry will not contain any restricted
2988 // modules either. (T152169, T185303)
2989 'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
2990 <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
2991 ) ? '1' : null,
2992 ] );
2993 $rlClient->setConfig( $this->getJSVars() );
2994 $rlClient->setModules( $this->getModules( /*filter*/ true ) );
2995 $rlClient->setModuleStyles( $moduleStyles );
2996 $rlClient->setExemptStates( $exemptStates );
2997 $this->rlClient = $rlClient;
2998 }
2999 return $this->rlClient;
3000 }
3001
3007 public function headElement( Skin $sk, $includeStyle = true ) {
3008 $config = $this->getConfig();
3009 $userdir = $this->getLanguage()->getDir();
3010 $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
3011
3012 $pieces = [];
3013 $htmlAttribs = Sanitizer::mergeAttributes(
3014 $this->getRlClient()->getDocumentAttributes(),
3016 );
3017 $pieces[] = Html::htmlHeader( $htmlAttribs );
3018 $pieces[] = Html::openElement( 'head' );
3019
3020 if ( $this->getHTMLTitle() == '' ) {
3021 $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3022 }
3023
3024 if ( !Html::isXmlMimeType( $config->get( 'MimeType' ) ) ) {
3025 // Add <meta charset="UTF-8">
3026 // This should be before <title> since it defines the charset used by
3027 // text including the text inside <title>.
3028 // The spec recommends defining XHTML5's charset using the XML declaration
3029 // instead of meta.
3030 // Our XML declaration is output by Html::htmlHeader.
3031 // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
3032 // https://html.spec.whatwg.org/multipage/semantics.html#charset
3033 $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
3034 }
3035
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 ) );
3041
3042 // This library is intended to run on older browsers that MediaWiki no longer
3043 // supports as Grade A. For these Grade C browsers, we provide an experience
3044 // using only HTML and CSS. But, where standards-compliant browsers are able to
3045 // style unknown HTML elements without issue, old IE ignores these styles.
3046 // The html5shiv library fixes that.
3047 // Use an IE conditional comment to serve the script only to old IE
3048 $shivUrl = $config->get( 'ResourceBasePath' ) . '/resources/lib/html5shiv/html5shiv.js';
3049 $pieces[] = '<!--[if lt IE 9]>' .
3050 Html::linkedScript( $shivUrl, $this->getCSPNonce() ) .
3051 '<![endif]-->';
3052
3053 $pieces[] = Html::closeElement( 'head' );
3054
3055 $bodyClasses = $this->mAdditionalBodyClasses;
3056 $bodyClasses[] = 'mediawiki';
3057
3058 # Classes for LTR/RTL directionality support
3059 $bodyClasses[] = $userdir;
3060 $bodyClasses[] = "sitedir-$sitedir";
3061
3062 $underline = $this->getUser()->getOption( 'underline' );
3063 if ( $underline < 2 ) {
3064 // The following classes can be used here:
3065 // * mw-underline-always
3066 // * mw-underline-never
3067 $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' );
3068 }
3069
3070 if ( $this->getLanguage()->capitalizeAllNouns() ) {
3071 # A <body> class is probably not the best way to do this . . .
3072 $bodyClasses[] = 'capitalize-all-nouns';
3073 }
3074
3075 // Parser feature migration class
3076 // The idea is that this will eventually be removed, after the wikitext
3077 // which requires it is cleaned up.
3078 $bodyClasses[] = 'mw-hide-empty-elt';
3079
3080 $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
3081 $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
3082 $bodyClasses[] =
3083 'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
3084
3085 $bodyAttrs = [];
3086 // While the implode() is not strictly needed, it's used for backwards compatibility
3087 // (this used to be built as a string and hooks likely still expect that).
3088 $bodyAttrs['class'] = implode( ' ', $bodyClasses );
3089
3090 // Allow skins and extensions to add body attributes they need
3091 $sk->addToBodyAttributes( $this, $bodyAttrs );
3092 Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
3093
3094 $pieces[] = Html::openElement( 'body', $bodyAttrs );
3095
3096 return self::combineWrappedStrings( $pieces );
3097 }
3098
3104 public function getResourceLoader() {
3105 if ( is_null( $this->mResourceLoader ) ) {
3106 // Lazy-initialise as needed
3107 $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
3108 }
3109 return $this->mResourceLoader;
3110 }
3111
3120 public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
3121 // Apply 'target' and 'origin' filters
3122 $modules = $this->filterModules( (array)$modules, null, $only );
3123
3125 $this->getRlClientContext(),
3126 $modules,
3127 $only,
3128 $extraQuery,
3129 $this->getCSPNonce()
3130 );
3131 }
3132
3139 protected static function combineWrappedStrings( array $chunks ) {
3140 // Filter out empty values
3141 $chunks = array_filter( $chunks, 'strlen' );
3142 return WrappedString::join( "\n", $chunks );
3143 }
3144
3151 public function getBottomScripts() {
3152 $chunks = [];
3153 $chunks[] = $this->getRlClient()->getBodyHtml();
3154
3155 // Legacy non-ResourceLoader scripts
3156 $chunks[] = $this->mScripts;
3157
3158 if ( $this->limitReportJSData ) {
3159 $chunks[] = ResourceLoader::makeInlineScript(
3160 ResourceLoader::makeConfigSetScript(
3161 [ 'wgPageParseReport' => $this->limitReportJSData ]
3162 ),
3163 $this->getCSPNonce()
3164 );
3165 }
3166
3167 return self::combineWrappedStrings( $chunks );
3168 }
3169
3176 public function getJsConfigVars() {
3177 return $this->mJsConfigVars;
3178 }
3179
3186 public function addJsConfigVars( $keys, $value = null ) {
3187 if ( is_array( $keys ) ) {
3188 foreach ( $keys as $key => $value ) {
3189 $this->mJsConfigVars[$key] = $value;
3190 }
3191 return;
3192 }
3193
3194 $this->mJsConfigVars[$keys] = $value;
3195 }
3196
3206 public function getJSVars() {
3207 $curRevisionId = 0;
3208 $articleId = 0;
3209 $canonicalSpecialPageName = false; # T23115
3210 $services = MediaWikiServices::getInstance();
3211
3212 $title = $this->getTitle();
3213 $ns = $title->getNamespace();
3214 $nsInfo = $services->getNamespaceInfo();
3215 $canonicalNamespace = $nsInfo->exists( $ns )
3216 ? $nsInfo->getCanonicalName( $ns )
3217 : $title->getNsText();
3218
3219 $sk = $this->getSkin();
3220 // Get the relevant title so that AJAX features can use the correct page name
3221 // when making API requests from certain special pages (T36972).
3222 $relevantTitle = $sk->getRelevantTitle();
3223 $relevantUser = $sk->getRelevantUser();
3224
3225 if ( $ns == NS_SPECIAL ) {
3226 list( $canonicalSpecialPageName, /*...*/ ) =
3227 $services->getSpecialPageFactory()->
3228 resolveAlias( $title->getDBkey() );
3229 } elseif ( $this->canUseWikiPage() ) {
3230 $wikiPage = $this->getWikiPage();
3231 $curRevisionId = $wikiPage->getLatest();
3232 $articleId = $wikiPage->getId();
3233 }
3234
3235 $lang = $title->getPageViewLanguage();
3236
3237 // Pre-process information
3238 $separatorTransTable = $lang->separatorTransformTable();
3239 $separatorTransTable = $separatorTransTable ?: [];
3240 $compactSeparatorTransTable = [
3241 implode( "\t", array_keys( $separatorTransTable ) ),
3242 implode( "\t", $separatorTransTable ),
3243 ];
3244 $digitTransTable = $lang->digitTransformTable();
3245 $digitTransTable = $digitTransTable ?: [];
3246 $compactDigitTransTable = [
3247 implode( "\t", array_keys( $digitTransTable ) ),
3248 implode( "\t", $digitTransTable ),
3249 ];
3250
3251 $user = $this->getUser();
3252
3253 $vars = [
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(),
3264 'wgAction' => Action::getActionName( $this->getContext() ),
3265 'wgUserName' => $user->isAnon() ? null : $user->getName(),
3266 'wgUserGroups' => $user->getEffectiveGroups(),
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(),
3278 'wgRequestId' => WebRequest::getRequestId(),
3279 'wgCSPNonce' => $this->getCSPNonce(),
3280 ];
3281
3282 if ( $user->isLoggedIn() ) {
3283 $vars['wgUserId'] = $user->getId();
3284 $vars['wgUserEditCount'] = $user->getEditCount();
3285 $userReg = $user->getRegistration();
3286 $vars['wgUserRegistration'] = $userReg ? (int)wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3287 // Get the revision ID of the oldest new message on the user's talk
3288 // page. This can be used for constructing new message alerts on
3289 // the client side.
3290 $vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
3291 }
3292
3293 $contLang = $services->getContentLanguage();
3294 if ( $contLang->hasVariants() ) {
3295 $vars['wgUserVariant'] = $contLang->getPreferredVariant();
3296 }
3297 // Same test as SkinTemplate
3298 $vars['wgIsProbablyEditable'] = $this->userCanEditOrCreate( $user, $title );
3299
3300 $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle &&
3301 $this->userCanEditOrCreate( $user, $relevantTitle );
3302
3303 foreach ( $title->getRestrictionTypes() as $type ) {
3304 // Following keys are set in $vars:
3305 // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
3306 $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3307 }
3308
3309 if ( $title->isMainPage() ) {
3310 $vars['wgIsMainPage'] = true;
3311 }
3312
3313 if ( $this->mRedirectedFrom ) {
3314 $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3315 }
3316
3317 if ( $relevantUser ) {
3318 $vars['wgRelevantUserName'] = $relevantUser->getName();
3319 }
3320
3321 // Allow extensions to add their custom variables to the mw.config map.
3322 // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3323 // page-dependant but site-wide (without state).
3324 // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3325 Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3326
3327 // Merge in variables from addJsConfigVars last
3328 return array_merge( $vars, $this->getJsConfigVars() );
3329 }
3330
3340 public function userCanPreview() {
3341 $request = $this->getRequest();
3342 if (
3343 $request->getVal( 'action' ) !== 'submit' ||
3344 !$request->wasPosted()
3345 ) {
3346 return false;
3347 }
3348
3349 $user = $this->getUser();
3350
3351 if ( !$user->isLoggedIn() ) {
3352 // Anons have predictable edit tokens
3353 return false;
3354 }
3355 if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3356 return false;
3357 }
3358
3359 $title = $this->getTitle();
3360 $errors = $title->getUserPermissionsErrors( 'edit', $user );
3361 if ( count( $errors ) !== 0 ) {
3362 return false;
3363 }
3364
3365 return true;
3366 }
3367
3373 private function userCanEditOrCreate(
3374 User $user,
3376 ) {
3377 $pm = MediaWikiServices::getInstance()->getPermissionManager();
3378 return $pm->quickUserCan( 'edit', $user, $title )
3379 && ( $this->getTitle()->exists() ||
3380 $pm->quickUserCan( 'create', $user, $title ) );
3381 }
3382
3386 public function getHeadLinksArray() {
3387 global $wgVersion;
3388
3389 $tags = [];
3390 $config = $this->getConfig();
3391
3392 $canonicalUrl = $this->mCanonicalUrl;
3393
3394 $tags['meta-generator'] = Html::element( 'meta', [
3395 'name' => 'generator',
3396 'content' => "MediaWiki $wgVersion",
3397 ] );
3398
3399 if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3400 // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
3401 // fallbacks should come before the primary value so we need to reverse the array.
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,
3406 ] );
3407 }
3408 }
3409
3410 $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3411 if ( $p !== 'index,follow' ) {
3412 // http://www.robotstxt.org/wc/meta-user.html
3413 // Only show if it's different from the default robots policy
3414 $tags['meta-robots'] = Html::element( 'meta', [
3415 'name' => 'robots',
3416 'content' => $p,
3417 ] );
3418 }
3419
3420 foreach ( $this->mMetatags as $tag ) {
3421 if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) {
3422 $a = 'http-equiv';
3423 $tag[0] = substr( $tag[0], 5 );
3424 } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) {
3425 $a = 'property';
3426 } else {
3427 $a = 'name';
3428 }
3429 $tagName = "meta-{$tag[0]}";
3430 if ( isset( $tags[$tagName] ) ) {
3431 $tagName .= $tag[1];
3432 }
3433 $tags[$tagName] = Html::element( 'meta',
3434 [
3435 $a => $tag[0],
3436 'content' => $tag[1]
3437 ]
3438 );
3439 }
3440
3441 foreach ( $this->mLinktags as $tag ) {
3442 $tags[] = Html::element( 'link', $tag );
3443 }
3444
3445 # Universal edit button
3446 if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3447 if ( $this->userCanEditOrCreate( $this->getUser(), $this->getTitle() ) ) {
3448 // Original UniversalEditButton
3449 $msg = $this->msg( 'edit' )->text();
3450 $tags['universal-edit-button'] = Html::element( 'link', [
3451 'rel' => 'alternate',
3452 'type' => 'application/x-wiki',
3453 'title' => $msg,
3454 'href' => $this->getTitle()->getEditURL(),
3455 ] );
3456 // Alternate edit link
3457 $tags['alternative-edit'] = Html::element( 'link', [
3458 'rel' => 'edit',
3459 'title' => $msg,
3460 'href' => $this->getTitle()->getEditURL(),
3461 ] );
3462 }
3463 }
3464
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' )
3473 ] );
3474 }
3475
3476 if ( $config->get( 'Favicon' ) !== false ) {
3477 $tags['favicon'] = Html::element( 'link', [
3478 'rel' => 'shortcut icon',
3479 'href' => $config->get( 'Favicon' )
3480 ] );
3481 }
3482
3483 # OpenSearch description link
3484 $tags['opensearch'] = Html::element( 'link', [
3485 'rel' => 'search',
3486 'type' => 'application/opensearchdescription+xml',
3487 'href' => wfScript( 'opensearch_desc' ),
3488 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3489 ] );
3490
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', [
3496 'rel' => 'EditURI',
3497 'type' => 'application/rsd+xml',
3498 // Output a protocol-relative URL here if $wgServer is protocol-relative.
3499 // Whether RSD accepts relative or protocol-relative URLs is completely
3500 // undocumented, though.
3501 'href' => wfExpandUrl( wfAppendQuery(
3502 wfScript( 'api' ),
3503 [ 'action' => 'rsd' ] ),
3505 ),
3506 ] );
3507
3508 # Language variants
3509 if ( !$config->get( 'DisableLangConversion' ) ) {
3510 $lang = $this->getTitle()->getPageLanguage();
3511 if ( $lang->hasVariants() ) {
3512 $variants = $lang->getVariants();
3513 foreach ( $variants as $variant ) {
3514 $tags["variant-$variant"] = Html::element( 'link', [
3515 'rel' => 'alternate',
3516 'hreflang' => LanguageCode::bcp47( $variant ),
3517 'href' => $this->getTitle()->getLocalURL(
3518 [ 'variant' => $variant ] )
3519 ]
3520 );
3521 }
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() ] );
3527 }
3528 }
3529
3530 # Copyright
3531 if ( $this->copyrightUrl !== null ) {
3532 $copyright = $this->copyrightUrl;
3533 } else {
3534 $copyright = '';
3535 if ( $config->get( 'RightsPage' ) ) {
3536 $copy = Title::newFromText( $config->get( 'RightsPage' ) );
3537
3538 if ( $copy ) {
3539 $copyright = $copy->getLocalURL();
3540 }
3541 }
3542
3543 if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3544 $copyright = $config->get( 'RightsUrl' );
3545 }
3546 }
3547
3548 if ( $copyright ) {
3549 $tags['copyright'] = Html::element( 'link', [
3550 'rel' => 'license',
3551 'href' => $copyright ]
3552 );
3553 }
3554
3555 # Feeds
3556 if ( $config->get( 'Feed' ) ) {
3557 $feedLinks = [];
3558
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
3563 # this low a level.
3564
3565 $feedLinks[] = $this->feedLink(
3566 $format,
3567 $link,
3568 # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3569 $this->msg(
3570 "page-{$format}-feed", $this->getTitle()->getPrefixedText()
3571 )->text()
3572 );
3573 }
3574
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 ) {
3586 // Note, this->feedLink escapes the url.
3587 $feedLinks[] = $this->feedLink(
3588 $type,
3589 $feedUrl,
3590 $this->msg( "site-{$type}-feed", $sitename )->text()
3591 );
3592 }
3593 } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3594 $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3595 foreach ( $this->getAdvertisedFeedTypes() as $format ) {
3596 $feedLinks[] = $this->feedLink(
3597 $format,
3598 $rctitle->getLocalURL( [ 'feed' => $format ] ),
3599 # For grep: 'site-rss-feed', 'site-atom-feed'
3600 $this->msg( "site-{$format}-feed", $sitename )->text()
3601 );
3602 }
3603 }
3604
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 ] );
3609
3610 $tags += $feedLinks;
3611 }
3612
3613 # Canonical URL
3614 if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3615 if ( $canonicalUrl !== false ) {
3616 $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3617 } elseif ( $this->isArticleRelated() ) {
3618 // This affects all requests where "setArticleRelated" is true. This is
3619 // typically all requests that show content (query title, curid, oldid, diff),
3620 // and all wikipage actions (edit, delete, purge, info, history etc.).
3621 // It does not apply to File pages and Special pages.
3622 // 'history' and 'info' actions address page metadata rather than the page
3623 // content itself, so they may not be canonicalized to the view page url.
3624 // TODO: this ought to be better encapsulated in the Action class.
3625 $action = Action::getActionName( $this->getContext() );
3626 if ( in_array( $action, [ 'history', 'info' ] ) ) {
3627 $query = "action={$action}";
3628 } else {
3629 $query = '';
3630 }
3631 $canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3632 } else {
3633 $reqUrl = $this->getRequest()->getRequestURL();
3634 $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3635 }
3636 }
3637 if ( $canonicalUrl !== false ) {
3638 $tags[] = Html::element( 'link', [
3639 'rel' => 'canonical',
3640 'href' => $canonicalUrl
3641 ] );
3642 }
3643
3644 // Allow extensions to add, remove and/or otherwise manipulate these links
3645 // If you want only to *add* <head> links, please use the addHeadItem()
3646 // (or addHeadItems() for multiple items) method instead.
3647 // This hook is provided as a last resort for extensions to modify these
3648 // links before the output is sent to client.
3649 Hooks::run( 'OutputPageAfterGetHeadLinksArray', [ &$tags, $this ] );
3650
3651 return $tags;
3652 }
3653
3662 private function feedLink( $type, $url, $text ) {
3663 return Html::element( 'link', [
3664 'rel' => 'alternate',
3665 'type' => "application/$type+xml",
3666 'title' => $text,
3667 'href' => $url ]
3668 );
3669 }
3670
3680 public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3681 $options = [];
3682 if ( $media ) {
3683 $options['media'] = $media;
3684 }
3685 if ( $condition ) {
3686 $options['condition'] = $condition;
3687 }
3688 if ( $dir ) {
3689 $options['dir'] = $dir;
3690 }
3691 $this->styles[$style] = $options;
3692 }
3693
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 );
3705 }
3706 $this->mInlineStyles .= Html::inlineStyle( $style_css );
3707 }
3708
3714 protected function buildExemptModules() {
3715 $chunks = [];
3716
3717 // Requirements:
3718 // - Within modules provided by the software (core, skin, extensions),
3719 // styles from skin stylesheets should be overridden by styles
3720 // from modules dynamically loaded with JavaScript.
3721 // - Styles from site-specific, private, and user modules should override
3722 // both of the above.
3723 //
3724 // The effective order for stylesheets must thus be:
3725 // 1. Page style modules, formatted server-side by ResourceLoaderClientHtml.
3726 // 2. Dynamically-loaded styles, inserted client-side by mw.loader.
3727 // 3. Styles that are site-specific, private or from the user, formatted
3728 // server-side by this function.
3729 //
3730 // The 'ResourceLoaderDynamicStyles' marker helps JavaScript know where
3731 // point #2 is.
3732
3733 // Add legacy styles added through addStyle()/addInlineStyle() here
3734 $chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3735
3736 // Things that go after the ResourceLoaderDynamicStyles marker
3737 $append = [];
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
3744 );
3745
3746 foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) {
3747 // These require their own dedicated request in order to support "@import"
3748 // syntax, which is incompatible with concatenation. (T147667, T37562)
3749 $append[] = $this->makeResourceLoaderLink( $name,
3750 ResourceLoaderModule::TYPE_STYLES
3751 );
3752 }
3753 }
3754 }
3755 if ( $append ) {
3756 $chunks[] = Html::element(
3757 'meta',
3758 [ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3759 );
3760 $chunks = array_merge( $chunks, $append );
3761 }
3762
3763 return self::combineWrappedStrings( $chunks );
3764 }
3765
3769 public function buildCssLinksArray() {
3770 $links = [];
3771
3772 foreach ( $this->styles as $file => $options ) {
3773 $link = $this->styleLink( $file, $options );
3774 if ( $link ) {
3775 $links[$file] = $link;
3776 }
3777 }
3778 return $links;
3779 }
3780
3788 protected function styleLink( $style, array $options ) {
3789 if ( isset( $options['dir'] ) && $this->getLanguage()->getDir() != $options['dir'] ) {
3790 return '';
3791 }
3792
3793 if ( isset( $options['media'] ) ) {
3794 $media = self::transformCssMedia( $options['media'] );
3795 if ( is_null( $media ) ) {
3796 return '';
3797 }
3798 } else {
3799 $media = 'all';
3800 }
3801
3802 if ( substr( $style, 0, 1 ) == '/' ||
3803 substr( $style, 0, 5 ) == 'http:' ||
3804 substr( $style, 0, 6 ) == 'https:' ) {
3805 $url = $style;
3806 } else {
3807 $config = $this->getConfig();
3808 // Append file hash as query parameter
3809 $url = self::transformResourcePath(
3810 $config,
3811 $config->get( 'StylePath' ) . '/' . $style
3812 );
3813 }
3814
3815 $link = Html::linkedStyle( $url, $media );
3816
3817 if ( isset( $options['condition'] ) ) {
3818 $condition = htmlspecialchars( $options['condition'] );
3819 $link = "<!--[if $condition]>$link<![endif]-->";
3820 }
3821 return $link;
3822 }
3823
3845 public static function transformResourcePath( Config $config, $path ) {
3846 global $IP;
3847
3848 $localDir = $IP;
3849 $remotePathPrefix = $config->get( 'ResourceBasePath' );
3850 if ( $remotePathPrefix === '' ) {
3851 // The configured base path is required to be empty string for
3852 // wikis in the domain root
3853 $remotePath = '/';
3854 } else {
3855 $remotePath = $remotePathPrefix;
3856 }
3857 if ( strpos( $path, $remotePath ) !== 0 || substr( $path, 0, 2 ) === '//' ) {
3858 // - Path is outside wgResourceBasePath, ignore.
3859 // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib.
3860 return $path;
3861 }
3862 // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here.
3863 // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth
3864 // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath)
3865 // which is not expected to be in wgResourceBasePath on CDNs. (T155146)
3866 $uploadPath = $config->get( 'UploadPath' );
3867 if ( strpos( $path, $uploadPath ) === 0 ) {
3868 $localDir = $config->get( 'UploadDirectory' );
3869 $remotePathPrefix = $remotePath = $uploadPath;
3870 }
3871
3872 $path = RelPath::getRelativePath( $path, $remotePath );
3873 return self::transformFilePath( $remotePathPrefix, $localDir, $path );
3874 }
3875
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" );
3891 $hash = '';
3892 }
3893 return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3894 }
3895
3903 public static function transformCssMedia( $media ) {
3904 global $wgRequest;
3905
3906 // https://www.w3.org/TR/css3-mediaqueries/#syntax
3907 $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
3908
3909 // Switch in on-screen display for media testing
3910 $switches = [
3911 'printable' => 'print',
3912 'handheld' => 'handheld',
3913 ];
3914 foreach ( $switches as $switch => $targetMedia ) {
3915 if ( $wgRequest->getBool( $switch ) ) {
3916 if ( $media == $targetMedia ) {
3917 $media = '';
3918 } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3919 /* This regex will not attempt to understand a comma-separated media_query_list
3920 *
3921 * Example supported values for $media:
3922 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
3923 * Example NOT supported value for $media:
3924 * '3d-glasses, screen, print and resolution > 90dpi'
3925 *
3926 * If it's a print request, we never want any kind of screen stylesheets
3927 * If it's a handheld request (currently the only other choice with a switch),
3928 * we don't want simple 'screen' but we might want screen queries that
3929 * have a max-width or something, so we'll pass all others on and let the
3930 * client do the query.
3931 */
3932 if ( $targetMedia == 'print' || $media == 'screen' ) {
3933 return null;
3934 }
3935 }
3936 }
3937 }
3938
3939 return $media;
3940 }
3941
3948 public function addWikiMsg( /*...*/ ) {
3949 $args = func_get_args();
3950 $name = array_shift( $args );
3951 $this->addWikiMsgArray( $name, $args );
3952 }
3953
3962 public function addWikiMsgArray( $name, $args ) {
3963 $this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
3964 }
3965
3991 public function wrapWikiMsg( $wrap /*, ...*/ ) {
3992 $msgSpecs = func_get_args();
3993 array_shift( $msgSpecs );
3994 $msgSpecs = array_values( $msgSpecs );
3995 $s = $wrap;
3996 foreach ( $msgSpecs as $n => $spec ) {
3997 if ( is_array( $spec ) ) {
3998 $args = $spec;
3999 $name = array_shift( $args );
4000 } else {
4001 $args = [];
4002 $name = $spec;
4003 }
4004 $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
4005 }
4006 $this->addWikiTextAsInterface( $s );
4007 }
4008
4014 public function isTOCEnabled() {
4015 return $this->mEnableTOC;
4016 }
4017
4025 public static function setupOOUI( $skinName = 'default', $dir = 'ltr' ) {
4026 $themes = ResourceLoaderOOUIModule::getSkinThemeMap();
4027 $theme = $themes[$skinName] ?? $themes['default'];
4028 // For example, 'OOUI\WikimediaUITheme'.
4029 $themeClass = "OOUI\\{$theme}Theme";
4030 OOUI\Theme::setSingleton( new $themeClass() );
4031 OOUI\Element::setDefaultDir( $dir );
4032 }
4033
4040 public function enableOOUI() {
4041 self::setupOOUI(
4042 strtolower( $this->getSkin()->getSkinName() ),
4043 $this->getLanguage()->getDir()
4044 );
4045 $this->addModuleStyles( [
4046 'oojs-ui-core.styles',
4047 'oojs-ui.styles.indicators',
4048 'mediawiki.widgets.styles',
4049 'oojs-ui-core.icons',
4050 ] );
4051 }
4052
4062 public function getCSPNonce() {
4064 return false;
4065 }
4066 if ( $this->CSPNonce === null ) {
4067 // XXX It might be expensive to generate randomness
4068 // on every request, on Windows.
4069 $rand = random_bytes( 15 );
4070 $this->CSPNonce = base64_encode( $rand );
4071 }
4072 return $this->CSPNonce;
4073 }
4074
4075}
getUser()
$wgVersion
MediaWiki version number.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
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...
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfClearOutputBuffers()
More legible than passing a 'false' parameter to wfResetOutputBuffers():
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getContext()
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:751
if( $line===false) $args
Definition cdb.php:64
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition Article.php:1047
static isNonceRequired(Config $config)
Should we set nonce attribute.
static sendHeaders(IContextSource $context)
Send CSP headers based on wiki config.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
canUseWikiPage()
Check whether a WikiPage object can be get with getWikiPage().
getWikiPage()
Get the WikiPage object.
setContext(IContextSource $context)
A mutable version of ResourceLoaderContext.
WebRequest clone which takes values from a provided array.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:61
exists()
Returns true if file exists in the repository.
Definition File.php:901
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
Content for JavaScript pages.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
setArray( $array)
Set the link list to a given 2-d array First key is the namespace, second is the DB key,...
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
This serves as the entry point to the MediaWiki session handling system.
The Message class provides methods which fulfil two basic services:
Definition Message.php:162
This is one of the Core classes and should be read at least once by any new developers.
isArticle()
Return whether the content displayed page is related to the source of the corresponding article on th...
addWikiMsg()
Add a wikitext-formatted message to the output.
addLinkHeader( $header)
Add an HTTP Link: header.
setFileVersion( $file)
Set the displayed file version.
$mScripts
Used for JavaScript (predates ResourceLoader)
getDisplayTitle()
Returns page display title.
string $CSPNonce
The nonce for Content-Security-Policy.
getCdnCacheEpoch( $reqTime, $maxAge)
disable()
Disable output completely, i.e.
addLink(array $linkarr)
Add a new <link> tag to the page header.
ResourceLoader $mResourceLoader
addWikiTextTitleInternal( $text, Title $title, $linestart, $interface, $wrapperClass=null)
Add wikitext with a custom Title object.
static array $cacheVaryCookies
A cache of the names of the cookies that will influence the cache.
getCanonicalUrl()
Returns the URL to be used for the <link rel=canonical>> if one is set.
getPageTitle()
Return the "page title", i.e.
isArticleRelated()
Return whether this page is related an article on the wiki.
addCategoryLinksToLBAndGetResult(array $categories)
getLanguageLinks()
Get the list of language links.
array $mModules
userCanEditOrCreate(User $user, LinkTarget $title)
addScriptFile( $file, $unused=null)
Add a JavaScript file to be loaded as <script> on this page.
array $mJsConfigVars
array $mImageTimeKeys
allowClickjacking()
Turn off frame-breaking.
filterModules(array $modules, $position=null, $type=ResourceLoaderModule::TYPE_COMBINED)
Filter an array of modules to remove insufficiently trustworthy members, and modules which are no lon...
wrapWikiTextAsInterface( $wrapperClass, $text)
Convert wikitext in the user interface language to HTML and add it to the buffer with a <div class="$...
getSyndicationLinks()
Return URLs for each supported syndication format for this page.
setArticleFlag( $newVal)
Set whether the displayed content is related to the source of the corresponding article on the wiki S...
addMeta( $name, $val)
Add a new "<meta>" tag To add an http-equiv meta tag, precede the name with "http:".
parseInline( $text, $linestart=true, $interface=false)
Parse wikitext, strip paragraph wrapper, and return the HTML.
$mFeedLinks
Handles the Atom / RSS links.
bool $mNewSectionLink
setCopyrightUrl( $url)
Set the copyright URL to send with the output.
parseInternal( $text, $title, $linestart, $tidy, $interface, $language)
Parse wikitext and return the HTML (internal implementation helper)
setRobotPolicy( $policy)
Set the robot policy for the page: http://www.robotstxt.org/meta.html
__construct(IContextSource $context)
Constructor for OutputPage.
setIndexPolicy( $policy)
Set the index policy for the page, but leave the follow policy un- touched.
getTemplateIds()
Get the templates used on this page.
array $mLinktags
setDisplayTitle( $html)
Same as page title but only contains name of the page, not any other text.
setPageTitle( $name)
"Page title" means the contents of <h1>.
output( $return=false)
Finally, all the text has been munged and accumulated into the object, let's actually output it:
array $mIndicators
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
getRedirect()
Get the URL to redirect to, or an empty string if not redirect URL set.
setLastModified( $timestamp)
Override the last modified timestamp.
setLanguageLinks(array $newLinkArray)
Reset the language links and add new language links.
addFeedLink( $format, $href)
Add a feed link to the page header.
setTitle(Title $t)
Set the Title object to use.
setCategoryLinks(array $categories)
Reset the category links (but not the category list) and add $categories.
getAdvertisedFeedTypes()
Return effective list of advertised feed types.
getFileVersion()
Get the displayed file version.
setSyndicated( $show=true)
Add or remove feed links in the page header This is mainly kept for backward compatibility,...
bool $mHideNewSectionLink
bool $mIsArticle
Is the displayed content related to the source of the corresponding wiki article.
adaptCdnTTL( $mtime, $minTTL=0, $maxTTL=0)
Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage()
showsCopyright()
Return whether the standard copyright should be shown for the current page.
sendCacheControl()
Send cache control HTTP headers.
setTarget( $target)
Sets ResourceLoader target for load.php links.
string[][] $mMetatags
Should be private.
addCategoryLinks(array $categories)
Add an array of categories, with names in the keys.
int $mRevisionId
To include the variable {{REVISIONID}}.
showLagWarning( $lag)
Show a warning about replica DB lag.
clearSubtitle()
Clear the subtitles.
showFatalError( $message)
Output an error page.
setCdnMaxage( $maxage)
Set the value of the "s-maxage" part of the "Cache-control" HTTP header.
getJsConfigVars()
Get the javascript config vars to include on this page.
getHTML()
Get the body HTML.
addContentOverride(LinkTarget $target, Content $content)
Add a mapping from a LinkTarget to a Content, for things like page preview.
getFeaturePolicyReportOnly()
parseAsContent( $text, $linestart=true)
Parse wikitext in the page content language and return the HTML.
setCopyright( $hasCopyright)
Set whether the standard copyright should be shown for the current page.
array $mAllowedModules
What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
addParserOutput(ParserOutput $parserOutput, $poOptions=[])
Add everything from a ParserOutput object.
getJSVars()
Get an array containing the variables to be set in mw.config in JavaScript.
bool $mPreventClickjacking
Controls if anti-clickjacking / frame-breaking headers will be sent.
haveCacheVaryCookies()
Check if the request has a cache-varying cookie header If it does, it's very important that we don't ...
isTOCEnabled()
Whether the output has a table of contents.
versionRequired( $version)
Display an error page indicating that a given version of MediaWiki is required to use it.
reduceAllowedModules( $type, $level)
Limit the highest level of CSS/JS untrustworthiness allowed.
addInlineStyle( $style_css, $flip='noflip')
Adds inline CSS styles Internal use only.
getRevisionTimestamp()
Get the timestamp of displayed revision.
preventClickjacking( $enable=true)
Set a flag which will cause an X-Frame-Options header appropriate for edit pages to be sent.
bool $mPrintable
We have to set isPrintable().
setFollowPolicy( $policy)
Set the follow policy for the page, but leave the index policy un- touched.
getHeadItemsArray()
Get an array of head items.
isPrintable()
Return whether the page is "printable".
getModules( $filter=false, $position=null, $param='mModules', $type=ResourceLoaderModule::TYPE_COMBINED)
Get the list of modules to include on this page.
setRedirectedFrom( $t)
Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
getFeedAppendQuery()
Will currently always return null.
addModuleStyles( $modules)
Load the styles of one or more ResourceLoader modules on this page.
setHTMLTitle( $name)
"HTML title" means the contents of "<title>".
loadSkinModules( $sk)
Transfer styles and JavaScript modules from skin.
array $mAdditionalBodyClasses
Additional <body> classes; there are also <body> classes from other sources.
returnToMain( $unused=null, $returnto=null, $returntoquery=null)
Add a "return to" link pointing to a specified title, or the title indicated in the request,...
array $mHeadItems
Array of elements in "<head>".
isSyndicated()
Should we output feed links for this page?
$mLinkHeader
Link: header contents.
styleLink( $style, array $options)
Generate <link> tags for stylesheets.
parseAsInterface( $text, $linestart=true)
Parse wikitext in the user interface language and return the HTML.
addParserOutputMetadata(ParserOutput $parserOutput)
Add all metadata associated with a ParserOutput object, but without the actual HTML.
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
string $mRedirect
addElement( $element, array $attribs=[], $contents='')
Shortcut for adding an Html::element via addHTML.
array $mModuleStyles
makeResourceLoaderLink( $modules, $only, array $extraQuery=[])
Explicily load or embed modules on a page.
getSubtitle()
Get the subtitle.
showPermissionsErrorPage(array $errors, $action=null)
Output a standard permission error page.
addSubtitle( $str)
Add $str to the subtitle.
static setupOOUI( $skinName='default', $dir='ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
setRevisionId( $revid)
Set the revision ID which will be seen by the wiki text parser for things such as embedded {{REVISION...
addParserOutputContent(ParserOutput $parserOutput, $poOptions=[])
Add the HTML and enhancements for it (like ResourceLoader modules) associated with a ParserOutput obj...
getPreventClickjacking()
Get the prevent-clickjacking flag.
enableOOUI()
Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with MediaW...
getCategories( $type='all')
Get the list of category names this page belongs to.
getIndicators()
Get the indicators associated with this page.
$mProperties
Additional key => value data.
parseInlineAsInterface( $text, $linestart=true)
Parse wikitext in the user interface language, strip paragraph wrapper, and return the HTML.
addWikiMsgArray( $name, $args)
Add a wikitext-formatted message to the output.
array $contentOverrides
Map Title to Content.
addBodyClasses( $classes)
Add a class to the <body> element.
addParserOutputText(ParserOutput $parserOutput, $poOptions=[])
Add the HTML associated with a ParserOutput object, without any metadata.
warnModuleTargetFilter( $moduleName)
string $mBodytext
Contains all of the "<body>" content.
int $mContainsNewMagic
addScript( $script)
Add raw HTML to the list of scripts (including <script> tag, etc.) Internal use only.
clearHTML()
Clear the body HTML.
array $mSubtitle
Contains the page subtitle.
addVaryHeader( $header, array $option=null)
Add an HTTP header that will influence on the cache.
addStyle( $style, $media='', $condition='', $dir='')
Add a local or specified stylesheet, with the given media options.
getLinkHeader()
Return a Link: header.
setPrintable()
Set the page as printable, i.e.
getResourceLoader()
Get a ResourceLoader object associated with this OutputPage.
feedLink( $type, $url, $text)
Generate a "<link rel/>" for a feed.
bool $mNoGallery
Comes from the parser.
getModuleStyles( $filter=false, $position=null)
Get the list of style-only modules to load on this page.
enableClientCache( $state)
Use enableClientCache(false) to force it to send nocache headers.
userCanPreview()
To make it harder for someone to slip a user a fake JavaScript or CSS preview, a random token is asso...
array $limitReportJSData
Profiling data.
getFileSearchOptions()
Get the files used on this page.
isRevisionCurrent()
Whether the revision displayed is the latest revision of the page.
addHTML( $text)
Append $text to the body HTML.
formatPermissionsErrorMessage(array $errors, $action=null)
Format a list of error messages.
string $mRevisionTimestamp
string null $copyrightUrl
The URL to send in a <link> element with rel=license.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
getHTMLTitle()
Return the "HTML title", i.e.
disallowUserJs()
Do not allow scripts which can be modified by wiki users to load on this page; only allow scripts bun...
string $mHTMLtitle
Stores contents of "<title>" tag.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
setCanonicalUrl( $url)
Set the URL to be used for the <link rel=canonical>>.
ParserOptions $mParserOptions
lazy initialised, use parserOptions()
addTemplate(&$template)
Add the output of a QuickTemplate to the output buffer.
getRlClient()
Call this to freeze the module queue and JS config and create a formatter.
static transformFilePath( $remotePathPrefix, $localPath, $file)
Utility method for transformResourceFilePath().
callable[] $contentOverrideCallbacks
int $mCdnMaxageLimit
Upper limit on mCdnMaxage.
ResourceLoaderClientHtml $rlClient
setArticleBodyOnly( $only)
Set whether the output should only contain the body of the article, without any skin,...
addInlineScript( $script)
Add a self-contained script tag with the given contents Internal use only.
addAcceptLanguage()
T23672: Add Accept-Language to Vary header if there's no 'variant' parameter in GET.
setStatusCode( $statusCode)
Set the HTTP status code to send with the output.
getCacheVaryCookies()
Get the list of cookie names that will influence the cache.
ResourceLoaderContext $rlClientContext
showErrorPage( $title, $msg, $params=[])
Output a standard error page.
getLinkTags()
Returns the current <link> tags.
getArticleBodyOnly()
Return whether the output will contain only the body of the article.
setProperty( $name, $value)
Set an additional output property.
setRevisionTimestamp( $timestamp)
Set the timestamp of the revision which will be displayed.
checkLastModified( $timestamp)
checkLastModified tells the client to use the client-cached page if possible.
getVaryHeader()
Return a Vary: header on which to vary caches.
string $mLastModified
Used for sending cache control.
addWikiTextAsContent( $text, $linestart=true, Title $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
showNewSectionLink()
Show an "add new section" link?
bool $mDoNothing
Whether output is disabled.
string $mPageTitle
The contents of.
string $mPageLinkTitle
Used by skin template.
addHeadItem( $name, $value)
Add or replace a head item to the output.
getRevisionId()
Get the displayed revision ID.
setIndicators(array $indicators)
Add an array of indicators, with their identifiers as array keys and HTML contents as values.
bool $mEnableTOC
Whether parser output contains a table of contents.
Title $mRedirectedFrom
If the current page was reached through a redirect, $mRedirectedFrom contains the Title of the redire...
lowerCdnMaxage( $maxage)
Set the value of the "s-maxage" part of the "Cache-control" HTTP header to $maxage if that is lower t...
forceHideNewSectionLink()
Forcibly hide the new section link?
bool $mHasCopyright
Is the content subject to copyright.
string null $mTarget
ResourceLoader target for load.php links.
int $mCdnMaxage
Cache stuff.
addBacklinkSubtitle(Title $title, $query=[])
Add a subtitle containing a backlink to a page.
array $mTemplateIds
getAllowedModules( $type)
Show what level of JavaScript / CSS untrustworthiness is allowed on this page.
parserOptions( $options=null)
Get/set the ParserOptions object to use for wikitext parsing.
string $displayTitle
The displayed title of the page.
prependHTML( $text)
Prepend $text to the body HTML.
addLanguageLinks(array $newLinkArray)
Add new language links.
array $mLanguageLinks
Array of Interwiki Prefixed (non DB key) Titles (e.g.
wrapWikiMsg( $wrap)
This function takes a number of message/argument specifications, wraps them in some overall structure...
setArticleRelated( $newVal)
Set whether this page is related an article on the wiki Setting false will cause the change of "artic...
string $mInlineStyles
Inline CSS styles.
array $mFileVersion
addModules( $modules)
Load one or more ResourceLoader modules on this page.
buildExemptModules()
Build exempt modules and legacy non-ResourceLoader styles.
string $mRedirectCode
getProperty( $name)
Get an additional output property.
addWikiTextAsInterface( $text, $linestart=true, Title $title=null)
Convert wikitext in the user interface language to HTML and add it to the buffer.
headElement(Skin $sk, $includeStyle=true)
prepareErrorPage( $pageTitle, $htmlTitle=false)
Prepare this object to display an error page; disable caching and indexing, clear the current text an...
array $mCategoryLinks
addContentOverrideCallback(callable $callback)
Add a callback for mapping from a Title to a Content object, for things like page preview.
static buildBacklinkSubtitle(Title $title, $query=[])
Build message object for a subtitle containing a backlink to a page.
setFeedAppendQuery( $val)
Add default feeds to the page header This is mainly kept for backward compatibility,...
getBottomScripts()
JS stuff to put at the bottom of the <body>.
addReturnTo( $title, array $query=[], $text=null, $options=[])
Add a "return to" link pointing to a specified title.
getMetaTags()
Returns the current <meta> tags.
getOriginTrials()
Get the Origin-Trial header values.
string bool $mCanonicalUrl
bool $mArticleBodyOnly
Flag if output should only contain the body of the article.
setSubtitle( $str)
Replace the subtitle with $str.
array $rlExemptStyleModules
getCategoryLinks()
Get the list of category links, in a 2-D array with the following format: $arr[$type][] = $link,...
getFrameOptions()
Get the X-Frame-Options header value (without the name part), or false if there isn't one.
addHeadItems( $values)
Add one or more head items to the output.
static combineWrappedStrings(array $chunks)
Combine WrappedString chunks and filter out empty ones.
bool $mIsArticleRelated
Stores "article flag" toggle.
array $mCategories
parse( $text, $linestart=true, $interface=false, $language=null)
Parse wikitext and return the HTML.
getUnprefixedDisplayTitle()
Returns page display title without namespace prefix if possible.
getCSPNonce()
Get (and set if not yet set) the CSP nonce.
array $mVaryHeader
Headers that cause the cache to vary.
hasHeadItem( $name)
Check if the header item $name is already set.
array $styles
An array of stylesheet filenames (relative from skins path), with options for CSS media,...
isDisabled()
Return whether the output will be completely disabled.
Set options of the Parser.
preventClickjacking( $flag=null)
Get or set the prevent-clickjacking flag.
getText( $options=[])
Get the output HTML.
Load and configure a ResourceLoader client on an HTML page.
setModules(array $modules)
Ensure one or more modules are loaded.
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicily load or embed modules on a page.
setConfig(array $vars)
Set mw.config variables.
setExemptStates(array $states)
Set state of special modules that are handled by the caller manually.
setModuleStyles(array $modules)
Ensure the styles of one or more modules are loaded.
Context object that contains information about the state of a specific ResourceLoader web request.
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
getOrigin()
Get this module's origin.
ResourceLoader is a loading system for JavaScript and CSS resources.
The main skin class which provides methods and properties for all other skins.
Definition Skin.php:38
getHtmlElementAttributes()
Return values for <html> element.
Definition Skin.php:481
getSkinName()
Definition Skin.php:158
addToBodyAttributes( $out, &$bodyAttrs)
This will be called by OutputPage::headElement when it is creating the "<body>" tag,...
Definition Skin.php:497
getPageClasses( $title)
TODO: document.
Definition Skin.php:443
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:64
$IP
Definition update.php:3
const PROTO_CANONICAL
Definition Defines.php:212
const PROTO_CURRENT
Definition Defines.php:211
const NS_SPECIAL
Definition Defines.php:58
const PROTO_RELATIVE
Definition Defines.php:210
const NS_CATEGORY
Definition Defines.php:83
Interface for configuration instances.
Definition Config.php:28
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Base interface for content objects.
Definition Content.php:34
Interface for objects which can provide a MediaWiki context on request.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
Result wrapper for grabbing data queried from an IDatabase object.
$resourceLoader
Definition load.php:44
$context
Definition load.php:45
$filter
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:78
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!isset( $args[0])) $lang
$header