Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.63% covered (warning)
74.63%
150 / 201
55.56% covered (warning)
55.56%
10 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContentSecurityPolicy
75.00% covered (warning)
75.00%
150 / 200
55.56% covered (warning)
55.56%
10 / 18
190.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getDirectives
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 sendHeaders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getHeaderName
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 makeCSPDirectives
89.61% covered (warning)
89.61%
69 / 77
0.00% covered (danger)
0.00%
0 / 1
37.45
 getReportUri
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 prepareUrlForCSP
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
9
 getAdditionalSelfUrlsScript
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 getAdditionalSelfUrls
87.10% covered (warning)
87.10%
27 / 31
0.00% covered (danger)
0.00%
0 / 1
7.11
 getCORSSources
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 escapeUrlForCSP
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 falsePositiveBrowser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isNonceRequired
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 isNonceRequiredArray
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getNonce
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addDefaultSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addStyleSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addScriptSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Request;
22
23use MediaWiki\Config\Config;
24use MediaWiki\HookContainer\HookContainer;
25use MediaWiki\HookContainer\HookRunner;
26use MediaWiki\MainConfigNames;
27use MediaWiki\MediaWikiServices;
28use UnexpectedValueException;
29
30/**
31 * Handle sending Content-Security-Policy headers
32 *
33 * @author Copyright 2015–2018 Brian Wolff
34 * @see https://www.w3.org/TR/CSP2/
35 * @since 1.32
36 */
37class ContentSecurityPolicy {
38    public const REPORT_ONLY_MODE = 1;
39    public const FULL_MODE = 2;
40
41    /** @var Config The site configuration object */
42    private $mwConfig;
43    /** @var WebResponse */
44    private $response;
45
46    /** @var string[] */
47    private $extraDefaultSrc = [];
48    /** @var string[] */
49    private $extraScriptSrc = [];
50    /** @var string[] */
51    private $extraStyleSrc = [];
52
53    /** @var HookRunner */
54    private $hookRunner;
55
56    /**
57     * @note As a general rule, you would not construct this class directly
58     *  but use the instance from OutputPage::getCSP()
59     * @internal
60     * @param WebResponse $response
61     * @param Config $mwConfig
62     * @param HookContainer $hookContainer
63     * @since 1.35 Method signature changed
64     */
65    public function __construct(
66        WebResponse $response,
67        Config $mwConfig,
68        HookContainer $hookContainer
69    ) {
70        $this->response = $response;
71        $this->mwConfig = $mwConfig;
72        $this->hookRunner = new HookRunner( $hookContainer );
73    }
74
75    /**
76     * Get the CSP directives for the wiki.
77     * @return string[] Array of CSP directives (header name => header value). The array keys will be
78     *    ContentSecurityPolicy::FULL_MODE and ContentSecurityPolicy::REPORT_ONLY_MODE; they might not
79     *    be present if the wiki is configured no to use the given type of CSP.
80     * @phan-return array{Content-Security-Policy?:string,Content-Security-Policy-Report-Only?:string}
81     * @since 1.42
82     */
83    public function getDirectives() {
84        $cspConfig = $this->mwConfig->get( MainConfigNames::CSPHeader );
85        $cspConfigReportOnly = $this->mwConfig->get( MainConfigNames::CSPReportOnlyHeader );
86
87        $cspPolicy = $this->makeCSPDirectives( $cspConfig, self::FULL_MODE );
88        $cspReportOnlyPolicy = $this->makeCSPDirectives( $cspConfigReportOnly, self::REPORT_ONLY_MODE );
89
90        return array_filter( [
91            $this->getHeaderName( self::FULL_MODE ) => $cspPolicy,
92            $this->getHeaderName( self::REPORT_ONLY_MODE ) => $cspReportOnlyPolicy,
93        ] );
94    }
95
96    /**
97     * Send CSP headers based on wiki config
98     *
99     * Main method that callers (OutputPage) are expected to use.
100     * As a general rule, you would never call this in an extension unless
101     * you have disabled OutputPage and are fully controlling the output.
102     *
103     * @since 1.35
104     */
105    public function sendHeaders() {
106        $directives = $this->getDirectives();
107        foreach ( $directives as $headerName => $policy ) {
108            $this->response->header( "$headerName$policy" );
109        }
110
111        // This used to insert a <meta> tag here, per advice at
112        // https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
113        // The goal was to prevent nonce from working after the page hit onready,
114        // This would help in old browsers that didn't support nonces, and
115        // also assist for Varnish-cached pages which repeat nonces.
116        // However, this is incompatible with how ResourceLoader runs code
117        // from mw.loader.store, so it was removed.
118    }
119
120    /**
121     * @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE
122     * @return string Name of http header
123     * @throws UnexpectedValueException
124     */
125    private function getHeaderName( $reportOnly ) {
126        if ( $reportOnly === self::REPORT_ONLY_MODE ) {
127            return 'Content-Security-Policy-Report-Only';
128        }
129
130        if ( $reportOnly === self::FULL_MODE ) {
131            return 'Content-Security-Policy';
132        }
133        throw new UnexpectedValueException( "Mode '$reportOnly' not recognised" );
134    }
135
136    /**
137     * Determine what CSP policies to set for this page
138     *
139     * @param array|bool $policyConfig Policy configuration
140     *   (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
141     * @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE
142     * @return string Policy directives, or empty string for no policy.
143     */
144    private function makeCSPDirectives( $policyConfig, $mode ) {
145        if ( $policyConfig === false ) {
146            // CSP is disabled
147            return '';
148        }
149        if ( $policyConfig === true ) {
150            $policyConfig = [];
151        }
152
153        $mwConfig = $this->mwConfig;
154
155        if (
156            self::isNonceRequired( $mwConfig ) ||
157            self::isNonceRequiredArray( [ $policyConfig ] )
158        ) {
159            wfDeprecated( 'wgCSPHeader "useNonces" option', '1.41' );
160        }
161
162        $additionalSelfUrls = $this->getAdditionalSelfUrls();
163        $additionalSelfUrlsScript = $this->getAdditionalSelfUrlsScript();
164
165        // If no default-src is sent at all, it seems browsers (or at least some),
166        // interpret that as allow anything, but the spec seems to imply that
167        // "data:" and "blob:" should be blocked.
168        $defaultSrc = [ '*', 'data:', 'blob:' ];
169
170        $imgSrc = false;
171        $scriptSrc = [ "'unsafe-eval'", "blob:", "'self'" ];
172
173        $scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript );
174        if ( isset( $policyConfig['script-src'] )
175            && is_array( $policyConfig['script-src'] )
176        ) {
177            foreach ( $policyConfig['script-src'] as $src ) {
178                $scriptSrc[] = $this->escapeUrlForCSP( $src );
179            }
180        }
181        // Note: default on if unspecified.
182        if ( $policyConfig['unsafeFallback'] ?? true ) {
183            // unsafe-inline should be ignored on browsers that support 'nonce-foo' sources.
184            // Some older versions of firefox don't follow this rule, but new browsers do.
185            // (Should be for at least Firefox 40+).
186            $scriptSrc[] = "'unsafe-inline'";
187        }
188        // If default source option set to true or an array of urls,
189        // set a restrictive default-src.
190        // If set to false, we send a lenient default-src,
191        // see the code above where $defaultSrc is set initially.
192        if ( isset( $policyConfig['default-src'] )
193            && $policyConfig['default-src'] !== false
194        ) {
195            $defaultSrc = array_merge(
196                [ "'self'", 'data:', 'blob:' ],
197                $additionalSelfUrls
198            );
199            if ( is_array( $policyConfig['default-src'] ) ) {
200                foreach ( $policyConfig['default-src'] as $src ) {
201                    $defaultSrc[] = $this->escapeUrlForCSP( $src );
202                }
203            }
204        }
205
206        if ( $policyConfig['includeCORS'] ?? true ) {
207            $CORSUrls = $this->getCORSSources();
208            if ( !in_array( '*', $defaultSrc ) ) {
209                $defaultSrc = array_merge( $defaultSrc, $CORSUrls );
210            }
211            // Unlikely to have * in scriptSrc, but doesn't
212            // hurt to check.
213            if ( !in_array( '*', $scriptSrc ) ) {
214                $scriptSrc = array_merge( $scriptSrc, $CORSUrls );
215            }
216        }
217
218        $defaultSrc = array_merge( $defaultSrc, $this->extraDefaultSrc );
219        $scriptSrc = array_merge( $scriptSrc, $this->extraScriptSrc );
220
221        $cssSrc = array_merge( $defaultSrc, $this->extraStyleSrc, [ "'unsafe-inline'" ] );
222
223        $this->hookRunner->onContentSecurityPolicyDefaultSource( $defaultSrc, $policyConfig, $mode );
224        $this->hookRunner->onContentSecurityPolicyScriptSource( $scriptSrc, $policyConfig, $mode );
225
226        if ( isset( $policyConfig['report-uri'] ) && $policyConfig['report-uri'] !== true ) {
227            if ( $policyConfig['report-uri'] === false ) {
228                $reportUri = false;
229            } else {
230                $reportUri = $this->escapeUrlForCSP( $policyConfig['report-uri'] );
231            }
232        } else {
233            $reportUri = $this->getReportUri( $mode );
234        }
235
236        // Only send an img-src, if we're sending a restrictive default.
237        if ( !is_array( $defaultSrc )
238            || !in_array( '*', $defaultSrc )
239            || !in_array( 'data:', $defaultSrc )
240            || !in_array( 'blob:', $defaultSrc )
241        ) {
242            // A future todo might be to make the allow options only
243            // add all the allowed sites to the header, instead of
244            // allowing all (Assuming there is a small number of sites).
245            // For now, the external image feature disables the limits
246            // CSP puts on external images.
247            if ( $mwConfig->get( MainConfigNames::AllowExternalImages )
248                || $mwConfig->get( MainConfigNames::AllowExternalImagesFrom )
249            ) {
250                $imgSrc = [ '*', 'data:', 'blob:' ];
251            } elseif ( $mwConfig->get( MainConfigNames::EnableImageWhitelist ) ) {
252                $whitelist = wfMessage( 'external_image_whitelist' )
253                    ->inContentLanguage()
254                    ->plain();
255                if ( preg_match( '/^\s*[^\s#]/m', $whitelist ) ) {
256                    $imgSrc = [ '*', 'data:', 'blob:' ];
257                }
258            }
259        }
260        // Default value 'none'. true is none, false is nothing, string is single directive,
261        // array is list.
262        if ( !isset( $policyConfig['object-src'] ) || $policyConfig['object-src'] === true ) {
263            $objectSrc = [ "'none'" ];
264        } else {
265            $objectSrc = (array)( $policyConfig['object-src'] ?: [] );
266        }
267        $objectSrc = array_map( [ $this, 'escapeUrlForCSP' ], $objectSrc );
268
269        $directives = [];
270        if ( $scriptSrc ) {
271            $directives[] = 'script-src ' . implode( ' ', array_unique( $scriptSrc ) );
272        }
273        if ( $defaultSrc ) {
274            $directives[] = 'default-src ' . implode( ' ', array_unique( $defaultSrc ) );
275        }
276        if ( $cssSrc ) {
277            $directives[] = 'style-src ' . implode( ' ', array_unique( $cssSrc ) );
278        }
279        if ( $imgSrc ) {
280            $directives[] = 'img-src ' . implode( ' ', array_unique( $imgSrc ) );
281        }
282        if ( $objectSrc ) {
283            $directives[] = 'object-src ' . implode( ' ', $objectSrc );
284        }
285        if ( $reportUri ) {
286            $directives[] = 'report-uri ' . $reportUri;
287        }
288
289        $this->hookRunner->onContentSecurityPolicyDirectives( $directives, $policyConfig, $mode );
290
291        return implode( '; ', $directives );
292    }
293
294    /**
295     * Get the default report uri.
296     *
297     * @param int $mode self::*_MODE constant.
298     * @return string The URI to send reports to.
299     * @throws UnexpectedValueException if given invalid mode.
300     */
301    private function getReportUri( $mode ) {
302        $apiArguments = [
303            'action' => 'cspreport',
304            'format' => 'json'
305        ];
306        if ( $mode === self::REPORT_ONLY_MODE ) {
307            $apiArguments['reportonly'] = '1';
308        }
309        $reportUri = wfAppendQuery( wfScript( 'api' ), $apiArguments );
310
311        // Per spec, ';' and ',' must be hex-escaped in report URI
312        $reportUri = $this->escapeUrlForCSP( $reportUri );
313        return $reportUri;
314    }
315
316    /**
317     * Given a url, convert to form needed for CSP.
318     *
319     * Currently this does either scheme + host, or
320     * if protocol relative, just the host. Future versions
321     * could potentially preserve some of the path, if its determined
322     * that that would be a good idea.
323     *
324     * @note This does the extra escaping for CSP, but assumes the url
325     *   has already had normal url escaping applied.
326     * @note This discards urls same as server name, as 'self' directive
327     *   takes care of that.
328     * @param string $url
329     * @return string|bool Converted url or false on failure
330     */
331    private function prepareUrlForCSP( $url ) {
332        $result = false;
333        if ( preg_match( '/^[a-z][a-z0-9+.-]*:$/i', $url ) ) {
334            // A schema source (e.g. blob: or data:)
335            return $url;
336        }
337        $bits = wfGetUrlUtils()->parse( $url );
338        if ( !$bits && strpos( $url, '/' ) === false ) {
339            // probably something like example.com.
340            // try again protocol-relative.
341            $url = '//' . $url;
342            $bits = wfGetUrlUtils()->parse( $url );
343        }
344        if ( $bits && isset( $bits['host'] )
345            && $bits['host'] !== $this->mwConfig->get( MainConfigNames::ServerName )
346        ) {
347            $result = $bits['host'];
348            if ( $bits['scheme'] !== '' ) {
349                $result = $bits['scheme'] . $bits['delimiter'] . $result;
350            }
351            if ( isset( $bits['port'] ) ) {
352                $result .= ':' . $bits['port'];
353            }
354            $result = $this->escapeUrlForCSP( $result );
355        }
356        return $result;
357    }
358
359    /**
360     * @return string[] Additional sources for loading scripts from
361     */
362    private function getAdditionalSelfUrlsScript() {
363        $additionalUrls = [];
364        // wgExtensionAssetsPath for ?debug=true mode
365        $pathVars = [
366            MainConfigNames::LoadScript,
367            MainConfigNames::ExtensionAssetsPath,
368            MainConfigNames::ResourceBasePath,
369        ];
370
371        foreach ( $pathVars as $path ) {
372            $url = $this->mwConfig->get( $path );
373            $preparedUrl = $this->prepareUrlForCSP( $url );
374            if ( $preparedUrl ) {
375                $additionalUrls[] = $preparedUrl;
376            }
377        }
378        $RLSources = $this->mwConfig->get( MainConfigNames::ResourceLoaderSources );
379        foreach ( $RLSources as $sources ) {
380            foreach ( $sources as $value ) {
381                $url = $this->prepareUrlForCSP( $value );
382                if ( $url ) {
383                    $additionalUrls[] = $url;
384                }
385            }
386        }
387
388        return array_unique( $additionalUrls );
389    }
390
391    /**
392     * Get additional host names for the wiki (e.g. if static content loaded elsewhere)
393     *
394     * @note These are general load sources, not script sources
395     * @return string[] Array of other urls for wiki (for use in default-src)
396     */
397    private function getAdditionalSelfUrls() {
398        // XXX on a foreign repo, the included description page can have anything on it,
399        // including inline scripts. But nobody does that.
400
401        // In principle, you can have even more complex configs... (e.g. The urlsByExt option)
402        $pathUrls = [];
403        $additionalSelfUrls = [];
404
405        // Future todo: The zone urls should never go into
406        // style-src. They should either be only in img-src, or if
407        // img-src unspecified they should be in default-src. Similarly,
408        // the DescriptionStylesheetUrl only needs to be in style-src
409        // (or default-src if style-src unspecified).
410        $callback = static function ( $repo, &$urls ) {
411            $urls[] = $repo->getZoneUrl( 'public' );
412            $urls[] = $repo->getZoneUrl( 'transcoded' );
413            $urls[] = $repo->getZoneUrl( 'thumb' );
414            $urls[] = $repo->getDescriptionStylesheetUrl();
415        };
416        $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
417        $localRepo = $repoGroup->getRepo( 'local' );
418        $callback( $localRepo, $pathUrls );
419        $repoGroup->forEachForeignRepo( $callback, [ &$pathUrls ] );
420
421        // Globals that might point to a different domain
422        $pathGlobals = [
423            MainConfigNames::LoadScript,
424            MainConfigNames::ExtensionAssetsPath,
425            MainConfigNames::StylePath,
426            MainConfigNames::ResourceBasePath,
427        ];
428        foreach ( $pathGlobals as $path ) {
429            $pathUrls[] = $this->mwConfig->get( $path );
430        }
431        foreach ( $pathUrls as $path ) {
432            $preparedUrl = $this->prepareUrlForCSP( $path );
433            if ( $preparedUrl !== false ) {
434                $additionalSelfUrls[] = $preparedUrl;
435            }
436        }
437        $RLSources = $this->mwConfig->get( MainConfigNames::ResourceLoaderSources );
438
439        foreach ( $RLSources as $sources ) {
440            foreach ( $sources as $value ) {
441                $url = $this->prepareUrlForCSP( $value );
442                if ( $url ) {
443                    $additionalSelfUrls[] = $url;
444                }
445            }
446        }
447
448        return array_unique( $additionalSelfUrls );
449    }
450
451    /**
452     * include domains that are allowed to send us CORS requests.
453     *
454     * Technically, $wgCrossSiteAJAXdomains lists things that are allowed to talk to us
455     * not things that we are allowed to talk to - but if something is allowed to talk to us,
456     * then there is a good chance that we should probably be allowed to talk to it.
457     *
458     * This is configurable with the 'includeCORS' key in the CSP config, and enabled
459     * by default.
460     * @note CORS domains with single character ('?') wildcards, are not included.
461     * @return array Additional hosts
462     */
463    private function getCORSSources() {
464        $additionalUrls = [];
465        $CORSSources = $this->mwConfig->get( MainConfigNames::CrossSiteAJAXdomains );
466        foreach ( $CORSSources as $source ) {
467            if ( strpos( $source, '?' ) !== false ) {
468                // CSP doesn't support single char wildcard
469                continue;
470            }
471            $url = $this->prepareUrlForCSP( $source );
472            if ( $url ) {
473                $additionalUrls[] = $url;
474            }
475        }
476        return $additionalUrls;
477    }
478
479    /**
480     * CSP spec says ',' and ';' are not allowed to appear in urls.
481     *
482     * @note This assumes that normal escaping has been applied to the url
483     * @param string $url URL (or possibly just part of one)
484     * @return string
485     */
486    private function escapeUrlForCSP( $url ) {
487        return str_replace(
488            [ ';', ',' ],
489            [ '%3B', '%2C' ],
490            $url
491        );
492    }
493
494    /**
495     * Does this browser give false positive reports?
496     *
497     * Some versions of firefox (40-42) incorrectly report a CSP
498     * violation for nonce sources, despite allowing them.
499     *
500     * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1026520
501     * @param string $ua User-agent header
502     * @return bool
503     */
504    public static function falsePositiveBrowser( $ua ) {
505        return (bool)preg_match( '!Firefox/4[0-2]\.!', $ua );
506    }
507
508    /**
509     * Should we set nonce attribute
510     *
511     * @param Config $config
512     * @return bool
513     */
514    public static function isNonceRequired( Config $config ) {
515        $configs = [
516            $config->get( MainConfigNames::CSPHeader ),
517            $config->get( MainConfigNames::CSPReportOnlyHeader ),
518        ];
519        return self::isNonceRequiredArray( $configs );
520    }
521
522    /**
523     * Does a specific config require a nonce
524     *
525     * @param array $configs An array of CSP config arrays
526     * @return bool
527     */
528    private static function isNonceRequiredArray( array $configs ) {
529        foreach ( $configs as $headerConfig ) {
530            if (
531                is_array( $headerConfig ) &&
532                isset( $headerConfig['useNonces'] ) &&
533                $headerConfig['useNonces']
534            ) {
535                return true;
536            }
537        }
538        return false;
539    }
540
541    /**
542     * Get the nonce if nonce is in use
543     *
544     * Not currently supported or implemented.
545     *
546     * @since 1.35
547     * @return false
548     */
549    public function getNonce() {
550        return false;
551    }
552
553    /**
554     * If possible you should use a more specific source type then default.
555     *
556     * So for example, if an extension added a special page that loaded something
557     * it might call $this->getOutput()->getCSP()->addDefaultSrc( '*.example.com' );
558     *
559     * @since 1.35
560     * @param string $source Source to add.
561     *   e.g. blob:, *.example.com, %https://example.com, example.com/foo
562     */
563    public function addDefaultSrc( $source ) {
564        $this->extraDefaultSrc[] = $this->prepareUrlForCSP( $source );
565    }
566
567    /**
568     * So for example, if an extension added a special page that loaded external CSS
569     * it might call $this->getOutput()->getCSP()->addStyleSrc( '*.example.com' );
570     *
571     * @since 1.35
572     * @param string $source Source to add.
573     *   e.g. blob:, *.example.com, %https://example.com, example.com/foo
574     */
575    public function addStyleSrc( $source ) {
576        $this->extraStyleSrc[] = $this->prepareUrlForCSP( $source );
577    }
578
579    /**
580     * So for example, if an extension added a special page that loaded something
581     * it might call $this->getOutput()->getCSP()->addScriptSrc( '*.example.com' );
582     *
583     * @since 1.35
584     * @warning Be careful including external scripts, as they can take over accounts.
585     * @param string $source Source to add.
586     *   e.g. blob:, *.example.com, %https://example.com, example.com/foo
587     */
588    public function addScriptSrc( $source ) {
589        $this->extraScriptSrc[] = $this->prepareUrlForCSP( $source );
590    }
591}
592
593/** @deprecated class alias since 1.40 */
594class_alias( ContentSecurityPolicy::class, 'ContentSecurityPolicy' );