MediaWiki REL1_36
OutputPage.php
Go to the documentation of this file.
1<?php
23use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
31use Wikimedia\RelPath;
32use Wikimedia\WrappedString;
33use Wikimedia\WrappedStringList;
34
51 use ProtectedHookAccessorTrait;
52
54 protected $mMetatags = [];
55
57 protected $mLinktags = [];
58
60 protected $mCanonicalUrl = false;
61
65 private $mPageTitle = '';
66
75
77 private $cacheIsFinal = false;
78
83 public $mBodytext = '';
84
86 private $mHTMLtitle = '';
87
92 private $mIsArticle = false;
93
95 private $mIsArticleRelated = true;
96
98 private $mHasCopyright = false;
99
104 private $mPrintable = false;
105
110 private $mSubtitle = [];
111
113 public $mRedirect = '';
114
116 protected $mStatusCode;
117
122 protected $mLastModified = '';
123
125 protected $mCategoryLinks = [];
126
128 protected $mCategories = [
129 'hidden' => [],
130 'normal' => [],
131 ];
132
134 protected $mIndicators = [];
135
137 private $mLanguageLinks = [];
138
145 private $mScripts = '';
146
148 protected $mInlineStyles = '';
149
154 public $mPageLinkTitle = '';
155
161
163 protected $mHeadItems = [];
164
167
169 protected $mModules = [];
170
172 protected $mModuleStyles = [];
173
176
178 private $rlClient;
179
182
185
187 protected $mJsConfigVars = [];
188
190 protected $mTemplateIds = [];
191
193 protected $mImageTimeKeys = [];
194
196 public $mRedirectCode = '';
197
198 protected $mFeedLinksAppendQuery = null;
199
205 protected $mAllowedModules = [
206 ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
207 ];
208
210 protected $mDoNothing = false;
211
212 // Parser related.
213
215 protected $mContainsNewMagic = 0;
216
221 protected $mParserOptions = null;
222
228 private $mFeedLinks = [];
229
231 protected $mEnableClientCache = true;
232
234 private $mArticleBodyOnly = false;
235
237 protected $mNewSectionLink = false;
238
240 protected $mHideNewSectionLink = false;
241
247 public $mNoGallery = false;
248
250 protected $mCdnMaxage = 0;
252 protected $mCdnMaxageLimit = INF;
253
259 protected $mPreventClickjacking = true;
260
262 private $mRevisionId = null;
263
265 private $mRevisionTimestamp = null;
266
268 protected $mFileVersion = null;
269
278 protected $styles = [];
279
280 private $mIndexPolicy = 'index';
281 private $mFollowPolicy = 'follow';
282
288 private $mVaryHeader = [
289 'Accept-Encoding' => null,
290 ];
291
298 private $mRedirectedFrom = null;
299
303 private $mProperties = [];
304
308 private $mTarget = null;
309
313 private $mEnableTOC = false;
314
319
321 private $limitReportJSData = [];
322
324 private $contentOverrides = [];
325
328
332 private $mLinkHeader = [];
333
337 private $CSP;
338
342 private static $cacheVaryCookies = null;
343
350 public function __construct( IContextSource $context ) {
351 $this->setContext( $context );
352 $this->CSP = new ContentSecurityPolicy(
353 $context->getRequest()->response(),
354 $context->getConfig(),
355 $this->getHookContainer()
356 );
357 }
358
365 public function redirect( $url, $responsecode = '302' ) {
366 # Strip newlines as a paranoia check for header injection in PHP<5.1.2
367 $this->mRedirect = str_replace( "\n", '', $url );
368 $this->mRedirectCode = (string)$responsecode;
369 }
370
376 public function getRedirect() {
377 return $this->mRedirect;
378 }
379
388 public function setCopyrightUrl( $url ) {
389 $this->copyrightUrl = $url;
390 }
391
397 public function setStatusCode( $statusCode ) {
398 $this->mStatusCode = $statusCode;
399 }
400
408 public function addMeta( $name, $val ) {
409 $this->mMetatags[] = [ $name, $val ];
410 }
411
418 public function getMetaTags() {
419 return $this->mMetatags;
420 }
421
429 public function addLink( array $linkarr ) {
430 $this->mLinktags[] = $linkarr;
431 }
432
439 public function getLinkTags() {
440 return $this->mLinktags;
441 }
442
448 public function setCanonicalUrl( $url ) {
449 $this->mCanonicalUrl = $url;
450 }
451
459 public function getCanonicalUrl() {
460 return $this->mCanonicalUrl;
461 }
462
470 public function addScript( $script ) {
471 $this->mScripts .= $script;
472 }
473
482 public function addScriptFile( $file, $unused = null ) {
483 $this->addScript( Html::linkedScript( $file, $this->CSP->getNonce() ) );
484 }
485
492 public function addInlineScript( $script ) {
493 $this->mScripts .= Html::inlineScript( "\n$script\n", $this->CSP->getNonce() ) . "\n";
494 }
495
504 protected function filterModules( array $modules, $position = null,
505 $type = ResourceLoaderModule::TYPE_COMBINED
506 ) {
507 $resourceLoader = $this->getResourceLoader();
508 $filteredModules = [];
509 foreach ( $modules as $val ) {
510 $module = $resourceLoader->getModule( $val );
511 if ( $module instanceof ResourceLoaderModule
512 && $module->getOrigin() <= $this->getAllowedModules( $type )
513 ) {
514 if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
515 $this->warnModuleTargetFilter( $module->getName() );
516 continue;
517 }
518 $filteredModules[] = $val;
519 }
520 }
521 return $filteredModules;
522 }
523
524 private function warnModuleTargetFilter( $moduleName ) {
525 static $warnings = [];
526 if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
527 return;
528 }
529 $warnings[$this->mTarget][$moduleName] = true;
530 $this->getResourceLoader()->getLogger()->debug(
531 'Module "{module}" not loadable on target "{target}".',
532 [
533 'module' => $moduleName,
534 'target' => $this->mTarget,
535 ]
536 );
537 }
538
548 public function getModules( $filter = false, $position = null, $param = 'mModules',
549 $type = ResourceLoaderModule::TYPE_COMBINED
550 ) {
551 $modules = array_values( array_unique( $this->$param ) );
552 return $filter
553 ? $this->filterModules( $modules, null, $type )
554 : $modules;
555 }
556
562 public function addModules( $modules ) {
563 $this->mModules = array_merge( $this->mModules, (array)$modules );
564 }
565
573 public function getModuleStyles( $filter = false, $position = null ) {
574 return $this->getModules( $filter, null, 'mModuleStyles',
575 ResourceLoaderModule::TYPE_STYLES
576 );
577 }
578
588 public function addModuleStyles( $modules ) {
589 $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
590 }
591
595 public function getTarget() {
596 return $this->mTarget;
597 }
598
604 public function setTarget( $target ) {
605 $this->mTarget = $target;
606 }
607
615 public function addContentOverride( LinkTarget $target, Content $content ) {
616 if ( !$this->contentOverrides ) {
617 // Register a callback for $this->contentOverrides on the first call
618 $this->addContentOverrideCallback( function ( LinkTarget $target ) {
619 $key = $target->getNamespace() . ':' . $target->getDBkey();
620 return $this->contentOverrides[$key] ?? null;
621 } );
622 }
623
624 $key = $target->getNamespace() . ':' . $target->getDBkey();
625 $this->contentOverrides[$key] = $content;
626 }
627
635 public function addContentOverrideCallback( callable $callback ) {
636 $this->contentOverrideCallbacks[] = $callback;
637 }
638
646 public function addHtmlClasses( $classes ) {
647 $this->mAdditionalHtmlClasses = array_merge( $this->mAdditionalHtmlClasses, (array)$classes );
648 }
649
655 public function getHeadItemsArray() {
656 return $this->mHeadItems;
657 }
658
671 public function addHeadItem( $name, $value ) {
672 $this->mHeadItems[$name] = $value;
673 }
674
681 public function addHeadItems( $values ) {
682 $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
683 }
684
691 public function hasHeadItem( $name ) {
692 return isset( $this->mHeadItems[$name] );
693 }
694
701 public function addBodyClasses( $classes ) {
702 $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes );
703 }
704
712 public function setArticleBodyOnly( $only ) {
713 $this->mArticleBodyOnly = $only;
714 }
715
721 public function getArticleBodyOnly() {
722 return $this->mArticleBodyOnly;
723 }
724
732 public function setProperty( $name, $value ) {
733 $this->mProperties[$name] = $value;
734 }
735
743 public function getProperty( $name ) {
744 return $this->mProperties[$name] ?? null;
745 }
746
758 public function checkLastModified( $timestamp ) {
759 if ( !$timestamp || $timestamp == '19700101000000' ) {
760 wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP" );
761 return false;
762 }
763 $config = $this->getConfig();
764 if ( !$config->get( 'CachePages' ) ) {
765 wfDebug( __METHOD__ . ": CACHE DISABLED" );
766 return false;
767 }
768
769 $timestamp = wfTimestamp( TS_MW, $timestamp );
770 $modifiedTimes = [
771 'page' => $timestamp,
772 'user' => $this->getUser()->getTouched(),
773 'epoch' => $config->get( 'CacheEpoch' )
774 ];
775 if ( $config->get( 'UseCdn' ) ) {
776 $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, $this->getCdnCacheEpoch(
777 time(),
778 $config->get( 'CdnMaxAge' )
779 ) );
780 }
781 $this->getHookRunner()->onOutputPageCheckLastModified( $modifiedTimes, $this );
782
783 $maxModified = max( $modifiedTimes );
784 $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
785
786 $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
787 if ( $clientHeader === false ) {
788 wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
789 return false;
790 }
791
792 # IE sends sizes after the date like this:
793 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
794 # this breaks strtotime().
795 $clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
796
797 Wikimedia\suppressWarnings(); // E_STRICT system time warnings
798 $clientHeaderTime = strtotime( $clientHeader );
799 Wikimedia\restoreWarnings();
800 if ( !$clientHeaderTime ) {
801 wfDebug( __METHOD__
802 . ": unable to parse the client's If-Modified-Since header: $clientHeader" );
803 return false;
804 }
805 $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
806
807 # Make debug info
808 $info = '';
809 foreach ( $modifiedTimes as $name => $value ) {
810 if ( $info !== '' ) {
811 $info .= ', ';
812 }
813 $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
814 }
815
816 wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
817 wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
818 wfDebug( __METHOD__ . ": effective Last-Modified: " .
819 wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
820 if ( $clientHeaderTime < $maxModified ) {
821 wfDebug( __METHOD__ . ": STALE, $info", 'private' );
822 return false;
823 }
824
825 # Not modified
826 # Give a 304 Not Modified response code and disable body output
827 wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
828 ini_set( 'zlib.output_compression', 0 );
829 $this->getRequest()->response()->statusHeader( 304 );
830 $this->sendCacheControl();
831 $this->disable();
832
833 // Don't output a compressed blob when using ob_gzhandler;
834 // it's technically against HTTP spec and seems to confuse
835 // Firefox when the response gets split over two packets.
836 wfResetOutputBuffers( false );
837
838 return true;
839 }
840
846 private function getCdnCacheEpoch( $reqTime, $maxAge ) {
847 // Ensure Last-Modified is never more than $wgCdnMaxAge in the past,
848 // because even if the wiki page content hasn't changed since, static
849 // resources may have changed (skin HTML, interface messages, urls, etc.)
850 // and must roll-over in a timely manner (T46570)
851 return $reqTime - $maxAge;
852 }
853
860 public function setLastModified( $timestamp ) {
861 $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
862 }
863
871 public function setRobotPolicy( $policy ) {
872 $policy = Article::formatRobotPolicy( $policy );
873
874 if ( isset( $policy['index'] ) ) {
875 $this->setIndexPolicy( $policy['index'] );
876 }
877 if ( isset( $policy['follow'] ) ) {
878 $this->setFollowPolicy( $policy['follow'] );
879 }
880 }
881
888 public function getRobotPolicy() {
889 return "{$this->mIndexPolicy},{$this->mFollowPolicy}";
890 }
891
898 public function setIndexPolicy( $policy ) {
899 $policy = trim( $policy );
900 if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
901 $this->mIndexPolicy = $policy;
902 }
903 }
904
910 public function getIndexPolicy() {
911 return $this->mIndexPolicy;
912 }
913
920 public function setFollowPolicy( $policy ) {
921 $policy = trim( $policy );
922 if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
923 $this->mFollowPolicy = $policy;
924 }
925 }
926
932 public function getFollowPolicy() {
933 return $this->mFollowPolicy;
934 }
935
942 public function setHTMLTitle( $name ) {
943 if ( $name instanceof Message ) {
944 $this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
945 } else {
946 $this->mHTMLtitle = $name;
947 }
948 }
949
955 public function getHTMLTitle() {
956 return $this->mHTMLtitle;
957 }
958
964 public function setRedirectedFrom( $t ) {
965 $this->mRedirectedFrom = $t;
966 }
967
980 public function setPageTitle( $name ) {
981 if ( $name instanceof Message ) {
982 $name = $name->setContext( $this->getContext() )->text();
983 }
984
985 # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
986 # but leave "<i>foobar</i>" alone
987 $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
988 $this->mPageTitle = $nameWithTags;
989
990 # change "<i>foo&amp;bar</i>" to "foo&bar"
991 $this->setHTMLTitle(
992 $this->msg( 'pagetitle' )->plaintextParams( Sanitizer::stripAllTags( $nameWithTags ) )
993 ->inContentLanguage()
994 );
995 }
996
1002 public function getPageTitle() {
1003 return $this->mPageTitle;
1004 }
1005
1013 public function setDisplayTitle( $html ) {
1014 $this->displayTitle = $html;
1015 }
1016
1025 public function getDisplayTitle() {
1026 $html = $this->displayTitle;
1027 if ( $html === null ) {
1028 $html = $this->getTitle()->getPrefixedText();
1029 }
1030
1031 return Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $html ) );
1032 }
1033
1040 public function getUnprefixedDisplayTitle() {
1041 $text = $this->getDisplayTitle();
1042 $nsPrefix = $this->getTitle()->getNsText() . ':';
1043 $prefix = preg_quote( $nsPrefix, '/' );
1044
1045 return preg_replace( "/^$prefix/i", '', $text );
1046 }
1047
1053 public function setTitle( Title $t ) {
1054 // @phan-suppress-next-next-line PhanUndeclaredMethod
1055 // @fixme Not all implementations of IContextSource have this method!
1056 $this->getContext()->setTitle( $t );
1057 }
1058
1064 public function setSubtitle( $str ) {
1065 $this->clearSubtitle();
1066 $this->addSubtitle( $str );
1067 }
1068
1074 public function addSubtitle( $str ) {
1075 if ( $str instanceof Message ) {
1076 $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1077 } else {
1078 $this->mSubtitle[] = $str;
1079 }
1080 }
1081
1090 public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1091 if ( $title->isRedirect() ) {
1092 $query['redirect'] = 'no';
1093 }
1094 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1095 return wfMessage( 'backlinksubtitle' )
1096 ->rawParams( $linkRenderer->makeLink( $title, null, [], $query ) );
1097 }
1098
1105 public function addBacklinkSubtitle( Title $title, $query = [] ) {
1106 $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1107 }
1108
1112 public function clearSubtitle() {
1113 $this->mSubtitle = [];
1114 }
1115
1119 public function getSubtitle() {
1120 return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1121 }
1122
1127 public function setPrintable() {
1128 $this->mPrintable = true;
1129 }
1130
1136 public function isPrintable() {
1137 return $this->mPrintable;
1138 }
1139
1143 public function disable() {
1144 $this->mDoNothing = true;
1145 }
1146
1152 public function isDisabled() {
1153 return $this->mDoNothing;
1154 }
1155
1161 public function showNewSectionLink() {
1162 return $this->mNewSectionLink;
1163 }
1164
1170 public function forceHideNewSectionLink() {
1171 return $this->mHideNewSectionLink;
1172 }
1173
1182 public function setSyndicated( $show = true ) {
1183 if ( $show ) {
1184 $this->setFeedAppendQuery( false );
1185 } else {
1186 $this->mFeedLinks = [];
1187 }
1188 }
1189
1196 protected function getAdvertisedFeedTypes() {
1197 if ( $this->getConfig()->get( 'Feed' ) ) {
1198 return $this->getConfig()->get( 'AdvertisedFeedTypes' );
1199 } else {
1200 return [];
1201 }
1202 }
1203
1213 public function setFeedAppendQuery( $val ) {
1214 $this->mFeedLinks = [];
1215
1216 foreach ( $this->getAdvertisedFeedTypes() as $type ) {
1217 $query = "feed=$type";
1218 if ( is_string( $val ) ) {
1219 $query .= '&' . $val;
1220 }
1221 $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1222 }
1223 }
1224
1231 public function addFeedLink( $format, $href ) {
1232 if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) {
1233 $this->mFeedLinks[$format] = $href;
1234 }
1235 }
1236
1241 public function isSyndicated() {
1242 return count( $this->mFeedLinks ) > 0;
1243 }
1244
1249 public function getSyndicationLinks() {
1250 return $this->mFeedLinks;
1251 }
1252
1258 public function getFeedAppendQuery() {
1259 return $this->mFeedLinksAppendQuery;
1260 }
1261
1269 public function setArticleFlag( $newVal ) {
1270 $this->mIsArticle = $newVal;
1271 if ( $newVal ) {
1272 $this->mIsArticleRelated = $newVal;
1273 }
1274 }
1275
1282 public function isArticle() {
1283 return $this->mIsArticle;
1284 }
1285
1292 public function setArticleRelated( $newVal ) {
1293 $this->mIsArticleRelated = $newVal;
1294 if ( !$newVal ) {
1295 $this->mIsArticle = false;
1296 }
1297 }
1298
1304 public function isArticleRelated() {
1305 return $this->mIsArticleRelated;
1306 }
1307
1313 public function setCopyright( $hasCopyright ) {
1314 $this->mHasCopyright = $hasCopyright;
1315 }
1316
1326 public function showsCopyright() {
1327 return $this->isArticle() || $this->mHasCopyright;
1328 }
1329
1336 public function addLanguageLinks( array $newLinkArray ) {
1337 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1338 }
1339
1346 public function setLanguageLinks( array $newLinkArray ) {
1347 $this->mLanguageLinks = $newLinkArray;
1348 }
1349
1355 public function getLanguageLinks() {
1356 return $this->mLanguageLinks;
1357 }
1358
1364 public function addCategoryLinks( array $categories ) {
1365 if ( !$categories ) {
1366 return;
1367 }
1368
1369 $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1370
1371 # Set all the values to 'normal'.
1372 $categories = array_fill_keys( array_keys( $categories ), 'normal' );
1373
1374 # Mark hidden categories
1375 foreach ( $res as $row ) {
1376 if ( isset( $row->pp_value ) ) {
1377 $categories[$row->page_title] = 'hidden';
1378 }
1379 }
1380
1381 # Add the remaining categories to the skin
1382 if ( $this->getHookRunner()->onOutputPageMakeCategoryLinks(
1383 $this, $categories, $this->mCategoryLinks )
1384 ) {
1385 $services = MediaWikiServices::getInstance();
1386 $linkRenderer = $services->getLinkRenderer();
1387 $languageConverter = $services->getLanguageConverterFactory()
1388 ->getLanguageConverter( $services->getContentLanguage() );
1389 foreach ( $categories as $category => $type ) {
1390 // array keys will cast numeric category names to ints, so cast back to string
1391 $category = (string)$category;
1392 $origcategory = $category;
1393 $title = Title::makeTitleSafe( NS_CATEGORY, $category );
1394 if ( !$title ) {
1395 continue;
1396 }
1397 $languageConverter->findVariantLink( $category, $title, true );
1398
1399 if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1400 continue;
1401 }
1402 $text = $languageConverter->convertHtml( $title->getText() );
1403 $this->mCategories[$type][] = $title->getText();
1404 $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) );
1405 }
1406 }
1407 }
1408
1413 protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1414 # Add the links to a LinkBatch
1415 $arr = [ NS_CATEGORY => $categories ];
1416 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
1417 $lb = $linkBatchFactory->newLinkBatch();
1418 $lb->setArray( $arr );
1419
1420 # Fetch existence plus the hiddencat property
1421 $dbr = wfGetDB( DB_REPLICA );
1422 $fields = array_merge(
1423 LinkCache::getSelectFields(),
1424 [ 'page_namespace', 'page_title', 'pp_value' ]
1425 );
1426
1427 $res = $dbr->select( [ 'page', 'page_props' ],
1428 $fields,
1429 $lb->constructSet( 'page', $dbr ),
1430 __METHOD__,
1431 [],
1432 [ 'page_props' => [ 'LEFT JOIN', [
1433 'pp_propname' => 'hiddencat',
1434 'pp_page = page_id'
1435 ] ] ]
1436 );
1437
1438 # Add the results to the link cache
1439 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1440 $lb->addResultToCache( $linkCache, $res );
1441
1442 return $res;
1443 }
1444
1450 public function setCategoryLinks( array $categories ) {
1451 $this->mCategoryLinks = [];
1452 $this->addCategoryLinks( $categories );
1453 }
1454
1463 public function getCategoryLinks() {
1464 return $this->mCategoryLinks;
1465 }
1466
1476 public function getCategories( $type = 'all' ) {
1477 if ( $type === 'all' ) {
1478 $allCategories = [];
1479 foreach ( $this->mCategories as $categories ) {
1480 $allCategories = array_merge( $allCategories, $categories );
1481 }
1482 return $allCategories;
1483 }
1484 if ( !isset( $this->mCategories[$type] ) ) {
1485 throw new InvalidArgumentException( 'Invalid category type given: ' . $type );
1486 }
1487 return $this->mCategories[$type];
1488 }
1489
1499 public function setIndicators( array $indicators ) {
1500 $this->mIndicators = $indicators + $this->mIndicators;
1501 // Keep ordered by key
1502 ksort( $this->mIndicators );
1503 }
1504
1513 public function getIndicators() {
1514 return $this->mIndicators;
1515 }
1516
1525 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1526 $this->addModuleStyles( 'mediawiki.helplink' );
1527 $text = $this->msg( 'helppage-top-gethelp' )->escaped();
1528
1529 if ( $overrideBaseUrl ) {
1530 $helpUrl = $to;
1531 } else {
1532 $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1533 $helpUrl = "https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1534 }
1535
1536 $link = Html::rawElement(
1537 'a',
1538 [
1539 'href' => $helpUrl,
1540 'target' => '_blank',
1541 'class' => 'mw-helplink',
1542 ],
1543 $text
1544 );
1545
1546 $this->setIndicators( [ 'mw-helplink' => $link ] );
1547 }
1548
1557 public function disallowUserJs() {
1558 $this->reduceAllowedModules(
1559 ResourceLoaderModule::TYPE_SCRIPTS,
1560 ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1561 );
1562
1563 // Site-wide styles are controlled by a config setting, see T73621
1564 // for background on why. User styles are never allowed.
1565 if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1566 $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1567 } else {
1568 $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1569 }
1570 $this->reduceAllowedModules(
1571 ResourceLoaderModule::TYPE_STYLES,
1572 $styleOrigin
1573 );
1574 }
1575
1582 public function getAllowedModules( $type ) {
1583 if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1584 return min( array_values( $this->mAllowedModules ) );
1585 } else {
1586 return $this->mAllowedModules[$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1587 }
1588 }
1589
1599 public function reduceAllowedModules( $type, $level ) {
1600 $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1601 }
1602
1608 public function prependHTML( $text ) {
1609 $this->mBodytext = $text . $this->mBodytext;
1610 }
1611
1617 public function addHTML( $text ) {
1618 $this->mBodytext .= $text;
1619 }
1620
1630 public function addElement( $element, array $attribs = [], $contents = '' ) {
1631 $this->addHTML( Html::element( $element, $attribs, $contents ) );
1632 }
1633
1637 public function clearHTML() {
1638 $this->mBodytext = '';
1639 }
1640
1646 public function getHTML() {
1647 return $this->mBodytext;
1648 }
1649
1656 public function parserOptions() {
1657 if ( !$this->mParserOptions ) {
1658 if ( !$this->getUser()->isSafeToLoad() ) {
1659 // Context user isn't unstubbable yet, so don't try to get a
1660 // ParserOptions for it. And don't cache this ParserOptions
1661 // either.
1662 $po = ParserOptions::newFromAnon();
1663 $po->setAllowUnsafeRawHtml( false );
1664 $po->isBogus = true;
1665 return $po;
1666 }
1667
1668 $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1669 $this->mParserOptions->setAllowUnsafeRawHtml( false );
1670 }
1671
1672 return $this->mParserOptions;
1673 }
1674
1682 public function setRevisionId( $revid ) {
1683 $val = $revid === null ? null : intval( $revid );
1684 return wfSetVar( $this->mRevisionId, $val, true );
1685 }
1686
1692 public function getRevisionId() {
1693 return $this->mRevisionId;
1694 }
1695
1702 public function isRevisionCurrent() {
1703 return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
1704 }
1705
1713 public function setRevisionTimestamp( $timestamp ) {
1714 return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
1715 }
1716
1723 public function getRevisionTimestamp() {
1724 return $this->mRevisionTimestamp;
1725 }
1726
1733 public function setFileVersion( $file ) {
1734 $val = null;
1735 if ( $file instanceof File && $file->exists() ) {
1736 $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1737 }
1738 return wfSetVar( $this->mFileVersion, $val, true );
1739 }
1740
1746 public function getFileVersion() {
1747 return $this->mFileVersion;
1748 }
1749
1756 public function getTemplateIds() {
1757 return $this->mTemplateIds;
1758 }
1759
1766 public function getFileSearchOptions() {
1767 return $this->mImageTimeKeys;
1768 }
1769
1786 public function addWikiTextAsInterface(
1787 $text, $linestart = true, Title $title = null
1788 ) {
1789 if ( $title === null ) {
1790 $title = $this->getTitle();
1791 }
1792 if ( !$title ) {
1793 throw new MWException( 'Title is null' );
1794 }
1795 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true );
1796 }
1797
1812 $wrapperClass, $text
1813 ) {
1815 $text, $this->getTitle(),
1816 /*linestart*/true, /*interface*/true,
1817 $wrapperClass
1818 );
1819 }
1820
1836 public function addWikiTextAsContent(
1837 $text, $linestart = true, Title $title = null
1838 ) {
1839 if ( $title === null ) {
1840 $title = $this->getTitle();
1841 }
1842 if ( !$title ) {
1843 throw new MWException( 'Title is null' );
1844 }
1845 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false );
1846 }
1847
1861 $text, Title $title, $linestart, $interface, $wrapperClass = null
1862 ) {
1863 $parserOutput = $this->parseInternal(
1864 $text, $title, $linestart, $interface
1865 );
1866
1867 $this->addParserOutput( $parserOutput, [
1868 'enableSectionEditLinks' => false,
1869 'wrapperDivClass' => $wrapperClass ?? '',
1870 ] );
1871 }
1872
1881 public function addParserOutputMetadata( ParserOutput $parserOutput ) {
1882 $this->mLanguageLinks =
1883 array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() );
1884 $this->addCategoryLinks( $parserOutput->getCategories() );
1885 $this->setIndicators( $parserOutput->getIndicators() );
1886 $this->mNewSectionLink = $parserOutput->getNewSection();
1887 $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1888
1889 if ( !$parserOutput->isCacheable() ) {
1890 $this->enableClientCache( false );
1891 }
1892 $this->mNoGallery = $parserOutput->getNoGallery();
1893 $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1894 $this->addModules( $parserOutput->getModules() );
1895 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1896 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1897 $this->mPreventClickjacking = $this->mPreventClickjacking
1898 || $parserOutput->preventClickjacking();
1899 $scriptSrcs = $parserOutput->getExtraCSPScriptSrcs();
1900 foreach ( $scriptSrcs as $src ) {
1901 $this->getCSP()->addScriptSrc( $src );
1902 }
1903 $defaultSrcs = $parserOutput->getExtraCSPDefaultSrcs();
1904 foreach ( $defaultSrcs as $src ) {
1905 $this->getCSP()->addDefaultSrc( $src );
1906 }
1907 $styleSrcs = $parserOutput->getExtraCSPStyleSrcs();
1908 foreach ( $styleSrcs as $src ) {
1909 $this->getCSP()->addStyleSrc( $src );
1910 }
1911
1912 // If $wgImagePreconnect is true, and if the output contains
1913 // images, give the user-agent a hint about foreign repos from
1914 // which those images may be served. See T123582.
1915 //
1916 // TODO: We don't have an easy way to know from which remote(s)
1917 // the image(s) will be served. For now, we only hint the first
1918 // valid one.
1919 if ( $this->getConfig()->get( 'ImagePreconnect' ) && count( $parserOutput->getImages() ) ) {
1920 $preconnect = [];
1921 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
1922 $repoGroup->forEachForeignRepo( static function ( $repo ) use ( &$preconnect ) {
1923 $preconnect[] = wfParseUrl( $repo->getZoneUrl( 'thumb' ) )['host'];
1924 } );
1925 $preconnect[] = wfParseUrl( $repoGroup->getLocalRepo()->getZoneUrl( 'thumb' ) )['host'];
1926 foreach ( $preconnect as $host ) {
1927 if ( $host ) {
1928 $this->addLink( [ 'rel' => 'preconnect', 'href' => '//' . $host ] );
1929 break;
1930 }
1931 }
1932 }
1933
1934 // Template versioning...
1935 foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1936 if ( isset( $this->mTemplateIds[$ns] ) ) {
1937 $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1938 } else {
1939 $this->mTemplateIds[$ns] = $dbks;
1940 }
1941 }
1942 // File versioning...
1943 foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1944 $this->mImageTimeKeys[$dbk] = $data;
1945 }
1946
1947 // Hooks registered in the object
1948 $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1949 foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1950 list( $hookName, $data ) = $hookInfo;
1951 if ( isset( $parserOutputHooks[$hookName] ) ) {
1952 $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1953 }
1954 }
1955
1956 // Enable OOUI if requested via ParserOutput
1957 if ( $parserOutput->getEnableOOUI() ) {
1958 $this->enableOOUI();
1959 }
1960
1961 // Include parser limit report
1962 if ( !$this->limitReportJSData ) {
1963 $this->limitReportJSData = $parserOutput->getLimitReportJSData();
1964 }
1965
1966 // Link flags are ignored for now, but may in the future be
1967 // used to mark individual language links.
1968 $linkFlags = [];
1969 $this->getHookRunner()->onLanguageLinks( $this->getTitle(), $this->mLanguageLinks, $linkFlags );
1970 $this->getHookRunner()->onOutputPageParserOutput( $this, $parserOutput );
1971
1972 // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
1973 // so that extensions may modify ParserOutput to toggle TOC.
1974 // This cannot be moved to addParserOutputText because that is not
1975 // called by EditPage for Preview.
1976 if ( $parserOutput->getTOCHTML() ) {
1977 $this->mEnableTOC = true;
1978 }
1979 }
1980
1989 public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) {
1990 $this->addParserOutputText( $parserOutput, $poOptions );
1991
1992 $this->addModules( $parserOutput->getModules() );
1993 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1994
1995 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1996 }
1997
2005 public function addParserOutputText( ParserOutput $parserOutput, $poOptions = [] ) {
2006 $text = $parserOutput->getText( $poOptions );
2007 $this->getHookRunner()->onOutputPageBeforeHTML( $this, $text );
2008 $this->addHTML( $text );
2009 }
2010
2017 public function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) {
2018 $this->addParserOutputMetadata( $parserOutput );
2019 $this->addParserOutputText( $parserOutput, $poOptions );
2020 }
2021
2027 public function addTemplate( &$template ) {
2028 $this->addHTML( $template->getHTML() );
2029 }
2030
2042 public function parseAsContent( $text, $linestart = true ) {
2043 return $this->parseInternal(
2044 $text, $this->getTitle(), $linestart, /*interface*/false
2045 )->getText( [
2046 'enableSectionEditLinks' => false,
2047 'wrapperDivClass' => ''
2048 ] );
2049 }
2050
2063 public function parseAsInterface( $text, $linestart = true ) {
2064 return $this->parseInternal(
2065 $text, $this->getTitle(), $linestart, /*interface*/true
2066 )->getText( [
2067 'enableSectionEditLinks' => false,
2068 'wrapperDivClass' => ''
2069 ] );
2070 }
2071
2086 public function parseInlineAsInterface( $text, $linestart = true ) {
2088 $this->parseAsInterface( $text, $linestart )
2089 );
2090 }
2091
2104 private function parseInternal( $text, $title, $linestart, $interface ) {
2105 if ( $title === null ) {
2106 throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
2107 }
2108
2109 $popts = $this->parserOptions();
2110
2111 $oldInterface = $popts->setInterfaceMessage( (bool)$interface );
2112
2113 $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
2114 $text, $title, $popts,
2115 $linestart, true, $this->mRevisionId
2116 );
2117
2118 $popts->setInterfaceMessage( $oldInterface );
2119
2120 return $parserOutput;
2121 }
2122
2128 public function setCdnMaxage( $maxage ) {
2129 $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2130 }
2131
2141 public function lowerCdnMaxage( $maxage ) {
2142 $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2143 $this->setCdnMaxage( $this->mCdnMaxage );
2144 }
2145
2158 public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2159 $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
2160 $maxTTL = $maxTTL ?: $this->getConfig()->get( 'CdnMaxAge' );
2161
2162 if ( $mtime === null || $mtime === false ) {
2163 return; // entity does not exist
2164 }
2165
2166 $age = MWTimestamp::time() - (int)wfTimestamp( TS_UNIX, $mtime );
2167 $adaptiveTTL = max( 0.9 * $age, $minTTL );
2168 $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2169
2170 $this->lowerCdnMaxage( (int)$adaptiveTTL );
2171 }
2172
2180 public function enableClientCache( $state ) {
2181 return wfSetVar( $this->mEnableClientCache, $state );
2182 }
2183
2190 public function couldBePublicCached() {
2191 if ( !$this->cacheIsFinal ) {
2192 // - The entry point handles its own caching and/or doesn't use OutputPage.
2193 // (such as load.php, AjaxDispatcher, or MediaWiki\Rest\EntryPoint).
2194 //
2195 // - Or, we haven't finished processing the main part of the request yet
2196 // (e.g. Action::show, SpecialPage::execute), and the state may still
2197 // change via enableClientCache().
2198 return true;
2199 }
2200 // e.g. various error-type pages disable all client caching
2201 return $this->mEnableClientCache;
2202 }
2203
2213 public function considerCacheSettingsFinal() {
2214 $this->cacheIsFinal = true;
2215 }
2216
2222 public function getCacheVaryCookies() {
2223 if ( self::$cacheVaryCookies === null ) {
2224 $config = $this->getConfig();
2225 self::$cacheVaryCookies = array_values( array_unique( array_merge(
2226 SessionManager::singleton()->getVaryCookies(),
2227 [
2228 'forceHTTPS',
2229 ],
2230 $config->get( 'CacheVaryCookies' )
2231 ) ) );
2232 $this->getHookRunner()->onGetCacheVaryCookies( $this, self::$cacheVaryCookies );
2233 }
2234 return self::$cacheVaryCookies;
2235 }
2236
2243 public function haveCacheVaryCookies() {
2244 $request = $this->getRequest();
2245 foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2246 if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
2247 wfDebug( __METHOD__ . ": found $cookieName" );
2248 return true;
2249 }
2250 }
2251 wfDebug( __METHOD__ . ": no cache-varying cookies found" );
2252 return false;
2253 }
2254
2264 public function addVaryHeader( $header, array $option = null ) {
2265 if ( $option !== null && count( $option ) > 0 ) {
2267 'The $option parameter to addVaryHeader is ignored since MediaWiki 1.34',
2268 '1.34' );
2269 }
2270 if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2271 $this->mVaryHeader[$header] = null;
2272 }
2273 }
2274
2281 public function getVaryHeader() {
2282 // If we vary on cookies, let's make sure it's always included here too.
2283 if ( $this->getCacheVaryCookies() ) {
2284 $this->addVaryHeader( 'Cookie' );
2285 }
2286
2287 foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2288 $this->addVaryHeader( $header, $options );
2289 }
2290 return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2291 }
2292
2298 public function addLinkHeader( $header ) {
2299 $this->mLinkHeader[] = $header;
2300 }
2301
2307 public function getLinkHeader() {
2308 if ( !$this->mLinkHeader ) {
2309 return false;
2310 }
2311
2312 return 'Link: ' . implode( ',', $this->mLinkHeader );
2313 }
2314
2322 private function addAcceptLanguage() {
2323 $title = $this->getTitle();
2324 if ( !$title instanceof Title ) {
2325 return;
2326 }
2327
2328 $languageConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
2329 ->getLanguageConverter( $title->getPageLanguage() );
2330 if ( !$this->getRequest()->getCheck( 'variant' ) && $languageConverter->hasVariants() ) {
2331 $this->addVaryHeader( 'Accept-Language' );
2332 }
2333 }
2334
2345 public function preventClickjacking( $enable = true ) {
2346 $this->mPreventClickjacking = $enable;
2347 }
2348
2354 public function allowClickjacking() {
2355 $this->mPreventClickjacking = false;
2356 }
2357
2364 public function getPreventClickjacking() {
2365 return $this->mPreventClickjacking;
2366 }
2367
2375 public function getFrameOptions() {
2376 $config = $this->getConfig();
2377 if ( $config->get( 'BreakFrames' ) ) {
2378 return 'DENY';
2379 } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2380 return $config->get( 'EditPageFrameOptions' );
2381 }
2382 return false;
2383 }
2384
2391 private function getOriginTrials() {
2392 $config = $this->getConfig();
2393
2394 return $config->get( 'OriginTrials' );
2395 }
2396
2397 private function getReportTo() {
2398 $config = $this->getConfig();
2399
2400 $expiry = $config->get( 'ReportToExpiry' );
2401
2402 if ( !$expiry ) {
2403 return false;
2404 }
2405
2406 $endpoints = $config->get( 'ReportToEndpoints' );
2407
2408 if ( !$endpoints ) {
2409 return false;
2410 }
2411
2412 $output = [ 'max_age' => $expiry, 'endpoints' => [] ];
2413
2414 foreach ( $endpoints as $endpoint ) {
2415 $output['endpoints'][] = [ 'url' => $endpoint ];
2416 }
2417
2418 return json_encode( $output, JSON_UNESCAPED_SLASHES );
2419 }
2420
2421 private function getFeaturePolicyReportOnly() {
2422 $config = $this->getConfig();
2423
2424 $features = $config->get( 'FeaturePolicyReportOnly' );
2425 return implode( ';', $features );
2426 }
2427
2431 public function sendCacheControl() {
2432 $response = $this->getRequest()->response();
2433 $config = $this->getConfig();
2434
2435 $this->addVaryHeader( 'Cookie' );
2436 $this->addAcceptLanguage();
2437
2438 # don't serve compressed data to clients who can't handle it
2439 # maintain different caches for logged-in users and non-logged in ones
2440 $response->header( $this->getVaryHeader() );
2441
2442 if ( $this->mEnableClientCache ) {
2443 if ( !$config->get( 'UseCdn' ) ) {
2444 $privateReason = 'config';
2445 } elseif ( $response->hasCookies() ) {
2446 $privateReason = 'set-cookies';
2447 // The client might use methods other than cookies to appear logged-in.
2448 // E.g. HTTP headers, or query parameter tokens, OAuth, etc.
2449 } elseif ( SessionManager::getGlobalSession()->isPersistent() ) {
2450 $privateReason = 'session';
2451 } elseif ( $this->isPrintable() ) {
2452 $privateReason = 'printable';
2453 } elseif ( $this->mCdnMaxage == 0 ) {
2454 $privateReason = 'no-maxage';
2455 } elseif ( $this->haveCacheVaryCookies() ) {
2456 $privateReason = 'cache-vary-cookies';
2457 } else {
2458 $privateReason = false;
2459 }
2460
2461 if ( $privateReason === false ) {
2462 # We'll purge the proxy cache for anons explicitly, but require end user agents
2463 # to revalidate against the proxy on each visit.
2464 # IMPORTANT! The CDN needs to replace the Cache-Control header with
2465 # Cache-Control: s-maxage=0, must-revalidate, max-age=0
2466 wfDebug( __METHOD__ .
2467 ": local proxy caching; {$this->mLastModified} **", 'private' );
2468 # start with a shorter timeout for initial testing
2469 # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2470 $response->header( "Cache-Control: " .
2471 "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2472 } else {
2473 # We do want clients to cache if they can, but they *must* check for updates
2474 # on revisiting the page, after the max-age period.
2475 wfDebug( __METHOD__ . ": private caching ($privateReason); {$this->mLastModified} **", 'private' );
2476
2477 if ( $response->hasCookies() || SessionManager::getGlobalSession()->isPersistent() ) {
2478 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2479 $response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2480 } else {
2481 $response->header(
2482 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $config->get( 'LoggedOutMaxAge' ) ) . ' GMT'
2483 );
2484 $response->header(
2485 "Cache-Control: private, must-revalidate, max-age={$config->get( 'LoggedOutMaxAge' )}"
2486 );
2487 }
2488 }
2489 if ( $this->mLastModified ) {
2490 $response->header( "Last-Modified: {$this->mLastModified}" );
2491 }
2492 } else {
2493 wfDebug( __METHOD__ . ": no caching **", 'private' );
2494
2495 # In general, the absence of a last modified header should be enough to prevent
2496 # the client from using its cache. We send a few other things just to make sure.
2497 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2498 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2499 $response->header( 'Pragma: no-cache' );
2500 }
2501 }
2502
2508 public function loadSkinModules( $sk ) {
2509 foreach ( $sk->getDefaultModules() as $group => $modules ) {
2510 if ( $group === 'styles' ) {
2511 foreach ( $modules as $key => $moduleMembers ) {
2512 $this->addModuleStyles( $moduleMembers );
2513 }
2514 } else {
2515 $this->addModules( $modules );
2516 }
2517 }
2518 }
2519
2530 public function output( $return = false ) {
2531 if ( $this->mDoNothing ) {
2532 return $return ? '' : null;
2533 }
2534
2535 $response = $this->getRequest()->response();
2536 $config = $this->getConfig();
2537
2538 if ( $this->mRedirect != '' ) {
2539 # Standards require redirect URLs to be absolute
2540 $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
2541
2542 $redirect = $this->mRedirect;
2543 $code = $this->mRedirectCode;
2544 $content = '';
2545
2546 if ( $this->getHookRunner()->onBeforePageRedirect( $this, $redirect, $code ) ) {
2547 if ( $code == '301' || $code == '303' ) {
2548 if ( !$config->get( 'DebugRedirects' ) ) {
2549 $response->statusHeader( $code );
2550 }
2551 $this->mLastModified = wfTimestamp( TS_RFC2822 );
2552 }
2553 if ( $config->get( 'VaryOnXFP' ) ) {
2554 $this->addVaryHeader( 'X-Forwarded-Proto' );
2555 }
2556 $this->sendCacheControl();
2557
2558 $response->header( "Content-Type: text/html; charset=utf-8" );
2559 if ( $config->get( 'DebugRedirects' ) ) {
2560 $url = htmlspecialchars( $redirect );
2561 $content = "<!DOCTYPE html>\n<html>\n<head>\n"
2562 . "<title>Redirect</title>\n</head>\n<body>\n"
2563 . "<p>Location: <a href=\"$url\">$url</a></p>\n"
2564 . "</body>\n</html>\n";
2565
2566 if ( !$return ) {
2568 }
2569
2570 } else {
2571 $response->header( 'Location: ' . $redirect );
2572 }
2573 }
2574
2575 return $return ? $content : null;
2576 } elseif ( $this->mStatusCode ) {
2577 $response->statusHeader( $this->mStatusCode );
2578 }
2579
2580 # Buffer output; final headers may depend on later processing
2581 ob_start();
2582
2583 $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2584 $response->header( 'Content-language: ' .
2585 MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2586
2587 $linkHeader = $this->getLinkHeader();
2588 if ( $linkHeader ) {
2589 $response->header( $linkHeader );
2590 }
2591
2592 // Prevent framing, if requested
2593 $frameOptions = $this->getFrameOptions();
2594 if ( $frameOptions ) {
2595 $response->header( "X-Frame-Options: $frameOptions" );
2596 }
2597
2598 $originTrials = $this->getOriginTrials();
2599 foreach ( $originTrials as $originTrial ) {
2600 $response->header( "Origin-Trial: $originTrial", false );
2601 }
2602
2603 $reportTo = $this->getReportTo();
2604 if ( $reportTo ) {
2605 $response->header( "Report-To: $reportTo" );
2606 }
2607
2608 $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
2609 if ( $featurePolicyReportOnly ) {
2610 $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" );
2611 }
2612
2613 if ( $this->mArticleBodyOnly ) {
2614 $this->CSP->sendHeaders();
2615 echo $this->mBodytext;
2616 } else {
2617 // Enable safe mode if requested (T152169)
2618 if ( $this->getRequest()->getBool( 'safemode' ) ) {
2619 $this->disallowUserJs();
2620 }
2621
2622 $sk = $this->getSkin();
2623 $this->loadSkinModules( $sk );
2624
2625 MWDebug::addModules( $this );
2626
2627 // Hook that allows last minute changes to the output page, e.g.
2628 // adding of CSS or Javascript by extensions, adding CSP sources.
2629 $this->getHookRunner()->onBeforePageDisplay( $this, $sk );
2630
2631 $this->CSP->sendHeaders();
2632
2633 try {
2634 $sk->outputPage();
2635 } catch ( Exception $e ) {
2636 ob_end_clean(); // bug T129657
2637 throw $e;
2638 }
2639 }
2640
2641 try {
2642 // This hook allows last minute changes to final overall output by modifying output buffer
2643 $this->getHookRunner()->onAfterFinalPageOutput( $this );
2644 } catch ( Exception $e ) {
2645 ob_end_clean(); // bug T129657
2646 throw $e;
2647 }
2648
2649 $this->sendCacheControl();
2650
2651 if ( $return ) {
2652 return ob_get_clean();
2653 } else {
2654 ob_end_flush();
2655 return null;
2656 }
2657 }
2658
2669 public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2670 $this->setPageTitle( $pageTitle );
2671 if ( $htmlTitle !== false ) {
2672 $this->setHTMLTitle( $htmlTitle );
2673 }
2674 $this->setRobotPolicy( 'noindex,nofollow' );
2675 $this->setArticleRelated( false );
2676 $this->enableClientCache( false );
2677 $this->mRedirect = '';
2678 $this->clearSubtitle();
2679 $this->clearHTML();
2680 }
2681
2694 public function showErrorPage( $title, $msg, $params = [] ) {
2695 if ( !$title instanceof Message ) {
2696 $title = $this->msg( $title );
2697 }
2698
2699 $this->prepareErrorPage( $title );
2700
2701 if ( $msg instanceof Message ) {
2702 if ( $params !== [] ) {
2703 trigger_error( 'Argument ignored: $params. The message parameters argument '
2704 . 'is discarded when the $msg argument is a Message object instead of '
2705 . 'a string.', E_USER_NOTICE );
2706 }
2707 $this->addHTML( $msg->parseAsBlock() );
2708 } else {
2709 $this->addWikiMsgArray( $msg, $params );
2710 }
2711
2712 $this->returnToMain();
2713 }
2714
2721 public function showPermissionsErrorPage( array $errors, $action = null ) {
2722 $services = MediaWikiServices::getInstance();
2723 $permissionManager = $services->getPermissionManager();
2724 foreach ( $errors as $key => $error ) {
2725 $errors[$key] = (array)$error;
2726 }
2727
2728 // For some action (read, edit, create and upload), display a "login to do this action"
2729 // error if all of the following conditions are met:
2730 // 1. the user is not logged in
2731 // 2. the only error is insufficient permissions (i.e. no block or something else)
2732 // 3. the error can be avoided simply by logging in
2733
2734 if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2735 && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2736 && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2737 && ( $permissionManager->groupHasPermission( 'user', $action )
2738 || $permissionManager->groupHasPermission( 'autoconfirmed', $action ) )
2739 ) {
2740 $displayReturnto = null;
2741
2742 # Due to T34276, if a user does not have read permissions,
2743 # $this->getTitle() will just give Special:Badtitle, which is
2744 # not especially useful as a returnto parameter. Use the title
2745 # from the request instead, if there was one.
2746 $request = $this->getRequest();
2747 $returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2748 if ( $action == 'edit' ) {
2749 $msg = 'whitelistedittext';
2750 $displayReturnto = $returnto;
2751 } elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2752 $msg = 'nocreatetext';
2753 } elseif ( $action == 'upload' ) {
2754 $msg = 'uploadnologintext';
2755 } else { # Read
2756 $msg = 'loginreqpagetext';
2757 $displayReturnto = Title::newMainPage();
2758 }
2759
2760 $query = [];
2761
2762 if ( $returnto ) {
2763 $query['returnto'] = $returnto->getPrefixedText();
2764
2765 if ( !$request->wasPosted() ) {
2766 $returntoquery = $request->getValues();
2767 unset( $returntoquery['title'] );
2768 unset( $returntoquery['returnto'] );
2769 unset( $returntoquery['returntoquery'] );
2770 $query['returntoquery'] = wfArrayToCgi( $returntoquery );
2771 }
2772 }
2773
2774 $title = SpecialPage::getTitleFor( 'Userlogin' );
2775 $linkRenderer = $services->getLinkRenderer();
2776 $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
2777 $loginLink = $linkRenderer->makeKnownLink(
2778 $title,
2779 $this->msg( 'loginreqlink' )->text(),
2780 [],
2781 $query
2782 );
2783
2784 $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2785 $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
2786
2787 # Don't return to a page the user can't read otherwise
2788 # we'll end up in a pointless loop
2789 if ( $displayReturnto && $this->getAuthority()->probablyCan( 'read', $displayReturnto ) ) {
2790 $this->returnToMain( null, $displayReturnto );
2791 }
2792 } else {
2793 $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2794 $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) );
2795 }
2796 }
2797
2804 public function versionRequired( $version ) {
2805 $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2806
2807 $this->addWikiMsg( 'versionrequiredtext', $version );
2808 $this->returnToMain();
2809 }
2810
2818 public function formatPermissionStatus( PermissionStatus $status, string $action = null ): string {
2819 if ( $status->isGood() ) {
2820 return '';
2821 }
2822 return $this->formatPermissionsErrorMessage( $status->toLegacyErrorArray(), $action );
2823 }
2824
2833 public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2834 if ( $action == null ) {
2835 $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2836 } else {
2837 $action_desc = $this->msg( "action-$action" )->plain();
2838 $text = $this->msg(
2839 'permissionserrorstext-withaction',
2840 count( $errors ),
2841 $action_desc
2842 )->plain() . "\n\n";
2843 }
2844
2845 if ( count( $errors ) > 1 ) {
2846 $text .= '<ul class="permissions-errors">' . "\n";
2847
2848 foreach ( $errors as $error ) {
2849 $text .= '<li>';
2850 $text .= $this->msg( ...$error )->plain();
2851 $text .= "</li>\n";
2852 }
2853 $text .= '</ul>';
2854 } else {
2855 $text .= "<div class=\"permissions-errors\">\n" .
2856 $this->msg( ...reset( $errors ) )->plain() .
2857 "\n</div>";
2858 }
2859
2860 return $text;
2861 }
2862
2872 public function showLagWarning( $lag ) {
2873 $config = $this->getConfig();
2874 if ( $lag >= $config->get( 'DatabaseReplicaLagWarning' ) ) {
2875 $lag = floor( $lag ); // floor to avoid nano seconds to display
2876 $message = $lag < $config->get( 'DatabaseReplicaLagCritical' )
2877 ? 'lag-warn-normal'
2878 : 'lag-warn-high';
2879 // For grep: mw-lag-warn-normal, mw-lag-warn-high
2880 $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2881 $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2882 }
2883 }
2884
2891 public function showFatalError( $message ) {
2892 $this->prepareErrorPage( $this->msg( 'internalerror' ) );
2893
2894 $this->addHTML( $message );
2895 }
2896
2905 public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2906 $linkRenderer = MediaWikiServices::getInstance()
2907 ->getLinkRendererFactory()->createFromLegacyOptions( $options );
2908 $link = $this->msg( 'returnto' )->rawParams(
2909 $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped();
2910 $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2911 }
2912
2921 public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2922 if ( $returnto == null ) {
2923 $returnto = $this->getRequest()->getText( 'returnto' );
2924 }
2925
2926 if ( $returntoquery == null ) {
2927 $returntoquery = $this->getRequest()->getText( 'returntoquery' );
2928 }
2929
2930 if ( $returnto === '' ) {
2931 $returnto = Title::newMainPage();
2932 }
2933
2934 if ( is_object( $returnto ) ) {
2935 $titleObj = $returnto;
2936 } else {
2937 $titleObj = Title::newFromText( $returnto );
2938 }
2939 // We don't want people to return to external interwiki. That
2940 // might potentially be used as part of a phishing scheme
2941 if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
2942 $titleObj = Title::newMainPage();
2943 }
2944
2945 $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
2946 }
2947
2948 private function getRlClientContext() {
2949 if ( !$this->rlClientContext ) {
2951 [], // modules; not relevant
2952 $this->getLanguage()->getCode(),
2953 $this->getSkin()->getSkinName(),
2954 $this->getUser()->isRegistered() ? $this->getUser()->getName() : null,
2955 null, // version; not relevant
2957 null, // only; not relevant
2958 $this->isPrintable(),
2959 $this->getRequest()->getBool( 'handheld' )
2960 );
2961 $this->rlClientContext = new ResourceLoaderContext(
2962 $this->getResourceLoader(),
2963 new FauxRequest( $query )
2964 );
2965 if ( $this->contentOverrideCallbacks ) {
2966 $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
2967 $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) {
2968 foreach ( $this->contentOverrideCallbacks as $callback ) {
2969 $content = $callback( $title );
2970 if ( $content !== null ) {
2971 $text = ContentHandler::getContentText( $content );
2972 if ( strpos( $text, '</script>' ) !== false ) {
2973 // Proactively replace this so that we can display a message
2974 // to the user, instead of letting it go to Html::inlineScript(),
2975 // where it would be considered a server-side issue.
2976 $titleFormatted = $title->getPrefixedText();
2978 Xml::encodeJsCall( 'mw.log.error', [
2979 "Cannot preview $titleFormatted due to script-closing tag."
2980 ] )
2981 );
2982 }
2983 return $content;
2984 }
2985 }
2986 return null;
2987 } );
2988 }
2989 }
2990 return $this->rlClientContext;
2991 }
2992
3004 public function getRlClient() {
3005 if ( !$this->rlClient ) {
3006 $context = $this->getRlClientContext();
3007 $rl = $this->getResourceLoader();
3008 $this->addModules( [
3009 'user',
3010 'user.options',
3011 ] );
3012 $this->addModuleStyles( [
3013 'site.styles',
3014 'noscript',
3015 'user.styles',
3016 ] );
3017 $this->getSkin()->doSetupSkinUserCss( $this );
3018
3019 // Prepare exempt modules for buildExemptModules()
3020 $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
3021 $exemptStates = [];
3022 $moduleStyles = $this->getModuleStyles( /*filter*/ true );
3023
3024 // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
3025 // Separate user-specific batch for improved cache-hit ratio.
3026 $userBatch = [ 'user.styles', 'user' ];
3027 $siteBatch = array_diff( $moduleStyles, $userBatch );
3028 $dbr = wfGetDB( DB_REPLICA );
3029 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $siteBatch );
3030 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
3031
3032 // Filter out modules handled by buildExemptModules()
3033 $moduleStyles = array_filter( $moduleStyles,
3034 static function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
3035 $module = $rl->getModule( $name );
3036 if ( $module ) {
3037 $group = $module->getGroup();
3038 if ( isset( $exemptGroups[$group] ) ) {
3039 $exemptStates[$name] = 'ready';
3040 if ( !$module->isKnownEmpty( $context ) ) {
3041 // E.g. Don't output empty <styles>
3042 $exemptGroups[$group][] = $name;
3043 }
3044 return false;
3045 }
3046 }
3047 return true;
3048 }
3049 );
3050 $this->rlExemptStyleModules = $exemptGroups;
3051
3052 $rlClient = new ResourceLoaderClientHtml( $context, [
3053 'target' => $this->getTarget(),
3054 'nonce' => $this->CSP->getNonce(),
3055 // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
3056 // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
3057 // modules enqueud for loading on this page is filtered to just those.
3058 // However, to make sure we also apply the restriction to dynamic dependencies and
3059 // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
3060 // StartupModule so that the client-side registry will not contain any restricted
3061 // modules either. (T152169, T185303)
3062 'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
3063 <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
3064 ) ? '1' : null,
3065 ] );
3066 $rlClient->setConfig( $this->getJSVars() );
3067 $rlClient->setModules( $this->getModules( /*filter*/ true ) );
3068 $rlClient->setModuleStyles( $moduleStyles );
3069 $rlClient->setExemptStates( $exemptStates );
3070 $this->rlClient = $rlClient;
3071 }
3072 return $this->rlClient;
3073 }
3074
3080 public function headElement( Skin $sk, $includeStyle = true ) {
3081 $config = $this->getConfig();
3082 $userdir = $this->getLanguage()->getDir();
3083 $services = MediaWikiServices::getInstance();
3084 $sitedir = $services->getContentLanguage()->getDir();
3085
3086 $pieces = [];
3087 $htmlAttribs = Sanitizer::mergeAttributes( Sanitizer::mergeAttributes(
3088 $this->getRlClient()->getDocumentAttributes(),
3090 ), [ 'class' => implode( ' ', $this->mAdditionalHtmlClasses ) ] );
3091 $pieces[] = Html::htmlHeader( $htmlAttribs );
3092 $pieces[] = Html::openElement( 'head' );
3093
3094 if ( $this->getHTMLTitle() == '' ) {
3095 $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3096 }
3097
3098 if ( !Html::isXmlMimeType( $config->get( 'MimeType' ) ) ) {
3099 // Add <meta charset="UTF-8">
3100 // This should be before <title> since it defines the charset used by
3101 // text including the text inside <title>.
3102 // The spec recommends defining XHTML5's charset using the XML declaration
3103 // instead of meta.
3104 // Our XML declaration is output by Html::htmlHeader.
3105 // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
3106 // https://html.spec.whatwg.org/multipage/semantics.html#charset
3107 $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
3108 }
3109
3110 $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
3111 $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null );
3112 $pieces[] = $this->buildExemptModules();
3113 $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
3114 $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
3115
3116 $pieces[] = Html::closeElement( 'head' );
3117
3118 $bodyClasses = $this->mAdditionalBodyClasses;
3119 $bodyClasses[] = 'mediawiki';
3120
3121 # Classes for LTR/RTL directionality support
3122 $bodyClasses[] = $userdir;
3123 $bodyClasses[] = "sitedir-$sitedir";
3124
3125 $underline = $services->getUserOptionsLookup()->getOption( $this->getUser(), 'underline' );
3126 if ( $underline < 2 ) {
3127 // The following classes can be used here:
3128 // * mw-underline-always
3129 // * mw-underline-never
3130 $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' );
3131 }
3132
3133 if ( $this->getLanguage()->capitalizeAllNouns() ) {
3134 # A <body> class is probably not the best way to do this . . .
3135 $bodyClasses[] = 'capitalize-all-nouns';
3136 }
3137
3138 // Parser feature migration class
3139 // The idea is that this will eventually be removed, after the wikitext
3140 // which requires it is cleaned up.
3141 $bodyClasses[] = 'mw-hide-empty-elt';
3142
3143 $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
3144 $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
3145 $bodyClasses[] =
3146 'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
3147
3148 if ( $sk->isResponsive() ) {
3149 $bodyClasses[] = 'skin--responsive';
3150 }
3151
3152 $bodyAttrs = [];
3153 // While the implode() is not strictly needed, it's used for backwards compatibility
3154 // (this used to be built as a string and hooks likely still expect that).
3155 $bodyAttrs['class'] = implode( ' ', $bodyClasses );
3156
3157 // Allow skins and extensions to add body attributes they need
3158 // Get ones from deprecated method
3159 if ( method_exists( $sk, 'addToBodyAttributes' ) ) {
3161 $sk->addToBodyAttributes( $this, $bodyAttrs );
3162 wfDeprecated( 'Skin::addToBodyAttributes method to add body attributes', '1.35' );
3163 }
3164
3165 // Then run the hook, the recommended way of adding body attributes now
3166 $this->getHookRunner()->onOutputPageBodyAttributes( $this, $sk, $bodyAttrs );
3167
3168 $pieces[] = Html::openElement( 'body', $bodyAttrs );
3169
3170 return self::combineWrappedStrings( $pieces );
3171 }
3172
3178 public function getResourceLoader() {
3179 if ( $this->mResourceLoader === null ) {
3180 // Lazy-initialise as needed
3181 $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
3182 }
3183 return $this->mResourceLoader;
3184 }
3185
3194 public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
3195 // Apply 'target' and 'origin' filters
3196 $modules = $this->filterModules( (array)$modules, null, $only );
3197
3199 $this->getRlClientContext(),
3200 $modules,
3201 $only,
3202 $extraQuery,
3203 $this->CSP->getNonce()
3204 );
3205 }
3206
3213 protected static function combineWrappedStrings( array $chunks ) {
3214 // Filter out empty values
3215 $chunks = array_filter( $chunks, 'strlen' );
3216 return WrappedString::join( "\n", $chunks );
3217 }
3218
3225 public function getBottomScripts() {
3226 $chunks = [];
3227 $chunks[] = $this->getRlClient()->getBodyHtml();
3228
3229 // Legacy non-ResourceLoader scripts
3230 $chunks[] = $this->mScripts;
3231
3232 if ( $this->limitReportJSData ) {
3235 [ 'wgPageParseReport' => $this->limitReportJSData ]
3236 ),
3237 $this->CSP->getNonce()
3238 );
3239 }
3240
3241 return self::combineWrappedStrings( $chunks );
3242 }
3243
3250 public function getJsConfigVars() {
3251 return $this->mJsConfigVars;
3252 }
3253
3260 public function addJsConfigVars( $keys, $value = null ) {
3261 if ( is_array( $keys ) ) {
3262 foreach ( $keys as $key => $value ) {
3263 $this->mJsConfigVars[$key] = $value;
3264 }
3265 return;
3266 }
3267
3268 $this->mJsConfigVars[$keys] = $value;
3269 }
3270
3280 public function getJSVars() {
3281 $curRevisionId = 0;
3282 $articleId = 0;
3283 $canonicalSpecialPageName = false; # T23115
3284 $services = MediaWikiServices::getInstance();
3285
3286 $title = $this->getTitle();
3287 $ns = $title->getNamespace();
3288 $nsInfo = $services->getNamespaceInfo();
3289 $canonicalNamespace = $nsInfo->exists( $ns )
3290 ? $nsInfo->getCanonicalName( $ns )
3291 : $title->getNsText();
3292
3293 $sk = $this->getSkin();
3294 // Get the relevant title so that AJAX features can use the correct page name
3295 // when making API requests from certain special pages (T36972).
3296 $relevantTitle = $sk->getRelevantTitle();
3297
3298 if ( $ns === NS_SPECIAL ) {
3299 list( $canonicalSpecialPageName, /*...*/ ) =
3300 $services->getSpecialPageFactory()->
3301 resolveAlias( $title->getDBkey() );
3302 } elseif ( $this->canUseWikiPage() ) {
3303 $wikiPage = $this->getWikiPage();
3304 $curRevisionId = $wikiPage->getLatest();
3305 $articleId = $wikiPage->getId();
3306 }
3307
3308 $lang = $title->getPageViewLanguage();
3309
3310 // Pre-process information
3311 $separatorTransTable = $lang->separatorTransformTable();
3312 $separatorTransTable = $separatorTransTable ?: [];
3313 $compactSeparatorTransTable = [
3314 implode( "\t", array_keys( $separatorTransTable ) ),
3315 implode( "\t", $separatorTransTable ),
3316 ];
3317 $digitTransTable = $lang->digitTransformTable();
3318 $digitTransTable = $digitTransTable ?: [];
3319 $compactDigitTransTable = [
3320 implode( "\t", array_keys( $digitTransTable ) ),
3321 implode( "\t", $digitTransTable ),
3322 ];
3323
3324 $user = $this->getUser();
3325
3326 // Internal variables for MediaWiki core
3327 $vars = [
3328 // @internal For mediawiki.page.ready
3329 'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3330
3331 // @internal For jquery.tablesorter
3332 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3333 'wgDigitTransformTable' => $compactDigitTransTable,
3334 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3335 'wgMonthNames' => $lang->getMonthNamesArray(),
3336
3337 // @internal For debugging purposes
3338 'wgRequestId' => WebRequest::getRequestId(),
3339
3340 // @internal For mw.loader
3341 'wgCSPNonce' => $this->CSP->getNonce(),
3342 ];
3343
3344 // Start of supported and stable config vars (for use by extensions/gadgets).
3345 $vars += [
3346 'wgCanonicalNamespace' => $canonicalNamespace,
3347 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3348 'wgNamespaceNumber' => $title->getNamespace(),
3349 'wgPageName' => $title->getPrefixedDBkey(),
3350 'wgTitle' => $title->getText(),
3351 'wgCurRevisionId' => $curRevisionId,
3352 'wgRevisionId' => (int)$this->getRevisionId(),
3353 'wgArticleId' => $articleId,
3354 'wgIsArticle' => $this->isArticle(),
3355 'wgIsRedirect' => $title->isRedirect(),
3356 'wgAction' => Action::getActionName( $this->getContext() ),
3357 'wgUserName' => $user->isAnon() ? null : $user->getName(),
3358 'wgUserGroups' => $services->getUserGroupManager()->getUserEffectiveGroups( $user ),
3359 'wgCategories' => $this->getCategories(),
3360 'wgPageContentLanguage' => $lang->getCode(),
3361 'wgPageContentModel' => $title->getContentModel(),
3362 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3363 'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3364 ];
3365 if ( $user->isRegistered() ) {
3366 $vars['wgUserId'] = $user->getId();
3367 $vars['wgUserEditCount'] = $user->getEditCount();
3368 $userReg = $user->getRegistration();
3369 $vars['wgUserRegistration'] = $userReg ? (int)wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3370 // Get the revision ID of the oldest new message on the user's talk
3371 // page. This can be used for constructing new message alerts on
3372 // the client side.
3373 $userNewMsgRevId = $this->getLastSeenUserTalkRevId();
3374 // Only occupy precious space in the <head> when it is non-null (T53640)
3375 // mw.config.get returns null by default.
3376 if ( $userNewMsgRevId ) {
3377 $vars['wgUserNewMsgRevisionId'] = $userNewMsgRevId;
3378 }
3379 }
3380 $languageConverter = $services->getLanguageConverterFactory()
3381 ->getLanguageConverter( $services->getContentLanguage() );
3382 if ( $languageConverter->hasVariants() ) {
3383 $vars['wgUserVariant'] = $languageConverter->getPreferredVariant();
3384 }
3385 // Same test as SkinTemplate
3386 $vars['wgIsProbablyEditable'] = $this->performerCanEditOrCreate( $this->getAuthority(), $title );
3387 $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle &&
3388 $this->performerCanEditOrCreate( $this->getAuthority(), $relevantTitle );
3389 foreach ( $title->getRestrictionTypes() as $type ) {
3390 // Following keys are set in $vars:
3391 // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
3392 $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3393 }
3394 if ( $title->isMainPage() ) {
3395 $vars['wgIsMainPage'] = true;
3396 }
3397
3398 $relevantUser = $sk->getRelevantUser();
3399 if ( $relevantUser ) {
3400 $vars['wgRelevantUserName'] = $relevantUser->getName();
3401 }
3402 // End of stable config vars
3403
3404 if ( $this->mRedirectedFrom ) {
3405 // @internal For skin JS
3406 $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3407 }
3408
3409 // Allow extensions to add their custom variables to the mw.config map.
3410 // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3411 // page-dependent but site-wide (without state).
3412 // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3413 $this->getHookRunner()->onMakeGlobalVariablesScript( $vars, $this );
3414
3415 // Merge in variables from addJsConfigVars last
3416 return array_merge( $vars, $this->getJsConfigVars() );
3417 }
3418
3424 private function getLastSeenUserTalkRevId() {
3425 $services = MediaWikiServices::getInstance();
3426 $user = $this->getUser();
3427 $userHasNewMessages = $services
3428 ->getTalkPageNotificationManager()
3429 ->userHasNewMessages( $user );
3430 if ( !$userHasNewMessages ) {
3431 return null;
3432 }
3433
3434 $timestamp = $services
3435 ->getTalkPageNotificationManager()
3436 ->getLatestSeenMessageTimestamp( $user );
3437
3438 if ( !$timestamp ) {
3439 return null;
3440 }
3441
3442 $revRecord = $services->getRevisionLookup()->getRevisionByTimestamp(
3443 $user->getTalkPage(),
3444 $timestamp
3445 );
3446
3447 if ( !$revRecord ) {
3448 return null;
3449 }
3450
3451 return $revRecord->getId();
3452 }
3453
3463 public function userCanPreview() {
3464 $request = $this->getRequest();
3465 if (
3466 $request->getVal( 'action' ) !== 'submit' ||
3467 !$request->wasPosted()
3468 ) {
3469 return false;
3470 }
3471
3472 $user = $this->getUser();
3473
3474 if ( !$user->isRegistered() ) {
3475 // Anons have predictable edit tokens
3476 return false;
3477 }
3478 if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3479 return false;
3480 }
3481
3482 $title = $this->getTitle();
3483 if ( !$this->getAuthority()->probablyCan( 'edit', $title ) ) {
3484 return false;
3485 }
3486
3487 return true;
3488 }
3489
3496 Authority $performer,
3497 PageIdentity $page
3498 ) {
3499 return $performer->probablyCan( 'edit', $page )
3500 && ( $page->exists() || $performer->probablyCan( 'create', $page ) );
3501 }
3502
3506 public function getHeadLinksArray() {
3507 $tags = [];
3508 $config = $this->getConfig();
3509
3510 $canonicalUrl = $this->mCanonicalUrl;
3511
3512 $tags['meta-generator'] = Html::element( 'meta', [
3513 'name' => 'generator',
3514 'content' => 'MediaWiki ' . MW_VERSION,
3515 ] );
3516
3517 if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3518 // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
3519 // fallbacks should come before the primary value so we need to reverse the array.
3520 foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
3521 $tags["meta-referrer-$i"] = Html::element( 'meta', [
3522 'name' => 'referrer',
3523 'content' => $policy,
3524 ] );
3525 }
3526 }
3527
3528 $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3529 if ( $p !== 'index,follow' ) {
3530 // http://www.robotstxt.org/wc/meta-user.html
3531 // Only show if it's different from the default robots policy
3532 $tags['meta-robots'] = Html::element( 'meta', [
3533 'name' => 'robots',
3534 'content' => $p,
3535 ] );
3536 }
3537
3538 foreach ( $this->mMetatags as $tag ) {
3539 if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) {
3540 $a = 'http-equiv';
3541 $tag[0] = substr( $tag[0], 5 );
3542 } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) {
3543 $a = 'property';
3544 } else {
3545 $a = 'name';
3546 }
3547 $tagName = "meta-{$tag[0]}";
3548 if ( isset( $tags[$tagName] ) ) {
3549 $tagName .= $tag[1];
3550 }
3551 $tags[$tagName] = Html::element( 'meta',
3552 [
3553 $a => $tag[0],
3554 'content' => $tag[1]
3555 ]
3556 );
3557 }
3558
3559 foreach ( $this->mLinktags as $tag ) {
3560 $tags[] = Html::element( 'link', $tag );
3561 }
3562
3563 # Universal edit button
3564 if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3565 if ( $this->performerCanEditOrCreate( $this->getAuthority(), $this->getTitle() ) ) {
3566 // Original UniversalEditButton
3567 $msg = $this->msg( 'edit' )->text();
3568 $tags['universal-edit-button'] = Html::element( 'link', [
3569 'rel' => 'alternate',
3570 'type' => 'application/x-wiki',
3571 'title' => $msg,
3572 'href' => $this->getTitle()->getEditURL(),
3573 ] );
3574 // Alternate edit link
3575 $tags['alternative-edit'] = Html::element( 'link', [
3576 'rel' => 'edit',
3577 'title' => $msg,
3578 'href' => $this->getTitle()->getEditURL(),
3579 ] );
3580 }
3581 }
3582
3583 # Generally the order of the favicon and apple-touch-icon links
3584 # should not matter, but Konqueror (3.5.9 at least) incorrectly
3585 # uses whichever one appears later in the HTML source. Make sure
3586 # apple-touch-icon is specified first to avoid this.
3587 if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3588 $tags['apple-touch-icon'] = Html::element( 'link', [
3589 'rel' => 'apple-touch-icon',
3590 'href' => $config->get( 'AppleTouchIcon' )
3591 ] );
3592 }
3593
3594 if ( $config->get( 'Favicon' ) !== false ) {
3595 $tags['favicon'] = Html::element( 'link', [
3596 'rel' => 'shortcut icon',
3597 'href' => $config->get( 'Favicon' )
3598 ] );
3599 }
3600
3601 # OpenSearch description link
3602 $tags['opensearch'] = Html::element( 'link', [
3603 'rel' => 'search',
3604 'type' => 'application/opensearchdescription+xml',
3605 'href' => wfScript( 'opensearch_desc' ),
3606 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3607 ] );
3608
3609 # Real Simple Discovery link, provides auto-discovery information
3610 # for the MediaWiki API (and potentially additional custom API
3611 # support such as WordPress or Twitter-compatible APIs for a
3612 # blogging extension, etc)
3613 $tags['rsd'] = Html::element( 'link', [
3614 'rel' => 'EditURI',
3615 'type' => 'application/rsd+xml',
3616 // Output a protocol-relative URL here if $wgServer is protocol-relative.
3617 // Whether RSD accepts relative or protocol-relative URLs is completely
3618 // undocumented, though.
3619 'href' => wfExpandUrl( wfAppendQuery(
3620 wfScript( 'api' ),
3621 [ 'action' => 'rsd' ] ),
3623 ),
3624 ] );
3625
3626 # Language variants
3627 $services = MediaWikiServices::getInstance();
3628 $languageConverterFactory = $services->getLanguageConverterFactory();
3629 $disableLangConversion = $languageConverterFactory->isConversionDisabled();
3630 if ( !$disableLangConversion ) {
3631 $lang = $this->getTitle()->getPageLanguage();
3632 $languageConverter = $languageConverterFactory->getLanguageConverter( $lang );
3633 if ( $languageConverter->hasVariants() ) {
3634 $variants = $languageConverter->getVariants();
3635 foreach ( $variants as $variant ) {
3636 $tags["variant-$variant"] = Html::element( 'link', [
3637 'rel' => 'alternate',
3638 'hreflang' => LanguageCode::bcp47( $variant ),
3639 'href' => $this->getTitle()->getLocalURL(
3640 [ 'variant' => $variant ] )
3641 ]
3642 );
3643 }
3644 # x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3645 $tags["variant-x-default"] = Html::element( 'link', [
3646 'rel' => 'alternate',
3647 'hreflang' => 'x-default',
3648 'href' => $this->getTitle()->getLocalURL() ] );
3649 }
3650 }
3651
3652 # Copyright
3653 if ( $this->copyrightUrl !== null ) {
3654 $copyright = $this->copyrightUrl;
3655 } else {
3656 $copyright = '';
3657 if ( $config->get( 'RightsPage' ) ) {
3658 $copy = Title::newFromText( $config->get( 'RightsPage' ) );
3659
3660 if ( $copy ) {
3661 $copyright = $copy->getLocalURL();
3662 }
3663 }
3664
3665 if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3666 $copyright = $config->get( 'RightsUrl' );
3667 }
3668 }
3669
3670 if ( $copyright ) {
3671 $tags['copyright'] = Html::element( 'link', [
3672 'rel' => 'license',
3673 'href' => $copyright ]
3674 );
3675 }
3676
3677 # Feeds
3678 if ( $config->get( 'Feed' ) ) {
3679 $feedLinks = [];
3680
3681 foreach ( $this->getSyndicationLinks() as $format => $link ) {
3682 # Use the page name for the title. In principle, this could
3683 # lead to issues with having the same name for different feeds
3684 # corresponding to the same page, but we can't avoid that at
3685 # this low a level.
3686
3687 $feedLinks[] = $this->feedLink(
3688 $format,
3689 $link,
3690 # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3691 $this->msg(
3692 "page-{$format}-feed", $this->getTitle()->getPrefixedText()
3693 )->text()
3694 );
3695 }
3696
3697 # Recent changes feed should appear on every page (except recentchanges,
3698 # that would be redundant). Put it after the per-page feed to avoid
3699 # changing existing behavior. It's still available, probably via a
3700 # menu in your browser. Some sites might have a different feed they'd
3701 # like to promote instead of the RC feed (maybe like a "Recent New Articles"
3702 # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3703 # If so, use it instead.
3704 $sitename = $config->get( 'Sitename' );
3705 $overrideSiteFeed = $config->get( 'OverrideSiteFeed' );
3706 if ( $overrideSiteFeed ) {
3707 foreach ( $overrideSiteFeed as $type => $feedUrl ) {
3708 // Note, this->feedLink escapes the url.
3709 $feedLinks[] = $this->feedLink(
3710 $type,
3711 $feedUrl,
3712 $this->msg( "site-{$type}-feed", $sitename )->text()
3713 );
3714 }
3715 } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3716 $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3717 foreach ( $this->getAdvertisedFeedTypes() as $format ) {
3718 $feedLinks[] = $this->feedLink(
3719 $format,
3720 $rctitle->getLocalURL( [ 'feed' => $format ] ),
3721 # For grep: 'site-rss-feed', 'site-atom-feed'
3722 $this->msg( "site-{$format}-feed", $sitename )->text()
3723 );
3724 }
3725 }
3726
3727 # Allow extensions to change the list pf feeds. This hook is primarily for changing,
3728 # manipulating or removing existing feed tags. If you want to add new feeds, you should
3729 # use OutputPage::addFeedLink() instead.
3730 $this->getHookRunner()->onAfterBuildFeedLinks( $feedLinks );
3731
3732 $tags += $feedLinks;
3733 }
3734
3735 # Canonical URL
3736 if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3737 if ( $canonicalUrl !== false ) {
3738 $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3739 } elseif ( $this->isArticleRelated() ) {
3740 // This affects all requests where "setArticleRelated" is true. This is
3741 // typically all requests that show content (query title, curid, oldid, diff),
3742 // and all wikipage actions (edit, delete, purge, info, history etc.).
3743 // It does not apply to File pages and Special pages.
3744 // 'history' and 'info' actions address page metadata rather than the page
3745 // content itself, so they may not be canonicalized to the view page url.
3746 // TODO: this ought to be better encapsulated in the Action class.
3747 $action = Action::getActionName( $this->getContext() );
3748 if ( in_array( $action, [ 'history', 'info' ] ) ) {
3749 $query = "action={$action}";
3750 } else {
3751 $query = '';
3752 }
3753 $canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3754 } else {
3755 $reqUrl = $this->getRequest()->getRequestURL();
3756 $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3757 }
3758 }
3759 if ( $canonicalUrl !== false ) {
3760 $tags[] = Html::element( 'link', [
3761 'rel' => 'canonical',
3762 'href' => $canonicalUrl
3763 ] );
3764 }
3765
3766 // Allow extensions to add, remove and/or otherwise manipulate these links
3767 // If you want only to *add* <head> links, please use the addHeadItem()
3768 // (or addHeadItems() for multiple items) method instead.
3769 // This hook is provided as a last resort for extensions to modify these
3770 // links before the output is sent to client.
3771 $this->getHookRunner()->onOutputPageAfterGetHeadLinksArray( $tags, $this );
3772
3773 return $tags;
3774 }
3775
3784 private function feedLink( $type, $url, $text ) {
3785 return Html::element( 'link', [
3786 'rel' => 'alternate',
3787 'type' => "application/$type+xml",
3788 'title' => $text,
3789 'href' => $url ]
3790 );
3791 }
3792
3802 public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3803 $options = [];
3804 if ( $media ) {
3805 $options['media'] = $media;
3806 }
3807 if ( $condition ) {
3808 $options['condition'] = $condition;
3809 }
3810 if ( $dir ) {
3811 $options['dir'] = $dir;
3812 }
3813 $this->styles[$style] = $options;
3814 }
3815
3823 public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3824 if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3825 # If wanted, and the interface is right-to-left, flip the CSS
3826 $style_css = CSSJanus::transform( $style_css, true, false );
3827 }
3828 $this->mInlineStyles .= Html::inlineStyle( $style_css );
3829 }
3830
3836 protected function buildExemptModules() {
3837 $chunks = [];
3838
3839 // Requirements:
3840 // - Within modules provided by the software (core, skin, extensions),
3841 // styles from skin stylesheets should be overridden by styles
3842 // from modules dynamically loaded with JavaScript.
3843 // - Styles from site-specific, private, and user modules should override
3844 // both of the above.
3845 //
3846 // The effective order for stylesheets must thus be:
3847 // 1. Page style modules, formatted server-side by ResourceLoaderClientHtml.
3848 // 2. Dynamically-loaded styles, inserted client-side by mw.loader.
3849 // 3. Styles that are site-specific, private or from the user, formatted
3850 // server-side by this function.
3851 //
3852 // The 'ResourceLoaderDynamicStyles' marker helps JavaScript know where
3853 // point #2 is.
3854
3855 // Add legacy styles added through addStyle()/addInlineStyle() here
3856 $chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3857
3858 // Things that go after the ResourceLoaderDynamicStyles marker
3859 $append = [];
3860 $separateReq = [ 'site.styles', 'user.styles' ];
3861 foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3862 if ( $moduleNames ) {
3863 $append[] = $this->makeResourceLoaderLink(
3864 array_diff( $moduleNames, $separateReq ),
3865 ResourceLoaderModule::TYPE_STYLES
3866 );
3867
3868 foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) {
3869 // These require their own dedicated request in order to support "@import"
3870 // syntax, which is incompatible with concatenation. (T147667, T37562)
3871 $append[] = $this->makeResourceLoaderLink( $name,
3872 ResourceLoaderModule::TYPE_STYLES
3873 );
3874 }
3875 }
3876 }
3877 if ( $append ) {
3878 $chunks[] = Html::element(
3879 'meta',
3880 [ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3881 );
3882 $chunks = array_merge( $chunks, $append );
3883 }
3884
3885 return self::combineWrappedStrings( $chunks );
3886 }
3887
3891 public function buildCssLinksArray() {
3892 $links = [];
3893
3894 foreach ( $this->styles as $file => $options ) {
3895 $link = $this->styleLink( $file, $options );
3896 if ( $link ) {
3897 $links[$file] = $link;
3898 }
3899 }
3900 return $links;
3901 }
3902
3910 protected function styleLink( $style, array $options ) {
3911 if ( isset( $options['dir'] ) && $this->getLanguage()->getDir() != $options['dir'] ) {
3912 return '';
3913 }
3914
3915 if ( isset( $options['media'] ) ) {
3916 $media = self::transformCssMedia( $options['media'] );
3917 if ( $media === null ) {
3918 return '';
3919 }
3920 } else {
3921 $media = 'all';
3922 }
3923
3924 if ( substr( $style, 0, 1 ) == '/' ||
3925 substr( $style, 0, 5 ) == 'http:' ||
3926 substr( $style, 0, 6 ) == 'https:' ) {
3927 $url = $style;
3928 } else {
3929 $config = $this->getConfig();
3930 // Append file hash as query parameter
3931 $url = self::transformResourcePath(
3932 $config,
3933 $config->get( 'StylePath' ) . '/' . $style
3934 );
3935 }
3936
3937 $link = Html::linkedStyle( $url, $media );
3938
3939 if ( isset( $options['condition'] ) ) {
3940 $condition = htmlspecialchars( $options['condition'] );
3941 $link = "<!--[if $condition]>$link<![endif]-->";
3942 }
3943 return $link;
3944 }
3945
3967 public static function transformResourcePath( Config $config, $path ) {
3968 global $IP;
3969
3970 $localDir = $IP;
3971 $remotePathPrefix = $config->get( 'ResourceBasePath' );
3972 if ( $remotePathPrefix === '' ) {
3973 // The configured base path is required to be empty string for
3974 // wikis in the domain root
3975 $remotePath = '/';
3976 } else {
3977 $remotePath = $remotePathPrefix;
3978 }
3979 if ( strpos( $path, $remotePath ) !== 0 || substr( $path, 0, 2 ) === '//' ) {
3980 // - Path is outside wgResourceBasePath, ignore.
3981 // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib.
3982 return $path;
3983 }
3984 // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here.
3985 // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth
3986 // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath)
3987 // which is not expected to be in wgResourceBasePath on CDNs. (T155146)
3988 $uploadPath = $config->get( 'UploadPath' );
3989 if ( strpos( $path, $uploadPath ) === 0 ) {
3990 $localDir = $config->get( 'UploadDirectory' );
3991 $remotePathPrefix = $remotePath = $uploadPath;
3992 }
3993
3994 $path = RelPath::getRelativePath( $path, $remotePath );
3995 return self::transformFilePath( $remotePathPrefix, $localDir, $path );
3996 }
3997
4009 public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
4010 // This MUST match the equivalent logic in CSSMin::remapOne()
4011 $hash = md5_file( "$localPath/$file" );
4012 if ( $hash === false ) {
4013 wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
4014 $hash = '';
4015 }
4016 return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
4017 }
4018
4026 public static function transformCssMedia( $media ) {
4027 global $wgRequest;
4028
4029 // https://www.w3.org/TR/css3-mediaqueries/#syntax
4030 $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
4031
4032 // Switch in on-screen display for media testing
4033 $switches = [
4034 'printable' => 'print',
4035 'handheld' => 'handheld',
4036 ];
4037 foreach ( $switches as $switch => $targetMedia ) {
4038 if ( $wgRequest->getBool( $switch ) ) {
4039 if ( $media == $targetMedia ) {
4040 $media = '';
4041 } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
4042 /* This regex will not attempt to understand a comma-separated media_query_list
4043 *
4044 * Example supported values for $media:
4045 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
4046 * Example NOT supported value for $media:
4047 * '3d-glasses, screen, print and resolution > 90dpi'
4048 *
4049 * If it's a print request, we never want any kind of screen stylesheets
4050 * If it's a handheld request (currently the only other choice with a switch),
4051 * we don't want simple 'screen' but we might want screen queries that
4052 * have a max-width or something, so we'll pass all others on and let the
4053 * client do the query.
4054 */
4055 if ( $targetMedia == 'print' || $media == 'screen' ) {
4056 return null;
4057 }
4058 }
4059 }
4060 }
4061
4062 return $media;
4063 }
4064
4073 public function addWikiMsg( ...$args ) {
4074 $name = array_shift( $args );
4075 $this->addWikiMsgArray( $name, $args );
4076 }
4077
4086 public function addWikiMsgArray( $name, $args ) {
4087 $this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
4088 }
4089
4116 public function wrapWikiMsg( $wrap, ...$msgSpecs ) {
4117 $s = $wrap;
4118 foreach ( $msgSpecs as $n => $spec ) {
4119 if ( is_array( $spec ) ) {
4120 $args = $spec;
4121 $name = array_shift( $args );
4122 } else {
4123 $args = [];
4124 $name = $spec;
4125 }
4126 $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
4127 }
4128 $this->addWikiTextAsInterface( $s );
4129 }
4130
4136 public function isTOCEnabled() {
4137 return $this->mEnableTOC;
4138 }
4139
4147 public static function setupOOUI( $skinName = 'default', $dir = 'ltr' ) {
4148 $themes = ResourceLoaderOOUIModule::getSkinThemeMap();
4149 $theme = $themes[$skinName] ?? $themes['default'];
4150 // For example, 'OOUI\WikimediaUITheme'.
4151 $themeClass = "OOUI\\{$theme}Theme";
4152 OOUI\Theme::setSingleton( new $themeClass() );
4153 OOUI\Element::setDefaultDir( $dir );
4154 }
4155
4162 public function enableOOUI() {
4163 self::setupOOUI(
4164 strtolower( $this->getSkin()->getSkinName() ),
4165 $this->getLanguage()->getDir()
4166 );
4167 $this->addModuleStyles( [
4168 'oojs-ui-core.styles',
4169 'oojs-ui.styles.indicators',
4170 'mediawiki.widgets.styles',
4171 'oojs-ui-core.icons',
4172 ] );
4173 }
4174
4185 public function getCSPNonce() {
4186 return $this->CSP->getNonce();
4187 }
4188
4195 public function getCSP() {
4196 return $this->CSP;
4197 }
4198}
getAuthority()
const PROTO_CANONICAL
Definition Defines.php:207
const PROTO_CURRENT
Definition Defines.php:206
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
const NS_SPECIAL
Definition Defines.php:53
const PROTO_RELATIVE
Definition Defines.php:205
const NS_CATEGORY
Definition Defines.php:78
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,...
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
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...
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
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)
Logs a warning that $function is deprecated.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
getContext()
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:682
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:87
$IP
Definition WebStart.php:49
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()
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:63
exists()
Returns true if file exists in the repository.
Definition File.php:966
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Content for JavaScript pages.
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A StatusValue for permission errors.
This serves as the entry point to the MediaWiki session handling system.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:136
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...
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.
getCdnCacheEpoch( $reqTime, $maxAge)
disable()
Disable output completely, i.e.
addLink(array $linkarr)
Add a new <link> tag to the page header.
addWikiMsg(... $args)
Add a wikitext-formatted message to the output.
ResourceLoader $mResourceLoader
ContentSecurityPolicy $CSP
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
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:".
$mFeedLinks
Handles the Atom / RSS links.
bool $mNewSectionLink
setCopyrightUrl( $url)
Set the copyright URL to send with the output.
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
formatPermissionStatus(PermissionStatus $status, string $action=null)
Format permission $status obtained from Authority for display.
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:
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.
considerCacheSettingsFinal()
Set the expectation that cache control will not change after this point.
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.
showLagWarning( $lag)
Show a warning about replica DB lag.
clearSubtitle()
Clear the subtitles.
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
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".
string[][] $mCategories
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.
bool $cacheIsFinal
See OutputPage::couldBePublicCached.
addModuleStyles( $modules)
Load the styles of one or more style-only 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.
addHtmlClasses( $classes)
Add a class to the <html> element.
performerCanEditOrCreate(Authority $performer, PageIdentity $page)
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.
bool $mEnableClientCache
Gwicke work on squid caching? Roughly from 2003.
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.
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...
getIndexPolicy()
Get the current index policy for the page as a string.
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.
parseInternal( $text, $title, $linestart, $interface)
Parse wikitext and return the HTML (internal implementation helper)
getFileSearchOptions()
Get the files used on this page.
array $mAdditionalHtmlClasses
Additional <html> classes; This should be rarely modified; prefer mAdditionalBodyClasses.
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.
getCSP()
Get the ContentSecurityPolicy object.
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,...
string[][] $mCategoryLinks
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.
parserOptions()
Get/set the ParserOptions object to use for wikitext parsing.
getCacheVaryCookies()
Get the list of cookie names that will influence the cache.
getLastSeenUserTalkRevId()
Get the revision ID for the last user talk page revision viewed by the talk page owner.
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[] $mIndicators
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...
getRobotPolicy()
Get the current robot policy for the page as a string in the form <index policy>,<follow policy>.
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.
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.
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.
getFollowPolicy()
Get the current follow policy for the page as a string.
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...
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.
int null $mRevisionId
To include the variable {{REVISIONID}}.
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.
couldBePublicCached()
Whether the output might become publicly cached.
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.
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.
getExtraCSPStyleSrcs()
Get extra Content-Security-Policy 'style-src' directives.
getExtraCSPScriptSrcs()
Get extra Content-Security-Policy 'script-src' directives.
getText( $options=[])
Get the output HTML.
getExtraCSPDefaultSrcs()
Get extra Content-Security-Policy 'default-src' directives.
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6307
Load and configure a ResourceLoader client on an HTML page.
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicitly load or embed modules on a page.
setConfig(array $vars)
Set mw.config variables.
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.
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
static inDebugMode()
Determine whether debug mode is on.
static makeConfigSetScript(array $configuration)
Return JS code which will set the MediaWiki configuration array to the given value.
static makeLoaderQuery(array $modules, $lang, $skin, $user=null, $version=null, $debug=ResourceLoaderContext::DEBUG_OFF, $only=null, $printable=false, $handheld=false, array $extraQuery=[])
Build a query array (array representation of query string) for load.php.
The main skin class which provides methods and properties for all other skins.
Definition Skin.php:42
getHtmlElementAttributes()
Return values for <html> element.
Definition Skin.php:558
isResponsive()
Indicates if this skin is responsive.
Definition Skin.php:204
getSkinName()
Definition Skin.php:191
getPageClasses( $title)
TODO: document.
Definition Skin.php:523
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Represents a title within MediaWiki.
Definition Title.php:48
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:69
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Base interface for content objects.
Definition Content.php:35
Interface for objects which can provide a MediaWiki context on request.
getConfig()
Get the site configuration.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
Interface for objects (potentially) representing an editable wiki page.
exists()
Checks if the page currently exists.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:35
probablyCan(string $action, PageIdentity $target, PermissionStatus $status=null)
Checks whether this authority can probably perform the given action on the given target page.
Result wrapper for grabbing data queried from an IDatabase object.
if( $line===false) $args
Definition mcc.php:124
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:76
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