wfGetUrlUtils() is also deprecated, but less so, so we can do this first and then properly replace the individual uses with dependency injection in local pieces of work. Also: * Switching Parser::getExternalLinkRel to UrlUtils::matchesDomainList exposed a type error in media.txt where $wgNoFollowDomainExceptions was set to a string (which is invalid) instead of an array. Bug: T319340 Change-Id: Icb512d7241954ee155b64c57f3782b86acfd9a4c
594 lines
19 KiB
PHP
594 lines
19 KiB
PHP
<?php
|
||
/**
|
||
* This program is free software; you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation; either version 2 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License along
|
||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
* http://www.gnu.org/copyleft/gpl.html
|
||
*
|
||
* @file
|
||
*/
|
||
|
||
namespace MediaWiki\Request;
|
||
|
||
use MediaWiki\Config\Config;
|
||
use MediaWiki\HookContainer\HookContainer;
|
||
use MediaWiki\HookContainer\HookRunner;
|
||
use MediaWiki\MainConfigNames;
|
||
use MediaWiki\MediaWikiServices;
|
||
use UnexpectedValueException;
|
||
|
||
/**
|
||
* Handle sending Content-Security-Policy headers
|
||
*
|
||
* @author Copyright 2015–2018 Brian Wolff
|
||
* @see https://www.w3.org/TR/CSP2/
|
||
* @since 1.32
|
||
*/
|
||
class ContentSecurityPolicy {
|
||
public const REPORT_ONLY_MODE = 1;
|
||
public const FULL_MODE = 2;
|
||
|
||
/** @var Config The site configuration object */
|
||
private $mwConfig;
|
||
/** @var WebResponse */
|
||
private $response;
|
||
|
||
/** @var string[] */
|
||
private $extraDefaultSrc = [];
|
||
/** @var string[] */
|
||
private $extraScriptSrc = [];
|
||
/** @var string[] */
|
||
private $extraStyleSrc = [];
|
||
|
||
/** @var HookRunner */
|
||
private $hookRunner;
|
||
|
||
/**
|
||
* @note As a general rule, you would not construct this class directly
|
||
* but use the instance from OutputPage::getCSP()
|
||
* @internal
|
||
* @param WebResponse $response
|
||
* @param Config $mwConfig
|
||
* @param HookContainer $hookContainer
|
||
* @since 1.35 Method signature changed
|
||
*/
|
||
public function __construct(
|
||
WebResponse $response,
|
||
Config $mwConfig,
|
||
HookContainer $hookContainer
|
||
) {
|
||
$this->response = $response;
|
||
$this->mwConfig = $mwConfig;
|
||
$this->hookRunner = new HookRunner( $hookContainer );
|
||
}
|
||
|
||
/**
|
||
* Get the CSP directives for the wiki.
|
||
* @return string[] Array of CSP directives (header name => header value). The array keys will be
|
||
* ContentSecurityPolicy::FULL_MODE and ContentSecurityPolicy::REPORT_ONLY_MODE; they might not
|
||
* be present if the wiki is configured no to use the given type of CSP.
|
||
* @phan-return array{Content-Security-Policy?:string,Content-Security-Policy-Report-Only?:string}
|
||
* @since 1.42
|
||
*/
|
||
public function getDirectives() {
|
||
$cspConfig = $this->mwConfig->get( MainConfigNames::CSPHeader );
|
||
$cspConfigReportOnly = $this->mwConfig->get( MainConfigNames::CSPReportOnlyHeader );
|
||
|
||
$cspPolicy = $this->makeCSPDirectives( $cspConfig, self::FULL_MODE );
|
||
$cspReportOnlyPolicy = $this->makeCSPDirectives( $cspConfigReportOnly, self::REPORT_ONLY_MODE );
|
||
|
||
return array_filter( [
|
||
$this->getHeaderName( self::FULL_MODE ) => $cspPolicy,
|
||
$this->getHeaderName( self::REPORT_ONLY_MODE ) => $cspReportOnlyPolicy,
|
||
] );
|
||
}
|
||
|
||
/**
|
||
* Send CSP headers based on wiki config
|
||
*
|
||
* Main method that callers (OutputPage) are expected to use.
|
||
* As a general rule, you would never call this in an extension unless
|
||
* you have disabled OutputPage and are fully controlling the output.
|
||
*
|
||
* @since 1.35
|
||
*/
|
||
public function sendHeaders() {
|
||
$directives = $this->getDirectives();
|
||
foreach ( $directives as $headerName => $policy ) {
|
||
$this->response->header( "$headerName: $policy" );
|
||
}
|
||
|
||
// This used to insert a <meta> tag here, per advice at
|
||
// https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
|
||
// The goal was to prevent nonce from working after the page hit onready,
|
||
// This would help in old browsers that didn't support nonces, and
|
||
// also assist for Varnish-cached pages which repeat nonces.
|
||
// However, this is incompatible with how ResourceLoader runs code
|
||
// from mw.loader.store, so it was removed.
|
||
}
|
||
|
||
/**
|
||
* @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE
|
||
* @return string Name of http header
|
||
* @throws UnexpectedValueException
|
||
*/
|
||
private function getHeaderName( $reportOnly ) {
|
||
if ( $reportOnly === self::REPORT_ONLY_MODE ) {
|
||
return 'Content-Security-Policy-Report-Only';
|
||
}
|
||
|
||
if ( $reportOnly === self::FULL_MODE ) {
|
||
return 'Content-Security-Policy';
|
||
}
|
||
throw new UnexpectedValueException( "Mode '$reportOnly' not recognised" );
|
||
}
|
||
|
||
/**
|
||
* Determine what CSP policies to set for this page
|
||
*
|
||
* @param array|bool $policyConfig Policy configuration
|
||
* (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
|
||
* @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE
|
||
* @return string Policy directives, or empty string for no policy.
|
||
*/
|
||
private function makeCSPDirectives( $policyConfig, $mode ) {
|
||
if ( $policyConfig === false ) {
|
||
// CSP is disabled
|
||
return '';
|
||
}
|
||
if ( $policyConfig === true ) {
|
||
$policyConfig = [];
|
||
}
|
||
|
||
$mwConfig = $this->mwConfig;
|
||
|
||
if (
|
||
self::isNonceRequired( $mwConfig ) ||
|
||
self::isNonceRequiredArray( [ $policyConfig ] )
|
||
) {
|
||
wfDeprecated( 'wgCSPHeader "useNonces" option', '1.41' );
|
||
}
|
||
|
||
$additionalSelfUrls = $this->getAdditionalSelfUrls();
|
||
$additionalSelfUrlsScript = $this->getAdditionalSelfUrlsScript();
|
||
|
||
// If no default-src is sent at all, it seems browsers (or at least some),
|
||
// interpret that as allow anything, but the spec seems to imply that
|
||
// "data:" and "blob:" should be blocked.
|
||
$defaultSrc = [ '*', 'data:', 'blob:' ];
|
||
|
||
$imgSrc = false;
|
||
$scriptSrc = [ "'unsafe-eval'", "blob:", "'self'" ];
|
||
|
||
$scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript );
|
||
if ( isset( $policyConfig['script-src'] )
|
||
&& is_array( $policyConfig['script-src'] )
|
||
) {
|
||
foreach ( $policyConfig['script-src'] as $src ) {
|
||
$scriptSrc[] = $this->escapeUrlForCSP( $src );
|
||
}
|
||
}
|
||
// Note: default on if unspecified.
|
||
if ( $policyConfig['unsafeFallback'] ?? true ) {
|
||
// unsafe-inline should be ignored on browsers that support 'nonce-foo' sources.
|
||
// Some older versions of firefox don't follow this rule, but new browsers do.
|
||
// (Should be for at least Firefox 40+).
|
||
$scriptSrc[] = "'unsafe-inline'";
|
||
}
|
||
// If default source option set to true or an array of urls,
|
||
// set a restrictive default-src.
|
||
// If set to false, we send a lenient default-src,
|
||
// see the code above where $defaultSrc is set initially.
|
||
if ( isset( $policyConfig['default-src'] )
|
||
&& $policyConfig['default-src'] !== false
|
||
) {
|
||
$defaultSrc = array_merge(
|
||
[ "'self'", 'data:', 'blob:' ],
|
||
$additionalSelfUrls
|
||
);
|
||
if ( is_array( $policyConfig['default-src'] ) ) {
|
||
foreach ( $policyConfig['default-src'] as $src ) {
|
||
$defaultSrc[] = $this->escapeUrlForCSP( $src );
|
||
}
|
||
}
|
||
}
|
||
|
||
if ( $policyConfig['includeCORS'] ?? true ) {
|
||
$CORSUrls = $this->getCORSSources();
|
||
if ( !in_array( '*', $defaultSrc ) ) {
|
||
$defaultSrc = array_merge( $defaultSrc, $CORSUrls );
|
||
}
|
||
// Unlikely to have * in scriptSrc, but doesn't
|
||
// hurt to check.
|
||
if ( !in_array( '*', $scriptSrc ) ) {
|
||
$scriptSrc = array_merge( $scriptSrc, $CORSUrls );
|
||
}
|
||
}
|
||
|
||
$defaultSrc = array_merge( $defaultSrc, $this->extraDefaultSrc );
|
||
$scriptSrc = array_merge( $scriptSrc, $this->extraScriptSrc );
|
||
|
||
$cssSrc = array_merge( $defaultSrc, $this->extraStyleSrc, [ "'unsafe-inline'" ] );
|
||
|
||
$this->hookRunner->onContentSecurityPolicyDefaultSource( $defaultSrc, $policyConfig, $mode );
|
||
$this->hookRunner->onContentSecurityPolicyScriptSource( $scriptSrc, $policyConfig, $mode );
|
||
|
||
if ( isset( $policyConfig['report-uri'] ) && $policyConfig['report-uri'] !== true ) {
|
||
if ( $policyConfig['report-uri'] === false ) {
|
||
$reportUri = false;
|
||
} else {
|
||
$reportUri = $this->escapeUrlForCSP( $policyConfig['report-uri'] );
|
||
}
|
||
} else {
|
||
$reportUri = $this->getReportUri( $mode );
|
||
}
|
||
|
||
// Only send an img-src, if we're sending a restrictive default.
|
||
if ( !is_array( $defaultSrc )
|
||
|| !in_array( '*', $defaultSrc )
|
||
|| !in_array( 'data:', $defaultSrc )
|
||
|| !in_array( 'blob:', $defaultSrc )
|
||
) {
|
||
// A future todo might be to make the allow options only
|
||
// add all the allowed sites to the header, instead of
|
||
// allowing all (Assuming there is a small number of sites).
|
||
// For now, the external image feature disables the limits
|
||
// CSP puts on external images.
|
||
if ( $mwConfig->get( MainConfigNames::AllowExternalImages )
|
||
|| $mwConfig->get( MainConfigNames::AllowExternalImagesFrom )
|
||
) {
|
||
$imgSrc = [ '*', 'data:', 'blob:' ];
|
||
} elseif ( $mwConfig->get( MainConfigNames::EnableImageWhitelist ) ) {
|
||
$whitelist = wfMessage( 'external_image_whitelist' )
|
||
->inContentLanguage()
|
||
->plain();
|
||
if ( preg_match( '/^\s*[^\s#]/m', $whitelist ) ) {
|
||
$imgSrc = [ '*', 'data:', 'blob:' ];
|
||
}
|
||
}
|
||
}
|
||
// Default value 'none'. true is none, false is nothing, string is single directive,
|
||
// array is list.
|
||
if ( !isset( $policyConfig['object-src'] ) || $policyConfig['object-src'] === true ) {
|
||
$objectSrc = [ "'none'" ];
|
||
} else {
|
||
$objectSrc = (array)( $policyConfig['object-src'] ?: [] );
|
||
}
|
||
$objectSrc = array_map( [ $this, 'escapeUrlForCSP' ], $objectSrc );
|
||
|
||
$directives = [];
|
||
if ( $scriptSrc ) {
|
||
$directives[] = 'script-src ' . implode( ' ', array_unique( $scriptSrc ) );
|
||
}
|
||
if ( $defaultSrc ) {
|
||
$directives[] = 'default-src ' . implode( ' ', array_unique( $defaultSrc ) );
|
||
}
|
||
if ( $cssSrc ) {
|
||
$directives[] = 'style-src ' . implode( ' ', array_unique( $cssSrc ) );
|
||
}
|
||
if ( $imgSrc ) {
|
||
$directives[] = 'img-src ' . implode( ' ', array_unique( $imgSrc ) );
|
||
}
|
||
if ( $objectSrc ) {
|
||
$directives[] = 'object-src ' . implode( ' ', $objectSrc );
|
||
}
|
||
if ( $reportUri ) {
|
||
$directives[] = 'report-uri ' . $reportUri;
|
||
}
|
||
|
||
$this->hookRunner->onContentSecurityPolicyDirectives( $directives, $policyConfig, $mode );
|
||
|
||
return implode( '; ', $directives );
|
||
}
|
||
|
||
/**
|
||
* Get the default report uri.
|
||
*
|
||
* @param int $mode self::*_MODE constant.
|
||
* @return string The URI to send reports to.
|
||
* @throws UnexpectedValueException if given invalid mode.
|
||
*/
|
||
private function getReportUri( $mode ) {
|
||
$apiArguments = [
|
||
'action' => 'cspreport',
|
||
'format' => 'json'
|
||
];
|
||
if ( $mode === self::REPORT_ONLY_MODE ) {
|
||
$apiArguments['reportonly'] = '1';
|
||
}
|
||
$reportUri = wfAppendQuery( wfScript( 'api' ), $apiArguments );
|
||
|
||
// Per spec, ';' and ',' must be hex-escaped in report URI
|
||
$reportUri = $this->escapeUrlForCSP( $reportUri );
|
||
return $reportUri;
|
||
}
|
||
|
||
/**
|
||
* Given a url, convert to form needed for CSP.
|
||
*
|
||
* Currently this does either scheme + host, or
|
||
* if protocol relative, just the host. Future versions
|
||
* could potentially preserve some of the path, if its determined
|
||
* that that would be a good idea.
|
||
*
|
||
* @note This does the extra escaping for CSP, but assumes the url
|
||
* has already had normal url escaping applied.
|
||
* @note This discards urls same as server name, as 'self' directive
|
||
* takes care of that.
|
||
* @param string $url
|
||
* @return string|bool Converted url or false on failure
|
||
*/
|
||
private function prepareUrlForCSP( $url ) {
|
||
$result = false;
|
||
if ( preg_match( '/^[a-z][a-z0-9+.-]*:$/i', $url ) ) {
|
||
// A schema source (e.g. blob: or data:)
|
||
return $url;
|
||
}
|
||
$bits = wfGetUrlUtils()->parse( $url );
|
||
if ( !$bits && strpos( $url, '/' ) === false ) {
|
||
// probably something like example.com.
|
||
// try again protocol-relative.
|
||
$url = '//' . $url;
|
||
$bits = wfGetUrlUtils()->parse( $url );
|
||
}
|
||
if ( $bits && isset( $bits['host'] )
|
||
&& $bits['host'] !== $this->mwConfig->get( MainConfigNames::ServerName )
|
||
) {
|
||
$result = $bits['host'];
|
||
if ( $bits['scheme'] !== '' ) {
|
||
$result = $bits['scheme'] . $bits['delimiter'] . $result;
|
||
}
|
||
if ( isset( $bits['port'] ) ) {
|
||
$result .= ':' . $bits['port'];
|
||
}
|
||
$result = $this->escapeUrlForCSP( $result );
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* @return string[] Additional sources for loading scripts from
|
||
*/
|
||
private function getAdditionalSelfUrlsScript() {
|
||
$additionalUrls = [];
|
||
// wgExtensionAssetsPath for ?debug=true mode
|
||
$pathVars = [
|
||
MainConfigNames::LoadScript,
|
||
MainConfigNames::ExtensionAssetsPath,
|
||
MainConfigNames::ResourceBasePath,
|
||
];
|
||
|
||
foreach ( $pathVars as $path ) {
|
||
$url = $this->mwConfig->get( $path );
|
||
$preparedUrl = $this->prepareUrlForCSP( $url );
|
||
if ( $preparedUrl ) {
|
||
$additionalUrls[] = $preparedUrl;
|
||
}
|
||
}
|
||
$RLSources = $this->mwConfig->get( MainConfigNames::ResourceLoaderSources );
|
||
foreach ( $RLSources as $sources ) {
|
||
foreach ( $sources as $value ) {
|
||
$url = $this->prepareUrlForCSP( $value );
|
||
if ( $url ) {
|
||
$additionalUrls[] = $url;
|
||
}
|
||
}
|
||
}
|
||
|
||
return array_unique( $additionalUrls );
|
||
}
|
||
|
||
/**
|
||
* Get additional host names for the wiki (e.g. if static content loaded elsewhere)
|
||
*
|
||
* @note These are general load sources, not script sources
|
||
* @return string[] Array of other urls for wiki (for use in default-src)
|
||
*/
|
||
private function getAdditionalSelfUrls() {
|
||
// XXX on a foreign repo, the included description page can have anything on it,
|
||
// including inline scripts. But nobody does that.
|
||
|
||
// In principle, you can have even more complex configs... (e.g. The urlsByExt option)
|
||
$pathUrls = [];
|
||
$additionalSelfUrls = [];
|
||
|
||
// Future todo: The zone urls should never go into
|
||
// style-src. They should either be only in img-src, or if
|
||
// img-src unspecified they should be in default-src. Similarly,
|
||
// the DescriptionStylesheetUrl only needs to be in style-src
|
||
// (or default-src if style-src unspecified).
|
||
$callback = static function ( $repo, &$urls ) {
|
||
$urls[] = $repo->getZoneUrl( 'public' );
|
||
$urls[] = $repo->getZoneUrl( 'transcoded' );
|
||
$urls[] = $repo->getZoneUrl( 'thumb' );
|
||
$urls[] = $repo->getDescriptionStylesheetUrl();
|
||
};
|
||
$repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
|
||
$localRepo = $repoGroup->getRepo( 'local' );
|
||
$callback( $localRepo, $pathUrls );
|
||
$repoGroup->forEachForeignRepo( $callback, [ &$pathUrls ] );
|
||
|
||
// Globals that might point to a different domain
|
||
$pathGlobals = [
|
||
MainConfigNames::LoadScript,
|
||
MainConfigNames::ExtensionAssetsPath,
|
||
MainConfigNames::StylePath,
|
||
MainConfigNames::ResourceBasePath,
|
||
];
|
||
foreach ( $pathGlobals as $path ) {
|
||
$pathUrls[] = $this->mwConfig->get( $path );
|
||
}
|
||
foreach ( $pathUrls as $path ) {
|
||
$preparedUrl = $this->prepareUrlForCSP( $path );
|
||
if ( $preparedUrl !== false ) {
|
||
$additionalSelfUrls[] = $preparedUrl;
|
||
}
|
||
}
|
||
$RLSources = $this->mwConfig->get( MainConfigNames::ResourceLoaderSources );
|
||
|
||
foreach ( $RLSources as $sources ) {
|
||
foreach ( $sources as $value ) {
|
||
$url = $this->prepareUrlForCSP( $value );
|
||
if ( $url ) {
|
||
$additionalSelfUrls[] = $url;
|
||
}
|
||
}
|
||
}
|
||
|
||
return array_unique( $additionalSelfUrls );
|
||
}
|
||
|
||
/**
|
||
* include domains that are allowed to send us CORS requests.
|
||
*
|
||
* Technically, $wgCrossSiteAJAXdomains lists things that are allowed to talk to us
|
||
* not things that we are allowed to talk to - but if something is allowed to talk to us,
|
||
* then there is a good chance that we should probably be allowed to talk to it.
|
||
*
|
||
* This is configurable with the 'includeCORS' key in the CSP config, and enabled
|
||
* by default.
|
||
* @note CORS domains with single character ('?') wildcards, are not included.
|
||
* @return array Additional hosts
|
||
*/
|
||
private function getCORSSources() {
|
||
$additionalUrls = [];
|
||
$CORSSources = $this->mwConfig->get( MainConfigNames::CrossSiteAJAXdomains );
|
||
foreach ( $CORSSources as $source ) {
|
||
if ( strpos( $source, '?' ) !== false ) {
|
||
// CSP doesn't support single char wildcard
|
||
continue;
|
||
}
|
||
$url = $this->prepareUrlForCSP( $source );
|
||
if ( $url ) {
|
||
$additionalUrls[] = $url;
|
||
}
|
||
}
|
||
return $additionalUrls;
|
||
}
|
||
|
||
/**
|
||
* CSP spec says ',' and ';' are not allowed to appear in urls.
|
||
*
|
||
* @note This assumes that normal escaping has been applied to the url
|
||
* @param string $url URL (or possibly just part of one)
|
||
* @return string
|
||
*/
|
||
private function escapeUrlForCSP( $url ) {
|
||
return str_replace(
|
||
[ ';', ',' ],
|
||
[ '%3B', '%2C' ],
|
||
$url
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Does this browser give false positive reports?
|
||
*
|
||
* Some versions of firefox (40-42) incorrectly report a CSP
|
||
* violation for nonce sources, despite allowing them.
|
||
*
|
||
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1026520
|
||
* @param string $ua User-agent header
|
||
* @return bool
|
||
*/
|
||
public static function falsePositiveBrowser( $ua ) {
|
||
return (bool)preg_match( '!Firefox/4[0-2]\.!', $ua );
|
||
}
|
||
|
||
/**
|
||
* Should we set nonce attribute
|
||
*
|
||
* @param Config $config
|
||
* @return bool
|
||
*/
|
||
public static function isNonceRequired( Config $config ) {
|
||
$configs = [
|
||
$config->get( MainConfigNames::CSPHeader ),
|
||
$config->get( MainConfigNames::CSPReportOnlyHeader ),
|
||
];
|
||
return self::isNonceRequiredArray( $configs );
|
||
}
|
||
|
||
/**
|
||
* Does a specific config require a nonce
|
||
*
|
||
* @param array $configs An array of CSP config arrays
|
||
* @return bool
|
||
*/
|
||
private static function isNonceRequiredArray( array $configs ) {
|
||
foreach ( $configs as $headerConfig ) {
|
||
if (
|
||
is_array( $headerConfig ) &&
|
||
isset( $headerConfig['useNonces'] ) &&
|
||
$headerConfig['useNonces']
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Get the nonce if nonce is in use
|
||
*
|
||
* Not currently supported or implemented.
|
||
*
|
||
* @since 1.35
|
||
* @return false
|
||
*/
|
||
public function getNonce() {
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* If possible you should use a more specific source type then default.
|
||
*
|
||
* So for example, if an extension added a special page that loaded something
|
||
* it might call $this->getOutput()->getCSP()->addDefaultSrc( '*.example.com' );
|
||
*
|
||
* @since 1.35
|
||
* @param string $source Source to add.
|
||
* e.g. blob:, *.example.com, %https://example.com, example.com/foo
|
||
*/
|
||
public function addDefaultSrc( $source ) {
|
||
$this->extraDefaultSrc[] = $this->prepareUrlForCSP( $source );
|
||
}
|
||
|
||
/**
|
||
* So for example, if an extension added a special page that loaded external CSS
|
||
* it might call $this->getOutput()->getCSP()->addStyleSrc( '*.example.com' );
|
||
*
|
||
* @since 1.35
|
||
* @param string $source Source to add.
|
||
* e.g. blob:, *.example.com, %https://example.com, example.com/foo
|
||
*/
|
||
public function addStyleSrc( $source ) {
|
||
$this->extraStyleSrc[] = $this->prepareUrlForCSP( $source );
|
||
}
|
||
|
||
/**
|
||
* So for example, if an extension added a special page that loaded something
|
||
* it might call $this->getOutput()->getCSP()->addScriptSrc( '*.example.com' );
|
||
*
|
||
* @since 1.35
|
||
* @warning Be careful including external scripts, as they can take over accounts.
|
||
* @param string $source Source to add.
|
||
* e.g. blob:, *.example.com, %https://example.com, example.com/foo
|
||
*/
|
||
public function addScriptSrc( $source ) {
|
||
$this->extraScriptSrc[] = $this->prepareUrlForCSP( $source );
|
||
}
|
||
}
|
||
|
||
/** @deprecated class alias since 1.40 */
|
||
class_alias( ContentSecurityPolicy::class, 'ContentSecurityPolicy' );
|