MediaWiki  1.32.0
OutputPage.php
Go to the documentation of this file.
1 <?php
28 use Wikimedia\RelPath;
29 use Wikimedia\WrappedString;
30 use Wikimedia\WrappedStringList;
31 
47 class OutputPage extends ContextSource {
49  protected $mMetatags = [];
50 
52  protected $mLinktags = [];
53 
55  protected $mCanonicalUrl = false;
56 
59  private $mPageTitle = '';
60 
68  private $displayTitle;
69 
74  public $mBodytext = '';
75 
77  private $mHTMLtitle = '';
78 
83  private $mIsArticle = false;
84 
86  private $mIsArticleRelated = true;
87 
89  private $mHasCopyright = false;
90 
95  private $mPrintable = false;
96 
101  private $mSubtitle = [];
102 
104  public $mRedirect = '';
105 
107  protected $mStatusCode;
108 
113  protected $mLastModified = '';
114 
116  protected $mCategoryLinks = [];
117 
119  protected $mCategories = [
120  'hidden' => [],
121  'normal' => [],
122  ];
123 
125  protected $mIndicators = [];
126 
128  private $mLanguageLinks = [];
129 
136  private $mScripts = '';
137 
139  protected $mInlineStyles = '';
140 
145  public $mPageLinkTitle = '';
146 
148  protected $mHeadItems = [];
149 
151  protected $mAdditionalBodyClasses = [];
152 
154  protected $mModules = [];
155 
157  protected $mModuleScripts = [];
158 
160  protected $mModuleStyles = [];
161 
163  protected $mResourceLoader;
164 
166  private $rlClient;
167 
169  private $rlClientContext;
170 
172  private $rlExemptStyleModules;
173 
175  protected $mJsConfigVars = [];
176 
178  protected $mTemplateIds = [];
179 
181  protected $mImageTimeKeys = [];
182 
184  public $mRedirectCode = '';
185 
186  protected $mFeedLinksAppendQuery = null;
187 
193  protected $mAllowedModules = [
195  ];
196 
198  protected $mDoNothing = false;
199 
200  // Parser related.
201 
203  protected $mContainsNewMagic = 0;
204 
209  protected $mParserOptions = null;
210 
216  private $mFeedLinks = [];
217 
218  // Gwicke work on squid caching? Roughly from 2003.
219  protected $mEnableClientCache = true;
220 
222  private $mArticleBodyOnly = false;
223 
225  protected $mNewSectionLink = false;
226 
228  protected $mHideNewSectionLink = false;
229 
235  public $mNoGallery = false;
236 
238  protected $mCdnMaxage = 0;
240  protected $mCdnMaxageLimit = INF;
241 
247  protected $mPreventClickjacking = true;
248 
250  private $mRevisionId = null;
251 
253  private $mRevisionTimestamp = null;
254 
256  protected $mFileVersion = null;
257 
266  protected $styles = [];
267 
268  private $mIndexPolicy = 'index';
269  private $mFollowPolicy = 'follow';
270 
275  private $mVaryHeader = [
276  'Accept-Encoding' => [ 'match=gzip' ],
277  ];
278 
285  private $mRedirectedFrom = null;
286 
290  private $mProperties = [];
291 
295  private $mTarget = null;
296 
300  private $mEnableTOC = false;
301 
305  private $copyrightUrl;
306 
308  private $limitReportJSData = [];
309 
311  private $contentOverrides = [];
312 
314  private $contentOverrideCallbacks = [];
315 
319  private $mLinkHeader = [];
320 
324  private $CSPNonce;
325 
329  private static $cacheVaryCookies = null;
330 
337  function __construct( IContextSource $context ) {
338  $this->setContext( $context );
339  }
340 
347  public function redirect( $url, $responsecode = '302' ) {
348  # Strip newlines as a paranoia check for header injection in PHP<5.1.2
349  $this->mRedirect = str_replace( "\n", '', $url );
350  $this->mRedirectCode = $responsecode;
351  }
352 
358  public function getRedirect() {
359  return $this->mRedirect;
360  }
361 
370  public function setCopyrightUrl( $url ) {
371  $this->copyrightUrl = $url;
372  }
373 
379  public function setStatusCode( $statusCode ) {
380  $this->mStatusCode = $statusCode;
381  }
382 
390  function addMeta( $name, $val ) {
391  array_push( $this->mMetatags, [ $name, $val ] );
392  }
393 
400  public function getMetaTags() {
401  return $this->mMetatags;
402  }
403 
411  function addLink( array $linkarr ) {
412  array_push( $this->mLinktags, $linkarr );
413  }
414 
421  public function getLinkTags() {
422  return $this->mLinktags;
423  }
424 
430  function setCanonicalUrl( $url ) {
431  $this->mCanonicalUrl = $url;
432  }
433 
441  public function getCanonicalUrl() {
442  return $this->mCanonicalUrl;
443  }
444 
452  function addScript( $script ) {
453  $this->mScripts .= $script;
454  }
455 
464  public function addScriptFile( $file, $unused = null ) {
465  if ( substr( $file, 0, 1 ) !== '/' && !preg_match( '#^[a-z]*://#i', $file ) ) {
466  // This is not an absolute path, protocol-relative url, or full scheme url,
467  // presumed to be an old call intended to include a file from /w/skins/common,
468  // which doesn't exist anymore as of MediaWiki 1.24 per T71277. Ignore.
469  wfDeprecated( __METHOD__, '1.24' );
470  return;
471  }
472  $this->addScript( Html::linkedScript( $file, $this->getCSPNonce() ) );
473  }
474 
481  public function addInlineScript( $script ) {
482  $this->mScripts .= Html::inlineScript( "\n$script\n", $this->getCSPNonce() ) . "\n";
483  }
484 
493  protected function filterModules( array $modules, $position = null,
495  ) {
496  $resourceLoader = $this->getResourceLoader();
497  $filteredModules = [];
498  foreach ( $modules as $val ) {
499  $module = $resourceLoader->getModule( $val );
500  if ( $module instanceof ResourceLoaderModule
501  && $module->getOrigin() <= $this->getAllowedModules( $type )
502  ) {
503  if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
504  $this->warnModuleTargetFilter( $module->getName() );
505  continue;
506  }
507  $filteredModules[] = $val;
508  }
509  }
510  return $filteredModules;
511  }
512 
513  private function warnModuleTargetFilter( $moduleName ) {
514  static $warnings = [];
515  if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
516  return;
517  }
518  $warnings[$this->mTarget][$moduleName] = true;
519  $this->getResourceLoader()->getLogger()->debug(
520  'Module "{module}" not loadable on target "{target}".',
521  [
522  'module' => $moduleName,
523  'target' => $this->mTarget,
524  ]
525  );
526  }
527 
537  public function getModules( $filter = false, $position = null, $param = 'mModules',
539  ) {
540  $modules = array_values( array_unique( $this->$param ) );
541  return $filter
542  ? $this->filterModules( $modules, null, $type )
543  : $modules;
544  }
545 
551  public function addModules( $modules ) {
552  $this->mModules = array_merge( $this->mModules, (array)$modules );
553  }
554 
562  public function getModuleScripts( $filter = false, $position = null ) {
563  return $this->getModules( $filter, null, 'mModuleScripts',
565  );
566  }
567 
578  public function addModuleScripts( $modules ) {
579  $this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules );
580  }
581 
589  public function getModuleStyles( $filter = false, $position = null ) {
590  return $this->getModules( $filter, null, 'mModuleStyles',
592  );
593  }
594 
604  public function addModuleStyles( $modules ) {
605  $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
606  }
607 
611  public function getTarget() {
612  return $this->mTarget;
613  }
614 
620  public function setTarget( $target ) {
621  $this->mTarget = $target;
622  }
623 
631  public function addContentOverride( LinkTarget $target, Content $content ) {
632  if ( !$this->contentOverrides ) {
633  // Register a callback for $this->contentOverrides on the first call
634  $this->addContentOverrideCallback( function ( LinkTarget $target ) {
635  $key = $target->getNamespace() . ':' . $target->getDBkey();
636  return $this->contentOverrides[$key] ?? null;
637  } );
638  }
639 
640  $key = $target->getNamespace() . ':' . $target->getDBkey();
641  $this->contentOverrides[$key] = $content;
642  }
643 
651  public function addContentOverrideCallback( callable $callback ) {
652  $this->contentOverrideCallbacks[] = $callback;
653  }
654 
660  function getHeadItemsArray() {
661  return $this->mHeadItems;
662  }
663 
676  public function addHeadItem( $name, $value ) {
677  $this->mHeadItems[$name] = $value;
678  }
679 
686  public function addHeadItems( $values ) {
687  $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
688  }
689 
696  public function hasHeadItem( $name ) {
697  return isset( $this->mHeadItems[$name] );
698  }
699 
706  public function addBodyClasses( $classes ) {
707  $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes );
708  }
709 
717  public function setArticleBodyOnly( $only ) {
718  $this->mArticleBodyOnly = $only;
719  }
720 
726  public function getArticleBodyOnly() {
727  return $this->mArticleBodyOnly;
728  }
729 
737  public function setProperty( $name, $value ) {
738  $this->mProperties[$name] = $value;
739  }
740 
748  public function getProperty( $name ) {
749  return $this->mProperties[$name] ?? null;
750  }
751 
763  public function checkLastModified( $timestamp ) {
764  if ( !$timestamp || $timestamp == '19700101000000' ) {
765  wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
766  return false;
767  }
768  $config = $this->getConfig();
769  if ( !$config->get( 'CachePages' ) ) {
770  wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
771  return false;
772  }
773 
774  $timestamp = wfTimestamp( TS_MW, $timestamp );
775  $modifiedTimes = [
776  'page' => $timestamp,
777  'user' => $this->getUser()->getTouched(),
778  'epoch' => $config->get( 'CacheEpoch' )
779  ];
780  if ( $config->get( 'UseSquid' ) ) {
781  $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, $this->getCdnCacheEpoch(
782  time(),
783  $config->get( 'SquidMaxage' )
784  ) );
785  }
786  Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
787 
788  $maxModified = max( $modifiedTimes );
789  $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
790 
791  $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
792  if ( $clientHeader === false ) {
793  wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
794  return false;
795  }
796 
797  # IE sends sizes after the date like this:
798  # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
799  # this breaks strtotime().
800  $clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
801 
802  Wikimedia\suppressWarnings(); // E_STRICT system time warnings
803  $clientHeaderTime = strtotime( $clientHeader );
804  Wikimedia\restoreWarnings();
805  if ( !$clientHeaderTime ) {
806  wfDebug( __METHOD__
807  . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
808  return false;
809  }
810  $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
811 
812  # Make debug info
813  $info = '';
814  foreach ( $modifiedTimes as $name => $value ) {
815  if ( $info !== '' ) {
816  $info .= ', ';
817  }
818  $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
819  }
820 
821  wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
822  wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
823  wfDebug( __METHOD__ . ": effective Last-Modified: " .
824  wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
825  if ( $clientHeaderTime < $maxModified ) {
826  wfDebug( __METHOD__ . ": STALE, $info", 'private' );
827  return false;
828  }
829 
830  # Not modified
831  # Give a 304 Not Modified response code and disable body output
832  wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
833  ini_set( 'zlib.output_compression', 0 );
834  $this->getRequest()->response()->statusHeader( 304 );
835  $this->sendCacheControl();
836  $this->disable();
837 
838  // Don't output a compressed blob when using ob_gzhandler;
839  // it's technically against HTTP spec and seems to confuse
840  // Firefox when the response gets split over two packets.
842 
843  return true;
844  }
845 
851  private function getCdnCacheEpoch( $reqTime, $maxAge ) {
852  // Ensure Last-Modified is never more than (wgSquidMaxage) in the past,
853  // because even if the wiki page content hasn't changed since, static
854  // resources may have changed (skin HTML, interface messages, urls, etc.)
855  // and must roll-over in a timely manner (T46570)
856  return $reqTime - $maxAge;
857  }
858 
865  public function setLastModified( $timestamp ) {
866  $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
867  }
868 
877  public function setRobotPolicy( $policy ) {
878  $policy = Article::formatRobotPolicy( $policy );
879 
880  if ( isset( $policy['index'] ) ) {
881  $this->setIndexPolicy( $policy['index'] );
882  }
883  if ( isset( $policy['follow'] ) ) {
884  $this->setFollowPolicy( $policy['follow'] );
885  }
886  }
887 
895  public function setIndexPolicy( $policy ) {
896  $policy = trim( $policy );
897  if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
898  $this->mIndexPolicy = $policy;
899  }
900  }
901 
909  public function setFollowPolicy( $policy ) {
910  $policy = trim( $policy );
911  if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
912  $this->mFollowPolicy = $policy;
913  }
914  }
915 
922  public function setHTMLTitle( $name ) {
923  if ( $name instanceof Message ) {
924  $this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
925  } else {
926  $this->mHTMLtitle = $name;
927  }
928  }
929 
935  public function getHTMLTitle() {
936  return $this->mHTMLtitle;
937  }
938 
944  public function setRedirectedFrom( $t ) {
945  $this->mRedirectedFrom = $t;
946  }
947 
958  public function setPageTitle( $name ) {
959  if ( $name instanceof Message ) {
960  $name = $name->setContext( $this->getContext() )->text();
961  }
962 
963  # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
964  # but leave "<i>foobar</i>" alone
965  $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
966  $this->mPageTitle = $nameWithTags;
967 
968  # change "<i>foo&amp;bar</i>" to "foo&bar"
969  $this->setHTMLTitle(
970  $this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) )
971  ->inContentLanguage()
972  );
973  }
974 
980  public function getPageTitle() {
981  return $this->mPageTitle;
982  }
983 
991  public function setDisplayTitle( $html ) {
992  $this->displayTitle = $html;
993  }
994 
1003  public function getDisplayTitle() {
1004  $html = $this->displayTitle;
1005  if ( $html === null ) {
1006  $html = $this->getTitle()->getPrefixedText();
1007  }
1008 
1009  return Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $html ) );
1010  }
1011 
1018  public function getUnprefixedDisplayTitle() {
1019  $text = $this->getDisplayTitle();
1020  $nsPrefix = $this->getTitle()->getNsText() . ':';
1021  $prefix = preg_quote( $nsPrefix, '/' );
1022 
1023  return preg_replace( "/^$prefix/i", '', $text );
1024  }
1025 
1031  public function setTitle( Title $t ) {
1032  $this->getContext()->setTitle( $t );
1033  }
1034 
1040  public function setSubtitle( $str ) {
1041  $this->clearSubtitle();
1042  $this->addSubtitle( $str );
1043  }
1044 
1050  public function addSubtitle( $str ) {
1051  if ( $str instanceof Message ) {
1052  $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1053  } else {
1054  $this->mSubtitle[] = $str;
1055  }
1056  }
1057 
1066  public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1067  if ( $title->isRedirect() ) {
1068  $query['redirect'] = 'no';
1069  }
1070  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1071  return wfMessage( 'backlinksubtitle' )
1072  ->rawParams( $linkRenderer->makeLink( $title, null, [], $query ) );
1073  }
1074 
1081  public function addBacklinkSubtitle( Title $title, $query = [] ) {
1082  $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1083  }
1084 
1088  public function clearSubtitle() {
1089  $this->mSubtitle = [];
1090  }
1091 
1097  public function getSubtitle() {
1098  return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1099  }
1100 
1105  public function setPrintable() {
1106  $this->mPrintable = true;
1107  }
1108 
1114  public function isPrintable() {
1115  return $this->mPrintable;
1116  }
1117 
1121  public function disable() {
1122  $this->mDoNothing = true;
1123  }
1124 
1130  public function isDisabled() {
1131  return $this->mDoNothing;
1132  }
1133 
1139  public function showNewSectionLink() {
1140  return $this->mNewSectionLink;
1141  }
1142 
1148  public function forceHideNewSectionLink() {
1149  return $this->mHideNewSectionLink;
1150  }
1151 
1160  public function setSyndicated( $show = true ) {
1161  if ( $show ) {
1162  $this->setFeedAppendQuery( false );
1163  } else {
1164  $this->mFeedLinks = [];
1165  }
1166  }
1167 
1177  public function setFeedAppendQuery( $val ) {
1178  $this->mFeedLinks = [];
1179 
1180  foreach ( $this->getConfig()->get( 'AdvertisedFeedTypes' ) as $type ) {
1181  $query = "feed=$type";
1182  if ( is_string( $val ) ) {
1183  $query .= '&' . $val;
1184  }
1185  $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1186  }
1187  }
1188 
1195  public function addFeedLink( $format, $href ) {
1196  if ( in_array( $format, $this->getConfig()->get( 'AdvertisedFeedTypes' ) ) ) {
1197  $this->mFeedLinks[$format] = $href;
1198  }
1199  }
1200 
1205  public function isSyndicated() {
1206  return count( $this->mFeedLinks ) > 0;
1207  }
1208 
1213  public function getSyndicationLinks() {
1214  return $this->mFeedLinks;
1215  }
1216 
1222  public function getFeedAppendQuery() {
1223  return $this->mFeedLinksAppendQuery;
1224  }
1225 
1233  public function setArticleFlag( $newVal ) {
1234  $this->mIsArticle = $newVal;
1235  if ( $newVal ) {
1236  $this->mIsArticleRelated = $newVal;
1237  }
1238  }
1239 
1246  public function isArticle() {
1247  return $this->mIsArticle;
1248  }
1249 
1256  public function setArticleRelated( $newVal ) {
1257  $this->mIsArticleRelated = $newVal;
1258  if ( !$newVal ) {
1259  $this->mIsArticle = false;
1260  }
1261  }
1262 
1268  public function isArticleRelated() {
1269  return $this->mIsArticleRelated;
1270  }
1271 
1277  public function setCopyright( $hasCopyright ) {
1278  $this->mHasCopyright = $hasCopyright;
1279  }
1280 
1290  public function showsCopyright() {
1291  return $this->isArticle() || $this->mHasCopyright;
1292  }
1293 
1300  public function addLanguageLinks( array $newLinkArray ) {
1301  $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1302  }
1303 
1310  public function setLanguageLinks( array $newLinkArray ) {
1311  $this->mLanguageLinks = $newLinkArray;
1312  }
1313 
1319  public function getLanguageLinks() {
1320  return $this->mLanguageLinks;
1321  }
1322 
1328  public function addCategoryLinks( array $categories ) {
1329  if ( !$categories ) {
1330  return;
1331  }
1332 
1333  $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1334 
1335  # Set all the values to 'normal'.
1336  $categories = array_fill_keys( array_keys( $categories ), 'normal' );
1337 
1338  # Mark hidden categories
1339  foreach ( $res as $row ) {
1340  if ( isset( $row->pp_value ) ) {
1341  $categories[$row->page_title] = 'hidden';
1342  }
1343  }
1344 
1345  // Avoid PHP 7.1 warning of passing $this by reference
1346  $outputPage = $this;
1347  # Add the remaining categories to the skin
1348  if ( Hooks::run(
1349  'OutputPageMakeCategoryLinks',
1350  [ &$outputPage, $categories, &$this->mCategoryLinks ] )
1351  ) {
1352  $services = MediaWikiServices::getInstance();
1353  $linkRenderer = $services->getLinkRenderer();
1354  foreach ( $categories as $category => $type ) {
1355  // array keys will cast numeric category names to ints, so cast back to string
1356  $category = (string)$category;
1357  $origcategory = $category;
1358  $title = Title::makeTitleSafe( NS_CATEGORY, $category );
1359  if ( !$title ) {
1360  continue;
1361  }
1362  $services->getContentLanguage()->findVariantLink( $category, $title, true );
1363  if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1364  continue;
1365  }
1366  $text = $services->getContentLanguage()->convertHtml( $title->getText() );
1367  $this->mCategories[$type][] = $title->getText();
1368  $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) );
1369  }
1370  }
1371  }
1372 
1377  protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1378  # Add the links to a LinkBatch
1379  $arr = [ NS_CATEGORY => $categories ];
1380  $lb = new LinkBatch;
1381  $lb->setArray( $arr );
1382 
1383  # Fetch existence plus the hiddencat property
1384  $dbr = wfGetDB( DB_REPLICA );
1385  $fields = array_merge(
1387  [ 'page_namespace', 'page_title', 'pp_value' ]
1388  );
1389 
1390  $res = $dbr->select( [ 'page', 'page_props' ],
1391  $fields,
1392  $lb->constructSet( 'page', $dbr ),
1393  __METHOD__,
1394  [],
1395  [ 'page_props' => [ 'LEFT JOIN', [
1396  'pp_propname' => 'hiddencat',
1397  'pp_page = page_id'
1398  ] ] ]
1399  );
1400 
1401  # Add the results to the link cache
1402  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1403  $lb->addResultToCache( $linkCache, $res );
1404 
1405  return $res;
1406  }
1407 
1413  public function setCategoryLinks( array $categories ) {
1414  $this->mCategoryLinks = [];
1415  $this->addCategoryLinks( $categories );
1416  }
1417 
1426  public function getCategoryLinks() {
1427  return $this->mCategoryLinks;
1428  }
1429 
1439  public function getCategories( $type = 'all' ) {
1440  if ( $type === 'all' ) {
1441  $allCategories = [];
1442  foreach ( $this->mCategories as $categories ) {
1443  $allCategories = array_merge( $allCategories, $categories );
1444  }
1445  return $allCategories;
1446  }
1447  if ( !isset( $this->mCategories[$type] ) ) {
1448  throw new InvalidArgumentException( 'Invalid category type given: ' . $type );
1449  }
1450  return $this->mCategories[$type];
1451  }
1452 
1462  public function setIndicators( array $indicators ) {
1463  $this->mIndicators = $indicators + $this->mIndicators;
1464  // Keep ordered by key
1465  ksort( $this->mIndicators );
1466  }
1467 
1476  public function getIndicators() {
1477  return $this->mIndicators;
1478  }
1479 
1488  public function addHelpLink( $to, $overrideBaseUrl = false ) {
1489  $this->addModuleStyles( 'mediawiki.helplink' );
1490  $text = $this->msg( 'helppage-top-gethelp' )->escaped();
1491 
1492  if ( $overrideBaseUrl ) {
1493  $helpUrl = $to;
1494  } else {
1495  $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1496  $helpUrl = "//www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1497  }
1498 
1500  'a',
1501  [
1502  'href' => $helpUrl,
1503  'target' => '_blank',
1504  'class' => 'mw-helplink',
1505  ],
1506  $text
1507  );
1508 
1509  $this->setIndicators( [ 'mw-helplink' => $link ] );
1510  }
1511 
1520  public function disallowUserJs() {
1521  $this->reduceAllowedModules(
1524  );
1525 
1526  // Site-wide styles are controlled by a config setting, see T73621
1527  // for background on why. User styles are never allowed.
1528  if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1530  } else {
1532  }
1533  $this->reduceAllowedModules(
1535  $styleOrigin
1536  );
1537  }
1538 
1545  public function getAllowedModules( $type ) {
1547  return min( array_values( $this->mAllowedModules ) );
1548  } else {
1549  return $this->mAllowedModules[$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1550  }
1551  }
1552 
1562  public function reduceAllowedModules( $type, $level ) {
1563  $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1564  }
1565 
1571  public function prependHTML( $text ) {
1572  $this->mBodytext = $text . $this->mBodytext;
1573  }
1574 
1580  public function addHTML( $text ) {
1581  $this->mBodytext .= $text;
1582  }
1583 
1593  public function addElement( $element, array $attribs = [], $contents = '' ) {
1594  $this->addHTML( Html::element( $element, $attribs, $contents ) );
1595  }
1596 
1600  public function clearHTML() {
1601  $this->mBodytext = '';
1602  }
1603 
1609  public function getHTML() {
1610  return $this->mBodytext;
1611  }
1612 
1620  public function parserOptions( $options = null ) {
1621  if ( $options !== null ) {
1622  wfDeprecated( __METHOD__ . ' with non-null $options', '1.31' );
1623  }
1624 
1625  if ( $options !== null && !empty( $options->isBogus ) ) {
1626  // Someone is trying to set a bogus pre-$wgUser PO. Check if it has
1627  // been changed somehow, and keep it if so.
1628  $anonPO = ParserOptions::newFromAnon();
1629  $anonPO->setAllowUnsafeRawHtml( false );
1630  if ( !$options->matches( $anonPO ) ) {
1631  wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
1632  $options->isBogus = false;
1633  }
1634  }
1635 
1636  if ( !$this->mParserOptions ) {
1637  if ( !$this->getContext()->getUser()->isSafeToLoad() ) {
1638  // $wgUser isn't unstubbable yet, so don't try to get a
1639  // ParserOptions for it. And don't cache this ParserOptions
1640  // either.
1642  $po->setAllowUnsafeRawHtml( false );
1643  $po->isBogus = true;
1644  if ( $options !== null ) {
1645  $this->mParserOptions = empty( $options->isBogus ) ? $options : null;
1646  }
1647  return $po;
1648  }
1649 
1650  $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1651  $this->mParserOptions->setAllowUnsafeRawHtml( false );
1652  }
1653 
1654  if ( $options !== null && !empty( $options->isBogus ) ) {
1655  // They're trying to restore the bogus pre-$wgUser PO. Do the right
1656  // thing.
1657  return wfSetVar( $this->mParserOptions, null, true );
1658  } else {
1659  return wfSetVar( $this->mParserOptions, $options );
1660  }
1661  }
1662 
1670  public function setRevisionId( $revid ) {
1671  $val = is_null( $revid ) ? null : intval( $revid );
1672  return wfSetVar( $this->mRevisionId, $val, true );
1673  }
1674 
1680  public function getRevisionId() {
1681  return $this->mRevisionId;
1682  }
1683 
1691  public function setRevisionTimestamp( $timestamp ) {
1692  return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
1693  }
1694 
1701  public function getRevisionTimestamp() {
1702  return $this->mRevisionTimestamp;
1703  }
1704 
1711  public function setFileVersion( $file ) {
1712  $val = null;
1713  if ( $file instanceof File && $file->exists() ) {
1714  $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1715  }
1716  return wfSetVar( $this->mFileVersion, $val, true );
1717  }
1718 
1724  public function getFileVersion() {
1725  return $this->mFileVersion;
1726  }
1727 
1734  public function getTemplateIds() {
1735  return $this->mTemplateIds;
1736  }
1737 
1744  public function getFileSearchOptions() {
1745  return $this->mImageTimeKeys;
1746  }
1747 
1760  public function addWikiText( $text, $linestart = true, $interface = true ) {
1761  $title = $this->getTitle();
1762  if ( !$title ) {
1763  throw new MWException( 'Title is null' );
1764  }
1765  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/false, $interface );
1766  }
1767 
1784  public function addWikiTextAsInterface(
1785  $text, $linestart = true, Title $title = null
1786  ) {
1787  if ( $title === null ) {
1788  $title = $this->getTitle();
1789  }
1790  if ( !$title ) {
1791  throw new MWException( 'Title is null' );
1792  }
1793  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/true );
1794  }
1795 
1809  public function wrapWikiTextAsInterface(
1810  $wrapperClass, $text
1811  ) {
1812  $this->addWikiTextTitleInternal(
1813  $text, $this->getTitle(),
1814  /*linestart*/true, /*tidy*/true, /*interface*/true,
1815  $wrapperClass
1816  );
1817  }
1818 
1834  public function addWikiTextAsContent(
1835  $text, $linestart = true, Title $title = null
1836  ) {
1837  if ( $title === null ) {
1838  $title = $this->getTitle();
1839  }
1840  if ( !$title ) {
1841  throw new MWException( 'Title is null' );
1842  }
1843  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
1844  }
1845 
1855  public function addWikiTextWithTitle( $text, Title $title, $linestart = true ) {
1856  wfDeprecated( __METHOD__, '1.32' );
1857  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/false, /*interface*/false );
1858  }
1859 
1870  function addWikiTextTitleTidy( $text, Title $title, $linestart = true ) {
1871  wfDeprecated( __METHOD__, '1.32' );
1872  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
1873  }
1874 
1883  public function addWikiTextTidy( $text, $linestart = true ) {
1884  wfDeprecated( __METHOD__, '1.32' );
1885  $title = $this->getTitle();
1886  if ( !$title ) {
1887  throw new MWException( 'Title is null' );
1888  }
1889  $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
1890  }
1891 
1911  public function addWikiTextTitle( $text, Title $title, $linestart,
1912  $tidy = false, $interface = false
1913  ) {
1914  wfDeprecated( __METHOD__, '1.32' );
1915  return $this->addWikiTextTitleInternal( $text, $title, $linestart, $tidy, $interface );
1916  }
1917 
1934  private function addWikiTextTitleInternal(
1935  $text, Title $title, $linestart, $tidy, $interface, $wrapperClass = null
1936  ) {
1937  $parserOutput = $this->parseInternal(
1938  $text, $title, $linestart, $tidy, $interface, /*language*/null
1939  );
1940 
1941  $this->addParserOutput( $parserOutput, [
1942  'enableSectionEditLinks' => false,
1943  'wrapperDivClass' => $wrapperClass ?? '',
1944  ] );
1945  }
1946 
1955  public function addParserOutputMetadata( ParserOutput $parserOutput ) {
1956  $this->mLanguageLinks =
1957  array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() );
1958  $this->addCategoryLinks( $parserOutput->getCategories() );
1959  $this->setIndicators( $parserOutput->getIndicators() );
1960  $this->mNewSectionLink = $parserOutput->getNewSection();
1961  $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1962 
1963  if ( !$parserOutput->isCacheable() ) {
1964  $this->enableClientCache( false );
1965  }
1966  $this->mNoGallery = $parserOutput->getNoGallery();
1967  $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1968  $this->addModules( $parserOutput->getModules() );
1969  $this->addModuleScripts( $parserOutput->getModuleScripts() );
1970  $this->addModuleStyles( $parserOutput->getModuleStyles() );
1971  $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1972  $this->mPreventClickjacking = $this->mPreventClickjacking
1973  || $parserOutput->preventClickjacking();
1974 
1975  // Template versioning...
1976  foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1977  if ( isset( $this->mTemplateIds[$ns] ) ) {
1978  $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1979  } else {
1980  $this->mTemplateIds[$ns] = $dbks;
1981  }
1982  }
1983  // File versioning...
1984  foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1985  $this->mImageTimeKeys[$dbk] = $data;
1986  }
1987 
1988  // Hooks registered in the object
1989  $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1990  foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1991  list( $hookName, $data ) = $hookInfo;
1992  if ( isset( $parserOutputHooks[$hookName] ) ) {
1993  $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1994  }
1995  }
1996 
1997  // Enable OOUI if requested via ParserOutput
1998  if ( $parserOutput->getEnableOOUI() ) {
1999  $this->enableOOUI();
2000  }
2001 
2002  // Include parser limit report
2003  if ( !$this->limitReportJSData ) {
2004  $this->limitReportJSData = $parserOutput->getLimitReportJSData();
2005  }
2006 
2007  // Link flags are ignored for now, but may in the future be
2008  // used to mark individual language links.
2009  $linkFlags = [];
2010  // Avoid PHP 7.1 warning of passing $this by reference
2011  $outputPage = $this;
2012  Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
2013  Hooks::runWithoutAbort( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] );
2014 
2015  // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
2016  // so that extensions may modify ParserOutput to toggle TOC.
2017  // This cannot be moved to addParserOutputText because that is not
2018  // called by EditPage for Preview.
2019  if ( $parserOutput->getTOCHTML() ) {
2020  $this->mEnableTOC = true;
2021  }
2022  }
2023 
2032  public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) {
2033  $this->addParserOutputText( $parserOutput, $poOptions );
2034 
2035  $this->addModules( $parserOutput->getModules() );
2036  $this->addModuleScripts( $parserOutput->getModuleScripts() );
2037  $this->addModuleStyles( $parserOutput->getModuleStyles() );
2038 
2039  $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
2040  }
2041 
2049  public function addParserOutputText( ParserOutput $parserOutput, $poOptions = [] ) {
2050  $text = $parserOutput->getText( $poOptions );
2051  // Avoid PHP 7.1 warning of passing $this by reference
2052  $outputPage = $this;
2053  Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] );
2054  $this->addHTML( $text );
2055  }
2056 
2063  function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) {
2064  $this->addParserOutputMetadata( $parserOutput );
2065  $this->addParserOutputText( $parserOutput, $poOptions );
2066  }
2067 
2073  public function addTemplate( &$template ) {
2074  $this->addHTML( $template->getHTML() );
2075  }
2076 
2095  public function parse( $text, $linestart = true, $interface = false, $language = null ) {
2096  return $this->parseInternal(
2097  $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, $language
2098  )->getText( [
2099  'enableSectionEditLinks' => false,
2100  ] );
2101  }
2102 
2114  public function parseAsContent( $text, $linestart = true ) {
2115  return $this->parseInternal(
2116  $text, $this->getTitle(), $linestart, /*tidy*/true, /*interface*/false, /*language*/null
2117  )->getText( [
2118  'enableSectionEditLinks' => false,
2119  'wrapperDivClass' => ''
2120  ] );
2121  }
2122 
2135  public function parseAsInterface( $text, $linestart = true ) {
2136  return $this->parseInternal(
2137  $text, $this->getTitle(), $linestart, /*tidy*/true, /*interface*/true, /*language*/null
2138  )->getText( [
2139  'enableSectionEditLinks' => false,
2140  'wrapperDivClass' => ''
2141  ] );
2142  }
2143 
2158  public function parseInlineAsInterface( $text, $linestart = true ) {
2159  return Parser::stripOuterParagraph(
2160  $this->parseAsInterface( $text, $linestart )
2161  );
2162  }
2163 
2177  public function parseInline( $text, $linestart = true, $interface = false ) {
2178  $parsed = $this->parseInternal(
2179  $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, /*language*/null
2180  )->getText( [
2181  'enableSectionEditLinks' => false,
2182  'wrapperDivClass' => '', /* no wrapper div */
2183  ] );
2184  return Parser::stripOuterParagraph( $parsed );
2185  }
2186 
2201  private function parseInternal( $text, $title, $linestart, $tidy, $interface, $language ) {
2202  global $wgParser;
2203 
2204  if ( is_null( $title ) ) {
2205  throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
2206  }
2207 
2208  $popts = $this->parserOptions();
2209  $oldTidy = $popts->setTidy( $tidy );
2210  $oldInterface = $popts->setInterfaceMessage( (bool)$interface );
2211 
2212  if ( $language !== null ) {
2213  $oldLang = $popts->setTargetLanguage( $language );
2214  }
2215 
2216  $parserOutput = $wgParser->getFreshParser()->parse(
2217  $text, $title, $popts,
2218  $linestart, true, $this->mRevisionId
2219  );
2220 
2221  $popts->setTidy( $oldTidy );
2222  $popts->setInterfaceMessage( $oldInterface );
2223 
2224  if ( $language !== null ) {
2225  $popts->setTargetLanguage( $oldLang );
2226  }
2227 
2228  return $parserOutput;
2229  }
2230 
2236  public function setCdnMaxage( $maxage ) {
2237  $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2238  }
2239 
2249  public function lowerCdnMaxage( $maxage ) {
2250  $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2251  $this->setCdnMaxage( $this->mCdnMaxage );
2252  }
2253 
2266  public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2267  $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
2268  $maxTTL = $maxTTL ?: $this->getConfig()->get( 'SquidMaxage' );
2269 
2270  if ( $mtime === null || $mtime === false ) {
2271  return $minTTL; // entity does not exist
2272  }
2273 
2274  $age = MWTimestamp::time() - wfTimestamp( TS_UNIX, $mtime );
2275  $adaptiveTTL = max( 0.9 * $age, $minTTL );
2276  $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2277 
2278  $this->lowerCdnMaxage( (int)$adaptiveTTL );
2279  }
2280 
2288  public function enableClientCache( $state ) {
2289  return wfSetVar( $this->mEnableClientCache, $state );
2290  }
2291 
2297  function getCacheVaryCookies() {
2298  if ( self::$cacheVaryCookies === null ) {
2299  $config = $this->getConfig();
2300  self::$cacheVaryCookies = array_values( array_unique( array_merge(
2301  SessionManager::singleton()->getVaryCookies(),
2302  [
2303  'forceHTTPS',
2304  ],
2305  $config->get( 'CacheVaryCookies' )
2306  ) ) );
2307  Hooks::run( 'GetCacheVaryCookies', [ $this, &self::$cacheVaryCookies ] );
2308  }
2309  return self::$cacheVaryCookies;
2310  }
2311 
2318  function haveCacheVaryCookies() {
2319  $request = $this->getRequest();
2320  foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2321  if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
2322  wfDebug( __METHOD__ . ": found $cookieName\n" );
2323  return true;
2324  }
2325  }
2326  wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
2327  return false;
2328  }
2329 
2338  public function addVaryHeader( $header, array $option = null ) {
2339  if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2340  $this->mVaryHeader[$header] = [];
2341  }
2342  if ( !is_array( $option ) ) {
2343  $option = [];
2344  }
2345  $this->mVaryHeader[$header] =
2346  array_unique( array_merge( $this->mVaryHeader[$header], $option ) );
2347  }
2348 
2355  public function getVaryHeader() {
2356  // If we vary on cookies, let's make sure it's always included here too.
2357  if ( $this->getCacheVaryCookies() ) {
2358  $this->addVaryHeader( 'Cookie' );
2359  }
2360 
2361  foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2362  $this->addVaryHeader( $header, $options );
2363  }
2364  return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2365  }
2366 
2372  public function addLinkHeader( $header ) {
2373  $this->mLinkHeader[] = $header;
2374  }
2375 
2381  public function getLinkHeader() {
2382  if ( !$this->mLinkHeader ) {
2383  return false;
2384  }
2385 
2386  return 'Link: ' . implode( ',', $this->mLinkHeader );
2387  }
2388 
2396  public function getKeyHeader() {
2397  wfDeprecated( '$wgUseKeyHeader', '1.32' );
2398 
2399  $cvCookies = $this->getCacheVaryCookies();
2400 
2401  $cookiesOption = [];
2402  foreach ( $cvCookies as $cookieName ) {
2403  $cookiesOption[] = 'param=' . $cookieName;
2404  }
2405  $this->addVaryHeader( 'Cookie', $cookiesOption );
2406 
2407  foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2408  $this->addVaryHeader( $header, $options );
2409  }
2410 
2411  $headers = [];
2412  foreach ( $this->mVaryHeader as $header => $option ) {
2413  $newheader = $header;
2414  if ( is_array( $option ) && count( $option ) > 0 ) {
2415  $newheader .= ';' . implode( ';', $option );
2416  }
2417  $headers[] = $newheader;
2418  }
2419  $key = 'Key: ' . implode( ',', $headers );
2420 
2421  return $key;
2422  }
2423 
2431  private function addAcceptLanguage() {
2432  $title = $this->getTitle();
2433  if ( !$title instanceof Title ) {
2434  return;
2435  }
2436 
2437  $lang = $title->getPageLanguage();
2438  if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2439  $variants = $lang->getVariants();
2440  $aloption = [];
2441  foreach ( $variants as $variant ) {
2442  if ( $variant === $lang->getCode() ) {
2443  continue;
2444  }
2445 
2446  // XXX Note that this code is not strictly correct: we
2447  // do a case-insensitive match in
2448  // LanguageConverter::getHeaderVariant() while the
2449  // (abandoned, draft) spec for the `Key` header only
2450  // allows case-sensitive matches. To match the logic
2451  // in LanguageConverter::getHeaderVariant() we should
2452  // also be looking at fallback variants and deprecated
2453  // mediawiki-internal codes, as well as BCP 47
2454  // normalized forms.
2455 
2456  $aloption[] = "substr=$variant";
2457 
2458  // IE and some other browsers use BCP 47 standards in their Accept-Language header,
2459  // like "zh-CN" or "zh-Hant". We should handle these too.
2460  $variantBCP47 = LanguageCode::bcp47( $variant );
2461  if ( $variantBCP47 !== $variant ) {
2462  $aloption[] = "substr=$variantBCP47";
2463  }
2464  }
2465  $this->addVaryHeader( 'Accept-Language', $aloption );
2466  }
2467  }
2468 
2479  public function preventClickjacking( $enable = true ) {
2480  $this->mPreventClickjacking = $enable;
2481  }
2482 
2488  public function allowClickjacking() {
2489  $this->mPreventClickjacking = false;
2490  }
2491 
2498  public function getPreventClickjacking() {
2499  return $this->mPreventClickjacking;
2500  }
2501 
2509  public function getFrameOptions() {
2510  $config = $this->getConfig();
2511  if ( $config->get( 'BreakFrames' ) ) {
2512  return 'DENY';
2513  } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2514  return $config->get( 'EditPageFrameOptions' );
2515  }
2516  return false;
2517  }
2518 
2522  public function sendCacheControl() {
2523  $response = $this->getRequest()->response();
2524  $config = $this->getConfig();
2525 
2526  $this->addVaryHeader( 'Cookie' );
2527  $this->addAcceptLanguage();
2528 
2529  # don't serve compressed data to clients who can't handle it
2530  # maintain different caches for logged-in users and non-logged in ones
2531  $response->header( $this->getVaryHeader() );
2532 
2533  if ( $config->get( 'UseKeyHeader' ) ) {
2534  $response->header( $this->getKeyHeader() );
2535  }
2536 
2537  if ( $this->mEnableClientCache ) {
2538  if (
2539  $config->get( 'UseSquid' ) &&
2540  !$response->hasCookies() &&
2541  !SessionManager::getGlobalSession()->isPersistent() &&
2542  !$this->isPrintable() &&
2543  $this->mCdnMaxage != 0 &&
2544  !$this->haveCacheVaryCookies()
2545  ) {
2546  if ( $config->get( 'UseESI' ) ) {
2547  # We'll purge the proxy cache explicitly, but require end user agents
2548  # to revalidate against the proxy on each visit.
2549  # Surrogate-Control controls our CDN, Cache-Control downstream caches
2550  wfDebug( __METHOD__ .
2551  ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
2552  # start with a shorter timeout for initial testing
2553  # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
2554  $response->header(
2555  "Surrogate-Control: max-age={$config->get( 'SquidMaxage' )}" .
2556  "+{$this->mCdnMaxage}, content=\"ESI/1.0\""
2557  );
2558  $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
2559  } else {
2560  # We'll purge the proxy cache for anons explicitly, but require end user agents
2561  # to revalidate against the proxy on each visit.
2562  # IMPORTANT! The CDN needs to replace the Cache-Control header with
2563  # Cache-Control: s-maxage=0, must-revalidate, max-age=0
2564  wfDebug( __METHOD__ .
2565  ": local proxy caching; {$this->mLastModified} **", 'private' );
2566  # start with a shorter timeout for initial testing
2567  # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2568  $response->header( "Cache-Control: " .
2569  "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2570  }
2571  } else {
2572  # We do want clients to cache if they can, but they *must* check for updates
2573  # on revisiting the page.
2574  wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **", 'private' );
2575  $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2576  $response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2577  }
2578  if ( $this->mLastModified ) {
2579  $response->header( "Last-Modified: {$this->mLastModified}" );
2580  }
2581  } else {
2582  wfDebug( __METHOD__ . ": no caching **", 'private' );
2583 
2584  # In general, the absence of a last modified header should be enough to prevent
2585  # the client from using its cache. We send a few other things just to make sure.
2586  $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2587  $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2588  $response->header( 'Pragma: no-cache' );
2589  }
2590  }
2591 
2597  public function loadSkinModules( $sk ) {
2598  foreach ( $sk->getDefaultModules() as $group => $modules ) {
2599  if ( $group === 'styles' ) {
2600  foreach ( $modules as $key => $moduleMembers ) {
2601  $this->addModuleStyles( $moduleMembers );
2602  }
2603  } else {
2604  $this->addModules( $modules );
2605  }
2606  }
2607  }
2608 
2619  public function output( $return = false ) {
2620  if ( $this->mDoNothing ) {
2621  return $return ? '' : null;
2622  }
2623 
2624  $response = $this->getRequest()->response();
2625  $config = $this->getConfig();
2626 
2627  if ( $this->mRedirect != '' ) {
2628  # Standards require redirect URLs to be absolute
2629  $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
2630 
2631  $redirect = $this->mRedirect;
2632  $code = $this->mRedirectCode;
2633 
2634  if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2635  if ( $code == '301' || $code == '303' ) {
2636  if ( !$config->get( 'DebugRedirects' ) ) {
2637  $response->statusHeader( $code );
2638  }
2639  $this->mLastModified = wfTimestamp( TS_RFC2822 );
2640  }
2641  if ( $config->get( 'VaryOnXFP' ) ) {
2642  $this->addVaryHeader( 'X-Forwarded-Proto' );
2643  }
2644  $this->sendCacheControl();
2645 
2646  $response->header( "Content-Type: text/html; charset=utf-8" );
2647  if ( $config->get( 'DebugRedirects' ) ) {
2648  $url = htmlspecialchars( $redirect );
2649  print "<!DOCTYPE html>\n<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2650  print "<p>Location: <a href=\"$url\">$url</a></p>\n";
2651  print "</body>\n</html>\n";
2652  } else {
2653  $response->header( 'Location: ' . $redirect );
2654  }
2655  }
2656 
2657  return $return ? '' : null;
2658  } elseif ( $this->mStatusCode ) {
2659  $response->statusHeader( $this->mStatusCode );
2660  }
2661 
2662  # Buffer output; final headers may depend on later processing
2663  ob_start();
2664 
2665  $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2666  $response->header( 'Content-language: ' .
2667  MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2668 
2669  if ( !$this->mArticleBodyOnly ) {
2670  $sk = $this->getSkin();
2671  }
2672 
2673  $linkHeader = $this->getLinkHeader();
2674  if ( $linkHeader ) {
2675  $response->header( $linkHeader );
2676  }
2677 
2678  // Prevent framing, if requested
2679  $frameOptions = $this->getFrameOptions();
2680  if ( $frameOptions ) {
2681  $response->header( "X-Frame-Options: $frameOptions" );
2682  }
2683 
2685 
2686  if ( $this->mArticleBodyOnly ) {
2687  echo $this->mBodytext;
2688  } else {
2689  // Enable safe mode if requested (T152169)
2690  if ( $this->getRequest()->getBool( 'safemode' ) ) {
2691  $this->disallowUserJs();
2692  }
2693 
2694  $sk = $this->getSkin();
2695  $this->loadSkinModules( $sk );
2696 
2697  MWDebug::addModules( $this );
2698 
2699  // Avoid PHP 7.1 warning of passing $this by reference
2700  $outputPage = $this;
2701  // Hook that allows last minute changes to the output page, e.g.
2702  // adding of CSS or Javascript by extensions.
2703  Hooks::runWithoutAbort( 'BeforePageDisplay', [ &$outputPage, &$sk ] );
2704 
2705  try {
2706  $sk->outputPage();
2707  } catch ( Exception $e ) {
2708  ob_end_clean(); // bug T129657
2709  throw $e;
2710  }
2711  }
2712 
2713  try {
2714  // This hook allows last minute changes to final overall output by modifying output buffer
2715  Hooks::runWithoutAbort( 'AfterFinalPageOutput', [ $this ] );
2716  } catch ( Exception $e ) {
2717  ob_end_clean(); // bug T129657
2718  throw $e;
2719  }
2720 
2721  $this->sendCacheControl();
2722 
2723  if ( $return ) {
2724  return ob_get_clean();
2725  } else {
2726  ob_end_flush();
2727  return null;
2728  }
2729  }
2730 
2741  public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2742  $this->setPageTitle( $pageTitle );
2743  if ( $htmlTitle !== false ) {
2744  $this->setHTMLTitle( $htmlTitle );
2745  }
2746  $this->setRobotPolicy( 'noindex,nofollow' );
2747  $this->setArticleRelated( false );
2748  $this->enableClientCache( false );
2749  $this->mRedirect = '';
2750  $this->clearSubtitle();
2751  $this->clearHTML();
2752  }
2753 
2766  public function showErrorPage( $title, $msg, $params = [] ) {
2767  if ( !$title instanceof Message ) {
2768  $title = $this->msg( $title );
2769  }
2770 
2771  $this->prepareErrorPage( $title );
2772 
2773  if ( $msg instanceof Message ) {
2774  if ( $params !== [] ) {
2775  trigger_error( 'Argument ignored: $params. The message parameters argument '
2776  . 'is discarded when the $msg argument is a Message object instead of '
2777  . 'a string.', E_USER_NOTICE );
2778  }
2779  $this->addHTML( $msg->parseAsBlock() );
2780  } else {
2781  $this->addWikiMsgArray( $msg, $params );
2782  }
2783 
2784  $this->returnToMain();
2785  }
2786 
2793  public function showPermissionsErrorPage( array $errors, $action = null ) {
2794  foreach ( $errors as $key => $error ) {
2795  $errors[$key] = (array)$error;
2796  }
2797 
2798  // For some action (read, edit, create and upload), display a "login to do this action"
2799  // error if all of the following conditions are met:
2800  // 1. the user is not logged in
2801  // 2. the only error is insufficient permissions (i.e. no block or something else)
2802  // 3. the error can be avoided simply by logging in
2803  if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2804  && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2805  && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2806  && ( User::groupHasPermission( 'user', $action )
2807  || User::groupHasPermission( 'autoconfirmed', $action ) )
2808  ) {
2809  $displayReturnto = null;
2810 
2811  # Due to T34276, if a user does not have read permissions,
2812  # $this->getTitle() will just give Special:Badtitle, which is
2813  # not especially useful as a returnto parameter. Use the title
2814  # from the request instead, if there was one.
2815  $request = $this->getRequest();
2816  $returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2817  if ( $action == 'edit' ) {
2818  $msg = 'whitelistedittext';
2819  $displayReturnto = $returnto;
2820  } elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2821  $msg = 'nocreatetext';
2822  } elseif ( $action == 'upload' ) {
2823  $msg = 'uploadnologintext';
2824  } else { # Read
2825  $msg = 'loginreqpagetext';
2826  $displayReturnto = Title::newMainPage();
2827  }
2828 
2829  $query = [];
2830 
2831  if ( $returnto ) {
2832  $query['returnto'] = $returnto->getPrefixedText();
2833 
2834  if ( !$request->wasPosted() ) {
2835  $returntoquery = $request->getValues();
2836  unset( $returntoquery['title'] );
2837  unset( $returntoquery['returnto'] );
2838  unset( $returntoquery['returntoquery'] );
2839  $query['returntoquery'] = wfArrayToCgi( $returntoquery );
2840  }
2841  }
2842  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
2843  $loginLink = $linkRenderer->makeKnownLink(
2844  SpecialPage::getTitleFor( 'Userlogin' ),
2845  $this->msg( 'loginreqlink' )->text(),
2846  [],
2847  $query
2848  );
2849 
2850  $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2851  $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() );
2852 
2853  # Don't return to a page the user can't read otherwise
2854  # we'll end up in a pointless loop
2855  if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
2856  $this->returnToMain( null, $displayReturnto );
2857  }
2858  } else {
2859  $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2860  $this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) );
2861  }
2862  }
2863 
2870  public function versionRequired( $version ) {
2871  $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2872 
2873  $this->addWikiMsg( 'versionrequiredtext', $version );
2874  $this->returnToMain();
2875  }
2876 
2884  public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2885  if ( $action == null ) {
2886  $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2887  } else {
2888  $action_desc = $this->msg( "action-$action" )->plain();
2889  $text = $this->msg(
2890  'permissionserrorstext-withaction',
2891  count( $errors ),
2892  $action_desc
2893  )->plain() . "\n\n";
2894  }
2895 
2896  if ( count( $errors ) > 1 ) {
2897  $text .= '<ul class="permissions-errors">' . "\n";
2898 
2899  foreach ( $errors as $error ) {
2900  $text .= '<li>';
2901  $text .= $this->msg( ...$error )->plain();
2902  $text .= "</li>\n";
2903  }
2904  $text .= '</ul>';
2905  } else {
2906  $text .= "<div class=\"permissions-errors\">\n" .
2907  $this->msg( ...reset( $errors ) )->plain() .
2908  "\n</div>";
2909  }
2910 
2911  return $text;
2912  }
2913 
2923  public function showLagWarning( $lag ) {
2924  $config = $this->getConfig();
2925  if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2926  $lag = floor( $lag ); // floor to avoid nano seconds to display
2927  $message = $lag < $config->get( 'SlaveLagCritical' )
2928  ? 'lag-warn-normal'
2929  : 'lag-warn-high';
2930  $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2931  $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2932  }
2933  }
2934 
2941  public function showFatalError( $message ) {
2942  $this->prepareErrorPage( $this->msg( 'internalerror' ) );
2943 
2944  $this->addHTML( $message );
2945  }
2946 
2950  public function showUnexpectedValueError( $name, $val ) {
2951  wfDeprecated( __METHOD__, '1.32' );
2952  $this->showFatalError( $this->msg( 'unexpected', $name, $val )->escaped() );
2953  }
2954 
2958  public function showFileCopyError( $old, $new ) {
2959  wfDeprecated( __METHOD__, '1.32' );
2960  $this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->escaped() );
2961  }
2962 
2966  public function showFileRenameError( $old, $new ) {
2967  wfDeprecated( __METHOD__, '1.32' );
2968  $this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->escpaed() );
2969  }
2970 
2974  public function showFileDeleteError( $name ) {
2975  wfDeprecated( __METHOD__, '1.32' );
2976  $this->showFatalError( $this->msg( 'filedeleteerror', $name )->escaped() );
2977  }
2978 
2982  public function showFileNotFoundError( $name ) {
2983  wfDeprecated( __METHOD__, '1.32' );
2984  $this->showFatalError( $this->msg( 'filenotfound', $name )->escaped() );
2985  }
2986 
2995  public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2996  $linkRenderer = MediaWikiServices::getInstance()
2997  ->getLinkRendererFactory()->createFromLegacyOptions( $options );
2998  $link = $this->msg( 'returnto' )->rawParams(
2999  $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped();
3000  $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
3001  }
3002 
3011  public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
3012  if ( $returnto == null ) {
3013  $returnto = $this->getRequest()->getText( 'returnto' );
3014  }
3015 
3016  if ( $returntoquery == null ) {
3017  $returntoquery = $this->getRequest()->getText( 'returntoquery' );
3018  }
3019 
3020  if ( $returnto === '' ) {
3021  $returnto = Title::newMainPage();
3022  }
3023 
3024  if ( is_object( $returnto ) ) {
3025  $titleObj = $returnto;
3026  } else {
3027  $titleObj = Title::newFromText( $returnto );
3028  }
3029  // We don't want people to return to external interwiki. That
3030  // might potentially be used as part of a phishing scheme
3031  if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
3032  $titleObj = Title::newMainPage();
3033  }
3034 
3035  $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
3036  }
3037 
3038  private function getRlClientContext() {
3039  if ( !$this->rlClientContext ) {
3040  $query = ResourceLoader::makeLoaderQuery(
3041  [], // modules; not relevant
3042  $this->getLanguage()->getCode(),
3043  $this->getSkin()->getSkinName(),
3044  $this->getUser()->isLoggedIn() ? $this->getUser()->getName() : null,
3045  null, // version; not relevant
3046  ResourceLoader::inDebugMode(),
3047  null, // only; not relevant
3048  $this->isPrintable(),
3049  $this->getRequest()->getBool( 'handheld' )
3050  );
3051  $this->rlClientContext = new ResourceLoaderContext(
3052  $this->getResourceLoader(),
3053  new FauxRequest( $query )
3054  );
3055  if ( $this->contentOverrideCallbacks ) {
3056  $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
3057  $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) {
3058  foreach ( $this->contentOverrideCallbacks as $callback ) {
3059  $content = $callback( $title );
3060  if ( $content !== null ) {
3062  if ( strpos( $text, '</script>' ) !== false ) {
3063  // Proactively replace this so that we can display a message
3064  // to the user, instead of letting it go to Html::inlineScript(),
3065  // where it would be considered a server-side issue.
3066  $titleFormatted = $title->getPrefixedText();
3068  Xml::encodeJsCall( 'mw.log.error', [
3069  "Cannot preview $titleFormatted due to script-closing tag."
3070  ] )
3071  );
3072  }
3073  return $content;
3074  }
3075  }
3076  return null;
3077  } );
3078  }
3079  }
3080  return $this->rlClientContext;
3081  }
3082 
3094  public function getRlClient() {
3095  if ( !$this->rlClient ) {
3096  $context = $this->getRlClientContext();
3097  $rl = $this->getResourceLoader();
3098  $this->addModules( [
3099  'user',
3100  'user.options',
3101  'user.tokens',
3102  ] );
3103  $this->addModuleStyles( [
3104  'site.styles',
3105  'noscript',
3106  'user.styles',
3107  ] );
3108  $this->getSkin()->setupSkinUserCss( $this );
3109 
3110  // Prepare exempt modules for buildExemptModules()
3111  $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
3112  $exemptStates = [];
3113  $moduleStyles = $this->getModuleStyles( /*filter*/ true );
3114 
3115  // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
3116  // Separate user-specific batch for improved cache-hit ratio.
3117  $userBatch = [ 'user.styles', 'user' ];
3118  $siteBatch = array_diff( $moduleStyles, $userBatch );
3119  $dbr = wfGetDB( DB_REPLICA );
3122 
3123  // Filter out modules handled by buildExemptModules()
3124  $moduleStyles = array_filter( $moduleStyles,
3125  function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
3126  $module = $rl->getModule( $name );
3127  if ( $module ) {
3128  $group = $module->getGroup();
3129  if ( isset( $exemptGroups[$group] ) ) {
3130  $exemptStates[$name] = 'ready';
3131  if ( !$module->isKnownEmpty( $context ) ) {
3132  // E.g. Don't output empty <styles>
3133  $exemptGroups[$group][] = $name;
3134  }
3135  return false;
3136  }
3137  }
3138  return true;
3139  }
3140  );
3141  $this->rlExemptStyleModules = $exemptGroups;
3142 
3143  $rlClient = new ResourceLoaderClientHtml( $context, [
3144  'target' => $this->getTarget(),
3145  'nonce' => $this->getCSPNonce(),
3146  // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
3147  // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
3148  // modules enqueud for loading on this page is filtered to just those.
3149  // However, to make sure we also apply the restriction to dynamic dependencies and
3150  // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
3151  // StartupModule so that the client-side registry will not contain any restricted
3152  // modules either. (T152169, T185303)
3153  'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
3155  ) ? '1' : null,
3156  ] );
3157  $rlClient->setConfig( $this->getJSVars() );
3158  $rlClient->setModules( $this->getModules( /*filter*/ true ) );
3159  $rlClient->setModuleStyles( $moduleStyles );
3160  $rlClient->setModuleScripts( $this->getModuleScripts( /*filter*/ true ) );
3161  $rlClient->setExemptStates( $exemptStates );
3162  $this->rlClient = $rlClient;
3163  }
3164  return $this->rlClient;
3165  }
3166 
3172  public function headElement( Skin $sk, $includeStyle = true ) {
3173  $userdir = $this->getLanguage()->getDir();
3174  $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
3175 
3176  $pieces = [];
3177  $pieces[] = Html::htmlHeader( Sanitizer::mergeAttributes(
3178  $this->getRlClient()->getDocumentAttributes(),
3180  ) );
3181  $pieces[] = Html::openElement( 'head' );
3182 
3183  if ( $this->getHTMLTitle() == '' ) {
3184  $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3185  }
3186 
3187  if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) {
3188  // Add <meta charset="UTF-8">
3189  // This should be before <title> since it defines the charset used by
3190  // text including the text inside <title>.
3191  // The spec recommends defining XHTML5's charset using the XML declaration
3192  // instead of meta.
3193  // Our XML declaration is output by Html::htmlHeader.
3194  // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
3195  // https://html.spec.whatwg.org/multipage/semantics.html#charset
3196  $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
3197  }
3198 
3199  $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
3200  $pieces[] = $this->getRlClient()->getHeadHtml();
3201  $pieces[] = $this->buildExemptModules();
3202  $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
3203  $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
3204 
3205  // Use an IE conditional comment to serve the script only to old IE
3206  $pieces[] = '<!--[if lt IE 9]>' .
3209  [ 'html5shiv' ],
3211  [ 'sync' => true ],
3212  $this->getCSPNonce()
3213  ) .
3214  '<![endif]-->';
3215 
3216  $pieces[] = Html::closeElement( 'head' );
3217 
3218  $bodyClasses = $this->mAdditionalBodyClasses;
3219  $bodyClasses[] = 'mediawiki';
3220 
3221  # Classes for LTR/RTL directionality support
3222  $bodyClasses[] = $userdir;
3223  $bodyClasses[] = "sitedir-$sitedir";
3224 
3225  $underline = $this->getUser()->getOption( 'underline' );
3226  if ( $underline < 2 ) {
3227  // The following classes can be used here:
3228  // * mw-underline-always
3229  // * mw-underline-never
3230  $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' );
3231  }
3232 
3233  if ( $this->getLanguage()->capitalizeAllNouns() ) {
3234  # A <body> class is probably not the best way to do this . . .
3235  $bodyClasses[] = 'capitalize-all-nouns';
3236  }
3237 
3238  // Parser feature migration class
3239  // The idea is that this will eventually be removed, after the wikitext
3240  // which requires it is cleaned up.
3241  $bodyClasses[] = 'mw-hide-empty-elt';
3242 
3243  $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
3244  $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
3245  $bodyClasses[] =
3246  'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
3247 
3248  $bodyAttrs = [];
3249  // While the implode() is not strictly needed, it's used for backwards compatibility
3250  // (this used to be built as a string and hooks likely still expect that).
3251  $bodyAttrs['class'] = implode( ' ', $bodyClasses );
3252 
3253  // Allow skins and extensions to add body attributes they need
3254  $sk->addToBodyAttributes( $this, $bodyAttrs );
3255  Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
3256 
3257  $pieces[] = Html::openElement( 'body', $bodyAttrs );
3258 
3259  return self::combineWrappedStrings( $pieces );
3260  }
3261 
3267  public function getResourceLoader() {
3268  if ( is_null( $this->mResourceLoader ) ) {
3269  $this->mResourceLoader = new ResourceLoader(
3270  $this->getConfig(),
3271  LoggerFactory::getInstance( 'resourceloader' )
3272  );
3273  }
3274  return $this->mResourceLoader;
3275  }
3276 
3285  public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
3286  // Apply 'target' and 'origin' filters
3287  $modules = $this->filterModules( (array)$modules, null, $only );
3288 
3290  $this->getRlClientContext(),
3291  $modules,
3292  $only,
3293  $extraQuery,
3294  $this->getCSPNonce()
3295  );
3296  }
3297 
3304  protected static function combineWrappedStrings( array $chunks ) {
3305  // Filter out empty values
3306  $chunks = array_filter( $chunks, 'strlen' );
3307  return WrappedString::join( "\n", $chunks );
3308  }
3309 
3316  public function getBottomScripts() {
3317  $chunks = [];
3318  $chunks[] = $this->getRlClient()->getBodyHtml();
3319 
3320  // Legacy non-ResourceLoader scripts
3321  $chunks[] = $this->mScripts;
3322 
3323  if ( $this->limitReportJSData ) {
3324  $chunks[] = ResourceLoader::makeInlineScript(
3325  ResourceLoader::makeConfigSetScript(
3326  [ 'wgPageParseReport' => $this->limitReportJSData ]
3327  ),
3328  $this->getCSPNonce()
3329  );
3330  }
3331 
3332  return self::combineWrappedStrings( $chunks );
3333  }
3334 
3341  public function getJsConfigVars() {
3342  return $this->mJsConfigVars;
3343  }
3344 
3351  public function addJsConfigVars( $keys, $value = null ) {
3352  if ( is_array( $keys ) ) {
3353  foreach ( $keys as $key => $value ) {
3354  $this->mJsConfigVars[$key] = $value;
3355  }
3356  return;
3357  }
3358 
3359  $this->mJsConfigVars[$keys] = $value;
3360  }
3361 
3371  public function getJSVars() {
3372  $curRevisionId = 0;
3373  $articleId = 0;
3374  $canonicalSpecialPageName = false; # T23115
3375  $services = MediaWikiServices::getInstance();
3376 
3377  $title = $this->getTitle();
3378  $ns = $title->getNamespace();
3379  $canonicalNamespace = MWNamespace::exists( $ns )
3381  : $title->getNsText();
3382 
3383  $sk = $this->getSkin();
3384  // Get the relevant title so that AJAX features can use the correct page name
3385  // when making API requests from certain special pages (T36972).
3386  $relevantTitle = $sk->getRelevantTitle();
3387  $relevantUser = $sk->getRelevantUser();
3388 
3389  if ( $ns == NS_SPECIAL ) {
3390  list( $canonicalSpecialPageName, /*...*/ ) =
3391  $services->getSpecialPageFactory()->
3392  resolveAlias( $title->getDBkey() );
3393  } elseif ( $this->canUseWikiPage() ) {
3394  $wikiPage = $this->getWikiPage();
3395  $curRevisionId = $wikiPage->getLatest();
3396  $articleId = $wikiPage->getId();
3397  }
3398 
3399  $lang = $title->getPageViewLanguage();
3400 
3401  // Pre-process information
3402  $separatorTransTable = $lang->separatorTransformTable();
3403  $separatorTransTable = $separatorTransTable ?: [];
3404  $compactSeparatorTransTable = [
3405  implode( "\t", array_keys( $separatorTransTable ) ),
3406  implode( "\t", $separatorTransTable ),
3407  ];
3408  $digitTransTable = $lang->digitTransformTable();
3409  $digitTransTable = $digitTransTable ?: [];
3410  $compactDigitTransTable = [
3411  implode( "\t", array_keys( $digitTransTable ) ),
3412  implode( "\t", $digitTransTable ),
3413  ];
3414 
3415  $user = $this->getUser();
3416 
3417  $vars = [
3418  'wgCanonicalNamespace' => $canonicalNamespace,
3419  'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3420  'wgNamespaceNumber' => $title->getNamespace(),
3421  'wgPageName' => $title->getPrefixedDBkey(),
3422  'wgTitle' => $title->getText(),
3423  'wgCurRevisionId' => $curRevisionId,
3424  'wgRevisionId' => (int)$this->getRevisionId(),
3425  'wgArticleId' => $articleId,
3426  'wgIsArticle' => $this->isArticle(),
3427  'wgIsRedirect' => $title->isRedirect(),
3428  'wgAction' => Action::getActionName( $this->getContext() ),
3429  'wgUserName' => $user->isAnon() ? null : $user->getName(),
3430  'wgUserGroups' => $user->getEffectiveGroups(),
3431  'wgCategories' => $this->getCategories(),
3432  'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3433  'wgPageContentLanguage' => $lang->getCode(),
3434  'wgPageContentModel' => $title->getContentModel(),
3435  'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3436  'wgDigitTransformTable' => $compactDigitTransTable,
3437  'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3438  'wgMonthNames' => $lang->getMonthNamesArray(),
3439  'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
3440  'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3441  'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3442  'wgRequestId' => WebRequest::getRequestId(),
3443  'wgCSPNonce' => $this->getCSPNonce(),
3444  ];
3445 
3446  if ( $user->isLoggedIn() ) {
3447  $vars['wgUserId'] = $user->getId();
3448  $vars['wgUserEditCount'] = $user->getEditCount();
3449  $userReg = $user->getRegistration();
3450  $vars['wgUserRegistration'] = $userReg ? wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3451  // Get the revision ID of the oldest new message on the user's talk
3452  // page. This can be used for constructing new message alerts on
3453  // the client side.
3454  $vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
3455  }
3456 
3457  $contLang = $services->getContentLanguage();
3458  if ( $contLang->hasVariants() ) {
3459  $vars['wgUserVariant'] = $contLang->getPreferredVariant();
3460  }
3461  // Same test as SkinTemplate
3462  $vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user )
3463  && ( $title->exists() || $title->quickUserCan( 'create', $user ) );
3464 
3465  $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle
3466  && $relevantTitle->quickUserCan( 'edit', $user )
3467  && ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) );
3468 
3469  foreach ( $title->getRestrictionTypes() as $type ) {
3470  // Following keys are set in $vars:
3471  // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
3472  $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3473  }
3474 
3475  if ( $title->isMainPage() ) {
3476  $vars['wgIsMainPage'] = true;
3477  }
3478 
3479  if ( $this->mRedirectedFrom ) {
3480  $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3481  }
3482 
3483  if ( $relevantUser ) {
3484  $vars['wgRelevantUserName'] = $relevantUser->getName();
3485  }
3486 
3487  // Allow extensions to add their custom variables to the mw.config map.
3488  // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3489  // page-dependant but site-wide (without state).
3490  // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3491  Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3492 
3493  // Merge in variables from addJsConfigVars last
3494  return array_merge( $vars, $this->getJsConfigVars() );
3495  }
3496 
3506  public function userCanPreview() {
3507  $request = $this->getRequest();
3508  if (
3509  $request->getVal( 'action' ) !== 'submit' ||
3510  !$request->wasPosted()
3511  ) {
3512  return false;
3513  }
3514 
3515  $user = $this->getUser();
3516 
3517  if ( !$user->isLoggedIn() ) {
3518  // Anons have predictable edit tokens
3519  return false;
3520  }
3521  if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3522  return false;
3523  }
3524 
3525  $title = $this->getTitle();
3526  $errors = $title->getUserPermissionsErrors( 'edit', $user );
3527  if ( count( $errors ) !== 0 ) {
3528  return false;
3529  }
3530 
3531  return true;
3532  }
3533 
3537  public function getHeadLinksArray() {
3538  global $wgVersion;
3539 
3540  $tags = [];
3541  $config = $this->getConfig();
3542 
3543  $canonicalUrl = $this->mCanonicalUrl;
3544 
3545  $tags['meta-generator'] = Html::element( 'meta', [
3546  'name' => 'generator',
3547  'content' => "MediaWiki $wgVersion",
3548  ] );
3549 
3550  if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3551  // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
3552  // fallbacks should come before the primary value so we need to reverse the array.
3553  foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
3554  $tags["meta-referrer-$i"] = Html::element( 'meta', [
3555  'name' => 'referrer',
3556  'content' => $policy,
3557  ] );
3558  }
3559  }
3560 
3561  $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3562  if ( $p !== 'index,follow' ) {
3563  // http://www.robotstxt.org/wc/meta-user.html
3564  // Only show if it's different from the default robots policy
3565  $tags['meta-robots'] = Html::element( 'meta', [
3566  'name' => 'robots',
3567  'content' => $p,
3568  ] );
3569  }
3570 
3571  foreach ( $this->mMetatags as $tag ) {
3572  if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) {
3573  $a = 'http-equiv';
3574  $tag[0] = substr( $tag[0], 5 );
3575  } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) {
3576  $a = 'property';
3577  } else {
3578  $a = 'name';
3579  }
3580  $tagName = "meta-{$tag[0]}";
3581  if ( isset( $tags[$tagName] ) ) {
3582  $tagName .= $tag[1];
3583  }
3584  $tags[$tagName] = Html::element( 'meta',
3585  [
3586  $a => $tag[0],
3587  'content' => $tag[1]
3588  ]
3589  );
3590  }
3591 
3592  foreach ( $this->mLinktags as $tag ) {
3593  $tags[] = Html::element( 'link', $tag );
3594  }
3595 
3596  # Universal edit button
3597  if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3598  $user = $this->getUser();
3599  if ( $this->getTitle()->quickUserCan( 'edit', $user )
3600  && ( $this->getTitle()->exists() ||
3601  $this->getTitle()->quickUserCan( 'create', $user ) )
3602  ) {
3603  // Original UniversalEditButton
3604  $msg = $this->msg( 'edit' )->text();
3605  $tags['universal-edit-button'] = Html::element( 'link', [
3606  'rel' => 'alternate',
3607  'type' => 'application/x-wiki',
3608  'title' => $msg,
3609  'href' => $this->getTitle()->getEditURL(),
3610  ] );
3611  // Alternate edit link
3612  $tags['alternative-edit'] = Html::element( 'link', [
3613  'rel' => 'edit',
3614  'title' => $msg,
3615  'href' => $this->getTitle()->getEditURL(),
3616  ] );
3617  }
3618  }
3619 
3620  # Generally the order of the favicon and apple-touch-icon links
3621  # should not matter, but Konqueror (3.5.9 at least) incorrectly
3622  # uses whichever one appears later in the HTML source. Make sure
3623  # apple-touch-icon is specified first to avoid this.
3624  if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3625  $tags['apple-touch-icon'] = Html::element( 'link', [
3626  'rel' => 'apple-touch-icon',
3627  'href' => $config->get( 'AppleTouchIcon' )
3628  ] );
3629  }
3630 
3631  if ( $config->get( 'Favicon' ) !== false ) {
3632  $tags['favicon'] = Html::element( 'link', [
3633  'rel' => 'shortcut icon',
3634  'href' => $config->get( 'Favicon' )
3635  ] );
3636  }
3637 
3638  # OpenSearch description link
3639  $tags['opensearch'] = Html::element( 'link', [
3640  'rel' => 'search',
3641  'type' => 'application/opensearchdescription+xml',
3642  'href' => wfScript( 'opensearch_desc' ),
3643  'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3644  ] );
3645 
3646  # Real Simple Discovery link, provides auto-discovery information
3647  # for the MediaWiki API (and potentially additional custom API
3648  # support such as WordPress or Twitter-compatible APIs for a
3649  # blogging extension, etc)
3650  $tags['rsd'] = Html::element( 'link', [
3651  'rel' => 'EditURI',
3652  'type' => 'application/rsd+xml',
3653  // Output a protocol-relative URL here if $wgServer is protocol-relative.
3654  // Whether RSD accepts relative or protocol-relative URLs is completely
3655  // undocumented, though.
3656  'href' => wfExpandUrl( wfAppendQuery(
3657  wfScript( 'api' ),
3658  [ 'action' => 'rsd' ] ),
3660  ),
3661  ] );
3662 
3663  # Language variants
3664  if ( !$config->get( 'DisableLangConversion' ) ) {
3665  $lang = $this->getTitle()->getPageLanguage();
3666  if ( $lang->hasVariants() ) {
3667  $variants = $lang->getVariants();
3668  foreach ( $variants as $variant ) {
3669  $tags["variant-$variant"] = Html::element( 'link', [
3670  'rel' => 'alternate',
3671  'hreflang' => LanguageCode::bcp47( $variant ),
3672  'href' => $this->getTitle()->getLocalURL(
3673  [ 'variant' => $variant ] )
3674  ]
3675  );
3676  }
3677  # x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3678  $tags["variant-x-default"] = Html::element( 'link', [
3679  'rel' => 'alternate',
3680  'hreflang' => 'x-default',
3681  'href' => $this->getTitle()->getLocalURL() ] );
3682  }
3683  }
3684 
3685  # Copyright
3686  if ( $this->copyrightUrl !== null ) {
3687  $copyright = $this->copyrightUrl;
3688  } else {
3689  $copyright = '';
3690  if ( $config->get( 'RightsPage' ) ) {
3691  $copy = Title::newFromText( $config->get( 'RightsPage' ) );
3692 
3693  if ( $copy ) {
3694  $copyright = $copy->getLocalURL();
3695  }
3696  }
3697 
3698  if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3699  $copyright = $config->get( 'RightsUrl' );
3700  }
3701  }
3702 
3703  if ( $copyright ) {
3704  $tags['copyright'] = Html::element( 'link', [
3705  'rel' => 'license',
3706  'href' => $copyright ]
3707  );
3708  }
3709 
3710  # Feeds
3711  if ( $config->get( 'Feed' ) ) {
3712  $feedLinks = [];
3713 
3714  foreach ( $this->getSyndicationLinks() as $format => $link ) {
3715  # Use the page name for the title. In principle, this could
3716  # lead to issues with having the same name for different feeds
3717  # corresponding to the same page, but we can't avoid that at
3718  # this low a level.
3719 
3720  $feedLinks[] = $this->feedLink(
3721  $format,
3722  $link,
3723  # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3724  $this->msg(
3725  "page-{$format}-feed", $this->getTitle()->getPrefixedText()
3726  )->text()
3727  );
3728  }
3729 
3730  # Recent changes feed should appear on every page (except recentchanges,
3731  # that would be redundant). Put it after the per-page feed to avoid
3732  # changing existing behavior. It's still available, probably via a
3733  # menu in your browser. Some sites might have a different feed they'd
3734  # like to promote instead of the RC feed (maybe like a "Recent New Articles"
3735  # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3736  # If so, use it instead.
3737  $sitename = $config->get( 'Sitename' );
3738  if ( $config->get( 'OverrideSiteFeed' ) ) {
3739  foreach ( $config->get( 'OverrideSiteFeed' ) as $type => $feedUrl ) {
3740  // Note, this->feedLink escapes the url.
3741  $feedLinks[] = $this->feedLink(
3742  $type,
3743  $feedUrl,
3744  $this->msg( "site-{$type}-feed", $sitename )->text()
3745  );
3746  }
3747  } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3748  $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3749  foreach ( $config->get( 'AdvertisedFeedTypes' ) as $format ) {
3750  $feedLinks[] = $this->feedLink(
3751  $format,
3752  $rctitle->getLocalURL( [ 'feed' => $format ] ),
3753  # For grep: 'site-rss-feed', 'site-atom-feed'
3754  $this->msg( "site-{$format}-feed", $sitename )->text()
3755  );
3756  }
3757  }
3758 
3759  # Allow extensions to change the list pf feeds. This hook is primarily for changing,
3760  # manipulating or removing existing feed tags. If you want to add new feeds, you should
3761  # use OutputPage::addFeedLink() instead.
3762  Hooks::run( 'AfterBuildFeedLinks', [ &$feedLinks ] );
3763 
3764  $tags += $feedLinks;
3765  }
3766 
3767  # Canonical URL
3768  if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3769  if ( $canonicalUrl !== false ) {
3770  $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3771  } else {
3772  if ( $this->isArticleRelated() ) {
3773  // This affects all requests where "setArticleRelated" is true. This is
3774  // typically all requests that show content (query title, curid, oldid, diff),
3775  // and all wikipage actions (edit, delete, purge, info, history etc.).
3776  // It does not apply to File pages and Special pages.
3777  // 'history' and 'info' actions address page metadata rather than the page
3778  // content itself, so they may not be canonicalized to the view page url.
3779  // TODO: this ought to be better encapsulated in the Action class.
3780  $action = Action::getActionName( $this->getContext() );
3781  if ( in_array( $action, [ 'history', 'info' ] ) ) {
3782  $query = "action={$action}";
3783  } else {
3784  $query = '';
3785  }
3786  $canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3787  } else {
3788  $reqUrl = $this->getRequest()->getRequestURL();
3789  $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3790  }
3791  }
3792  }
3793  if ( $canonicalUrl !== false ) {
3794  $tags[] = Html::element( 'link', [
3795  'rel' => 'canonical',
3796  'href' => $canonicalUrl
3797  ] );
3798  }
3799 
3800  // Allow extensions to add, remove and/or otherwise manipulate these links
3801  // If you want only to *add* <head> links, please use the addHeadItem()
3802  // (or addHeadItems() for multiple items) method instead.
3803  // This hook is provided as a last resort for extensions to modify these
3804  // links before the output is sent to client.
3805  Hooks::run( 'OutputPageAfterGetHeadLinksArray', [ &$tags, $this ] );
3806 
3807  return $tags;
3808  }
3809 
3818  private function feedLink( $type, $url, $text ) {
3819  return Html::element( 'link', [
3820  'rel' => 'alternate',
3821  'type' => "application/$type+xml",
3822  'title' => $text,
3823  'href' => $url ]
3824  );
3825  }
3826 
3836  public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3837  $options = [];
3838  if ( $media ) {
3839  $options['media'] = $media;
3840  }
3841  if ( $condition ) {
3842  $options['condition'] = $condition;
3843  }
3844  if ( $dir ) {
3845  $options['dir'] = $dir;
3846  }
3847  $this->styles[$style] = $options;
3848  }
3849 
3857  public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3858  if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3859  # If wanted, and the interface is right-to-left, flip the CSS
3860  $style_css = CSSJanus::transform( $style_css, true, false );
3861  }
3862  $this->mInlineStyles .= Html::inlineStyle( $style_css );
3863  }
3864 
3870  protected function buildExemptModules() {
3871  $chunks = [];
3872  // Things that go after the ResourceLoaderDynamicStyles marker
3873  $append = [];
3874 
3875  // We want site, private and user styles to override dynamically added styles from
3876  // general modules, but we want dynamically added styles to override statically added
3877  // style modules. So the order has to be:
3878  // - page style modules (formatted by ResourceLoaderClientHtml::getHeadHtml())
3879  // - dynamically loaded styles (added by mw.loader before ResourceLoaderDynamicStyles)
3880  // - ResourceLoaderDynamicStyles marker
3881  // - site/private/user styles
3882 
3883  // Add legacy styles added through addStyle()/addInlineStyle() here
3884  $chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3885 
3886  $chunks[] = Html::element(
3887  'meta',
3888  [ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3889  );
3890 
3891  $separateReq = [ 'site.styles', 'user.styles' ];
3892  foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3893  // Combinable modules
3894  $chunks[] = $this->makeResourceLoaderLink(
3895  array_diff( $moduleNames, $separateReq ),
3897  );
3898 
3899  foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) {
3900  // These require their own dedicated request in order to support "@import"
3901  // syntax, which is incompatible with concatenation. (T147667, T37562)
3902  $chunks[] = $this->makeResourceLoaderLink( $name,
3904  );
3905  }
3906  }
3907 
3908  return self::combineWrappedStrings( array_merge( $chunks, $append ) );
3909  }
3910 
3914  public function buildCssLinksArray() {
3915  $links = [];
3916 
3917  foreach ( $this->styles as $file => $options ) {
3918  $link = $this->styleLink( $file, $options );
3919  if ( $link ) {
3920  $links[$file] = $link;
3921  }
3922  }
3923  return $links;
3924  }
3925 
3933  protected function styleLink( $style, array $options ) {
3934  if ( isset( $options['dir'] ) ) {
3935  if ( $this->getLanguage()->getDir() != $options['dir'] ) {
3936  return '';
3937  }
3938  }
3939 
3940  if ( isset( $options['media'] ) ) {
3941  $media = self::transformCssMedia( $options['media'] );
3942  if ( is_null( $media ) ) {
3943  return '';
3944  }
3945  } else {
3946  $media = 'all';
3947  }
3948 
3949  if ( substr( $style, 0, 1 ) == '/' ||
3950  substr( $style, 0, 5 ) == 'http:' ||
3951  substr( $style, 0, 6 ) == 'https:' ) {
3952  $url = $style;
3953  } else {
3954  $config = $this->getConfig();
3955  // Append file hash as query parameter
3956  $url = self::transformResourcePath(
3957  $config,
3958  $config->get( 'StylePath' ) . '/' . $style
3959  );
3960  }
3961 
3962  $link = Html::linkedStyle( $url, $media );
3963 
3964  if ( isset( $options['condition'] ) ) {
3965  $condition = htmlspecialchars( $options['condition'] );
3966  $link = "<!--[if $condition]>$link<![endif]-->";
3967  }
3968  return $link;
3969  }
3970 
3992  public static function transformResourcePath( Config $config, $path ) {
3993  global $IP;
3994 
3995  $localDir = $IP;
3996  $remotePathPrefix = $config->get( 'ResourceBasePath' );
3997  if ( $remotePathPrefix === '' ) {
3998  // The configured base path is required to be empty string for
3999  // wikis in the domain root
4000  $remotePath = '/';
4001  } else {
4002  $remotePath = $remotePathPrefix;
4003  }
4004  if ( strpos( $path, $remotePath ) !== 0 || substr( $path, 0, 2 ) === '//' ) {
4005  // - Path is outside wgResourceBasePath, ignore.
4006  // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib.
4007  return $path;
4008  }
4009  // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here.
4010  // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth
4011  // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath)
4012  // which is not expected to be in wgResourceBasePath on CDNs. (T155146)
4013  $uploadPath = $config->get( 'UploadPath' );
4014  if ( strpos( $path, $uploadPath ) === 0 ) {
4015  $localDir = $config->get( 'UploadDirectory' );
4016  $remotePathPrefix = $remotePath = $uploadPath;
4017  }
4018 
4019  $path = RelPath::getRelativePath( $path, $remotePath );
4020  return self::transformFilePath( $remotePathPrefix, $localDir, $path );
4021  }
4022 
4034  public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
4035  $hash = md5_file( "$localPath/$file" );
4036  if ( $hash === false ) {
4037  wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
4038  $hash = '';
4039  }
4040  return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
4041  }
4042 
4050  public static function transformCssMedia( $media ) {
4051  global $wgRequest;
4052 
4053  // https://www.w3.org/TR/css3-mediaqueries/#syntax
4054  $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
4055 
4056  // Switch in on-screen display for media testing
4057  $switches = [
4058  'printable' => 'print',
4059  'handheld' => 'handheld',
4060  ];
4061  foreach ( $switches as $switch => $targetMedia ) {
4062  if ( $wgRequest->getBool( $switch ) ) {
4063  if ( $media == $targetMedia ) {
4064  $media = '';
4065  } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
4066  /* This regex will not attempt to understand a comma-separated media_query_list
4067  *
4068  * Example supported values for $media:
4069  * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
4070  * Example NOT supported value for $media:
4071  * '3d-glasses, screen, print and resolution > 90dpi'
4072  *
4073  * If it's a print request, we never want any kind of screen stylesheets
4074  * If it's a handheld request (currently the only other choice with a switch),
4075  * we don't want simple 'screen' but we might want screen queries that
4076  * have a max-width or something, so we'll pass all others on and let the
4077  * client do the query.
4078  */
4079  if ( $targetMedia == 'print' || $media == 'screen' ) {
4080  return null;
4081  }
4082  }
4083  }
4084  }
4085 
4086  return $media;
4087  }
4088 
4095  public function addWikiMsg( /*...*/ ) {
4096  $args = func_get_args();
4097  $name = array_shift( $args );
4098  $this->addWikiMsgArray( $name, $args );
4099  }
4100 
4109  public function addWikiMsgArray( $name, $args ) {
4110  $this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
4111  }
4112 
4138  public function wrapWikiMsg( $wrap /*, ...*/ ) {
4139  $msgSpecs = func_get_args();
4140  array_shift( $msgSpecs );
4141  $msgSpecs = array_values( $msgSpecs );
4142  $s = $wrap;
4143  foreach ( $msgSpecs as $n => $spec ) {
4144  if ( is_array( $spec ) ) {
4145  $args = $spec;
4146  $name = array_shift( $args );
4147  if ( isset( $args['options'] ) ) {
4148  unset( $args['options'] );
4149  wfDeprecated(
4150  'Adding "options" to ' . __METHOD__ . ' is no longer supported',
4151  '1.20'
4152  );
4153  }
4154  } else {
4155  $args = [];
4156  $name = $spec;
4157  }
4158  $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
4159  }
4160  $this->addWikiText( $s );
4161  }
4162 
4168  public function isTOCEnabled() {
4169  return $this->mEnableTOC;
4170  }
4171 
4178  public function enableSectionEditLinks( $flag = true ) {
4179  wfDeprecated( __METHOD__, '1.31' );
4180  }
4181 
4187  public function sectionEditLinksEnabled() {
4188  wfDeprecated( __METHOD__, '1.31' );
4189  return true;
4190  }
4191 
4199  public static function setupOOUI( $skinName = 'default', $dir = 'ltr' ) {
4201  $theme = $themes[$skinName] ?? $themes['default'];
4202  // For example, 'OOUI\WikimediaUITheme'.
4203  $themeClass = "OOUI\\{$theme}Theme";
4204  OOUI\Theme::setSingleton( new $themeClass() );
4205  OOUI\Element::setDefaultDir( $dir );
4206  }
4207 
4214  public function enableOOUI() {
4215  self::setupOOUI(
4216  strtolower( $this->getSkin()->getSkinName() ),
4217  $this->getLanguage()->getDir()
4218  );
4219  $this->addModuleStyles( [
4220  'oojs-ui-core.styles',
4221  'oojs-ui.styles.indicators',
4222  'oojs-ui.styles.textures',
4223  'mediawiki.widgets.styles',
4224  'oojs-ui.styles.icons-content',
4225  'oojs-ui.styles.icons-alerts',
4226  'oojs-ui.styles.icons-interactions',
4227  ] );
4228  }
4229 
4239  public function getCSPNonce() {
4241  return false;
4242  }
4243  if ( $this->CSPNonce === null ) {
4244  // XXX It might be expensive to generate randomness
4245  // on every request, on Windows.
4246  $rand = random_bytes( 15 );
4247  $this->CSPNonce = base64_encode( $rand );
4248  }
4249  return $this->CSPNonce;
4250  }
4251 
4252 }
Action\getActionName
static getActionName(IContextSource $context)
Get the action that will be executed, not necessarily the one passed passed through the "action" requ...
Definition: Action.php:124
ParserOutput\getEnableOOUI
getEnableOOUI()
Definition: ParserOutput.php:581
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:63
ResourceLoaderContext
Object passed around to modules which contains information about the state of a specific loader reque...
Definition: ResourceLoaderContext.php:32
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:244
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:280
$template
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition: hooks.txt:813
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:28
ResourceLoaderClientHtml
Bootstrap a ResourceLoader client on an HTML page.
Definition: ResourceLoaderClientHtml.php:29
PROTO_CANONICAL
const PROTO_CANONICAL
Definition: Defines.php:223
ParserOutput
Definition: ParserOutput.php:25
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
Article\formatRobotPolicy
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition: Article.php:1029
ResourceLoaderContext\newDummyContext
static newDummyContext()
Return a dummy ResourceLoaderContext object suitable for passing into things that don't "really" need...
Definition: ResourceLoaderContext.php:138
$context
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2675
$wgParser
$wgParser
Definition: Setup.php:913
ResourceLoaderModule\TYPE_COMBINED
const TYPE_COMBINED
Definition: ResourceLoaderModule.php:39
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:34
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:33
wfSetVar
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
Definition: GlobalFunctions.php:1673
captcha-old.count
count
Definition: captcha-old.py:249
ResourceLoaderModule\ORIGIN_USER_SITEWIDE
const ORIGIN_USER_SITEWIDE
Definition: ResourceLoaderModule.php:55
ContextSource\msg
msg( $key)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:168
ResourceLoaderClientHtml\setModuleScripts
setModuleScripts(array $modules)
Ensure the scripts of one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:110
Title\newMainPage
static newMainPage()
Create a new Title for the Main Page.
Definition: Title.php:597
ParserOutput\getModules
getModules()
Definition: ParserOutput.php:522
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1954
$wgVersion
$wgVersion
MediaWiki version number.
Definition: DefaultSettings.php:74
ParserOutput\getJsConfigVars
getJsConfigVars()
Definition: ParserOutput.php:538
wfUrlencode
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
Definition: GlobalFunctions.php:331
ParserOptions\newFromAnon
static newFromAnon()
Get a ParserOptions object for an anonymous user.
Definition: ParserOptions.php:1001
$params
$params
Definition: styleTest.css.php:44
Html\htmlHeader
static htmlHeader(array $attribs=[])
Constructs the opening html-tag with necessary doctypes depending on global variables.
Definition: Html.php:956
Skin\addToBodyAttributes
addToBodyAttributes( $out, &$bodyAttrs)
This will be called by OutputPage::headElement when it is creating the "<body>" tag,...
Definition: Skin.php:487
ResourceLoaderClientHtml\setExemptStates
setExemptStates(array $states)
Set state of special modules that are handled by the caller manually.
Definition: ResourceLoaderClientHtml.php:121
$s
$s
Definition: mergeMessageFileList.php:187
$linkRenderer
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing after in associative array form before processing starts Return false to skip default processing and return $ret $linkRenderer
Definition: hooks.txt:2036
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:82
ContextSource\canUseWikiPage
canUseWikiPage()
Check whether a WikiPage object can be get with getWikiPage().
Definition: ContextSource.php:91
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1145
$res
$res
Definition: database.txt:21
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:71
$resourceLoader
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext such as when responding to a resource loader request or generating HTML output & $resourceLoader
Definition: hooks.txt:2675
IExpiringStore\TTL_MINUTE
const TTL_MINUTE
Definition: IExpiringStore.php:34
User\groupHasPermission
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition: User.php:5014
ParserOutput\getHeadItems
getHeadItems()
Definition: ParserOutput.php:518
ContextSource\getUser
getUser()
Definition: ContextSource.php:120
ContextSource\getTitle
getTitle()
Definition: ContextSource.php:79
Skin\getHtmlElementAttributes
getHtmlElementAttributes()
Return values for <html> element.
Definition: Skin.php:471
LinkBatch\setArray
setArray( $array)
Set the link list to a given 2-d array First key is the namespace, second is the DB key,...
Definition: LinkBatch.php:100
LinkCache\getSelectFields
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:213
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
File\exists
exists()
Returns true if file exists in the repository.
Definition: File.php:896
$dbr
$dbr
Definition: testCompression.php:50
ResourceLoaderClientHtml\setConfig
setConfig(array $vars)
Set mw.config variables.
Definition: ResourceLoaderClientHtml.php:80
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:128
wfAppendQuery
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Definition: GlobalFunctions.php:460
ParserOutput\getModuleStyles
getModuleStyles()
Definition: ParserOutput.php:530
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:310
Xml\encodeJsCall
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:679
ContentSecurityPolicy\sendHeaders
static sendHeaders(IContextSource $context)
Send CSP headers based on wiki config.
Definition: ContentSecurityPolicy.php:73
Html\isXmlMimeType
static isXmlMimeType( $mimetype)
Determines if the given MIME type is xml.
Definition: Html.php:995
$query
null for the wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1627
Config
Interface for configuration instances.
Definition: Config.php:28
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:53
MediaWiki\Linker\LinkTarget\getNamespace
getNamespace()
Get the namespace index.
File
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:51
ParserOutput\getLimitReportJSData
getLimitReportJSData()
Definition: ParserOutput.php:569
Html\linkedScript
static linkedScript( $url, $nonce=null)
Output a "<script>" tag linking to the given URL, e.g., "<script src=foo.js></script>".
Definition: Html.php:593
$html
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:2036
MWException
MediaWiki exception.
Definition: MWException.php:26
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:964
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1118
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2771
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
ResourceLoaderWikiModule\preloadTitleInfo
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
Definition: ResourceLoaderWikiModule.php:451
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2693
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:29
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:104
$attribs
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition: hooks.txt:2036
$modules
$modules
Definition: HTMLFormElement.php:12
$IP
$IP
Definition: update.php:3
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:222
ContextSource\getSkin
getSkin()
Definition: ContextSource.php:136
ResourceLoaderModule\TYPE_SCRIPTS
const TYPE_SCRIPTS
Definition: ResourceLoaderModule.php:37
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
$code
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition: hooks.txt:813
wfCgiToArray
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
Definition: GlobalFunctions.php:413
$vars
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition: hooks.txt:2270
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
array
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
ParserOutput\getIndicators
getIndicators()
Definition: ParserOutput.php:467
ResourceLoaderModule\getOrigin
getOrigin()
Get this module's origin.
Definition: ResourceLoaderModule.php:123
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:988
string
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:175
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:55
ParserOutput\getLanguageLinks
& getLanguageLinks()
Definition: ParserOutput.php:447
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
Skin\outputPage
outputPage(OutputPage $out=null)
Outputs the HTML generated by other functions.
JavaScriptContent
Content for JavaScript pages.
Definition: JavaScriptContent.php:33
ParserOutput\getModuleScripts
getModuleScripts()
Definition: ParserOutput.php:526
ParserOutput\getTemplateIds
& getTemplateIds()
Definition: ParserOutput.php:495
ParserOutput\getTOCHTML
getTOCHTML()
Definition: ParserOutput.php:554
$request
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2675
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
Html\inlineStyle
static inlineStyle( $contents, $media='all', $attribs=[])
Output a "<style>" tag with the given contents for the given media type (if any).
Definition: Html.php:618
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:573
Skin\getRelevantTitle
getRelevantTitle()
Return the "relevant" title.
Definition: Skin.php:344
ParserOutput\getNewSection
getNewSection()
Definition: ParserOutput.php:678
Hooks\runWithoutAbort
static runWithoutAbort( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:231
Skin\getPageClasses
getPageClasses( $title)
TODO: document.
Definition: Skin.php:442
$e
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2213
wfClearOutputBuffers
wfClearOutputBuffers()
More legible than passing a 'false' parameter to wfResetOutputBuffers():
Definition: GlobalFunctions.php:1816
$value
$value
Definition: styleTest.css.php:49
Skin\getDefaultModules
getDefaultModules()
Defines the ResourceLoader modules that should be added to the skin It is recommended that skins wish...
Definition: Skin.php:176
ParserOptions\newFromContext
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
Definition: ParserOptions.php:1040
Html\inlineScript
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:567
$header
$header
Definition: updateCredits.php:35
ParserOutput\getOutputHooks
getOutputHooks()
Definition: ParserOutput.php:542
ParserOutput\getHideNewSection
getHideNewSection()
Definition: ParserOutput.php:675
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:50
DerivativeResourceLoaderContext
Allows changing specific properties of a context object, without changing the main one.
Definition: DerivativeResourceLoaderContext.php:30
MediaWiki\Linker\LinkTarget\getDBkey
getDBkey()
Get the main part with underscores.
PROTO_RELATIVE
const PROTO_RELATIVE
Definition: Defines.php:221
MWNamespace\exists
static exists( $index)
Returns whether the specified namespace exists.
Definition: MWNamespace.php:182
ParserOutput\getNoGallery
getNoGallery()
Definition: ParserOutput.php:514
ResourceLoaderModule\ORIGIN_CORE_INDIVIDUAL
const ORIGIN_CORE_INDIVIDUAL
Definition: ResourceLoaderModule.php:51
plain
either a plain
Definition: hooks.txt:2097
getSkinThemeMap
static getSkinThemeMap()
Return a map of skin names (in lowercase) to OOUI theme names, defining which theme a given skin shou...
Definition: ResourceLoaderOOUIModule.php:75
$response
this hook is for auditing only $response
Definition: hooks.txt:813
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
ResourceLoaderModule\ORIGIN_ALL
const ORIGIN_ALL
Definition: ResourceLoaderModule.php:61
wfGetAllCallers
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
Definition: GlobalFunctions.php:1536
Content
Base interface for content objects.
Definition: Content.php:34
ContentSecurityPolicy\isNonceRequired
static isNonceRequired(Config $config)
Should we set nonce attribute.
Definition: ContentSecurityPolicy.php:476
ResourceLoaderClientHtml\makeLoad
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicily load or embed modules on a page.
Definition: ResourceLoaderClientHtml.php:415
text
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
Definition: All_system_messages.txt:1267
$args
if( $line===false) $args
Definition: cdb.php:64
ParserOutput\getFileSearchOptions
& getFileSearchOptions()
Definition: ParserOutput.php:503
Title
Represents a title within MediaWiki.
Definition: Title.php:39
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:35
and
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
ContentHandler\getContentText
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
Definition: ContentHandler.php:83
$options
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:2036
ResourceLoaderClientHtml\setModules
setModules(array $modules)
Ensure one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:91
Html\linkedStyle
static linkedStyle( $url, $media='all')
Output a "<link rel=stylesheet>" linking to the given URL for the given media type (if any).
Definition: Html.php:647
ResourceLoaderClientHtml\setModuleStyles
setModuleStyles(array $modules)
Ensure the styles of one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:100
WebRequest\getRequestId
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:275
$path
$path
Definition: NoLocalSettings.php:25
ParserOutput\getCategories
& getCategories()
Definition: ParserOutput.php:459
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
LanguageCode\bcp47
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Definition: LanguageCode.php:182
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:252
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
messages
passed in as a query string parameter to the various URLs constructed here(i.e. $prevlink) $ldel you ll need to handle error messages
Definition: hooks.txt:1329
Skin\getRelevantUser
getRelevantUser()
Return the "relevant" user.
Definition: Skin.php:368
$keys
$keys
Definition: testCompression.php:67
$link
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3090
LoggerFactory
MediaWiki Logger LoggerFactory implements a PSR[0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method. MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances. The "Spi" in MediaWiki\Logger\Spi stands for "service provider interface". An SPI is an API intended to be implemented or extended by a third party. This software design pattern is intended to enable framework extension and replaceable components. It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki. The service provider interface allows the backend logging library to be implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime. This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance. Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
ResourceLoaderModule\TYPE_STYLES
const TYPE_STYLES
Definition: ResourceLoaderModule.php:38
MWDebug\addModules
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition: MWDebug.php:96
$content
$content
Definition: pageupdater.txt:72
CacheTime\isCacheable
isCacheable()
Definition: CacheTime.php:155
ParserOutput\getText
getText( $options=[])
Get the output HTML.
Definition: ParserOutput.php:308
$t
$t
Definition: testCompression.php:69
$services
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition: hooks.txt:2270
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:747
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
captcha-old.output
output
Definition: captcha-old.py:240
Skin\getSkinName
getSkinName()
Definition: Skin.php:155
MediaWikiServices
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:38
wfMessage
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
redirect
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second redirect
Definition: All_system_messages.txt:1267
MWNamespace\getCanonicalName
static getCanonicalName( $index)
Returns the canonical (English) name for a given index.
Definition: MWNamespace.php:255
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:512
ParserOutput\preventClickjacking
preventClickjacking( $flag=null)
Get or set the prevent-clickjacking flag.
Definition: ParserOutput.php:1246
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
Definition: GlobalFunctions.php:368
$type
$type
Definition: testCompression.php:48