1<?php
2
3use MediaWiki\MediaWikiServices;
4use Wikimedia\TestingAccessWrapper;
5
6class ContentSecurityPolicyTest extends MediaWikiIntegrationTestCase {
7	/** @var ContentSecurityPolicy */
8	private $csp;
9
10	protected function setUp() : void {
11		global $wgUploadDirectory;
12
13		parent::setUp();
14
15		$this->setMwGlobals( [
16			'wgAllowExternalImages' => false,
17			'wgAllowExternalImagesFrom' => [],
18			'wgAllowImageTag' => false,
19			'wgEnableImageWhitelist' => false,
20			'wgLoadScript' => false,
21			'wgExtensionAssetsPath' => false,
22			'wgStylePath' => false,
23			'wgResourceBasePath' => null,
24			'wgCrossSiteAJAXdomains' => [
25				'sister-site.somewhere.com',
26				'*.wikipedia.org',
27				'??.wikinews.org'
28			],
29			'wgScriptPath' => '/w',
30			'wgForeignFileRepos' => [ [
31				'class' => ForeignAPIRepo::class,
32				'name' => 'wikimediacommons',
33				'apibase' => 'https://commons.wikimedia.org/w/api.php',
34				'url' => 'https://upload.wikimedia.org/wikipedia/commons',
35				'thumbUrl' => 'https://upload.wikimedia.org/wikipedia/commons/thumb',
36				'hashLevels' => 2,
37				'transformVia404' => true,
38				'fetchDescription' => true,
39				'descriptionCacheExpiry' => 43200,
40				'apiThumbCacheExpiry' => 0,
41				'directory' => $wgUploadDirectory,
42				'backend' => 'wikimediacommons-backend',
43			] ],
44			'wgCSPHeader' => true, // enable nonce by default
45		] );
46		// Note, there are some obscure globals which
47		// could affect the results which aren't included above.
48
49		$context = RequestContext::getMain();
50		$resp = $context->getRequest()->response();
51		$conf = $context->getConfig();
52		$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
53		$csp = new ContentSecurityPolicy( $resp, $conf, $hookContainer );
54		$this->csp = TestingAccessWrapper::newFromObject( $csp );
55		$this->csp->nonce = 'secret';
56	}
57
58	/**
59	 * @covers ContentSecurityPolicy::getAdditionalSelfUrls
60	 */
61	public function testGetAdditionalSelfUrlsRespectsUrlSettings() {
62		$this->setMwGlobals( 'wgLoadScript', 'https://wgLoadScript.example.org/load.php' );
63		$this->setMwGlobals( 'wgExtensionAssetsPath',
64			'https://wgExtensionAssetsPath.example.org/assets/' );
65		$this->setMwGlobals( 'wgStylePath', 'https://wgStylePath.example.org/style/' );
66		$this->setMwGlobals( 'wgResourceBasePath', 'https://wgResourceBasePath.example.org/resources/' );
67
68		$this->assertEquals(
69			[
70				'https://upload.wikimedia.org',
71				'https://commons.wikimedia.org',
72				'https://wgLoadScript.example.org',
73				'https://wgExtensionAssetsPath.example.org',
74				'https://wgStylePath.example.org',
75				'https://wgResourceBasePath.example.org',
76			],
77			array_values( $this->csp->getAdditionalSelfUrls() )
78		);
79	}
80
81	/**
82	 * @dataProvider providerFalsePositiveBrowser
83	 * @covers ContentSecurityPolicy::falsePositiveBrowser
84	 */
85	public function testFalsePositiveBrowser( $ua, $expected ) {
86		$actual = ContentSecurityPolicy::falsePositiveBrowser( $ua );
87		$this->assertSame( $expected, $actual, $ua );
88	}
89
90	public function providerFalsePositiveBrowser() {
91		return [
92			[
93				'Mozilla/5.0 (X11; Linux i686; rv:41.0) Gecko/20100101 Firefox/41.0',
94				true
95			],
96			[
97				'Mozilla/5.0 (X11; U; Linux i686; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) ' .
98					'Version/5.0 Safari/531.2+ Debian/squeeze (2.30.6-1) Epiphany/2.30.6',
99				false
100			],
101		];
102	}
103
104	/**
105	 * @covers ContentSecurityPolicy::addScriptSrc
106	 * @covers ContentSecurityPolicy::makeCSPDirectives
107	 */
108	public function testAddScriptSrc() {
109		$this->csp->addScriptSrc( 'https://example.com:71' );
110		$actual = $this->csp->makeCSPDirectives( true, ContentSecurityPolicy::FULL_MODE );
111		$expected = "script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'" .
112			" sister-site.somewhere.com *.wikipedia.org https://example.com:71; default-src *" .
113			" data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri" .
114			" /w/api.php?action=cspreport&format=json";
115		$this->assertSame( $expected, $actual );
116	}
117
118	/**
119	 * @covers ContentSecurityPolicy::addStyleSrc
120	 * @covers ContentSecurityPolicy::makeCSPDirectives
121	 */
122	public function testAddStyleSrc() {
123		$this->csp->addStyleSrc( 'style.example.com' );
124		$actual = $this->csp->makeCSPDirectives( true, ContentSecurityPolicy::REPORT_ONLY_MODE );
125		$expected = "script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'" .
126			" sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:;" .
127			" style-src * data: blob: style.example.com 'unsafe-inline'; object-src 'none'; report-uri" .
128			" /w/api.php?action=cspreport&format=json&reportonly=1";
129		$this->assertSame( $expected, $actual );
130	}
131
132	/**
133	 * @covers ContentSecurityPolicy::addDefaultSrc
134	 * @covers ContentSecurityPolicy::makeCSPDirectives
135	 */
136	public function testAddDefaultSrc() {
137		$this->csp->addDefaultSrc( '*.example.com' );
138		$actual = $this->csp->makeCSPDirectives( true, ContentSecurityPolicy::FULL_MODE );
139		$expected = "script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'" .
140			" sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:" .
141			" *.example.com; style-src * data: blob: *.example.com 'unsafe-inline';" .
142			" object-src 'none'; report-uri /w/api.php?action=cspreport&format=json";
143		$this->assertSame( $expected, $actual );
144	}
145
146	/**
147	 * @dataProvider providerMakeCSPDirectives
148	 * @covers ContentSecurityPolicy::makeCSPDirectives
149	 */
150	public function testMakeCSPDirectives(
151		$policy,
152		$expectedFull,
153		$expectedReport
154	) {
155		$actualFull = $this->csp->makeCSPDirectives( $policy, ContentSecurityPolicy::FULL_MODE );
156		$actualReport = $this->csp->makeCSPDirectives(
157			$policy, ContentSecurityPolicy::REPORT_ONLY_MODE
158		);
159		$policyJson = FormatJson::encode( $policy );
160		$this->assertSame( $expectedFull, $actualFull, "full: " . $policyJson );
161		$this->assertSame( $expectedReport, $actualReport, "report: " . $policyJson );
162	}
163
164	public function providerMakeCSPDirectives() {
165		// phpcs:disable Generic.Files.LineLength
166		return [
167			[ false, '', '' ],
168			[
169				[ 'useNonces' => false ],
170				"script-src 'unsafe-eval' blob: 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
171				"script-src 'unsafe-eval' blob: 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
172				"script-src 'unsafe-eval' blob: 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'"
173			],
174			[
175				true,
176				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
177				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
178			],
179			[
180				[],
181				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
182				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
183			 ],
184			[
185				[ 'script-src' => [ 'http://example.com', 'http://something,else.com' ] ],
186				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' http://example.com http://something%2Celse.com 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
187				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' http://example.com http://something%2Celse.com 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
188			],
189			[
190				[ 'unsafeFallback' => false ],
191				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
192				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
193			],
194			[
195				[ 'unsafeFallback' => true ],
196				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
197				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
198			],
199			[
200				[ 'default-src' => false ],
201				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
202				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
203			],
204			[
205				[ 'default-src' => true ],
206				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
207				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
208			],
209			[
210				[ 'default-src' => [ 'https://foo.com', 'http://bar.com', 'baz.de' ] ],
211				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
212				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
213			],
214			[
215				[ 'includeCORS' => false ],
216				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
217				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
218			],
219			[
220				[ 'includeCORS' => false, 'default-src' => true ],
221				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
222				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
223			],
224			[
225				[ 'includeCORS' => true ],
226				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
227				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
228			],
229			[
230				[ 'report-uri' => false ],
231				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'",
232				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'",
233			],
234			[
235				[ 'report-uri' => true ],
236				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
237				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
238			],
239			[
240				[ 'report-uri' => 'https://example.com/index.php?foo;report=csp' ],
241				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri https://example.com/index.php?foo%3Breport=csp",
242				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri https://example.com/index.php?foo%3Breport=csp",
243			],
244			[
245				[ 'object-src' => false ],
246				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json",
247				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
248			],
249			[
250				[ 'object-src' => true ],
251				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json",
252				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
253			],
254			[
255				[ 'object-src' => "'self'" ],
256				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'self'; report-uri /w/api.php?action=cspreport&format=json",
257				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'self'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
258			],
259			[
260				[ 'object-src' => [ "'self'", 'https://example.com/f;d' ] ],
261				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'self' https://example.com/f%3Bd; report-uri /w/api.php?action=cspreport&format=json",
262				"script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'self' https://example.com/f%3Bd; report-uri /w/api.php?action=cspreport&format=json&reportonly=1",
263			],
264		];
265		// phpcs:enable
266	}
267
268	/**
269	 * @covers ContentSecurityPolicy::makeCSPDirectives
270	 */
271	public function testMakeCSPDirectivesImage() {
272		global $wgAllowImageTag;
273		$origImg = wfSetVar( $wgAllowImageTag, true );
274
275		$actual = $this->csp->makeCSPDirectives( true, ContentSecurityPolicy::FULL_MODE );
276
277		$wgAllowImageTag = $origImg;
278
279		// phpcs:ignore Generic.Files.LineLength
280		$expected = "script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json";
281		$this->assertSame( $expected, $actual );
282	}
283
284	/**
285	 * @covers ContentSecurityPolicy::makeCSPDirectives
286	 */
287	public function testMakeCSPDirectivesReportUri() {
288		$actual = $this->csp->makeCSPDirectives(
289			true,
290			ContentSecurityPolicy::REPORT_ONLY_MODE
291		);
292		// phpcs:ignore Generic.Files.LineLength
293		$expected = "script-src 'unsafe-eval' blob: 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; object-src 'none'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1";
294		$this->assertSame( $expected, $actual );
295	}
296
297	/**
298	 * @covers ContentSecurityPolicy::getHeaderName
299	 */
300	public function testGetHeaderName() {
301		$this->assertSame(
302			'Content-Security-Policy-Report-Only',
303			$this->csp->getHeaderName( ContentSecurityPolicy::REPORT_ONLY_MODE )
304		);
305		$this->assertSame(
306			'Content-Security-Policy',
307			$this->csp->getHeaderName( ContentSecurityPolicy::FULL_MODE )
308		);
309	}
310
311	/**
312	 * @covers ContentSecurityPolicy::getReportUri
313	 */
314	public function testGetReportUri() {
315		$full = $this->csp->getReportUri( ContentSecurityPolicy::FULL_MODE );
316		$fullExpected = '/w/api.php?action=cspreport&format=json';
317		$this->assertSame( $fullExpected, $full, 'normal report uri' );
318
319		$report = $this->csp->getReportUri( ContentSecurityPolicy::REPORT_ONLY_MODE );
320		$reportExpected = $fullExpected . '&reportonly=1';
321		$this->assertSame( $reportExpected, $report, 'report only' );
322
323		global $wgScriptPath;
324		$origPath = wfSetVar( $wgScriptPath, '/tl;dr/a,%20wiki' );
325		$esc = $this->csp->getReportUri( ContentSecurityPolicy::FULL_MODE );
326		$escExpected = '/tl%3Bdr/a%2C%20wiki/api.php?action=cspreport&format=json';
327		$wgScriptPath = $origPath;
328		$this->assertSame( $escExpected, $esc, 'test esc rules' );
329	}
330
331	/**
332	 * @dataProvider providerPrepareUrlForCSP
333	 * @covers ContentSecurityPolicy::prepareUrlForCSP
334	 */
335	public function testPrepareUrlForCSP( $url, $expected ) {
336		$actual = $this->csp->prepareUrlForCSP( $url );
337		$this->assertSame( $expected, $actual, $url );
338	}
339
340	public function providerPrepareUrlForCSP() {
341		global $wgServer;
342		return [
343			[ $wgServer, false ],
344			[ 'https://example.com', 'https://example.com' ],
345			[ 'https://example.com:200', 'https://example.com:200' ],
346			[ 'http://example.com', 'http://example.com' ],
347			[ 'example.com', 'example.com' ],
348			[ '*.example.com', '*.example.com' ],
349			[ 'https://*.example.com', 'https://*.example.com' ],
350			[ '//example.com', 'example.com' ],
351			[ 'https://example.com/path', 'https://example.com' ],
352			[ 'https://example.com/path:', 'https://example.com' ],
353			[ 'https://example.com/Wikipedia:NPOV', 'https://example.com' ],
354			[ 'https://tl;dr.com', 'https://tl%3Bdr.com' ],
355			[ 'yes,no.com', 'yes%2Cno.com' ],
356			[ '/relative-url', false ],
357			[ '/relativeUrl:withColon', false ],
358			[ 'data:', 'data:' ],
359			[ 'blob:', 'blob:' ],
360		];
361	}
362
363	/**
364	 * @covers ContentSecurityPolicy::escapeUrlForCSP
365	 */
366	public function testEscapeUrlForCSP() {
367		$escaped = $this->csp->escapeUrlForCSP( ',;%2B' );
368		$this->assertSame( '%2C%3B%2B', $escaped );
369	}
370
371	/**
372	 * @dataProvider providerCSPIsEnabled
373	 * @covers ContentSecurityPolicy::isNonceRequired
374	 */
375	public function testCSPIsEnabled( $main, $reportOnly, $expected ) {
376		$this->setMwGlobals( 'wgCSPReportOnlyHeader', $reportOnly );
377		$this->setMwGlobals( 'wgCSPHeader', $main );
378		$res = ContentSecurityPolicy::isNonceRequired( RequestContext::getMain()->getConfig() );
379		$this->assertSame( $expected, $res );
380	}
381
382	public function providerCSPIsEnabled() {
383		return [
384			[ true, true, true ],
385			[ false, true, true ],
386			[ true, false, true ],
387			[ false, false, false ],
388			[ false, [], true ],
389			[ [], false, true ],
390			[ [ 'default-src' => [ 'foo.example.com' ] ], false, true ],
391			[ [ 'useNonces' => false ], [ 'useNonces' => false ], false ],
392			[ [ 'useNonces' => true ], [ 'useNonces' => false ], true ],
393			[ [ 'useNonces' => false ], [ 'useNonces' => true ], true ],
394		];
395	}
396}
397