MediaWiki REL1_37
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( $target, Content $content ) {
616 if ( !$this->contentOverrides ) {
617 // Register a callback for $this->contentOverrides on the first call
618 $this->addContentOverrideCallback( function ( $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( PageReference $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( PageReference $t ) {
1054 $t = Title::castFromPageReference( $t );
1055
1056 // @phan-suppress-next-next-line PhanUndeclaredMethod
1057 // @fixme Not all implementations of IContextSource have this method!
1058 $this->getContext()->setTitle( $t );
1059 }
1060
1066 public function setSubtitle( $str ) {
1067 $this->clearSubtitle();
1068 $this->addSubtitle( $str );
1069 }
1070
1076 public function addSubtitle( $str ) {
1077 if ( $str instanceof Message ) {
1078 $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1079 } else {
1080 $this->mSubtitle[] = $str;
1081 }
1082 }
1083
1092 public static function buildBacklinkSubtitle( PageReference $page, $query = [] ) {
1093 if ( $page instanceof PageRecord || $page instanceof Title ) {
1094 // Callers will typically have a PageRecord
1095 if ( $page->isRedirect() ) {
1096 $query['redirect'] = 'no';
1097 }
1098 } elseif ( $page->getNamespace() !== NS_SPECIAL ) {
1099 // We don't know whether it's a redirect, so add the parameter, just to be sure.
1100 $query['redirect'] = 'no';
1101 }
1102
1103 $target = TitleValue::castPageToLinkTarget( $page );
1104 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1105 return wfMessage( 'backlinksubtitle' )
1106 ->rawParams( $linkRenderer->makeLink( $target, null, [], $query ) );
1107 }
1108
1115 public function addBacklinkSubtitle( PageReference $title, $query = [] ) {
1116 $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1117 }
1118
1122 public function clearSubtitle() {
1123 $this->mSubtitle = [];
1124 }
1125
1129 public function getSubtitle() {
1130 return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1131 }
1132
1137 public function setPrintable() {
1138 $this->mPrintable = true;
1139 }
1140
1146 public function isPrintable() {
1147 return $this->mPrintable;
1148 }
1149
1153 public function disable() {
1154 $this->mDoNothing = true;
1155 }
1156
1162 public function isDisabled() {
1163 return $this->mDoNothing;
1164 }
1165
1171 public function showNewSectionLink() {
1172 return $this->mNewSectionLink;
1173 }
1174
1180 public function forceHideNewSectionLink() {
1181 return $this->mHideNewSectionLink;
1182 }
1183
1192 public function setSyndicated( $show = true ) {
1193 if ( $show ) {
1194 $this->setFeedAppendQuery( false );
1195 } else {
1196 $this->mFeedLinks = [];
1197 }
1198 }
1199
1206 protected function getAdvertisedFeedTypes() {
1207 if ( $this->getConfig()->get( 'Feed' ) ) {
1208 return $this->getConfig()->get( 'AdvertisedFeedTypes' );
1209 } else {
1210 return [];
1211 }
1212 }
1213
1223 public function setFeedAppendQuery( $val ) {
1224 $this->mFeedLinks = [];
1225
1226 foreach ( $this->getAdvertisedFeedTypes() as $type ) {
1227 $query = "feed=$type";
1228 if ( is_string( $val ) ) {
1229 $query .= '&' . $val;
1230 }
1231 $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1232 }
1233 }
1234
1241 public function addFeedLink( $format, $href ) {
1242 if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) {
1243 $this->mFeedLinks[$format] = $href;
1244 }
1245 }
1246
1251 public function isSyndicated() {
1252 return count( $this->mFeedLinks ) > 0;
1253 }
1254
1259 public function getSyndicationLinks() {
1260 return $this->mFeedLinks;
1261 }
1262
1268 public function getFeedAppendQuery() {
1269 return $this->mFeedLinksAppendQuery;
1270 }
1271
1279 public function setArticleFlag( $newVal ) {
1280 $this->mIsArticle = $newVal;
1281 if ( $newVal ) {
1282 $this->mIsArticleRelated = $newVal;
1283 }
1284 }
1285
1292 public function isArticle() {
1293 return $this->mIsArticle;
1294 }
1295
1302 public function setArticleRelated( $newVal ) {
1303 $this->mIsArticleRelated = $newVal;
1304 if ( !$newVal ) {
1305 $this->mIsArticle = false;
1306 }
1307 }
1308
1314 public function isArticleRelated() {
1315 return $this->mIsArticleRelated;
1316 }
1317
1323 public function setCopyright( $hasCopyright ) {
1324 $this->mHasCopyright = $hasCopyright;
1325 }
1326
1336 public function showsCopyright() {
1337 return $this->isArticle() || $this->mHasCopyright;
1338 }
1339
1346 public function addLanguageLinks( array $newLinkArray ) {
1347 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1348 }
1349
1356 public function setLanguageLinks( array $newLinkArray ) {
1357 $this->mLanguageLinks = $newLinkArray;
1358 }
1359
1365 public function getLanguageLinks() {
1366 return $this->mLanguageLinks;
1367 }
1368
1374 public function addCategoryLinks( array $categories ) {
1375 if ( !$categories ) {
1376 return;
1377 }
1378
1379 $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1380
1381 # Set all the values to 'normal'.
1382 $categories = array_fill_keys( array_keys( $categories ), 'normal' );
1383
1384 # Mark hidden categories
1385 foreach ( $res as $row ) {
1386 if ( isset( $row->pp_value ) ) {
1387 $categories[$row->page_title] = 'hidden';
1388 }
1389 }
1390
1391 # Add the remaining categories to the skin
1392 if ( $this->getHookRunner()->onOutputPageMakeCategoryLinks(
1393 $this, $categories, $this->mCategoryLinks )
1394 ) {
1395 $services = MediaWikiServices::getInstance();
1396 $linkRenderer = $services->getLinkRenderer();
1397 $languageConverter = $services->getLanguageConverterFactory()
1398 ->getLanguageConverter( $services->getContentLanguage() );
1399 foreach ( $categories as $category => $type ) {
1400 // array keys will cast numeric category names to ints, so cast back to string
1401 $category = (string)$category;
1402 $origcategory = $category;
1403 $title = Title::makeTitleSafe( NS_CATEGORY, $category );
1404 if ( !$title ) {
1405 continue;
1406 }
1407 $languageConverter->findVariantLink( $category, $title, true );
1408
1409 if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1410 continue;
1411 }
1412 $text = $languageConverter->convertHtml( $title->getText() );
1413 $this->mCategories[$type][] = $title->getText();
1414 $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) );
1415 }
1416 }
1417 }
1418
1423 protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1424 # Add the links to a LinkBatch
1425 $arr = [ NS_CATEGORY => $categories ];
1426 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
1427 $lb = $linkBatchFactory->newLinkBatch();
1428 $lb->setArray( $arr );
1429
1430 # Fetch existence plus the hiddencat property
1431 $dbr = wfGetDB( DB_REPLICA );
1432 $fields = array_merge(
1433 LinkCache::getSelectFields(),
1434 [ 'page_namespace', 'page_title', 'pp_value' ]
1435 );
1436
1437 $res = $dbr->select( [ 'page', 'page_props' ],
1438 $fields,
1439 $lb->constructSet( 'page', $dbr ),
1440 __METHOD__,
1441 [],
1442 [ 'page_props' => [ 'LEFT JOIN', [
1443 'pp_propname' => 'hiddencat',
1444 'pp_page = page_id'
1445 ] ] ]
1446 );
1447
1448 # Add the results to the link cache
1449 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1450 $lb->addResultToCache( $linkCache, $res );
1451
1452 return $res;
1453 }
1454
1460 public function setCategoryLinks( array $categories ) {
1461 $this->mCategoryLinks = [];
1462 $this->addCategoryLinks( $categories );
1463 }
1464
1473 public function getCategoryLinks() {
1474 return $this->mCategoryLinks;
1475 }
1476
1486 public function getCategories( $type = 'all' ) {
1487 if ( $type === 'all' ) {
1488 $allCategories = [];
1489 foreach ( $this->mCategories as $categories ) {
1490 $allCategories = array_merge( $allCategories, $categories );
1491 }
1492 return $allCategories;
1493 }
1494 if ( !isset( $this->mCategories[$type] ) ) {
1495 throw new InvalidArgumentException( 'Invalid category type given: ' . $type );
1496 }
1497 return $this->mCategories[$type];
1498 }
1499
1509 public function setIndicators( array $indicators ) {
1510 $this->mIndicators = $indicators + $this->mIndicators;
1511 // Keep ordered by key
1512 ksort( $this->mIndicators );
1513 }
1514
1523 public function getIndicators() {
1524 return $this->mIndicators;
1525 }
1526
1535 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1536 $this->addModuleStyles( 'mediawiki.helplink' );
1537 $text = $this->msg( 'helppage-top-gethelp' )->escaped();
1538
1539 if ( $overrideBaseUrl ) {
1540 $helpUrl = $to;
1541 } else {
1542 $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1543 $helpUrl = "https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1544 }
1545
1546 $link = Html::rawElement(
1547 'a',
1548 [
1549 'href' => $helpUrl,
1550 'target' => '_blank',
1551 'class' => 'mw-helplink',
1552 ],
1553 $text
1554 );
1555
1556 $this->setIndicators( [ 'mw-helplink' => $link ] );
1557 }
1558
1567 public function disallowUserJs() {
1568 $this->reduceAllowedModules(
1569 ResourceLoaderModule::TYPE_SCRIPTS,
1570 ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1571 );
1572
1573 // Site-wide styles are controlled by a config setting, see T73621
1574 // for background on why. User styles are never allowed.
1575 if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1576 $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1577 } else {
1578 $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1579 }
1580 $this->reduceAllowedModules(
1581 ResourceLoaderModule::TYPE_STYLES,
1582 $styleOrigin
1583 );
1584 }
1585
1592 public function getAllowedModules( $type ) {
1593 if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1594 return min( array_values( $this->mAllowedModules ) );
1595 } else {
1596 return $this->mAllowedModules[$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1597 }
1598 }
1599
1609 public function reduceAllowedModules( $type, $level ) {
1610 $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1611 }
1612
1618 public function prependHTML( $text ) {
1619 $this->mBodytext = $text . $this->mBodytext;
1620 }
1621
1627 public function addHTML( $text ) {
1628 $this->mBodytext .= $text;
1629 }
1630
1640 public function addElement( $element, array $attribs = [], $contents = '' ) {
1641 $this->addHTML( Html::element( $element, $attribs, $contents ) );
1642 }
1643
1647 public function clearHTML() {
1648 $this->mBodytext = '';
1649 }
1650
1656 public function getHTML() {
1657 return $this->mBodytext;
1658 }
1659
1666 public function parserOptions() {
1667 if ( !$this->mParserOptions ) {
1668 if ( !$this->getUser()->isSafeToLoad() ) {
1669 // Context user isn't unstubbable yet, so don't try to get a
1670 // ParserOptions for it. And don't cache this ParserOptions
1671 // either.
1672 $po = ParserOptions::newFromAnon();
1673 $po->setAllowUnsafeRawHtml( false );
1674 $po->isBogus = true;
1675 return $po;
1676 }
1677
1678 $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1679 $this->mParserOptions->setAllowUnsafeRawHtml( false );
1680 }
1681
1682 return $this->mParserOptions;
1683 }
1684
1692 public function setRevisionId( $revid ) {
1693 $val = $revid === null ? null : intval( $revid );
1694 return wfSetVar( $this->mRevisionId, $val, true );
1695 }
1696
1702 public function getRevisionId() {
1703 return $this->mRevisionId;
1704 }
1705
1712 public function isRevisionCurrent() {
1713 return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
1714 }
1715
1723 public function setRevisionTimestamp( $timestamp ) {
1724 return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
1725 }
1726
1733 public function getRevisionTimestamp() {
1734 return $this->mRevisionTimestamp;
1735 }
1736
1743 public function setFileVersion( $file ) {
1744 $val = null;
1745 if ( $file instanceof File && $file->exists() ) {
1746 $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1747 }
1748 return wfSetVar( $this->mFileVersion, $val, true );
1749 }
1750
1756 public function getFileVersion() {
1757 return $this->mFileVersion;
1758 }
1759
1766 public function getTemplateIds() {
1767 return $this->mTemplateIds;
1768 }
1769
1776 public function getFileSearchOptions() {
1777 return $this->mImageTimeKeys;
1778 }
1779
1796 public function addWikiTextAsInterface(
1797 $text, $linestart = true, PageReference $title = null
1798 ) {
1799 if ( $title === null ) {
1800 $title = $this->getTitle();
1801 }
1802 if ( !$title ) {
1803 throw new MWException( 'Title is null' );
1804 }
1805 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true );
1806 }
1807
1822 $wrapperClass, $text
1823 ) {
1825 $text, $this->getTitle(),
1826 /*linestart*/true, /*interface*/true,
1827 $wrapperClass
1828 );
1829 }
1830
1846 public function addWikiTextAsContent(
1847 $text, $linestart = true, PageReference $title = null
1848 ) {
1849 if ( $title === null ) {
1850 $title = $this->getTitle();
1851 }
1852 if ( !$title ) {
1853 throw new MWException( 'Title is null' );
1854 }
1855 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false );
1856 }
1857
1871 $text, PageReference $title, $linestart, $interface, $wrapperClass = null
1872 ) {
1873 $parserOutput = $this->parseInternal(
1874 $text, $title, $linestart, $interface
1875 );
1876
1877 $this->addParserOutput( $parserOutput, [
1878 'enableSectionEditLinks' => false,
1879 'wrapperDivClass' => $wrapperClass ?? '',
1880 ] );
1881 }
1882
1891 public function addParserOutputMetadata( ParserOutput $parserOutput ) {
1892 $this->mLanguageLinks =
1893 array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() );
1894 $this->addCategoryLinks( $parserOutput->getCategories() );
1895 $this->setIndicators( $parserOutput->getIndicators() );
1896 $this->mNewSectionLink = $parserOutput->getNewSection();
1897 $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1898
1899 if ( !$parserOutput->isCacheable() ) {
1900 $this->enableClientCache( false );
1901 }
1902 $this->mNoGallery = $parserOutput->getNoGallery();
1903 $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1904 $this->addModules( $parserOutput->getModules() );
1905 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1906 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1907 $this->mPreventClickjacking = $this->mPreventClickjacking
1908 || $parserOutput->preventClickjacking();
1909 $scriptSrcs = $parserOutput->getExtraCSPScriptSrcs();
1910 foreach ( $scriptSrcs as $src ) {
1911 $this->getCSP()->addScriptSrc( $src );
1912 }
1913 $defaultSrcs = $parserOutput->getExtraCSPDefaultSrcs();
1914 foreach ( $defaultSrcs as $src ) {
1915 $this->getCSP()->addDefaultSrc( $src );
1916 }
1917 $styleSrcs = $parserOutput->getExtraCSPStyleSrcs();
1918 foreach ( $styleSrcs as $src ) {
1919 $this->getCSP()->addStyleSrc( $src );
1920 }
1921
1922 // If $wgImagePreconnect is true, and if the output contains
1923 // images, give the user-agent a hint about foreign repos from
1924 // which those images may be served. See T123582.
1925 //
1926 // TODO: We don't have an easy way to know from which remote(s)
1927 // the image(s) will be served. For now, we only hint the first
1928 // valid one.
1929 if ( $this->getConfig()->get( 'ImagePreconnect' ) && count( $parserOutput->getImages() ) ) {
1930 $preconnect = [];
1931 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
1932 $repoGroup->forEachForeignRepo( static function ( $repo ) use ( &$preconnect ) {
1933 $preconnect[] = wfParseUrl( $repo->getZoneUrl( 'thumb' ) )['host'];
1934 } );
1935 $preconnect[] = wfParseUrl( $repoGroup->getLocalRepo()->getZoneUrl( 'thumb' ) )['host'];
1936 foreach ( $preconnect as $host ) {
1937 if ( $host ) {
1938 $this->addLink( [ 'rel' => 'preconnect', 'href' => '//' . $host ] );
1939 break;
1940 }
1941 }
1942 }
1943
1944 // Template versioning...
1945 foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1946 if ( isset( $this->mTemplateIds[$ns] ) ) {
1947 $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1948 } else {
1949 $this->mTemplateIds[$ns] = $dbks;
1950 }
1951 }
1952 // File versioning...
1953 foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1954 $this->mImageTimeKeys[$dbk] = $data;
1955 }
1956
1957 // Hooks registered in the object
1958 $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1959 foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1960 list( $hookName, $data ) = $hookInfo;
1961 if ( isset( $parserOutputHooks[$hookName] ) ) {
1962 $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1963 }
1964 }
1965
1966 // Enable OOUI if requested via ParserOutput
1967 if ( $parserOutput->getEnableOOUI() ) {
1968 $this->enableOOUI();
1969 }
1970
1971 // Include parser limit report
1972 if ( !$this->limitReportJSData ) {
1973 $this->limitReportJSData = $parserOutput->getLimitReportJSData();
1974 }
1975
1976 // Link flags are ignored for now, but may in the future be
1977 // used to mark individual language links.
1978 $linkFlags = [];
1979 $this->getHookRunner()->onLanguageLinks( $this->getTitle(), $this->mLanguageLinks, $linkFlags );
1980 $this->getHookRunner()->onOutputPageParserOutput( $this, $parserOutput );
1981
1982 // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
1983 // so that extensions may modify ParserOutput to toggle TOC.
1984 // This cannot be moved to addParserOutputText because that is not
1985 // called by EditPage for Preview.
1986 if ( $parserOutput->getTOCHTML() ) {
1987 $this->mEnableTOC = true;
1988 }
1989 }
1990
1999 public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) {
2000 $this->addParserOutputText( $parserOutput, $poOptions );
2001
2002 $this->addModules( $parserOutput->getModules() );
2003 $this->addModuleStyles( $parserOutput->getModuleStyles() );
2004
2005 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
2006 }
2007
2015 public function addParserOutputText( ParserOutput $parserOutput, $poOptions = [] ) {
2016 $text = $parserOutput->getText( $poOptions );
2017 $this->getHookRunner()->onOutputPageBeforeHTML( $this, $text );
2018 $this->addHTML( $text );
2019 }
2020
2027 public function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) {
2028 $this->addParserOutputMetadata( $parserOutput );
2029 $this->addParserOutputText( $parserOutput, $poOptions );
2030 }
2031
2037 public function addTemplate( &$template ) {
2038 $this->addHTML( $template->getHTML() );
2039 }
2040
2052 public function parseAsContent( $text, $linestart = true ) {
2053 return $this->parseInternal(
2054 $text, $this->getTitle(), $linestart, /*interface*/false
2055 )->getText( [
2056 'enableSectionEditLinks' => false,
2057 'wrapperDivClass' => ''
2058 ] );
2059 }
2060
2073 public function parseAsInterface( $text, $linestart = true ) {
2074 return $this->parseInternal(
2075 $text, $this->getTitle(), $linestart, /*interface*/true
2076 )->getText( [
2077 'enableSectionEditLinks' => false,
2078 'wrapperDivClass' => ''
2079 ] );
2080 }
2081
2096 public function parseInlineAsInterface( $text, $linestart = true ) {
2098 $this->parseAsInterface( $text, $linestart )
2099 );
2100 }
2101
2114 private function parseInternal( $text, $title, $linestart, $interface ) {
2115 if ( $title === null ) {
2116 throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
2117 }
2118
2119 $popts = $this->parserOptions();
2120
2121 $oldInterface = $popts->setInterfaceMessage( (bool)$interface );
2122
2123 $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
2124 $text, $title, $popts,
2125 $linestart, true, $this->mRevisionId
2126 );
2127
2128 $popts->setInterfaceMessage( $oldInterface );
2129
2130 return $parserOutput;
2131 }
2132
2138 public function setCdnMaxage( $maxage ) {
2139 $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2140 }
2141
2151 public function lowerCdnMaxage( $maxage ) {
2152 $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2153 $this->setCdnMaxage( $this->mCdnMaxage );
2154 }
2155
2168 public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2169 $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
2170 $maxTTL = $maxTTL ?: $this->getConfig()->get( 'CdnMaxAge' );
2171
2172 if ( $mtime === null || $mtime === false ) {
2173 return; // entity does not exist
2174 }
2175
2176 $age = MWTimestamp::time() - (int)wfTimestamp( TS_UNIX, $mtime );
2177 $adaptiveTTL = max( 0.9 * $age, $minTTL );
2178 $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2179
2180 $this->lowerCdnMaxage( (int)$adaptiveTTL );
2181 }
2182
2190 public function enableClientCache( $state ) {
2191 return wfSetVar( $this->mEnableClientCache, $state );
2192 }
2193
2200 public function couldBePublicCached() {
2201 if ( !$this->cacheIsFinal ) {
2202 // - The entry point handles its own caching and/or doesn't use OutputPage.
2203 // (such as load.php, AjaxDispatcher, or MediaWiki\Rest\EntryPoint).
2204 //
2205 // - Or, we haven't finished processing the main part of the request yet
2206 // (e.g. Action::show, SpecialPage::execute), and the state may still
2207 // change via enableClientCache().
2208 return true;
2209 }
2210 // e.g. various error-type pages disable all client caching
2211 return $this->mEnableClientCache;
2212 }
2213
2223 public function considerCacheSettingsFinal() {
2224 $this->cacheIsFinal = true;
2225 }
2226
2232 public function getCacheVaryCookies() {
2233 if ( self::$cacheVaryCookies === null ) {
2234 $config = $this->getConfig();
2235 self::$cacheVaryCookies = array_values( array_unique( array_merge(
2236 SessionManager::singleton()->getVaryCookies(),
2237 [
2238 'forceHTTPS',
2239 ],
2240 $config->get( 'CacheVaryCookies' )
2241 ) ) );
2242 $this->getHookRunner()->onGetCacheVaryCookies( $this, self::$cacheVaryCookies );
2243 }
2244 return self::$cacheVaryCookies;
2245 }
2246
2253 public function haveCacheVaryCookies() {
2254 $request = $this->getRequest();
2255 foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2256 if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
2257 wfDebug( __METHOD__ . ": found $cookieName" );
2258 return true;
2259 }
2260 }
2261 wfDebug( __METHOD__ . ": no cache-varying cookies found" );
2262 return false;
2263 }
2264
2274 public function addVaryHeader( $header, array $option = null ) {
2275 if ( $option !== null && count( $option ) > 0 ) {
2277 'The $option parameter to addVaryHeader is ignored since MediaWiki 1.34',
2278 '1.34' );
2279 }
2280 if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2281 $this->mVaryHeader[$header] = null;
2282 }
2283 }
2284
2291 public function getVaryHeader() {
2292 // If we vary on cookies, let's make sure it's always included here too.
2293 if ( $this->getCacheVaryCookies() ) {
2294 $this->addVaryHeader( 'Cookie' );
2295 }
2296
2297 foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2298 $this->addVaryHeader( $header, $options );
2299 }
2300 return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2301 }
2302
2308 public function addLinkHeader( $header ) {
2309 $this->mLinkHeader[] = $header;
2310 }
2311
2317 public function getLinkHeader() {
2318 if ( !$this->mLinkHeader ) {
2319 return false;
2320 }
2321
2322 return 'Link: ' . implode( ',', $this->mLinkHeader );
2323 }
2324
2332 private function addAcceptLanguage() {
2333 $title = $this->getTitle();
2334 if ( !$title instanceof Title ) {
2335 return;
2336 }
2337
2338 $languageConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
2339 ->getLanguageConverter( $title->getPageLanguage() );
2340 if ( !$this->getRequest()->getCheck( 'variant' ) && $languageConverter->hasVariants() ) {
2341 $this->addVaryHeader( 'Accept-Language' );
2342 }
2343 }
2344
2355 public function preventClickjacking( $enable = true ) {
2356 $this->mPreventClickjacking = $enable;
2357 }
2358
2364 public function allowClickjacking() {
2365 $this->mPreventClickjacking = false;
2366 }
2367
2374 public function getPreventClickjacking() {
2375 return $this->mPreventClickjacking;
2376 }
2377
2385 public function getFrameOptions() {
2386 $config = $this->getConfig();
2387 if ( $config->get( 'BreakFrames' ) ) {
2388 return 'DENY';
2389 } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2390 return $config->get( 'EditPageFrameOptions' );
2391 }
2392 return false;
2393 }
2394
2401 private function getOriginTrials() {
2402 $config = $this->getConfig();
2403
2404 return $config->get( 'OriginTrials' );
2405 }
2406
2407 private function getReportTo() {
2408 $config = $this->getConfig();
2409
2410 $expiry = $config->get( 'ReportToExpiry' );
2411
2412 if ( !$expiry ) {
2413 return false;
2414 }
2415
2416 $endpoints = $config->get( 'ReportToEndpoints' );
2417
2418 if ( !$endpoints ) {
2419 return false;
2420 }
2421
2422 $output = [ 'max_age' => $expiry, 'endpoints' => [] ];
2423
2424 foreach ( $endpoints as $endpoint ) {
2425 $output['endpoints'][] = [ 'url' => $endpoint ];
2426 }
2427
2428 return json_encode( $output, JSON_UNESCAPED_SLASHES );
2429 }
2430
2431 private function getFeaturePolicyReportOnly() {
2432 $config = $this->getConfig();
2433
2434 $features = $config->get( 'FeaturePolicyReportOnly' );
2435 return implode( ';', $features );
2436 }
2437
2441 public function sendCacheControl() {
2442 $response = $this->getRequest()->response();
2443 $config = $this->getConfig();
2444
2445 $this->addVaryHeader( 'Cookie' );
2446 $this->addAcceptLanguage();
2447
2448 # don't serve compressed data to clients who can't handle it
2449 # maintain different caches for logged-in users and non-logged in ones
2450 $response->header( $this->getVaryHeader() );
2451
2452 if ( $this->mEnableClientCache ) {
2453 if ( !$config->get( 'UseCdn' ) ) {
2454 $privateReason = 'config';
2455 } elseif ( $response->hasCookies() ) {
2456 $privateReason = 'set-cookies';
2457 // The client might use methods other than cookies to appear logged-in.
2458 // E.g. HTTP headers, or query parameter tokens, OAuth, etc.
2459 } elseif ( SessionManager::getGlobalSession()->isPersistent() ) {
2460 $privateReason = 'session';
2461 } elseif ( $this->isPrintable() ) {
2462 $privateReason = 'printable';
2463 } elseif ( $this->mCdnMaxage == 0 ) {
2464 $privateReason = 'no-maxage';
2465 } elseif ( $this->haveCacheVaryCookies() ) {
2466 $privateReason = 'cache-vary-cookies';
2467 } else {
2468 $privateReason = false;
2469 }
2470
2471 if ( $privateReason === false ) {
2472 # We'll purge the proxy cache for anons explicitly, but require end user agents
2473 # to revalidate against the proxy on each visit.
2474 # IMPORTANT! The CDN needs to replace the Cache-Control header with
2475 # Cache-Control: s-maxage=0, must-revalidate, max-age=0
2476 wfDebug( __METHOD__ .
2477 ": local proxy caching; {$this->mLastModified} **", 'private' );
2478 # start with a shorter timeout for initial testing
2479 # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2480 $response->header( "Cache-Control: " .
2481 "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2482 } else {
2483 # We do want clients to cache if they can, but they *must* check for updates
2484 # on revisiting the page, after the max-age period.
2485 wfDebug( __METHOD__ . ": private caching ($privateReason); {$this->mLastModified} **", 'private' );
2486
2487 if ( $response->hasCookies() || SessionManager::getGlobalSession()->isPersistent() ) {
2488 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2489 $response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2490 } else {
2491 $response->header(
2492 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $config->get( 'LoggedOutMaxAge' ) ) . ' GMT'
2493 );
2494 $response->header(
2495 "Cache-Control: private, must-revalidate, max-age={$config->get( 'LoggedOutMaxAge' )}"
2496 );
2497 }
2498 }
2499 if ( $this->mLastModified ) {
2500 $response->header( "Last-Modified: {$this->mLastModified}" );
2501 }
2502 } else {
2503 wfDebug( __METHOD__ . ": no caching **", 'private' );
2504
2505 # In general, the absence of a last modified header should be enough to prevent
2506 # the client from using its cache. We send a few other things just to make sure.
2507 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2508 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2509 $response->header( 'Pragma: no-cache' );
2510 }
2511 }
2512
2518 public function loadSkinModules( $sk ) {
2519 foreach ( $sk->getDefaultModules() as $group => $modules ) {
2520 if ( $group === 'styles' ) {
2521 foreach ( $modules as $key => $moduleMembers ) {
2522 $this->addModuleStyles( $moduleMembers );
2523 }
2524 } else {
2525 $this->addModules( $modules );
2526 }
2527 }
2528 }
2529
2540 public function output( $return = false ) {
2541 if ( $this->mDoNothing ) {
2542 return $return ? '' : null;
2543 }
2544
2545 $response = $this->getRequest()->response();
2546 $config = $this->getConfig();
2547
2548 if ( $this->mRedirect != '' ) {
2549 # Standards require redirect URLs to be absolute
2550 $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
2551
2552 $redirect = $this->mRedirect;
2553 $code = $this->mRedirectCode;
2554 $content = '';
2555
2556 if ( $this->getHookRunner()->onBeforePageRedirect( $this, $redirect, $code ) ) {
2557 if ( $code == '301' || $code == '303' ) {
2558 if ( !$config->get( 'DebugRedirects' ) ) {
2559 $response->statusHeader( $code );
2560 }
2561 $this->mLastModified = wfTimestamp( TS_RFC2822 );
2562 }
2563 if ( $config->get( 'VaryOnXFP' ) ) {
2564 $this->addVaryHeader( 'X-Forwarded-Proto' );
2565 }
2566 $this->sendCacheControl();
2567
2568 $response->header( "Content-Type: text/html; charset=utf-8" );
2569 if ( $config->get( 'DebugRedirects' ) ) {
2570 $url = htmlspecialchars( $redirect );
2571 $content = "<!DOCTYPE html>\n<html>\n<head>\n"
2572 . "<title>Redirect</title>\n</head>\n<body>\n"
2573 . "<p>Location: <a href=\"$url\">$url</a></p>\n"
2574 . "</body>\n</html>\n";
2575
2576 if ( !$return ) {
2578 }
2579
2580 } else {
2581 $response->header( 'Location: ' . $redirect );
2582 }
2583 }
2584
2585 return $return ? $content : null;
2586 } elseif ( $this->mStatusCode ) {
2587 $response->statusHeader( $this->mStatusCode );
2588 }
2589
2590 # Buffer output; final headers may depend on later processing
2591 ob_start();
2592
2593 $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2594 $response->header( 'Content-language: ' .
2595 MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2596
2597 $linkHeader = $this->getLinkHeader();
2598 if ( $linkHeader ) {
2599 $response->header( $linkHeader );
2600 }
2601
2602 // Prevent framing, if requested
2603 $frameOptions = $this->getFrameOptions();
2604 if ( $frameOptions ) {
2605 $response->header( "X-Frame-Options: $frameOptions" );
2606 }
2607
2608 $originTrials = $this->getOriginTrials();
2609 foreach ( $originTrials as $originTrial ) {
2610 $response->header( "Origin-Trial: $originTrial", false );
2611 }
2612
2613 $reportTo = $this->getReportTo();
2614 if ( $reportTo ) {
2615 $response->header( "Report-To: $reportTo" );
2616 }
2617
2618 $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
2619 if ( $featurePolicyReportOnly ) {
2620 $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" );
2621 }
2622
2623 if ( $this->mArticleBodyOnly ) {
2624 $this->CSP->sendHeaders();
2625 echo $this->mBodytext;
2626 } else {
2627 // Enable safe mode if requested (T152169)
2628 if ( $this->getRequest()->getBool( 'safemode' ) ) {
2629 $this->disallowUserJs();
2630 }
2631
2632 $sk = $this->getSkin();
2633 $this->loadSkinModules( $sk );
2634
2635 MWDebug::addModules( $this );
2636
2637 // Hook that allows last minute changes to the output page, e.g.
2638 // adding of CSS or Javascript by extensions, adding CSP sources.
2639 $this->getHookRunner()->onBeforePageDisplay( $this, $sk );
2640
2641 $this->CSP->sendHeaders();
2642
2643 try {
2644 $sk->outputPage();
2645 } catch ( Exception $e ) {
2646 ob_end_clean(); // bug T129657
2647 throw $e;
2648 }
2649 }
2650
2651 try {
2652 // This hook allows last minute changes to final overall output by modifying output buffer
2653 $this->getHookRunner()->onAfterFinalPageOutput( $this );
2654 } catch ( Exception $e ) {
2655 ob_end_clean(); // bug T129657
2656 throw $e;
2657 }
2658
2659 $this->sendCacheControl();
2660
2661 if ( $return ) {
2662 return ob_get_clean();
2663 } else {
2664 ob_end_flush();
2665 return null;
2666 }
2667 }
2668
2679 public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2680 $this->setPageTitle( $pageTitle );
2681 if ( $htmlTitle !== false ) {
2682 $this->setHTMLTitle( $htmlTitle );
2683 }
2684 $this->setRobotPolicy( 'noindex,nofollow' );
2685 $this->setArticleRelated( false );
2686 $this->enableClientCache( false );
2687 $this->mRedirect = '';
2688 $this->clearSubtitle();
2689 $this->clearHTML();
2690 }
2691
2704 public function showErrorPage( $title, $msg, $params = [] ) {
2705 if ( !$title instanceof Message ) {
2706 $title = $this->msg( $title );
2707 }
2708
2709 $this->prepareErrorPage( $title );
2710
2711 if ( $msg instanceof Message ) {
2712 if ( $params !== [] ) {
2713 trigger_error( 'Argument ignored: $params. The message parameters argument '
2714 . 'is discarded when the $msg argument is a Message object instead of '
2715 . 'a string.', E_USER_NOTICE );
2716 }
2717 $this->addHTML( $msg->parseAsBlock() );
2718 } else {
2719 $this->addWikiMsgArray( $msg, $params );
2720 }
2721
2722 $this->returnToMain();
2723 }
2724
2731 public function showPermissionsErrorPage( array $errors, $action = null ) {
2732 $services = MediaWikiServices::getInstance();
2733 $permissionManager = $services->getPermissionManager();
2734 foreach ( $errors as $key => $error ) {
2735 $errors[$key] = (array)$error;
2736 }
2737
2738 // For some action (read, edit, create and upload), display a "login to do this action"
2739 // error if all of the following conditions are met:
2740 // 1. the user is not logged in
2741 // 2. the only error is insufficient permissions (i.e. no block or something else)
2742 // 3. the error can be avoided simply by logging in
2743
2744 if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2745 && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2746 && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2747 && ( $permissionManager->groupHasPermission( 'user', $action )
2748 || $permissionManager->groupHasPermission( 'autoconfirmed', $action ) )
2749 ) {
2750 $displayReturnto = null;
2751
2752 # Due to T34276, if a user does not have read permissions,
2753 # $this->getTitle() will just give Special:Badtitle, which is
2754 # not especially useful as a returnto parameter. Use the title
2755 # from the request instead, if there was one.
2756 $request = $this->getRequest();
2757 $returnto = Title::newFromText( $request->getText( 'title' ) );
2758 if ( $action == 'edit' ) {
2759 $msg = 'whitelistedittext';
2760 $displayReturnto = $returnto;
2761 } elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2762 $msg = 'nocreatetext';
2763 } elseif ( $action == 'upload' ) {
2764 $msg = 'uploadnologintext';
2765 } else { # Read
2766 $msg = 'loginreqpagetext';
2767 $displayReturnto = Title::newMainPage();
2768 }
2769
2770 $query = [];
2771
2772 if ( $returnto ) {
2773 $query['returnto'] = $returnto->getPrefixedText();
2774
2775 if ( !$request->wasPosted() ) {
2776 $returntoquery = $request->getValues();
2777 unset( $returntoquery['title'] );
2778 unset( $returntoquery['returnto'] );
2779 unset( $returntoquery['returntoquery'] );
2780 $query['returntoquery'] = wfArrayToCgi( $returntoquery );
2781 }
2782 }
2783
2784 $title = SpecialPage::getTitleFor( 'Userlogin' );
2785 $linkRenderer = $services->getLinkRenderer();
2786 $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
2787 $loginLink = $linkRenderer->makeKnownLink(
2788 $title,
2789 $this->msg( 'loginreqlink' )->text(),
2790 [],
2791 $query
2792 );
2793
2794 $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2795 $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
2796
2797 # Don't return to a page the user can't read otherwise
2798 # we'll end up in a pointless loop
2799 if ( $displayReturnto && $this->getAuthority()->probablyCan( 'read', $displayReturnto ) ) {
2800 $this->returnToMain( null, $displayReturnto );
2801 }
2802 } else {
2803 $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2804 $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) );
2805 }
2806 }
2807
2814 public function versionRequired( $version ) {
2815 $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2816
2817 $this->addWikiMsg( 'versionrequiredtext', $version );
2818 $this->returnToMain();
2819 }
2820
2828 public function formatPermissionStatus( PermissionStatus $status, string $action = null ): string {
2829 if ( $status->isGood() ) {
2830 return '';
2831 }
2832 return $this->formatPermissionsErrorMessage( $status->toLegacyErrorArray(), $action );
2833 }
2834
2843 public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2844 if ( $action == null ) {
2845 $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2846 } else {
2847 $action_desc = $this->msg( "action-$action" )->plain();
2848 $text = $this->msg(
2849 'permissionserrorstext-withaction',
2850 count( $errors ),
2851 $action_desc
2852 )->plain() . "\n\n";
2853 }
2854
2855 if ( count( $errors ) > 1 ) {
2856 $text .= '<ul class="permissions-errors">' . "\n";
2857
2858 foreach ( $errors as $error ) {
2859 $text .= '<li>';
2860 $text .= $this->msg( ...$error )->plain();
2861 $text .= "</li>\n";
2862 }
2863 $text .= '</ul>';
2864 } else {
2865 $text .= "<div class=\"permissions-errors\">\n" .
2866 $this->msg( ...reset( $errors ) )->plain() .
2867 "\n</div>";
2868 }
2869
2870 return $text;
2871 }
2872
2882 public function showLagWarning( $lag ) {
2883 $config = $this->getConfig();
2884 if ( $lag >= $config->get( 'DatabaseReplicaLagWarning' ) ) {
2885 $lag = floor( $lag ); // floor to avoid nano seconds to display
2886 $message = $lag < $config->get( 'DatabaseReplicaLagCritical' )
2887 ? 'lag-warn-normal'
2888 : 'lag-warn-high';
2889 // For grep: mw-lag-warn-normal, mw-lag-warn-high
2890 $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2891 $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2892 }
2893 }
2894
2901 public function showFatalError( $message ) {
2902 $this->prepareErrorPage( $this->msg( 'internalerror' ) );
2903
2904 $this->addHTML( $message );
2905 }
2906
2915 public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2916 $linkRenderer = MediaWikiServices::getInstance()
2917 ->getLinkRendererFactory()->createFromLegacyOptions( $options );
2918 $link = $this->msg( 'returnto' )->rawParams(
2919 $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped();
2920 $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2921 }
2922
2931 public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2932 if ( $returnto == null ) {
2933 $returnto = $this->getRequest()->getText( 'returnto' );
2934 }
2935
2936 if ( $returntoquery == null ) {
2937 $returntoquery = $this->getRequest()->getText( 'returntoquery' );
2938 }
2939
2940 if ( $returnto === '' ) {
2941 $returnto = Title::newMainPage();
2942 }
2943
2944 if ( is_object( $returnto ) ) {
2945 $linkTarget = TitleValue::castPageToLinkTarget( $returnto );
2946 } else {
2947 $linkTarget = Title::newFromText( $returnto );
2948 }
2949
2950 // We don't want people to return to external interwiki. That
2951 // might potentially be used as part of a phishing scheme
2952 if ( !is_object( $linkTarget ) || $linkTarget->isExternal() ) {
2953 $linkTarget = Title::newMainPage();
2954 }
2955
2956 $this->addReturnTo( $linkTarget, wfCgiToArray( $returntoquery ) );
2957 }
2958
2959 private function getRlClientContext() {
2960 if ( !$this->rlClientContext ) {
2962 [], // modules; not relevant
2963 $this->getLanguage()->getCode(),
2964 $this->getSkin()->getSkinName(),
2965 $this->getUser()->isRegistered() ? $this->getUser()->getName() : null,
2966 null, // version; not relevant
2968 null, // only; not relevant
2969 $this->isPrintable(),
2970 $this->getRequest()->getBool( 'handheld' )
2971 );
2972 $this->rlClientContext = new ResourceLoaderContext(
2973 $this->getResourceLoader(),
2974 new FauxRequest( $query )
2975 );
2976 if ( $this->contentOverrideCallbacks ) {
2977 $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
2978 $this->rlClientContext->setContentOverrideCallback( function ( $title ) {
2979 foreach ( $this->contentOverrideCallbacks as $callback ) {
2980 $content = $callback( $title );
2981 if ( $content !== null ) {
2982 $text = ContentHandler::getContentText( $content );
2983 if ( strpos( $text, '</script>' ) !== false ) {
2984 // Proactively replace this so that we can display a message
2985 // to the user, instead of letting it go to Html::inlineScript(),
2986 // where it would be considered a server-side issue.
2988 Xml::encodeJsCall( 'mw.log.error', [
2989 "Cannot preview $title due to script-closing tag."
2990 ] )
2991 );
2992 }
2993 return $content;
2994 }
2995 }
2996 return null;
2997 } );
2998 }
2999 }
3000 return $this->rlClientContext;
3001 }
3002
3014 public function getRlClient() {
3015 if ( !$this->rlClient ) {
3016 $context = $this->getRlClientContext();
3017 $rl = $this->getResourceLoader();
3018 $this->addModules( [
3019 'user',
3020 'user.options',
3021 ] );
3022 $this->addModuleStyles( [
3023 'site.styles',
3024 'noscript',
3025 'user.styles',
3026 ] );
3027
3028 // Prepare exempt modules for buildExemptModules()
3029 $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
3030 $exemptStates = [];
3031 $moduleStyles = $this->getModuleStyles( /*filter*/ true );
3032
3033 // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
3034 // Separate user-specific batch for improved cache-hit ratio.
3035 $userBatch = [ 'user.styles', 'user' ];
3036 $siteBatch = array_diff( $moduleStyles, $userBatch );
3037 $dbr = wfGetDB( DB_REPLICA );
3038 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $siteBatch );
3039 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
3040
3041 // Filter out modules handled by buildExemptModules()
3042 $moduleStyles = array_filter( $moduleStyles,
3043 static function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
3044 $module = $rl->getModule( $name );
3045 if ( $module ) {
3046 $group = $module->getGroup();
3047 if ( isset( $exemptGroups[$group] ) ) {
3048 $exemptStates[$name] = 'ready';
3049 if ( !$module->isKnownEmpty( $context ) ) {
3050 // E.g. Don't output empty <styles>
3051 $exemptGroups[$group][] = $name;
3052 }
3053 return false;
3054 }
3055 }
3056 return true;
3057 }
3058 );
3059 $this->rlExemptStyleModules = $exemptGroups;
3060
3061 $rlClient = new ResourceLoaderClientHtml( $context, [
3062 'target' => $this->getTarget(),
3063 'nonce' => $this->CSP->getNonce(),
3064 // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
3065 // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
3066 // modules enqueud for loading on this page is filtered to just those.
3067 // However, to make sure we also apply the restriction to dynamic dependencies and
3068 // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
3069 // StartupModule so that the client-side registry will not contain any restricted
3070 // modules either. (T152169, T185303)
3071 'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
3072 <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
3073 ) ? '1' : null,
3074 ] );
3075 $rlClient->setConfig( $this->getJSVars() );
3076 $rlClient->setModules( $this->getModules( /*filter*/ true ) );
3077 $rlClient->setModuleStyles( $moduleStyles );
3078 $rlClient->setExemptStates( $exemptStates );
3079 $this->rlClient = $rlClient;
3080 }
3081 return $this->rlClient;
3082 }
3083
3089 public function headElement( Skin $sk, $includeStyle = true ) {
3090 $config = $this->getConfig();
3091 $userdir = $this->getLanguage()->getDir();
3092 $services = MediaWikiServices::getInstance();
3093 $sitedir = $services->getContentLanguage()->getDir();
3094
3095 $pieces = [];
3096 $htmlAttribs = Sanitizer::mergeAttributes( Sanitizer::mergeAttributes(
3097 $this->getRlClient()->getDocumentAttributes(),
3099 ), [ 'class' => implode( ' ', $this->mAdditionalHtmlClasses ) ] );
3100 $pieces[] = Html::htmlHeader( $htmlAttribs );
3101 $pieces[] = Html::openElement( 'head' );
3102
3103 if ( $this->getHTMLTitle() == '' ) {
3104 $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3105 }
3106
3107 if ( !Html::isXmlMimeType( $config->get( 'MimeType' ) ) ) {
3108 // Add <meta charset="UTF-8">
3109 // This should be before <title> since it defines the charset used by
3110 // text including the text inside <title>.
3111 // The spec recommends defining XHTML5's charset using the XML declaration
3112 // instead of meta.
3113 // Our XML declaration is output by Html::htmlHeader.
3114 // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
3115 // https://html.spec.whatwg.org/multipage/semantics.html#charset
3116 $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
3117 }
3118
3119 $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
3120 $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null );
3121 $pieces[] = $this->buildExemptModules();
3122 $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
3123 $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
3124
3125 $pieces[] = Html::closeElement( 'head' );
3126
3127 $bodyClasses = $this->mAdditionalBodyClasses;
3128 $bodyClasses[] = 'mediawiki';
3129
3130 # Classes for LTR/RTL directionality support
3131 $bodyClasses[] = $userdir;
3132 $bodyClasses[] = "sitedir-$sitedir";
3133
3134 $underline = $services->getUserOptionsLookup()->getOption( $this->getUser(), 'underline' );
3135 if ( $underline < 2 ) {
3136 // The following classes can be used here:
3137 // * mw-underline-always
3138 // * mw-underline-never
3139 $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' );
3140 }
3141
3142 // Parser feature migration class
3143 // The idea is that this will eventually be removed, after the wikitext
3144 // which requires it is cleaned up.
3145 $bodyClasses[] = 'mw-hide-empty-elt';
3146
3147 $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
3148 $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
3149 $bodyClasses[] =
3150 'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
3151
3152 if ( $sk->isResponsive() ) {
3153 $bodyClasses[] = 'skin--responsive';
3154 }
3155
3156 $bodyAttrs = [];
3157 // While the implode() is not strictly needed, it's used for backwards compatibility
3158 // (this used to be built as a string and hooks likely still expect that).
3159 $bodyAttrs['class'] = implode( ' ', $bodyClasses );
3160
3161 // Allow skins and extensions to add body attributes they need
3162 // Get ones from deprecated method
3163 if ( method_exists( $sk, 'addToBodyAttributes' ) ) {
3165 $sk->addToBodyAttributes( $this, $bodyAttrs );
3166 wfDeprecated( 'Skin::addToBodyAttributes method to add body attributes', '1.35' );
3167 }
3168
3169 // Then run the hook, the recommended way of adding body attributes now
3170 $this->getHookRunner()->onOutputPageBodyAttributes( $this, $sk, $bodyAttrs );
3171
3172 $pieces[] = Html::openElement( 'body', $bodyAttrs );
3173
3174 return self::combineWrappedStrings( $pieces );
3175 }
3176
3182 public function getResourceLoader() {
3183 if ( $this->mResourceLoader === null ) {
3184 // Lazy-initialise as needed
3185 $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
3186 }
3187 return $this->mResourceLoader;
3188 }
3189
3198 public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
3199 // Apply 'target' and 'origin' filters
3200 $modules = $this->filterModules( (array)$modules, null, $only );
3201
3203 $this->getRlClientContext(),
3204 $modules,
3205 $only,
3206 $extraQuery,
3207 $this->CSP->getNonce()
3208 );
3209 }
3210
3217 protected static function combineWrappedStrings( array $chunks ) {
3218 // Filter out empty values
3219 $chunks = array_filter( $chunks, 'strlen' );
3220 return WrappedString::join( "\n", $chunks );
3221 }
3222
3230 public function getBottomScripts( $extraHtml = '' ) {
3231 $chunks = [];
3232 $chunks[] = $this->getRlClient()->getBodyHtml();
3233
3234 // Legacy non-ResourceLoader scripts
3235 $chunks[] = $this->mScripts;
3236
3237 if ( $this->limitReportJSData ) {
3240 [ 'wgPageParseReport' => $this->limitReportJSData ]
3241 ),
3242 $this->CSP->getNonce()
3243 );
3244 }
3245 // This should be added last because the extra html comes from
3246 // SkinAfterBottomScripts hook.
3247 // TODO: Run the hook here directly and remove the parameter.
3248 $chunks[] = $extraHtml;
3249
3250 return self::combineWrappedStrings( $chunks );
3251 }
3252
3259 public function getJsConfigVars() {
3260 return $this->mJsConfigVars;
3261 }
3262
3269 public function addJsConfigVars( $keys, $value = null ) {
3270 if ( is_array( $keys ) ) {
3271 foreach ( $keys as $key => $value ) {
3272 $this->mJsConfigVars[$key] = $value;
3273 }
3274 return;
3275 }
3276
3277 $this->mJsConfigVars[$keys] = $value;
3278 }
3279
3289 public function getJSVars() {
3290 $curRevisionId = 0;
3291 $articleId = 0;
3292 $canonicalSpecialPageName = false; # T23115
3293 $services = MediaWikiServices::getInstance();
3294
3295 $title = $this->getTitle();
3296 $ns = $title->getNamespace();
3297 $nsInfo = $services->getNamespaceInfo();
3298 $canonicalNamespace = $nsInfo->exists( $ns )
3299 ? $nsInfo->getCanonicalName( $ns )
3300 : $title->getNsText();
3301
3302 $sk = $this->getSkin();
3303 // Get the relevant title so that AJAX features can use the correct page name
3304 // when making API requests from certain special pages (T36972).
3305 $relevantTitle = $sk->getRelevantTitle();
3306
3307 if ( $ns === NS_SPECIAL ) {
3308 list( $canonicalSpecialPageName, /*...*/ ) =
3309 $services->getSpecialPageFactory()->
3310 resolveAlias( $title->getDBkey() );
3311 } elseif ( $this->canUseWikiPage() ) {
3312 $wikiPage = $this->getWikiPage();
3313 $curRevisionId = $wikiPage->getLatest();
3314 $articleId = $wikiPage->getId();
3315 }
3316
3317 $lang = $title->getPageViewLanguage();
3318
3319 // Pre-process information
3320 $separatorTransTable = $lang->separatorTransformTable();
3321 $separatorTransTable = $separatorTransTable ?: [];
3322 $compactSeparatorTransTable = [
3323 implode( "\t", array_keys( $separatorTransTable ) ),
3324 implode( "\t", $separatorTransTable ),
3325 ];
3326 $digitTransTable = $lang->digitTransformTable();
3327 $digitTransTable = $digitTransTable ?: [];
3328 $compactDigitTransTable = [
3329 implode( "\t", array_keys( $digitTransTable ) ),
3330 implode( "\t", $digitTransTable ),
3331 ];
3332
3333 $user = $this->getUser();
3334
3335 // Internal variables for MediaWiki core
3336 $vars = [
3337 // @internal For mediawiki.page.ready
3338 'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3339
3340 // @internal For jquery.tablesorter
3341 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3342 'wgDigitTransformTable' => $compactDigitTransTable,
3343 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3344 'wgMonthNames' => $lang->getMonthNamesArray(),
3345
3346 // @internal For debugging purposes
3347 'wgRequestId' => WebRequest::getRequestId(),
3348
3349 // @internal For mw.loader
3350 'wgCSPNonce' => $this->CSP->getNonce(),
3351 ];
3352
3353 // Start of supported and stable config vars (for use by extensions/gadgets).
3354 $vars += [
3355 'wgCanonicalNamespace' => $canonicalNamespace,
3356 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3357 'wgNamespaceNumber' => $title->getNamespace(),
3358 'wgPageName' => $title->getPrefixedDBkey(),
3359 'wgTitle' => $title->getText(),
3360 'wgCurRevisionId' => $curRevisionId,
3361 'wgRevisionId' => (int)$this->getRevisionId(),
3362 'wgArticleId' => $articleId,
3363 'wgIsArticle' => $this->isArticle(),
3364 'wgIsRedirect' => $title->isRedirect(),
3365 'wgAction' => Action::getActionName( $this->getContext() ),
3366 'wgUserName' => $user->isAnon() ? null : $user->getName(),
3367 'wgUserGroups' => $services->getUserGroupManager()->getUserEffectiveGroups( $user ),
3368 'wgCategories' => $this->getCategories(),
3369 'wgPageContentLanguage' => $lang->getCode(),
3370 'wgPageContentModel' => $title->getContentModel(),
3371 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3372 'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3373 ];
3374 if ( $user->isRegistered() ) {
3375 $vars['wgUserId'] = $user->getId();
3376 $vars['wgUserEditCount'] = $user->getEditCount();
3377 $userReg = $user->getRegistration();
3378 $vars['wgUserRegistration'] = $userReg ? (int)wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3379 // Get the revision ID of the oldest new message on the user's talk
3380 // page. This can be used for constructing new message alerts on
3381 // the client side.
3382 $userNewMsgRevId = $this->getLastSeenUserTalkRevId();
3383 // Only occupy precious space in the <head> when it is non-null (T53640)
3384 // mw.config.get returns null by default.
3385 if ( $userNewMsgRevId ) {
3386 $vars['wgUserNewMsgRevisionId'] = $userNewMsgRevId;
3387 }
3388 }
3389 $languageConverter = $services->getLanguageConverterFactory()
3390 ->getLanguageConverter( $services->getContentLanguage() );
3391 if ( $languageConverter->hasVariants() ) {
3392 $vars['wgUserVariant'] = $languageConverter->getPreferredVariant();
3393 }
3394 // Same test as SkinTemplate
3395 $vars['wgIsProbablyEditable'] = $this->getAuthority()->probablyCan( 'edit', $title );
3396 $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle &&
3397 $this->getAuthority()->probablyCan( 'edit', $relevantTitle );
3398 foreach ( $title->getRestrictionTypes() as $type ) {
3399 // Following keys are set in $vars:
3400 // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
3401 $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3402 }
3403 if ( $title->isMainPage() ) {
3404 $vars['wgIsMainPage'] = true;
3405 }
3406
3407 $relevantUser = $sk->getRelevantUser();
3408 if ( $relevantUser ) {
3409 $vars['wgRelevantUserName'] = $relevantUser->getName();
3410 }
3411 // End of stable config vars
3412
3413 $titleFormatter = $services->getTitleFormatter();
3414
3415 if ( $this->mRedirectedFrom ) {
3416 // @internal For skin JS
3417 $vars['wgRedirectedFrom'] = $titleFormatter->getPrefixedDBkey( $this->mRedirectedFrom );
3418 }
3419
3420 // Allow extensions to add their custom variables to the mw.config map.
3421 // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3422 // page-dependent but site-wide (without state).
3423 // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3424 $this->getHookRunner()->onMakeGlobalVariablesScript( $vars, $this );
3425
3426 // Merge in variables from addJsConfigVars last
3427 return array_merge( $vars, $this->getJsConfigVars() );
3428 }
3429
3435 private function getLastSeenUserTalkRevId() {
3436 $services = MediaWikiServices::getInstance();
3437 $user = $this->getUser();
3438 $userHasNewMessages = $services
3439 ->getTalkPageNotificationManager()
3440 ->userHasNewMessages( $user );
3441 if ( !$userHasNewMessages ) {
3442 return null;
3443 }
3444
3445 $timestamp = $services
3446 ->getTalkPageNotificationManager()
3447 ->getLatestSeenMessageTimestamp( $user );
3448
3449 if ( !$timestamp ) {
3450 return null;
3451 }
3452
3453 $revRecord = $services->getRevisionLookup()->getRevisionByTimestamp(
3454 $user->getTalkPage(),
3455 $timestamp
3456 );
3457
3458 if ( !$revRecord ) {
3459 return null;
3460 }
3461
3462 return $revRecord->getId();
3463 }
3464
3474 public function userCanPreview() {
3475 $request = $this->getRequest();
3476 if (
3477 $request->getRawVal( 'action' ) !== 'submit' ||
3478 !$request->wasPosted()
3479 ) {
3480 return false;
3481 }
3482
3483 $user = $this->getUser();
3484
3485 if ( !$user->isRegistered() ) {
3486 // Anons have predictable edit tokens
3487 return false;
3488 }
3489 if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3490 return false;
3491 }
3492
3493 $title = $this->getTitle();
3494 if ( !$this->getAuthority()->probablyCan( 'edit', $title ) ) {
3495 return false;
3496 }
3497
3498 return true;
3499 }
3500
3504 public function getHeadLinksArray() {
3505 $tags = [];
3506 $config = $this->getConfig();
3507
3508 $canonicalUrl = $this->mCanonicalUrl;
3509
3510 $tags['meta-generator'] = Html::element( 'meta', [
3511 'name' => 'generator',
3512 'content' => 'MediaWiki ' . MW_VERSION,
3513 ] );
3514
3515 if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3516 // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
3517 // fallbacks should come before the primary value so we need to reverse the array.
3518 foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
3519 $tags["meta-referrer-$i"] = Html::element( 'meta', [
3520 'name' => 'referrer',
3521 'content' => $policy,
3522 ] );
3523 }
3524 }
3525
3526 $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3527 if ( $p !== 'index,follow' ) {
3528 // http://www.robotstxt.org/wc/meta-user.html
3529 // Only show if it's different from the default robots policy
3530 $tags['meta-robots'] = Html::element( 'meta', [
3531 'name' => 'robots',
3532 'content' => $p,
3533 ] );
3534 }
3535
3536 # Browser based phonenumber detection
3537 if ( $config->get( 'BrowserFormatDetection' ) !== false ) {
3538 $tags['meta-format-detection'] = Html::element( 'meta', [
3539 'name' => 'format-detection',
3540 'content' => $config->get( 'BrowserFormatDetection' ),
3541 ] );
3542 }
3543
3544 foreach ( $this->mMetatags as $tag ) {
3545 if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) {
3546 $a = 'http-equiv';
3547 $tag[0] = substr( $tag[0], 5 );
3548 } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) {
3549 $a = 'property';
3550 } else {
3551 $a = 'name';
3552 }
3553 $tagName = "meta-{$tag[0]}";
3554 if ( isset( $tags[$tagName] ) ) {
3555 $tagName .= $tag[1];
3556 }
3557 $tags[$tagName] = Html::element( 'meta',
3558 [
3559 $a => $tag[0],
3560 'content' => $tag[1]
3561 ]
3562 );
3563 }
3564
3565 foreach ( $this->mLinktags as $tag ) {
3566 $tags[] = Html::element( 'link', $tag );
3567 }
3568
3569 # Universal edit button
3570 if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3571 if ( $this->getAuthority()->probablyCan( 'edit', $this->getTitle() ) ) {
3572 // Original UniversalEditButton
3573 $msg = $this->msg( 'edit' )->text();
3574 $tags['universal-edit-button'] = Html::element( 'link', [
3575 'rel' => 'alternate',
3576 'type' => 'application/x-wiki',
3577 'title' => $msg,
3578 'href' => $this->getTitle()->getEditURL(),
3579 ] );
3580 // Alternate edit link
3581 $tags['alternative-edit'] = Html::element( 'link', [
3582 'rel' => 'edit',
3583 'title' => $msg,
3584 'href' => $this->getTitle()->getEditURL(),
3585 ] );
3586 }
3587 }
3588
3589 # Generally the order of the favicon and apple-touch-icon links
3590 # should not matter, but Konqueror (3.5.9 at least) incorrectly
3591 # uses whichever one appears later in the HTML source. Make sure
3592 # apple-touch-icon is specified first to avoid this.
3593 if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3594 $tags['apple-touch-icon'] = Html::element( 'link', [
3595 'rel' => 'apple-touch-icon',
3596 'href' => $config->get( 'AppleTouchIcon' )
3597 ] );
3598 }
3599
3600 if ( $config->get( 'Favicon' ) !== false ) {
3601 $tags['favicon'] = Html::element( 'link', [
3602 'rel' => 'shortcut icon',
3603 'href' => $config->get( 'Favicon' )
3604 ] );
3605 }
3606
3607 # OpenSearch description link
3608 $tags['opensearch'] = Html::element( 'link', [
3609 'rel' => 'search',
3610 'type' => 'application/opensearchdescription+xml',
3611 'href' => wfScript( 'opensearch_desc' ),
3612 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3613 ] );
3614
3615 # Real Simple Discovery link, provides auto-discovery information
3616 # for the MediaWiki API (and potentially additional custom API
3617 # support such as WordPress or Twitter-compatible APIs for a
3618 # blogging extension, etc)
3619 $tags['rsd'] = Html::element( 'link', [
3620 'rel' => 'EditURI',
3621 'type' => 'application/rsd+xml',
3622 // Output a protocol-relative URL here if $wgServer is protocol-relative.
3623 // Whether RSD accepts relative or protocol-relative URLs is completely
3624 // undocumented, though.
3625 'href' => wfExpandUrl( wfAppendQuery(
3626 wfScript( 'api' ),
3627 [ 'action' => 'rsd' ] ),
3629 ),
3630 ] );
3631
3632 # Language variants
3633 $services = MediaWikiServices::getInstance();
3634 $languageConverterFactory = $services->getLanguageConverterFactory();
3635 $disableLangConversion = $languageConverterFactory->isConversionDisabled();
3636 if ( !$disableLangConversion ) {
3637 $lang = $this->getTitle()->getPageLanguage();
3638 $languageConverter = $languageConverterFactory->getLanguageConverter( $lang );
3639 if ( $languageConverter->hasVariants() ) {
3640 $variants = $languageConverter->getVariants();
3641 foreach ( $variants as $variant ) {
3642 $tags["variant-$variant"] = Html::element( 'link', [
3643 'rel' => 'alternate',
3644 'hreflang' => LanguageCode::bcp47( $variant ),
3645 'href' => $this->getTitle()->getLocalURL(
3646 [ 'variant' => $variant ] )
3647 ]
3648 );
3649 }
3650 # x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3651 $tags["variant-x-default"] = Html::element( 'link', [
3652 'rel' => 'alternate',
3653 'hreflang' => 'x-default',
3654 'href' => $this->getTitle()->getLocalURL() ] );
3655 }
3656 }
3657
3658 # Copyright
3659 if ( $this->copyrightUrl !== null ) {
3660 $copyright = $this->copyrightUrl;
3661 } else {
3662 $copyright = '';
3663 if ( $config->get( 'RightsPage' ) ) {
3664 $copy = Title::newFromText( $config->get( 'RightsPage' ) );
3665
3666 if ( $copy ) {
3667 $copyright = $copy->getLocalURL();
3668 }
3669 }
3670
3671 if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3672 $copyright = $config->get( 'RightsUrl' );
3673 }
3674 }
3675
3676 if ( $copyright ) {
3677 $tags['copyright'] = Html::element( 'link', [
3678 'rel' => 'license',
3679 'href' => $copyright ]
3680 );
3681 }
3682
3683 # Feeds
3684 if ( $config->get( 'Feed' ) ) {
3685 $feedLinks = [];
3686
3687 foreach ( $this->getSyndicationLinks() as $format => $link ) {
3688 # Use the page name for the title. In principle, this could
3689 # lead to issues with having the same name for different feeds
3690 # corresponding to the same page, but we can't avoid that at
3691 # this low a level.
3692
3693 $feedLinks[] = $this->feedLink(
3694 $format,
3695 $link,
3696 # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3697 $this->msg(
3698 "page-{$format}-feed", $this->getTitle()->getPrefixedText()
3699 )->text()
3700 );
3701 }
3702
3703 # Recent changes feed should appear on every page (except recentchanges,
3704 # that would be redundant). Put it after the per-page feed to avoid
3705 # changing existing behavior. It's still available, probably via a
3706 # menu in your browser. Some sites might have a different feed they'd
3707 # like to promote instead of the RC feed (maybe like a "Recent New Articles"
3708 # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3709 # If so, use it instead.
3710 $sitename = $config->get( 'Sitename' );
3711 $overrideSiteFeed = $config->get( 'OverrideSiteFeed' );
3712 if ( $overrideSiteFeed ) {
3713 foreach ( $overrideSiteFeed as $type => $feedUrl ) {
3714 // Note, this->feedLink escapes the url.
3715 $feedLinks[] = $this->feedLink(
3716 $type,
3717 $feedUrl,
3718 $this->msg( "site-{$type}-feed", $sitename )->text()
3719 );
3720 }
3721 } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3722 $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3723 foreach ( $this->getAdvertisedFeedTypes() as $format ) {
3724 $feedLinks[] = $this->feedLink(
3725 $format,
3726 $rctitle->getLocalURL( [ 'feed' => $format ] ),
3727 # For grep: 'site-rss-feed', 'site-atom-feed'
3728 $this->msg( "site-{$format}-feed", $sitename )->text()
3729 );
3730 }
3731 }
3732
3733 # Allow extensions to change the list pf feeds. This hook is primarily for changing,
3734 # manipulating or removing existing feed tags. If you want to add new feeds, you should
3735 # use OutputPage::addFeedLink() instead.
3736 $this->getHookRunner()->onAfterBuildFeedLinks( $feedLinks );
3737
3738 $tags += $feedLinks;
3739 }
3740
3741 # Canonical URL
3742 if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3743 if ( $canonicalUrl !== false ) {
3744 $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3745 } elseif ( $this->isArticleRelated() ) {
3746 // This affects all requests where "setArticleRelated" is true. This is
3747 // typically all requests that show content (query title, curid, oldid, diff),
3748 // and all wikipage actions (edit, delete, purge, info, history etc.).
3749 // It does not apply to File pages and Special pages.
3750 // 'history' and 'info' actions address page metadata rather than the page
3751 // content itself, so they may not be canonicalized to the view page url.
3752 // TODO: this ought to be better encapsulated in the Action class.
3753 $action = Action::getActionName( $this->getContext() );
3754 if ( in_array( $action, [ 'history', 'info' ] ) ) {
3755 $query = "action={$action}";
3756 } else {
3757 $query = '';
3758 }
3759 $canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3760 } else {
3761 $reqUrl = $this->getRequest()->getRequestURL();
3762 $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3763 }
3764 }
3765 if ( $canonicalUrl !== false ) {
3766 $tags[] = Html::element( 'link', [
3767 'rel' => 'canonical',
3768 'href' => $canonicalUrl
3769 ] );
3770 }
3771
3772 // Allow extensions to add, remove and/or otherwise manipulate these links
3773 // If you want only to *add* <head> links, please use the addHeadItem()
3774 // (or addHeadItems() for multiple items) method instead.
3775 // This hook is provided as a last resort for extensions to modify these
3776 // links before the output is sent to client.
3777 $this->getHookRunner()->onOutputPageAfterGetHeadLinksArray( $tags, $this );
3778
3779 return $tags;
3780 }
3781
3790 private function feedLink( $type, $url, $text ) {
3791 return Html::element( 'link', [
3792 'rel' => 'alternate',
3793 'type' => "application/$type+xml",
3794 'title' => $text,
3795 'href' => $url ]
3796 );
3797 }
3798
3808 public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3809 $options = [];
3810 if ( $media ) {
3811 $options['media'] = $media;
3812 }
3813 if ( $condition ) {
3814 $options['condition'] = $condition;
3815 }
3816 if ( $dir ) {
3817 $options['dir'] = $dir;
3818 }
3819 $this->styles[$style] = $options;
3820 }
3821
3829 public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3830 if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3831 # If wanted, and the interface is right-to-left, flip the CSS
3832 $style_css = CSSJanus::transform( $style_css, true, false );
3833 }
3834 $this->mInlineStyles .= Html::inlineStyle( $style_css );
3835 }
3836
3842 protected function buildExemptModules() {
3843 $chunks = [];
3844
3845 // Requirements:
3846 // - Within modules provided by the software (core, skin, extensions),
3847 // styles from skin stylesheets should be overridden by styles
3848 // from modules dynamically loaded with JavaScript.
3849 // - Styles from site-specific, private, and user modules should override
3850 // both of the above.
3851 //
3852 // The effective order for stylesheets must thus be:
3853 // 1. Page style modules, formatted server-side by ResourceLoaderClientHtml.
3854 // 2. Dynamically-loaded styles, inserted client-side by mw.loader.
3855 // 3. Styles that are site-specific, private or from the user, formatted
3856 // server-side by this function.
3857 //
3858 // The 'ResourceLoaderDynamicStyles' marker helps JavaScript know where
3859 // point #2 is.
3860
3861 // Add legacy styles added through addStyle()/addInlineStyle() here
3862 $chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3863
3864 // Things that go after the ResourceLoaderDynamicStyles marker
3865 $append = [];
3866 $separateReq = [ 'site.styles', 'user.styles' ];
3867 foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3868 if ( $moduleNames ) {
3869 $append[] = $this->makeResourceLoaderLink(
3870 array_diff( $moduleNames, $separateReq ),
3871 ResourceLoaderModule::TYPE_STYLES
3872 );
3873
3874 foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) {
3875 // These require their own dedicated request in order to support "@import"
3876 // syntax, which is incompatible with concatenation. (T147667, T37562)
3877 $append[] = $this->makeResourceLoaderLink( $name,
3878 ResourceLoaderModule::TYPE_STYLES
3879 );
3880 }
3881 }
3882 }
3883 if ( $append ) {
3884 $chunks[] = Html::element(
3885 'meta',
3886 [ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3887 );
3888 $chunks = array_merge( $chunks, $append );
3889 }
3890
3891 return self::combineWrappedStrings( $chunks );
3892 }
3893
3897 public function buildCssLinksArray() {
3898 $links = [];
3899
3900 foreach ( $this->styles as $file => $options ) {
3901 $link = $this->styleLink( $file, $options );
3902 if ( $link ) {
3903 $links[$file] = $link;
3904 }
3905 }
3906 return $links;
3907 }
3908
3916 protected function styleLink( $style, array $options ) {
3917 if ( isset( $options['dir'] ) && $this->getLanguage()->getDir() != $options['dir'] ) {
3918 return '';
3919 }
3920
3921 if ( isset( $options['media'] ) ) {
3922 $media = self::transformCssMedia( $options['media'] );
3923 if ( $media === null ) {
3924 return '';
3925 }
3926 } else {
3927 $media = 'all';
3928 }
3929
3930 if ( substr( $style, 0, 1 ) == '/' ||
3931 substr( $style, 0, 5 ) == 'http:' ||
3932 substr( $style, 0, 6 ) == 'https:' ) {
3933 $url = $style;
3934 } else {
3935 $config = $this->getConfig();
3936 // Append file hash as query parameter
3937 $url = self::transformResourcePath(
3938 $config,
3939 $config->get( 'StylePath' ) . '/' . $style
3940 );
3941 }
3942
3943 $link = Html::linkedStyle( $url, $media );
3944
3945 if ( isset( $options['condition'] ) ) {
3946 $condition = htmlspecialchars( $options['condition'] );
3947 $link = "<!--[if $condition]>$link<![endif]-->";
3948 }
3949 return $link;
3950 }
3951
3973 public static function transformResourcePath( Config $config, $path ) {
3974 global $IP;
3975
3976 $localDir = $IP;
3977 $remotePathPrefix = $config->get( 'ResourceBasePath' );
3978 if ( $remotePathPrefix === '' ) {
3979 // The configured base path is required to be empty string for
3980 // wikis in the domain root
3981 $remotePath = '/';
3982 } else {
3983 $remotePath = $remotePathPrefix;
3984 }
3985 if ( strpos( $path, $remotePath ) !== 0 || substr( $path, 0, 2 ) === '//' ) {
3986 // - Path is outside wgResourceBasePath, ignore.
3987 // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib.
3988 return $path;
3989 }
3990 // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here.
3991 // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth
3992 // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath)
3993 // which is not expected to be in wgResourceBasePath on CDNs. (T155146)
3994 $uploadPath = $config->get( 'UploadPath' );
3995 if ( strpos( $path, $uploadPath ) === 0 ) {
3996 $localDir = $config->get( 'UploadDirectory' );
3997 $remotePathPrefix = $remotePath = $uploadPath;
3998 }
3999
4000 $path = RelPath::getRelativePath( $path, $remotePath );
4001 return self::transformFilePath( $remotePathPrefix, $localDir, $path );
4002 }
4003
4015 public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
4016 // This MUST match the equivalent logic in CSSMin::remapOne()
4017 $localFile = "$localPath/$file";
4018 $url = "$remotePathPrefix/$file";
4019 if ( file_exists( $localFile ) ) {
4020 $hash = md5_file( $localFile );
4021 if ( $hash === false ) {
4022 wfLogWarning( __METHOD__ . ": Failed to hash $localFile" );
4023 $hash = '';
4024 }
4025 $url .= '?' . substr( $hash, 0, 5 );
4026 }
4027 return $url;
4028 }
4029
4037 public static function transformCssMedia( $media ) {
4038 global $wgRequest;
4039
4040 // https://www.w3.org/TR/css3-mediaqueries/#syntax
4041 $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
4042
4043 // Switch in on-screen display for media testing
4044 $switches = [
4045 'printable' => 'print',
4046 'handheld' => 'handheld',
4047 ];
4048 foreach ( $switches as $switch => $targetMedia ) {
4049 if ( $wgRequest->getBool( $switch ) ) {
4050 if ( $media == $targetMedia ) {
4051 $media = '';
4052 } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
4053 /* This regex will not attempt to understand a comma-separated media_query_list
4054 *
4055 * Example supported values for $media:
4056 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
4057 * Example NOT supported value for $media:
4058 * '3d-glasses, screen, print and resolution > 90dpi'
4059 *
4060 * If it's a print request, we never want any kind of screen stylesheets
4061 * If it's a handheld request (currently the only other choice with a switch),
4062 * we don't want simple 'screen' but we might want screen queries that
4063 * have a max-width or something, so we'll pass all others on and let the
4064 * client do the query.
4065 */
4066 if ( $targetMedia == 'print' || $media == 'screen' ) {
4067 return null;
4068 }
4069 }
4070 }
4071 }
4072
4073 return $media;
4074 }
4075
4084 public function addWikiMsg( ...$args ) {
4085 $name = array_shift( $args );
4086 $this->addWikiMsgArray( $name, $args );
4087 }
4088
4097 public function addWikiMsgArray( $name, $args ) {
4098 $this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
4099 }
4100
4127 public function wrapWikiMsg( $wrap, ...$msgSpecs ) {
4128 $s = $wrap;
4129 foreach ( $msgSpecs as $n => $spec ) {
4130 if ( is_array( $spec ) ) {
4131 $args = $spec;
4132 $name = array_shift( $args );
4133 } else {
4134 $args = [];
4135 $name = $spec;
4136 }
4137 $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
4138 }
4139 $this->addWikiTextAsInterface( $s );
4140 }
4141
4147 public function isTOCEnabled() {
4148 return $this->mEnableTOC;
4149 }
4150
4158 public static function setupOOUI( $skinName = 'default', $dir = 'ltr' ) {
4159 $themes = ResourceLoaderOOUIModule::getSkinThemeMap();
4160 $theme = $themes[$skinName] ?? $themes['default'];
4161 // For example, 'OOUI\WikimediaUITheme'.
4162 $themeClass = "OOUI\\{$theme}Theme";
4163 OOUI\Theme::setSingleton( new $themeClass() );
4164 OOUI\Element::setDefaultDir( $dir );
4165 }
4166
4173 public function enableOOUI() {
4174 self::setupOOUI(
4175 strtolower( $this->getSkin()->getSkinName() ),
4176 $this->getLanguage()->getDir()
4177 );
4178 $this->addModuleStyles( [
4179 'oojs-ui-core.styles',
4180 'oojs-ui.styles.indicators',
4181 'mediawiki.widgets.styles',
4182 'oojs-ui-core.icons',
4183 ] );
4184 }
4185
4196 public function getCSPNonce() {
4197 return $this->CSP->getNonce();
4198 }
4199
4206 public function getCSP() {
4207 return $this->CSP;
4208 }
4209
4219 public function tailElement( $skin ) {
4220 // T257704: Temporarily run skin hook here pending
4221 // creation dedicated outputpage hook for this
4222 $extraHtml = '';
4223 $this->getHookRunner()->onSkinAfterBottomScripts( $skin, $extraHtml );
4224
4225 $tail = [
4226 MWDebug::getDebugHTML( $skin ),
4227 $this->getBottomScripts( $extraHtml ),
4228 wfReportTime( $this->getCSP()->getNonce() ),
4229 MWDebug::getHTMLDebugLog()
4230 . Html::closeElement( 'body' )
4231 . Html::closeElement( 'html' )
4232 ];
4233
4234 return WrappedStringList::join( "\n", $tail );
4235 }
4236}
getAuthority()
const PROTO_CANONICAL
Definition Defines.php:196
const PROTO_CURRENT
Definition Defines.php:195
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:194
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.
wfReportTime( $nonce=null)
Returns a script tag that stores the amount of time it took MediaWiki to handle the request in millis...
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 a deprecated feature was used.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
getContext()
$wgRequest
Definition Setup.php:702
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
$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:66
exists()
Returns true if file exists in the repository.
Definition File.php:1028
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:138
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
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)
addWikiTextTitleInternal( $text, PageReference $title, $linestart, $interface, $wrapperClass=null)
Add wikitext with a custom Title object.
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:".
addBacklinkSubtitle(PageReference $title, $query=[])
Add a subtitle containing a backlink to a page.
$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.
tailElement( $skin)
The final bits that go to the bottom of a page HTML document including the closing tags.
getTemplateIds()
Get the templates used on this page.
array $mLinktags
formatPermissionStatus(PermissionStatus $status, string $action=null)
Format permission $status obtained from Authority for display.
static buildBacklinkSubtitle(PageReference $page, $query=[])
Build message object for a subtitle containing a backlink to a page.
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:
PageReference $mRedirectedFrom
If the current page was reached through a redirect, $mRedirectedFrom contains the title of the redire...
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.
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.
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.
addContentOverride( $target, Content $content)
Force the given Content object for the given page, for things like page preview.
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.
addWikiTextAsInterface( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the user interface language to HTML and add it to the buffer.
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".
getBottomScripts( $extraHtml='')
JS stuff to put at the bottom of the <body>.
string[][] $mCategories
getModules( $filter=false, $position=null, $param='mModules', $type=ResourceLoaderModule::TYPE_COMBINED)
Get the list of modules to include on this 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.
parseAsInterface( $text, $linestart=true)
Parse wikitext in the user interface language and return the HTML.
setTitle(PageReference $t)
Set the Title object to use.
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.
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.
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.
array $mTemplateIds
setRedirectedFrom(PageReference $t)
Set $mRedirectedFrom, the page which redirected us to the current page.
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.
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.
int null $mRevisionId
To include the variable {{REVISIONID}}.
setFeedAppendQuery( $val)
Add default feeds to the page header This is mainly kept for backward compatibility,...
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.
addWikiTextAsContent( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
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:6336
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:44
getHtmlElementAttributes()
Return values for <html> element.
Definition Skin.php:527
isResponsive()
Indicates if this skin is responsive.
Definition Skin.php:217
getSkinName()
Definition Skin.php:204
getPageClasses( $title)
TODO: document.
Definition Skin.php:492
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.
Data record representing a page that is (or used to be, or could be) an editable page on a wiki.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getNamespace()
Returns the page's namespace number.
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