Commit | Line | Data |
---|---|---|
5bd40408 PS |
1 | <?php |
2 | // This file is part of Moodle - http://moodle.org/ | |
3 | // | |
4 | // Moodle is free software: you can redistribute it and/or modify | |
5 | // it under the terms of the GNU General Public License as published by | |
6 | // the Free Software Foundation, either version 3 of the License, or | |
7 | // (at your option) any later version. | |
8 | // | |
9 | // Moodle is distributed in the hope that it will be useful, | |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | // GNU General Public License for more details. | |
13 | // | |
14 | // You should have received a copy of the GNU General Public License | |
15 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | /** | |
18 | * Unit tests for the HTMLPurifier integration | |
19 | * | |
7aea08e1 | 20 | * @package core |
5bd40408 PS |
21 | * @category phpunit |
22 | * @copyright 2012 Petr Skoda {@link http://skodak.org} | |
23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
24 | */ | |
25 | ||
26 | defined('MOODLE_INTERNAL') || die(); | |
27 | ||
28 | ||
7aea08e1 SH |
29 | /** |
30 | * HTMLPurifier test case | |
31 | * | |
32 | * @package core | |
33 | * @category phpunit | |
34 | * @copyright 2012 Petr Skoda {@link http://skodak.org} | |
35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
36 | */ | |
5bd40408 PS |
37 | class core_htmlpurifier_testcase extends basic_testcase { |
38 | ||
39 | /** | |
cd51a944 | 40 | * Verify _blank target is allowed. |
5bd40408 PS |
41 | */ |
42 | public function test_allow_blank_target() { | |
1688564a CB |
43 | // See MDL-52651 for an explanation as to why the rel="noreferrer" attribute is expected here. |
44 | // Also note we do not need to test links with an existing rel attribute as the HTML Purifier is configured to remove | |
45 | // the rel attribute. | |
5bd40408 | 46 | $text = '<a href="http://moodle.org" target="_blank">Some link</a>'; |
1688564a | 47 | $expected = '<a href="http://moodle.org" target="_blank" rel="noreferrer">Some link</a>'; |
5bd40408 | 48 | $result = format_text($text, FORMAT_HTML); |
1688564a | 49 | $this->assertSame($expected, $result); |
5bd40408 PS |
50 | |
51 | $result = format_text('<a href="http://moodle.org" target="some">Some link</a>', FORMAT_HTML); | |
52 | $this->assertSame('<a href="http://moodle.org">Some link</a>', $result); | |
53 | } | |
54 | ||
55 | /** | |
cd51a944 | 56 | * Verify our nolink tag accepted. |
5bd40408 PS |
57 | */ |
58 | public function test_nolink() { | |
cd51a944 | 59 | // We can not use format text because nolink changes result. |
5bd40408 PS |
60 | $text = '<nolink><div>no filters</div></nolink>'; |
61 | $result = purify_html($text, array()); | |
62 | $this->assertSame($text, $result); | |
63 | ||
64 | $text = '<nolink>xxx<em>xx</em><div>xxx</div></nolink>'; | |
65 | $result = purify_html($text, array()); | |
66 | $this->assertSame($text, $result); | |
67 | } | |
68 | ||
69 | /** | |
cd51a944 | 70 | * Verify our tex tag accepted. |
5bd40408 PS |
71 | */ |
72 | public function test_tex() { | |
73 | $text = '<tex>a+b=c</tex>'; | |
74 | $result = purify_html($text, array()); | |
75 | $this->assertSame($text, $result); | |
76 | } | |
77 | ||
78 | /** | |
cd51a944 | 79 | * Verify our algebra tag accepted. |
5bd40408 PS |
80 | */ |
81 | public function test_algebra() { | |
82 | $text = '<algebra>a+b=c</algebra>'; | |
83 | $result = purify_html($text, array()); | |
84 | $this->assertSame($text, $result); | |
85 | } | |
86 | ||
87 | /** | |
cd51a944 | 88 | * Verify our hacky multilang works. |
5bd40408 PS |
89 | */ |
90 | public function test_multilang() { | |
91 | $text = '<lang lang="en">hmmm</lang><lang lang="anything">hm</lang>'; | |
92 | $result = purify_html($text, array()); | |
93 | $this->assertSame($text, $result); | |
94 | ||
95 | $text = '<span lang="en" class="multilang">hmmm</span><span lang="anything" class="multilang">hm</span>'; | |
96 | $result = purify_html($text, array()); | |
97 | $this->assertSame($text, $result); | |
98 | ||
99 | $text = '<span lang="en">hmmm</span>'; | |
100 | $result = purify_html($text, array()); | |
101 | $this->assertNotSame($text, $result); | |
102 | ||
cd51a944 | 103 | // Keep standard lang tags. |
5bd40408 PS |
104 | |
105 | $text = '<span lang="de_DU" class="multilang">asas</span>'; | |
106 | $result = purify_html($text, array()); | |
107 | $this->assertSame($text, $result); | |
108 | ||
109 | $text = '<lang lang="de_DU">xxxxxx</lang>'; | |
110 | $result = purify_html($text, array()); | |
111 | $this->assertSame($text, $result); | |
112 | } | |
113 | ||
114 | /** | |
115 | * Tests the 'allowid' option for format_text. | |
5bd40408 PS |
116 | */ |
117 | public function test_format_text_allowid() { | |
cd51a944 | 118 | // Start off by not allowing ids (default). |
5bd40408 PS |
119 | $options = array( |
120 | 'nocache' => true | |
121 | ); | |
122 | $result = format_text('<div id="example">Frog</div>', FORMAT_HTML, $options); | |
123 | $this->assertSame('<div>Frog</div>', $result); | |
124 | ||
cd51a944 | 125 | // Now allow ids. |
5bd40408 PS |
126 | $options['allowid'] = true; |
127 | $result = format_text('<div id="example">Frog</div>', FORMAT_HTML, $options); | |
128 | $this->assertSame('<div id="example">Frog</div>', $result); | |
129 | } | |
130 | ||
528a7b44 PS |
131 | public function test_allowobjectembed() { |
132 | global $CFG; | |
133 | ||
134 | $this->assertSame('0', $CFG->allowobjectembed); | |
135 | ||
136 | $text = '<object width="425" height="350"> | |
137 | <param name="movie" value="http://www.youtube.com/v/AyPzM5WK8ys" /> | |
138 | <param name="wmode" value="transparent" /> | |
139 | <embed src="http://www.youtube.com/v/AyPzM5WK8ys" type="application/x-shockwave-flash" wmode="transparent" width="425" height="350" /> | |
140 | </object>hmmm'; | |
141 | $result = purify_html($text, array()); | |
142 | $this->assertSame('hmmm', trim($result)); | |
143 | ||
144 | $CFG->allowobjectembed = '1'; | |
145 | ||
146 | $expected = '<object width="425" height="350" data="http://www.youtube.com/v/AyPzM5WK8ys" type="application/x-shockwave-flash"> | |
147 | <param name="allowScriptAccess" value="never" /> | |
148 | <param name="allowNetworking" value="internal" /> | |
149 | <param name="movie" value="http://www.youtube.com/v/AyPzM5WK8ys" /> | |
150 | <param name="wmode" value="transparent" /> | |
151 | <embed src="http://www.youtube.com/v/AyPzM5WK8ys" type="application/x-shockwave-flash" wmode="transparent" width="425" height="350" allowscriptaccess="never" allownetworking="internal" /> | |
152 | </object>hmmm'; | |
153 | $result = purify_html($text, array()); | |
154 | $this->assertSame(str_replace("\n", '', $expected), str_replace("\n", '', $result)); | |
155 | ||
156 | $CFG->allowobjectembed = '0'; | |
157 | ||
158 | $result = purify_html($text, array()); | |
159 | $this->assertSame('hmmm', trim($result)); | |
160 | } | |
161 | ||
5bd40408 PS |
162 | /** |
163 | * Test if linebreaks kept unchanged. | |
5bd40408 | 164 | */ |
7aea08e1 | 165 | public function test_line_breaking() { |
5bd40408 PS |
166 | $text = "\n\raa\rsss\nsss\r"; |
167 | $this->assertSame($text, purify_html($text)); | |
168 | } | |
169 | ||
170 | /** | |
171 | * Test fixing of strict problems. | |
5bd40408 | 172 | */ |
7aea08e1 | 173 | public function test_tidy() { |
5bd40408 PS |
174 | $text = "<p>xx"; |
175 | $this->assertSame('<p>xx</p>', purify_html($text)); | |
176 | ||
177 | $text = "<P>xx</P>"; | |
178 | $this->assertSame('<p>xx</p>', purify_html($text)); | |
179 | ||
180 | $text = "xx<br>"; | |
181 | $this->assertSame('xx<br />', purify_html($text)); | |
182 | } | |
183 | ||
184 | /** | |
cd51a944 | 185 | * Test nesting - this used to cause problems in earlier versions. |
5bd40408 | 186 | */ |
7aea08e1 | 187 | public function test_nested_lists() { |
5bd40408 PS |
188 | $text = "<ul><li>One<ul><li>Two</li></ul></li><li>Three</li></ul>"; |
189 | $this->assertSame($text, purify_html($text)); | |
190 | } | |
191 | ||
192 | /** | |
193 | * Test that XSS protection works, complete smoke tests are in htmlpurifier itself. | |
5bd40408 | 194 | */ |
7aea08e1 | 195 | public function test_cleaning_nastiness() { |
5bd40408 PS |
196 | $text = "x<SCRIPT>alert('XSS')</SCRIPT>x"; |
197 | $this->assertSame('xx', purify_html($text)); | |
198 | ||
199 | $text = '<DIV STYLE="background-image:url(javascript:alert(\'XSS\'))">xx</DIV>'; | |
200 | $this->assertSame('<div>xx</div>', purify_html($text)); | |
201 | ||
202 | $text = '<DIV STYLE="width:expression(alert(\'XSS\'));">xx</DIV>'; | |
203 | $this->assertSame('<div>xx</div>', purify_html($text)); | |
204 | ||
205 | $text = 'x<IFRAME SRC="javascript:alert(\'XSS\');"></IFRAME>x'; | |
206 | $this->assertSame('xx', purify_html($text)); | |
207 | ||
208 | $text = 'x<OBJECT TYPE="text/x-scriptlet" DATA="http://ha.ckers.org/scriptlet.html"></OBJECT>x'; | |
209 | $this->assertSame('xx', purify_html($text)); | |
210 | ||
211 | $text = 'x<EMBED SRC="http://ha.ckers.org/xss.swf" AllowScriptAccess="always"></EMBED>x'; | |
212 | $this->assertSame('xx', purify_html($text)); | |
213 | ||
214 | $text = 'x<form></form>x'; | |
215 | $this->assertSame('xx', purify_html($text)); | |
216 | } | |
3f0fe2b8 PS |
217 | |
218 | /** | |
219 | * Test internal function used for clean_text() speedup. | |
3f0fe2b8 | 220 | */ |
d72bb486 | 221 | public function test_is_purify_html_necessary() { |
cd51a944 | 222 | // First our shortcuts. |
3f0fe2b8 PS |
223 | $text = ""; |
224 | $this->assertFalse(is_purify_html_necessary($text)); | |
225 | $this->assertSame($text, purify_html($text)); | |
226 | ||
227 | $text = "666"; | |
228 | $this->assertFalse(is_purify_html_necessary($text)); | |
229 | $this->assertSame($text, purify_html($text)); | |
230 | ||
231 | $text = "abc\ndef \" ' "; | |
232 | $this->assertFalse(is_purify_html_necessary($text)); | |
233 | $this->assertSame($text, purify_html($text)); | |
234 | ||
235 | $text = "abc\n<p>def</p>efg<p>hij</p>"; | |
236 | $this->assertFalse(is_purify_html_necessary($text)); | |
237 | $this->assertSame($text, purify_html($text)); | |
238 | ||
239 | $text = "<br />abc\n<p>def<em>efg</em><strong>hi<br />j</strong></p>"; | |
240 | $this->assertFalse(is_purify_html_necessary($text)); | |
241 | $this->assertSame($text, purify_html($text)); | |
242 | ||
cd51a944 | 243 | // Now failures. |
3f0fe2b8 PS |
244 | $text = " "; |
245 | $this->assertTrue(is_purify_html_necessary($text)); | |
246 | ||
247 | $text = "Gin & Tonic"; | |
248 | $this->assertTrue(is_purify_html_necessary($text)); | |
249 | ||
250 | $text = "Gin > Tonic"; | |
251 | $this->assertTrue(is_purify_html_necessary($text)); | |
252 | ||
253 | $text = "Gin < Tonic"; | |
254 | $this->assertTrue(is_purify_html_necessary($text)); | |
255 | ||
256 | $text = "<div>abc</div>"; | |
257 | $this->assertTrue(is_purify_html_necessary($text)); | |
258 | ||
259 | $text = "<span>abc</span>"; | |
260 | $this->assertTrue(is_purify_html_necessary($text)); | |
261 | ||
262 | $text = "<br>abc"; | |
263 | $this->assertTrue(is_purify_html_necessary($text)); | |
264 | ||
265 | $text = "<p class='xxx'>abc</p>"; | |
266 | $this->assertTrue(is_purify_html_necessary($text)); | |
267 | ||
268 | $text = "<p>abc<em></p></em>"; | |
269 | $this->assertTrue(is_purify_html_necessary($text)); | |
270 | ||
271 | $text = "<p>abc"; | |
272 | $this->assertTrue(is_purify_html_necessary($text)); | |
273 | } | |
d72bb486 PS |
274 | |
275 | public function test_allowed_schemes() { | |
cd51a944 | 276 | // First standard schemas. |
d72bb486 PS |
277 | $text = '<a href="http://www.example.com/course/view.php?id=5">link</a>'; |
278 | $this->assertSame($text, purify_html($text)); | |
279 | ||
280 | $text = '<a href="https://www.example.com/course/view.php?id=5">link</a>'; | |
281 | $this->assertSame($text, purify_html($text)); | |
282 | ||
283 | $text = '<a href="ftp://user@ftp.example.com/some/file.txt">link</a>'; | |
284 | $this->assertSame($text, purify_html($text)); | |
285 | ||
286 | $text = '<a href="nntp://example.com/group/123">link</a>'; | |
287 | $this->assertSame($text, purify_html($text)); | |
288 | ||
289 | $text = '<a href="news:groupname">link</a>'; | |
290 | $this->assertSame($text, purify_html($text)); | |
291 | ||
292 | $text = '<a href="mailto:user@example.com">link</a>'; | |
293 | $this->assertSame($text, purify_html($text)); | |
294 | ||
cd51a944 | 295 | // Extra schemes allowed in moodle. |
d72bb486 PS |
296 | $text = '<a href="irc://irc.example.com/3213?pass">link</a>'; |
297 | $this->assertSame($text, purify_html($text)); | |
298 | ||
299 | $text = '<a href="rtsp://www.example.com/movie.mov">link</a>'; | |
300 | $this->assertSame($text, purify_html($text)); | |
301 | ||
817b2020 TB |
302 | $text = '<a href="rtmp://www.example.com/video.f4v">link</a>'; |
303 | $this->assertSame($text, purify_html($text)); | |
304 | ||
d72bb486 PS |
305 | $text = '<a href="teamspeak://speak.example.com/?par=val?par2=val2">link</a>'; |
306 | $this->assertSame($text, purify_html($text)); | |
307 | ||
308 | $text = '<a href="gopher://gopher.example.com/resource">link</a>'; | |
309 | $this->assertSame($text, purify_html($text)); | |
310 | ||
311 | $text = '<a href="mms://www.example.com/movie.mms">link</a>'; | |
312 | $this->assertSame($text, purify_html($text)); | |
313 | ||
cd51a944 | 314 | // Now some borked or dangerous schemes. |
d72bb486 PS |
315 | $text = '<a href="javascript://www.example.com">link</a>'; |
316 | $this->assertSame('<a>link</a>', purify_html($text)); | |
317 | ||
318 | $text = '<a href="hmmm://www.example.com">link</a>'; | |
319 | $this->assertSame('<a>link</a>', purify_html($text)); | |
320 | } | |
37c10287 CB |
321 | |
322 | /** | |
323 | * Tests media tags. | |
324 | * | |
325 | * @dataProvider media_tags_provider | |
326 | * @param string $mediatag HTML media tag | |
327 | * @param string $expected expected result | |
328 | */ | |
329 | public function test_media_tags($mediatag, $expected) { | |
330 | $actual = format_text($mediatag, FORMAT_MOODLE, ['filter' => false, 'noclean' => true]); | |
331 | $this->assertEquals($expected, $actual); | |
332 | } | |
333 | ||
334 | /** | |
335 | * Test cases for the test_media_tags test. | |
336 | */ | |
337 | public function media_tags_provider() { | |
c353674f | 338 | // Takes an array of attributes, then generates a test for each of them. |
37c10287 | 339 | $generatetestcases = function($prefix, array $attrs, array $templates) use ($p) { |
c353674f CB |
340 | return array_reduce($attrs, function($carry, $attr) use ($prefix, $templates) { |
341 | $testcase = [$prefix . '/' . $attr => [ | |
342 | sprintf($templates[0], $attr), | |
343 | sprintf($templates[1], $attr) | |
37c10287 CB |
344 | ]]; |
345 | return empty(array_values($carry)[0]) ? $testcase : $carry + $testcase; | |
346 | }, [[]]); | |
347 | }; | |
348 | ||
349 | $audioattrs = [ | |
350 | 'preload="auto"', 'autoplay=""', 'loop=""', 'muted=""', 'controls=""', | |
351 | 'crossorigin="anonymous"', 'crossorigin="use-credentials"' | |
352 | ]; | |
353 | $videoattrs = [ | |
354 | 'crossorigin="anonymous"', 'crossorigin="use-credentials"', | |
355 | 'poster="https://upload.wikimedia.org/wikipedia/en/1/14/Space_jam.jpg"', | |
356 | 'preload=""', 'autoplay=""', 'playsinline=""', 'loop=""', 'muted=""', | |
357 | 'controls=""', 'width="420px"', 'height="69px"' | |
358 | ]; | |
359 | return $generatetestcases('Plain audio', $audioattrs + ['src="http://example.com/jam.wav"'], [ | |
360 | '<audio %1$s>Looks like you can\'t slam the jams.</audio>', | |
361 | '<div class="text_to_html"><audio %1$s>Looks like you can\'t slam the jams.</audio></div>' | |
362 | ]) + $generatetestcases('Audio with one source', $audioattrs, [ | |
363 | '<audio %1$s><source src="http://example.com/getup.wav">No tasty jams for you.</audio>', | |
364 | '<div class="text_to_html">' . | |
365 | '<audio %1$s>' . | |
366 | '<source src="http://example.com/getup.wav">' . | |
367 | 'No tasty jams for you.' . | |
368 | '</audio>' . | |
369 | '</div>' | |
370 | ]) + $generatetestcases('Audio with multiple sources', $audioattrs, [ | |
371 | '<audio %1$s>' . | |
372 | '<source src="http://example.com/getup.wav" type="audio/wav">' . | |
373 | '<source src="http://example.com/getup.mp3" type="audio/mpeg">' . | |
374 | '<source src="http://example.com/getup.ogg" type="audio/ogg">' . | |
375 | 'No tasty jams for you.' . | |
376 | '</audio>', | |
377 | '<div class="text_to_html">' . | |
378 | '<audio %1$s>' . | |
379 | '<source src="http://example.com/getup.wav" type="audio/wav">' . | |
380 | '<source src="http://example.com/getup.mp3" type="audio/mpeg">' . | |
381 | '<source src="http://example.com/getup.ogg" type="audio/ogg">' . | |
382 | 'No tasty jams for you.' . | |
383 | '</audio>' . | |
384 | '</div>' | |
385 | ]) + $generatetestcases('Plain video', $videoattrs + ['src="http://example.com/prettygood.mp4'], [ | |
386 | '<video %1$s>Oh, that\'s pretty bad 😦</video>', | |
387 | '<div class="text_to_html"><video %1$s>Oh, that\'s pretty bad 😦</video></div>' | |
388 | ]) + $generatetestcases('Video with one source', $videoattrs, [ | |
389 | '<video %1$s><source src="http://example.com/prettygood.mp4">Oh, that\'s pretty bad 😦</video>', | |
390 | '<div class="text_to_html">' . | |
391 | '<video %1$s>' . | |
392 | '<source src="http://example.com/prettygood.mp4">' . | |
393 | 'Oh, that\'s pretty bad 😦' . | |
394 | '</video>' . | |
395 | '</div>' | |
396 | ]) + $generatetestcases('Video with multiple sources', $videoattrs, [ | |
397 | '<video %1$s>' . | |
398 | '<source src="http://example.com/prettygood.mp4" type="video/mp4">' . | |
399 | '<source src="http://example.com/eljefe.mp4" type="video/mp4">' . | |
400 | '<source src="http://example.com/turnitup.mov type="video/mov"' . | |
401 | 'Oh, that\'s pretty bad 😦' . | |
402 | '</video>', | |
403 | '<div class="text_to_html">' . | |
404 | '<video %1$s>' . | |
405 | '<source src="http://example.com/prettygood.mp4" type="video/mp4">' . | |
406 | '<source src="http://example.com/eljefe.mp4" type="video/mp4">' . | |
407 | '<source src="http://example.com/turnitup.mov type="video/mov"' . | |
408 | 'Oh, that\'s pretty bad 😦' . | |
409 | '</video>' . | |
410 | '</div>' | |
411 | ] + [ | |
412 | 'Video with invalid crossorigin' => [ | |
413 | '<video src="http://example.com/turnitup.mov type="video/mov crossorigin="can i pls hab?">' . | |
414 | 'Oh, that\'s pretty bad 😦' . | |
415 | '</video>', | |
416 | '<div class="text_to_html">' . | |
417 | '<video src="http://example.com/turnitup.mov type="video/mov">' . | |
418 | 'Oh, that\'s pretty bad 😦' . | |
419 | '</video>', | |
420 | '</div>' | |
421 | ], | |
422 | 'Audio with invalid crossorigin' => [ | |
423 | '<audio src="http://example.com/getup.wav" type="audio/wav" crossorigin="give me. the jams.">' . | |
424 | 'nyemnyemnyem' . | |
425 | '</audio>', | |
426 | '<div class="text_to_html">' . | |
427 | '<audio src="http://example.com/getup.wav" type="audio/wav" crossorigin="give me. the jams.">' . | |
428 | 'nyemnyemnyem' . | |
429 | '</audio>' . | |
430 | '</div>' | |
431 | ] | |
432 | ]); | |
433 | } | |
5bd40408 | 434 | } |