Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / filter / tests / src / Kernel / FilterKernelTest.php
1 <?php
2
3 namespace Drupal\Tests\filter\Kernel;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Core\Language\Language;
7 use Drupal\Core\Render\RenderContext;
8 use Drupal\editor\EditorXssFilter\Standard;
9 use Drupal\filter\Entity\FilterFormat;
10 use Drupal\filter\FilterPluginCollection;
11 use Drupal\KernelTests\KernelTestBase;
12
13 /**
14  * Tests Filter module filters individually.
15  *
16  * @group filter
17  */
18 class FilterKernelTest extends KernelTestBase {
19
20   /**
21    * Modules to enable.
22    *
23    * @var array
24    */
25   public static $modules = ['system', 'filter'];
26
27   /**
28    * @var \Drupal\filter\Plugin\FilterInterface[]
29    */
30   protected $filters;
31
32   protected function setUp() {
33     parent::setUp();
34     $this->installConfig(['system']);
35
36     $manager = $this->container->get('plugin.manager.filter');
37     $bag = new FilterPluginCollection($manager, []);
38     $this->filters = $bag->getAll();
39   }
40
41   /**
42    * Tests the align filter.
43    */
44   public function testAlignFilter() {
45     $filter = $this->filters['filter_align'];
46
47     $test = function ($input) use ($filter) {
48       return $filter->process($input, 'und');
49     };
50
51     // No data-align attribute.
52     $input = '<img src="llama.jpg" />';
53     $expected = $input;
54     $this->assertIdentical($expected, $test($input)->getProcessedText());
55
56     // Data-align attribute: all 3 allowed values.
57     $input = '<img src="llama.jpg" data-align="left" />';
58     $expected = '<img src="llama.jpg" class="align-left" />';
59     $this->assertIdentical($expected, $test($input)->getProcessedText());
60     $input = '<img src="llama.jpg" data-align="center" />';
61     $expected = '<img src="llama.jpg" class="align-center" />';
62     $this->assertIdentical($expected, $test($input)->getProcessedText());
63     $input = '<img src="llama.jpg" data-align="right" />';
64     $expected = '<img src="llama.jpg" class="align-right" />';
65     $this->assertIdentical($expected, $test($input)->getProcessedText());
66
67     // Data-align attribute: a disallowed value.
68     $input = '<img src="llama.jpg" data-align="left foobar" />';
69     $expected = '<img src="llama.jpg" />';
70     $this->assertIdentical($expected, $test($input)->getProcessedText());
71
72     // Empty data-align attribute.
73     $input = '<img src="llama.jpg" data-align="" />';
74     $expected = '<img src="llama.jpg" />';
75     $this->assertIdentical($expected, $test($input)->getProcessedText());
76
77     // Ensure the filter also works with uncommon yet valid attribute quoting.
78     $input = '<img src=llama.jpg data-align=right />';
79     $expected = '<img src="llama.jpg" class="align-right" />';
80     $output = $test($input);
81     $this->assertIdentical($expected, $output->getProcessedText());
82
83     // Security test: attempt to inject an additional class.
84     $input = '<img src="llama.jpg" data-align="center another-class-here" />';
85     $expected = '<img src="llama.jpg" />';
86     $output = $test($input);
87     $this->assertIdentical($expected, $output->getProcessedText());
88
89     // Security test: attempt an XSS.
90     $input = '<img src="llama.jpg" data-align="center \'onclick=\'alert(foo);" />';
91     $expected = '<img src="llama.jpg" />';
92     $output = $test($input);
93     $this->assertIdentical($expected, $output->getProcessedText());
94   }
95
96   /**
97    * Tests the caption filter.
98    */
99   public function testCaptionFilter() {
100     /** @var \Drupal\Core\Render\RendererInterface $renderer */
101     $renderer = \Drupal::service('renderer');
102     $filter = $this->filters['filter_caption'];
103
104     $test = function ($input) use ($filter, $renderer) {
105       return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter) {
106         return $filter->process($input, 'und');
107       });
108     };
109
110     $attached_library = [
111       'library' => [
112         'filter/caption',
113       ],
114     ];
115
116     // No data-caption attribute.
117     $input = '<img src="llama.jpg" />';
118     $expected = $input;
119     $this->assertIdentical($expected, $test($input)->getProcessedText());
120
121     // Data-caption attribute.
122     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" />';
123     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
124     $output = $test($input);
125     $this->assertIdentical($expected, $output->getProcessedText());
126     $this->assertIdentical($attached_library, $output->getAttachments());
127
128     // Empty data-caption attribute.
129     $input = '<img src="llama.jpg" data-caption="" />';
130     $expected = '<img src="llama.jpg" />';
131     $this->assertIdentical($expected, $test($input)->getProcessedText());
132
133     // HTML entities in the caption.
134     $input = '<img src="llama.jpg" data-caption="&ldquo;Loquacious llama!&rdquo;" />';
135     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>“Loquacious llama!”</figcaption></figure>';
136     $output = $test($input);
137     $this->assertIdentical($expected, $output->getProcessedText());
138     $this->assertIdentical($attached_library, $output->getAttachments());
139
140     // HTML encoded as HTML entities in data-caption attribute.
141     $input = '<img src="llama.jpg" data-caption="&lt;em&gt;Loquacious llama!&lt;/em&gt;" />';
142     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>';
143     $output = $test($input);
144     $this->assertIdentical($expected, $output->getProcessedText());
145     $this->assertIdentical($attached_library, $output->getAttachments());
146
147     // HTML (not encoded as HTML entities) in data-caption attribute, which is
148     // not allowed by the HTML spec, but may happen when people manually write
149     // HTML, so we explicitly support it.
150     $input = '<img src="llama.jpg" data-caption="<em>Loquacious llama!</em>" />';
151     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>';
152     $output = $test($input);
153     $this->assertIdentical($expected, $output->getProcessedText());
154     $this->assertIdentical($attached_library, $output->getAttachments());
155
156     // Security test: attempt an XSS.
157     $input = '<img src="llama.jpg" data-caption="<script>alert(\'Loquacious llama!\')</script>" />';
158     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>alert(\'Loquacious llama!\')</figcaption></figure>';
159     $output = $test($input);
160     $this->assertIdentical($expected, $output->getProcessedText());
161     $this->assertIdentical($attached_library, $output->getAttachments());
162
163     // Ensure the filter also works with uncommon yet valid attribute quoting.
164     $input = '<img src=llama.jpg data-caption=\'Loquacious llama!\' />';
165     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
166     $output = $test($input);
167     $this->assertIdentical($expected, $output->getProcessedText());
168     $this->assertIdentical($attached_library, $output->getAttachments());
169
170     // Finally, ensure that this also works on any other tag.
171     $input = '<video src="llama.jpg" data-caption="Loquacious llama!" />';
172     $expected = '<figure role="group"><video src="llama.jpg"></video><figcaption>Loquacious llama!</figcaption></figure>';
173     $output = $test($input);
174     $this->assertIdentical($expected, $output->getProcessedText());
175     $this->assertIdentical($attached_library, $output->getAttachments());
176     $input = '<foobar data-caption="Loquacious llama!">baz</foobar>';
177     $expected = '<figure role="group"><foobar>baz</foobar><figcaption>Loquacious llama!</figcaption></figure>';
178     $output = $test($input);
179     $this->assertIdentical($expected, $output->getProcessedText());
180     $this->assertIdentical($attached_library, $output->getAttachments());
181
182     // Ensure the caption filter works for linked images.
183     $input = '<a href="http://example.com/llamas/are/awesome/but/kittens/are/cool/too"><img src="llama.jpg" data-caption="Loquacious llama!" /></a>';
184     $expected = '<figure role="group"><a href="http://example.com/llamas/are/awesome/but/kittens/are/cool/too"><img src="llama.jpg" /></a>' . "\n" . '<figcaption>Loquacious llama!</figcaption></figure>';
185     $output = $test($input);
186     $this->assertIdentical($expected, $output->getProcessedText());
187     $this->assertIdentical($attached_library, $output->getAttachments());
188
189     // So far we've tested that the caption filter works correctly. But we also
190     // want to make sure that it works well in tandem with the "Limit allowed
191     // HTML tags" filter, which it is typically used with.
192     $html_filter = $this->filters['filter_html'];
193     $html_filter->setConfiguration([
194       'settings' => [
195         'allowed_html' => '<img src data-align data-caption>',
196         'filter_html_help' => 1,
197         'filter_html_nofollow' => 0,
198       ]
199     ]);
200     $test_with_html_filter = function ($input) use ($filter, $html_filter, $renderer) {
201       return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter, $html_filter) {
202         // 1. Apply HTML filter's processing step.
203         $output = $html_filter->process($input, 'und');
204         // 2. Apply caption filter's processing step.
205         $output = $filter->process($output, 'und');
206         return $output->getProcessedText();
207       });
208     };
209     // Editor XSS filter.
210     $test_editor_xss_filter = function ($input) {
211       $dummy_filter_format = FilterFormat::create();
212       return Standard::filterXss($input, $dummy_filter_format);
213     };
214
215     // All the tricky cases encountered at https://www.drupal.org/node/2105841.
216     // A plain URL preceded by text.
217     $input = '<img data-caption="See https://www.drupal.org" src="llama.jpg" />';
218     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>See https://www.drupal.org</figcaption></figure>';
219     $this->assertIdentical($expected, $test_with_html_filter($input));
220     $this->assertIdentical($input, $test_editor_xss_filter($input));
221
222     // An anchor.
223     $input = '<img data-caption="This is a &lt;a href=&quot;https://www.drupal.org&quot;&gt;quick&lt;/a&gt; test…" src="llama.jpg" />';
224     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>This is a <a href="https://www.drupal.org">quick</a> test…</figcaption></figure>';
225     $this->assertIdentical($expected, $test_with_html_filter($input));
226     $this->assertIdentical($input, $test_editor_xss_filter($input));
227
228     // A plain URL surrounded by parentheses.
229     $input = '<img data-caption="(https://www.drupal.org)" src="llama.jpg" />';
230     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>(https://www.drupal.org)</figcaption></figure>';
231     $this->assertIdentical($expected, $test_with_html_filter($input));
232     $this->assertIdentical($input, $test_editor_xss_filter($input));
233
234     // A source being credited.
235     $input = '<img data-caption="Source: Wikipedia" src="llama.jpg" />';
236     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>Source: Wikipedia</figcaption></figure>';
237     $this->assertIdentical($expected, $test_with_html_filter($input));
238     $this->assertIdentical($input, $test_editor_xss_filter($input));
239
240     // A source being credited, without a space after the colon.
241     $input = '<img data-caption="Source:Wikipedia" src="llama.jpg" />';
242     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>Source:Wikipedia</figcaption></figure>';
243     $this->assertIdentical($expected, $test_with_html_filter($input));
244     $this->assertIdentical($input, $test_editor_xss_filter($input));
245
246     // A pretty crazy edge case where we have two colons.
247     $input = '<img data-caption="Interesting (Scope resolution operator ::)" src="llama.jpg" />';
248     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>Interesting (Scope resolution operator ::)</figcaption></figure>';
249     $this->assertIdentical($expected, $test_with_html_filter($input));
250     $this->assertIdentical($input, $test_editor_xss_filter($input));
251
252     // An evil anchor (to ensure XSS filtering is applied to the caption also).
253     $input = '<img data-caption="This is an &lt;a href=&quot;javascript:alert();&quot;&gt;evil&lt;/a&gt; test…" src="llama.jpg" />';
254     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>This is an <a href="alert();">evil</a> test…</figcaption></figure>';
255     $this->assertIdentical($expected, $test_with_html_filter($input));
256     $expected_xss_filtered = '<img data-caption="This is an &lt;a href=&quot;alert();&quot;&gt;evil&lt;/a&gt; test…" src="llama.jpg" />';
257     $this->assertIdentical($expected_xss_filtered, $test_editor_xss_filter($input));
258   }
259
260   /**
261    * Tests the combination of the align and caption filters.
262    */
263   public function testAlignAndCaptionFilters() {
264     /** @var \Drupal\Core\Render\RendererInterface $renderer */
265     $renderer = \Drupal::service('renderer');
266     $align_filter = $this->filters['filter_align'];
267     $caption_filter = $this->filters['filter_caption'];
268
269     $test = function ($input) use ($align_filter, $caption_filter, $renderer) {
270       return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $align_filter, $caption_filter) {
271         return $caption_filter->process($align_filter->process($input, 'und'), 'und');
272       });
273     };
274
275     $attached_library = [
276       'library' => [
277         'filter/caption',
278       ],
279     ];
280
281     // Both data-caption and data-align attributes: all 3 allowed values for the
282     // data-align attribute.
283     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left" />';
284     $expected = '<figure role="group" class="align-left"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
285     $output = $test($input);
286     $this->assertIdentical($expected, $output->getProcessedText());
287     $this->assertIdentical($attached_library, $output->getAttachments());
288     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center" />';
289     $expected = '<figure role="group" class="align-center"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
290     $output = $test($input);
291     $this->assertIdentical($expected, $output->getProcessedText());
292     $this->assertIdentical($attached_library, $output->getAttachments());
293     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="right" />';
294     $expected = '<figure role="group" class="align-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
295     $output = $test($input);
296     $this->assertIdentical($expected, $output->getProcessedText());
297     $this->assertIdentical($attached_library, $output->getAttachments());
298
299     // Both data-caption and data-align attributes, but a disallowed data-align
300     // attribute value.
301     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left foobar" />';
302     $expected = '<figure role="group"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
303     $output = $test($input);
304     $this->assertIdentical($expected, $output->getProcessedText());
305     $this->assertIdentical($attached_library, $output->getAttachments());
306
307     // Ensure both filters together work for linked images.
308     $input = '<a href="http://example.com/llamas/are/awesome/but/kittens/are/cool/too"><img src="llama.jpg" data-caption="Loquacious llama!" data-align="center" /></a>';
309     $expected = '<figure role="group" class="align-center"><a href="http://example.com/llamas/are/awesome/but/kittens/are/cool/too"><img src="llama.jpg" /></a>' . "\n" . '<figcaption>Loquacious llama!</figcaption></figure>';
310     $output = $test($input);
311     $this->assertIdentical($expected, $output->getProcessedText());
312     $this->assertIdentical($attached_library, $output->getAttachments());
313   }
314
315   /**
316    * Tests the line break filter.
317    */
318   public function testLineBreakFilter() {
319     // Get FilterAutoP object.
320     $filter = $this->filters['filter_autop'];
321
322     // Since the line break filter naturally needs plenty of newlines in test
323     // strings and expectations, we're using "\n" instead of regular newlines
324     // here.
325     $tests = [
326       // Single line breaks should be changed to <br /> tags, while paragraphs
327       // separated with double line breaks should be enclosed with <p></p> tags.
328       "aaa\nbbb\n\nccc" => [
329         "<p>aaa<br />\nbbb</p>\n<p>ccc</p>" => TRUE,
330       ],
331       // Skip contents of certain block tags entirely.
332       "<script>aaa\nbbb\n\nccc</script>
333 <style>aaa\nbbb\n\nccc</style>
334 <pre>aaa\nbbb\n\nccc</pre>
335 <object>aaa\nbbb\n\nccc</object>
336 <iframe>aaa\nbbb\n\nccc</iframe>
337 " => [
338         "<script>aaa\nbbb\n\nccc</script>" => TRUE,
339         "<style>aaa\nbbb\n\nccc</style>" => TRUE,
340         "<pre>aaa\nbbb\n\nccc</pre>" => TRUE,
341         "<object>aaa\nbbb\n\nccc</object>" => TRUE,
342         "<iframe>aaa\nbbb\n\nccc</iframe>" => TRUE,
343       ],
344       // Skip comments entirely.
345       "One. <!-- comment --> Two.\n<!--\nThree.\n-->\n" => [
346         '<!-- comment -->' => TRUE,
347         "<!--\nThree.\n-->" => TRUE,
348       ],
349       // Resulting HTML should produce matching paragraph tags.
350       '<p><div>  </div></p>' => [
351         "<p>\n<div>  </div>\n</p>" => TRUE,
352       ],
353       '<div><p>  </p></div>' => [
354         "<div>\n</div>" => TRUE,
355       ],
356       '<blockquote><pre>aaa</pre></blockquote>' => [
357         "<blockquote><pre>aaa</pre></blockquote>" => TRUE,
358       ],
359       "<pre>aaa\nbbb\nccc</pre>\nddd\neee" => [
360         "<pre>aaa\nbbb\nccc</pre>" => TRUE,
361         "<p>ddd<br />\neee</p>" => TRUE,
362       ],
363       // Comments remain unchanged and subsequent lines/paragraphs are
364       // transformed normally.
365       "aaa<!--comment-->\n\nbbb\n\nccc\n\nddd<!--comment\nwith linebreak-->\n\neee\n\nfff" => [
366         "<p>aaa</p>\n<!--comment--><p>\nbbb</p>\n<p>ccc</p>\n<p>ddd</p>" => TRUE,
367         "<!--comment\nwith linebreak--><p>\neee</p>\n<p>fff</p>" => TRUE,
368       ],
369       // Check that a comment in a PRE will result that the text after
370       // the comment, but still in PRE, is not transformed.
371       "<pre>aaa\nbbb<!-- comment -->\n\nccc</pre>\nddd" => [
372         "<pre>aaa\nbbb<!-- comment -->\n\nccc</pre>" => TRUE,
373       ],
374       // Bug 810824, paragraphs were appearing around iframe tags.
375       "<iframe>aaa</iframe>\n\n" => [
376         "<p><iframe>aaa</iframe></p>" => FALSE,
377       ],
378     ];
379     $this->assertFilteredString($filter, $tests);
380
381     // Very long string hitting PCRE limits.
382     $limit = max(ini_get('pcre.backtrack_limit'), ini_get('pcre.recursion_limit'));
383     $source = $this->randomMachineName($limit);
384     $result = _filter_autop($source);
385     $success = $this->assertEqual($result, '<p>' . $source . "</p>\n", 'Line break filter can process very long strings.');
386     if (!$success) {
387       $this->verbose("\n" . $source . "\n<hr />\n" . $result);
388     }
389   }
390
391
392   /**
393    * Tests filter settings, defaults, access restrictions and similar.
394    *
395    * @todo This is for functions like filter_filter and check_markup, whose
396    *   functionality is not completely focused on filtering. Some ideas:
397    *   restricting formats according to user permissions, proper cache
398    *   handling, defaults -- allowed tags/attributes/protocols.
399    *
400    * @todo It is possible to add script, iframe etc. to allowed tags, but this
401    *   makes HTML filter completely ineffective.
402    *
403    * @todo Class, id, name and xmlns should be added to disallowed attributes,
404    *   or better a whitelist approach should be used for that too.
405    */
406   public function testHtmlFilter() {
407     // Get FilterHtml object.
408     $filter = $this->filters['filter_html'];
409     $filter->setConfiguration([
410       'settings' => [
411         'allowed_html' => '<a> <p> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
412         'filter_html_help' => 1,
413         'filter_html_nofollow' => 0,
414       ]
415     ]);
416
417     // HTML filter is not able to secure some tags, these should never be
418     // allowed.
419     $f = (string) $filter->process('<script />', Language::LANGCODE_NOT_SPECIFIED);
420     $this->assertIdentical($f, '', 'HTML filter should remove script tags.');
421
422     $f = (string) $filter->process('<iframe />', Language::LANGCODE_NOT_SPECIFIED);
423     $this->assertIdentical($f, '', 'HTML filter should remove iframe tags.');
424
425     $f = (string) $filter->process('<object />', Language::LANGCODE_NOT_SPECIFIED);
426     $this->assertIdentical($f, '', 'HTML filter should remove object tags.');
427
428     $f = (string) $filter->process('<style />', Language::LANGCODE_NOT_SPECIFIED);
429     $this->assertIdentical($f, '', 'HTML filter should remove style tags.');
430
431     // Some tags make CSRF attacks easier, let the user take the risk herself.
432     $f = (string) $filter->process('<img />', Language::LANGCODE_NOT_SPECIFIED);
433     $this->assertIdentical($f, '', 'HTML filter should remove img tags by default.');
434
435     $f = (string) $filter->process('<input />', Language::LANGCODE_NOT_SPECIFIED);
436     $this->assertIdentical($f, '', 'HTML filter should remove input tags by default.');
437
438     // Filtering content of some attributes is infeasible, these shouldn't be
439     // allowed too.
440     $f = (string) $filter->process('<p style="display: none;" />', Language::LANGCODE_NOT_SPECIFIED);
441     $this->assertNoNormalized($f, 'style', 'HTML filter should remove style attributes.');
442     $this->assertIdentical($f, '<p></p>');
443
444     $f = (string) $filter->process('<p onerror="alert(0);"></p>', Language::LANGCODE_NOT_SPECIFIED);
445     $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove on* attributes.');
446     $this->assertIdentical($f, '<p></p>');
447
448     $f = (string) $filter->process('<code onerror>&nbsp;</code>', Language::LANGCODE_NOT_SPECIFIED);
449     $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove empty on* attributes.');
450     // Note - this string has a decoded &nbsp; character.
451     $this->assertIdentical($f, '<code> </code>');
452
453     $f = (string) $filter->process('<br>', Language::LANGCODE_NOT_SPECIFIED);
454     $this->assertNormalized($f, '<br />', 'HTML filter should allow line breaks.');
455
456     $f = (string) $filter->process('<br />', Language::LANGCODE_NOT_SPECIFIED);
457     $this->assertNormalized($f, '<br />', 'HTML filter should allow self-closing line breaks.');
458
459     // All attributes of whitelisted tags are stripped by default.
460     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
461     $this->assertNormalized($f, '<a>link</a>', 'HTML filter should remove attributes that are not explicitly allowed.');
462
463     // Now whitelist the "llama" attribute on <a>.
464     $filter->setConfiguration([
465       'settings' => [
466         'allowed_html' => '<a href llama> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
467         'filter_html_help' => 1,
468         'filter_html_nofollow' => 0,
469       ]
470     ]);
471     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
472     $this->assertNormalized($f, '<a llama="awesome">link</a>', 'HTML filter keeps explicitly allowed attributes, and removes attributes that are not explicitly allowed.');
473
474     // Restrict the whitelisted "llama" attribute on <a> to only allow the value
475     // "majestical", or "epic".
476     $filter->setConfiguration([
477       'settings' => [
478         'allowed_html' => '<a href llama="majestical epic"> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
479         'filter_html_help' => 1,
480         'filter_html_nofollow' => 0,
481       ]
482     ]);
483     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
484     $this->assertIdentical($f, '<a>link</a>', 'HTML filter removes allowed attributes that do not have an explicitly allowed value.');
485     $f = (string) $filter->process('<a kitten="cute" llama="majestical">link</a>', Language::LANGCODE_NOT_SPECIFIED);
486     $this->assertIdentical($f, '<a llama="majestical">link</a>', 'HTML filter keeps explicitly allowed attributes with an attribute value that is also explicitly allowed.');
487     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
488     $this->assertNormalized($f, '<a>link</a>', 'HTML filter removes allowed attributes that have a not explicitly allowed value.');
489     $f = (string) $filter->process('<a href="/beautiful-animals" kitten="cute" llama="epic majestical">link</a>', Language::LANGCODE_NOT_SPECIFIED);
490     $this->assertIdentical($f, '<a href="/beautiful-animals" llama="epic majestical">link</a>', 'HTML filter keeps explicitly allowed attributes with an attribute value that is also explicitly allowed.');
491   }
492
493   /**
494    * Tests the spam deterrent.
495    */
496   public function testNoFollowFilter() {
497     // Get FilterHtml object.
498     $filter = $this->filters['filter_html'];
499     $filter->setConfiguration([
500       'settings' => [
501         'allowed_html' => '<a href>',
502         'filter_html_help' => 1,
503         'filter_html_nofollow' => 1,
504       ]
505     ]);
506
507     // Test if the rel="nofollow" attribute is added, even if we try to prevent
508     // it.
509     $f = (string) $filter->process('<a href="http://www.example.com/">text</a>', Language::LANGCODE_NOT_SPECIFIED);
510     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent -- no evasion.');
511
512     $f = (string) $filter->process('<A href="http://www.example.com/">text</a>', Language::LANGCODE_NOT_SPECIFIED);
513     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- capital A.');
514
515     $f = (string) $filter->process("<a/href=\"http://www.example.com/\">text</a>", Language::LANGCODE_NOT_SPECIFIED);
516     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- non whitespace character after tag name.');
517
518     $f = (string) $filter->process("<\0a\0 href=\"http://www.example.com/\">text</a>", Language::LANGCODE_NOT_SPECIFIED);
519     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- some nulls.');
520
521     $f = (string) $filter->process('<a href="http://www.example.com/" rel="follow">text</a>', Language::LANGCODE_NOT_SPECIFIED);
522     $this->assertNoNormalized($f, 'rel="follow"', 'Spam deterrent evasion -- with rel set - rel="follow" removed.');
523     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- with rel set - rel="nofollow" added.');
524   }
525
526   /**
527    * Tests the HTML escaping filter.
528    */
529   public function testHtmlEscapeFilter() {
530     // Get FilterHtmlEscape object.
531     $filter = $this->filters['filter_html_escape'];
532
533     $tests = [
534       "   One. <!-- \"comment\" --> Two'.\n<p>Three.</p>\n    " => [
535         "One. &lt;!-- &quot;comment&quot; --&gt; Two&#039;.\n&lt;p&gt;Three.&lt;/p&gt;" => TRUE,
536         '   One.' => FALSE,
537         "</p>\n    " => FALSE,
538       ],
539     ];
540     $this->assertFilteredString($filter, $tests);
541   }
542
543   /**
544    * Tests the URL filter.
545    */
546   public function testUrlFilter() {
547     // Get FilterUrl object.
548     $filter = $this->filters['filter_url'];
549     $filter->setConfiguration([
550       'settings' => [
551         'filter_url_length' => 496,
552       ]
553     ]);
554
555     // @todo Possible categories:
556     // - absolute, mail, partial
557     // - characters/encoding, surrounding markup, security
558
559     // Create a email that is too long.
560     $long_email = str_repeat('a', 254) . '@example.com';
561     $too_long_email = str_repeat('b', 255) . '@example.com';
562     $email_with_plus_sign = 'one+two@example.com';
563
564     // Filter selection/pattern matching.
565     $tests = [
566       // HTTP URLs.
567       '
568 http://example.com or www.example.com
569 ' => [
570         '<a href="http://example.com">http://example.com</a>' => TRUE,
571         '<a href="http://www.example.com">www.example.com</a>' => TRUE,
572       ],
573       // MAILTO URLs.
574       '
575 person@example.com or mailto:person2@example.com or ' . $email_with_plus_sign . ' or ' . $long_email . ' but not ' . $too_long_email . '
576 ' => [
577         '<a href="mailto:person@example.com">person@example.com</a>' => TRUE,
578         '<a href="mailto:person2@example.com">mailto:person2@example.com</a>' => TRUE,
579         '<a href="mailto:' . $long_email . '">' . $long_email . '</a>' => TRUE,
580         '<a href="mailto:' . $too_long_email . '">' . $too_long_email . '</a>' => FALSE,
581         '<a href="mailto:' . $email_with_plus_sign . '">' . $email_with_plus_sign . '</a>' => TRUE,
582       ],
583       // URI parts and special characters.
584       '
585 http://trailingslash.com/ or www.trailingslash.com/
586 http://host.com/some/path?query=foo&bar[baz]=beer#fragment or www.host.com/some/path?query=foo&bar[baz]=beer#fragment
587 http://twitter.com/#!/example/status/22376963142324226
588 http://example.com/@user/
589 ftp://user:pass@ftp.example.com/~home/dir1
590 sftp://user@nonstandardport:222/dir
591 ssh://192.168.0.100/srv/git/drupal.git
592 ' => [
593         '<a href="http://trailingslash.com/">http://trailingslash.com/</a>' => TRUE,
594         '<a href="http://www.trailingslash.com/">www.trailingslash.com/</a>' => TRUE,
595         '<a href="http://host.com/some/path?query=foo&amp;bar[baz]=beer#fragment">http://host.com/some/path?query=foo&amp;bar[baz]=beer#fragment</a>' => TRUE,
596         '<a href="http://www.host.com/some/path?query=foo&amp;bar[baz]=beer#fragment">www.host.com/some/path?query=foo&amp;bar[baz]=beer#fragment</a>' => TRUE,
597         '<a href="http://twitter.com/#!/example/status/22376963142324226">http://twitter.com/#!/example/status/22376963142324226</a>' => TRUE,
598         '<a href="http://example.com/@user/">http://example.com/@user/</a>' => TRUE,
599         '<a href="ftp://user:pass@ftp.example.com/~home/dir1">ftp://user:pass@ftp.example.com/~home/dir1</a>' => TRUE,
600         '<a href="sftp://user@nonstandardport:222/dir">sftp://user@nonstandardport:222/dir</a>' => TRUE,
601         '<a href="ssh://192.168.0.100/srv/git/drupal.git">ssh://192.168.0.100/srv/git/drupal.git</a>' => TRUE,
602       ],
603       // International Unicode characters.
604       '
605 http://пример.испытание/
606 http://مثال.إختبار/
607 http://例子.測試/
608 http://12345.中国/
609 http://例え.テスト/
610 http://dréißig-bücher.de/
611 http://méxico-mañana.es/
612 ' => [
613         '<a href="http://пример.испытание/">http://пример.испытание/</a>' => TRUE,
614         '<a href="http://مثال.إختبار/">http://مثال.إختبار/</a>' => TRUE,
615         '<a href="http://例子.測試/">http://例子.測試/</a>' => TRUE,
616         '<a href="http://12345.中国/">http://12345.中国/</a>' => TRUE,
617         '<a href="http://例え.テスト/">http://例え.テスト/</a>' => TRUE,
618         '<a href="http://dréißig-bücher.de/">http://dréißig-bücher.de/</a>' => TRUE,
619         '<a href="http://méxico-mañana.es/">http://méxico-mañana.es/</a>' => TRUE,
620       ],
621       // Encoding.
622       '
623 http://ampersand.com/?a=1&b=2
624 http://encoded.com/?a=1&amp;b=2
625 ' => [
626         '<a href="http://ampersand.com/?a=1&amp;b=2">http://ampersand.com/?a=1&amp;b=2</a>' => TRUE,
627         '<a href="http://encoded.com/?a=1&amp;b=2">http://encoded.com/?a=1&amp;b=2</a>' => TRUE,
628       ],
629       // Domain name length.
630       '
631 www.ex.ex or www.example.example or www.toolongdomainexampledomainexampledomainexampledomainexampledomain or
632 me@me.tv
633 ' => [
634         '<a href="http://www.ex.ex">www.ex.ex</a>' => TRUE,
635         '<a href="http://www.example.example">www.example.example</a>' => TRUE,
636         'http://www.toolong' => FALSE,
637         '<a href="mailto:me@me.tv">me@me.tv</a>' => TRUE,
638       ],
639       // Absolute URL protocols.
640       // The list to test is found in the beginning of _filter_url() at
641       // $protocols = \Drupal::getContainer()->getParameter('filter_protocols').
642       '
643 https://example.com,
644 ftp://ftp.example.com,
645 news://example.net,
646 telnet://example,
647 irc://example.host,
648 ssh://odd.geek,
649 sftp://secure.host?,
650 webcal://calendar,
651 rtsp://127.0.0.1,
652 not foo://disallowed.com.
653 ' => [
654         'href="https://example.com"' => TRUE,
655         'href="ftp://ftp.example.com"' => TRUE,
656         'href="news://example.net"' => TRUE,
657         'href="telnet://example"' => TRUE,
658         'href="irc://example.host"' => TRUE,
659         'href="ssh://odd.geek"' => TRUE,
660         'href="sftp://secure.host"' => TRUE,
661         'href="webcal://calendar"' => TRUE,
662         'href="rtsp://127.0.0.1"' => TRUE,
663         'href="foo://disallowed.com"' => FALSE,
664         'not foo://disallowed.com.' => TRUE,
665       ],
666     ];
667     $this->assertFilteredString($filter, $tests);
668
669     // Surrounding text/punctuation.
670     $tests = [
671       '
672 Partial URL with trailing period www.partial.com.
673 Email with trailing comma person@example.com,
674 Absolute URL with trailing question http://www.absolute.com?
675 Query string with trailing exclamation www.query.com/index.php?a=!
676 Partial URL with 3 trailing www.partial.periods...
677 Email with 3 trailing exclamations@example.com!!!
678 Absolute URL and query string with 2 different punctuation characters (http://www.example.com/q=abc).
679 Partial URL with brackets in the URL as well as surrounded brackets (www.foo.com/more_(than)_one_(parens)).
680 Absolute URL with square brackets in the URL as well as surrounded brackets [https://www.drupal.org/?class[]=1]
681 Absolute URL with quotes "https://www.drupal.org/sample"
682
683 ' => [
684         'period <a href="http://www.partial.com">www.partial.com</a>.' => TRUE,
685         'comma <a href="mailto:person@example.com">person@example.com</a>,' => TRUE,
686         'question <a href="http://www.absolute.com">http://www.absolute.com</a>?' => TRUE,
687         'exclamation <a href="http://www.query.com/index.php?a=">www.query.com/index.php?a=</a>!' => TRUE,
688         'trailing <a href="http://www.partial.periods">www.partial.periods</a>...' => TRUE,
689         'trailing <a href="mailto:exclamations@example.com">exclamations@example.com</a>!!!' => TRUE,
690         'characters (<a href="http://www.example.com/q=abc">http://www.example.com/q=abc</a>).' => TRUE,
691         'brackets (<a href="http://www.foo.com/more_(than)_one_(parens)">www.foo.com/more_(than)_one_(parens)</a>).' => TRUE,
692         'brackets [<a href="https://www.drupal.org/?class[]=1">https://www.drupal.org/?class[]=1</a>]' => TRUE,
693         'quotes "<a href="https://www.drupal.org/sample">https://www.drupal.org/sample</a>"' => TRUE,
694       ],
695       '
696 (www.parenthesis.com/dir?a=1&b=2#a)
697 ' => [
698         '(<a href="http://www.parenthesis.com/dir?a=1&amp;b=2#a">www.parenthesis.com/dir?a=1&amp;b=2#a</a>)' => TRUE,
699       ],
700     ];
701     $this->assertFilteredString($filter, $tests);
702
703     // Surrounding markup.
704     $tests = [
705       '
706 <p xmlns="www.namespace.com" />
707 <p xmlns="http://namespace.com">
708 An <a href="http://example.com" title="Read more at www.example.info...">anchor</a>.
709 </p>
710 ' => [
711         '<p xmlns="www.namespace.com" />' => TRUE,
712         '<p xmlns="http://namespace.com">' => TRUE,
713         'href="http://www.namespace.com"' => FALSE,
714         'href="http://namespace.com"' => FALSE,
715         'An <a href="http://example.com" title="Read more at www.example.info...">anchor</a>.' => TRUE,
716       ],
717       '
718 Not <a href="foo">www.relative.com</a> or <a href="http://absolute.com">www.absolute.com</a>
719 but <strong>http://www.strong.net</strong> or <em>www.emphasis.info</em>
720 ' => [
721         '<a href="foo">www.relative.com</a>' => TRUE,
722         'href="http://www.relative.com"' => FALSE,
723         '<a href="http://absolute.com">www.absolute.com</a>' => TRUE,
724         '<strong><a href="http://www.strong.net">http://www.strong.net</a></strong>' => TRUE,
725         '<em><a href="http://www.emphasis.info">www.emphasis.info</a></em>' => TRUE,
726       ],
727       '
728 Test <code>using www.example.com the code tag</code>.
729 ' => [
730         'href' => FALSE,
731         'http' => FALSE,
732       ],
733       '
734 Intro.
735 <blockquote>
736 Quoted text linking to www.example.com, written by person@example.com, originating from http://origin.example.com. <code>@see www.usage.example.com or <em>www.example.info</em> bla bla</code>.
737 </blockquote>
738
739 Outro.
740 ' => [
741         'href="http://www.example.com"' => TRUE,
742         'href="mailto:person@example.com"' => TRUE,
743         'href="http://origin.example.com"' => TRUE,
744         'http://www.usage.example.com' => FALSE,
745         'http://www.example.info' => FALSE,
746         'Intro.' => TRUE,
747         'Outro.' => TRUE,
748       ],
749       '
750 Unknown tag <x>containing x and www.example.com</x>? And a tag <pooh>beginning with p and containing www.example.pooh with p?</pooh>
751 ' => [
752         'href="http://www.example.com"' => TRUE,
753         'href="http://www.example.pooh"' => TRUE,
754       ],
755       '
756 <p>Test &lt;br/&gt;: This is a www.example17.com example <strong>with</strong> various http://www.example18.com tags. *<br/>
757  It is important www.example19.com to *<br/>test different URLs and http://www.example20.com in the same paragraph. *<br>
758 HTML www.example21.com soup by person@example22.com can litererally http://www.example23.com contain *img*<img> anything. Just a www.example24.com with http://www.example25.com thrown in. www.example26.com from person@example27.com with extra http://www.example28.com.
759 ' => [
760         'href="http://www.example17.com"' => TRUE,
761         'href="http://www.example18.com"' => TRUE,
762         'href="http://www.example19.com"' => TRUE,
763         'href="http://www.example20.com"' => TRUE,
764         'href="http://www.example21.com"' => TRUE,
765         'href="mailto:person@example22.com"' => TRUE,
766         'href="http://www.example23.com"' => TRUE,
767         'href="http://www.example24.com"' => TRUE,
768         'href="http://www.example25.com"' => TRUE,
769         'href="http://www.example26.com"' => TRUE,
770         'href="mailto:person@example27.com"' => TRUE,
771         'href="http://www.example28.com"' => TRUE,
772       ],
773       '
774 <script>
775 <!--
776   // @see www.example.com
777   var exampleurl = "http://example.net";
778 -->
779 <!--//--><![CDATA[//><!--
780   // @see www.example.com
781   var exampleurl = "http://example.net";
782 //--><!]]>
783 </script>
784 ' => [
785         'href="http://www.example.com"' => FALSE,
786         'href="http://example.net"' => FALSE,
787       ],
788       '
789 <style>body {
790   background: url(http://example.com/pixel.gif);
791 }</style>
792 ' => [
793         'href' => FALSE,
794       ],
795       '
796 <!-- Skip any URLs like www.example.com in comments -->
797 ' => [
798         'href' => FALSE,
799       ],
800       '
801 <!-- Skip any URLs like
802 www.example.com with a newline in comments -->
803 ' => [
804         'href' => FALSE,
805       ],
806       '
807 <!-- Skip any URLs like www.comment.com in comments. <p>Also ignore http://commented.out/markup.</p> -->
808 ' => [
809         'href' => FALSE,
810       ],
811       '
812 <dl>
813 <dt>www.example.com</dt>
814 <dd>http://example.com</dd>
815 <dd>person@example.com</dd>
816 <dt>Check www.example.net</dt>
817 <dd>Some text around http://www.example.info by person@example.info?</dd>
818 </dl>
819 ' => [
820         'href="http://www.example.com"' => TRUE,
821         'href="http://example.com"' => TRUE,
822         'href="mailto:person@example.com"' => TRUE,
823         'href="http://www.example.net"' => TRUE,
824         'href="http://www.example.info"' => TRUE,
825         'href="mailto:person@example.info"' => TRUE,
826       ],
827       '
828 <div>www.div.com</div>
829 <ul>
830 <li>http://listitem.com</li>
831 <li class="odd">www.class.listitem.com</li>
832 </ul>
833 ' => [
834         '<div><a href="http://www.div.com">www.div.com</a></div>' => TRUE,
835         '<li><a href="http://listitem.com">http://listitem.com</a></li>' => TRUE,
836         '<li class="odd"><a href="http://www.class.listitem.com">www.class.listitem.com</a></li>' => TRUE,
837       ],
838     ];
839     $this->assertFilteredString($filter, $tests);
840
841     // URL trimming.
842     $filter->setConfiguration([
843       'settings' => [
844         'filter_url_length' => 20,
845       ]
846     ]);
847     $tests = [
848       'www.trimmed.com/d/ff.ext?a=1&b=2#a1' => [
849         '<a href="http://www.trimmed.com/d/ff.ext?a=1&amp;b=2#a1">www.trimmed.com/d/f…</a>' => TRUE,
850       ],
851     ];
852     $this->assertFilteredString($filter, $tests);
853   }
854
855   /**
856    * Asserts multiple filter output expectations for multiple input strings.
857    *
858    * @param FilterInterface $filter
859    *   A input filter object.
860    * @param array $tests
861    *   An associative array, whereas each key is an arbitrary input string and
862    *   each value is again an associative array whose keys are filter output
863    *   strings and whose values are Booleans indicating whether the output is
864    *   expected or not. For example:
865    *   @code
866    *   $tests = array(
867    *     'Input string' => array(
868    *       '<p>Input string</p>' => TRUE,
869    *       'Input string<br' => FALSE,
870    *     ),
871    *   );
872    *   @endcode
873    */
874   public function assertFilteredString($filter, $tests) {
875     foreach ($tests as $source => $tasks) {
876       $result = $filter->process($source, $filter)->getProcessedText();
877       foreach ($tasks as $value => $is_expected) {
878         // Not using assertIdentical, since combination with strpos() is hard to grok.
879         if ($is_expected) {
880           $success = $this->assertTrue(strpos($result, $value) !== FALSE, format_string('@source: @value found. Filtered result: @result.', [
881             '@source' => var_export($source, TRUE),
882             '@value' => var_export($value, TRUE),
883             '@result' => var_export($result, TRUE),
884           ]));
885         }
886         else {
887           $success = $this->assertTrue(strpos($result, $value) === FALSE, format_string('@source: @value not found. Filtered result: @result.', [
888             '@source' => var_export($source, TRUE),
889             '@value' => var_export($value, TRUE),
890             '@result' => var_export($result, TRUE),
891           ]));
892         }
893         if (!$success) {
894           $this->verbose('Source:<pre>' . Html::escape(var_export($source, TRUE)) . '</pre>'
895             . '<hr />' . 'Result:<pre>' . Html::escape(var_export($result, TRUE)) . '</pre>'
896             . '<hr />' . ($is_expected ? 'Expected:' : 'Not expected:')
897             . '<pre>' . Html::escape(var_export($value, TRUE)) . '</pre>'
898           );
899         }
900       }
901     }
902   }
903
904   /**
905    * Tests URL filter on longer content.
906    *
907    * Filters based on regular expressions should also be tested with a more
908    * complex content than just isolated test lines.
909    * The most common errors are:
910    * - accidental '*' (greedy) match instead of '*?' (minimal) match.
911    * - only matching first occurrence instead of all.
912    * - newlines not matching '.*'.
913    *
914    * This test covers:
915    * - Document with multiple newlines and paragraphs (two newlines).
916    * - Mix of several HTML tags, invalid non-HTML tags, tags to ignore and HTML
917    *   comments.
918    * - Empty HTML tags (BR, IMG).
919    * - Mix of absolute and partial URLs, and email addresses in one content.
920    */
921   public function testUrlFilterContent() {
922     // Get FilterUrl object.
923     $filter = $this->filters['filter_url'];
924     $filter->setConfiguration([
925       'settings' => [
926         'filter_url_length' => 496,
927       ]
928     ]);
929     $path = __DIR__ . '/../..';
930
931     $input = file_get_contents($path . '/filter.url-input.txt');
932     $expected = file_get_contents($path . '/filter.url-output.txt');
933     $result = _filter_url($input, $filter);
934     $this->assertIdentical($result, $expected, 'Complex HTML document was correctly processed.');
935   }
936
937   /**
938    * Tests the HTML corrector filter.
939    *
940    * @todo This test could really use some validity checking function.
941    */
942   public function testHtmlCorrectorFilter() {
943     // Tag closing.
944     $f = Html::normalize('<p>text');
945     $this->assertEqual($f, '<p>text</p>', 'HTML corrector -- tag closing at the end of input.');
946
947     $f = Html::normalize('<p>text<p><p>text');
948     $this->assertEqual($f, '<p>text</p><p></p><p>text</p>', 'HTML corrector -- tag closing.');
949
950     $f = Html::normalize("<ul><li>e1<li>e2");
951     $this->assertEqual($f, "<ul><li>e1</li><li>e2</li></ul>", 'HTML corrector -- unclosed list tags.');
952
953     $f = Html::normalize('<div id="d">content');
954     $this->assertEqual($f, '<div id="d">content</div>', 'HTML corrector -- unclosed tag with attribute.');
955
956     // XHTML slash for empty elements.
957     $f = Html::normalize('<hr><br>');
958     $this->assertEqual($f, '<hr /><br />', 'HTML corrector -- XHTML closing slash.');
959
960     $f = Html::normalize('<P>test</P>');
961     $this->assertEqual($f, '<p>test</p>', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
962
963     $f = Html::normalize('<P>test</p>');
964     $this->assertEqual($f, '<p>test</p>', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
965
966     $f = Html::normalize('test<hr />');
967     $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through.');
968
969     $f = Html::normalize('test<hr/>');
970     $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through, but ensure there is a single space before the closing slash.');
971
972     $f = Html::normalize('test<hr    />');
973     $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through, but ensure there are not too many spaces before the closing slash.');
974
975     $f = Html::normalize('<span class="test" />');
976     $this->assertEqual($f, '<span class="test"></span>', 'HTML corrector -- Convert XHTML that is properly formed but that would not be compatible with typical HTML user agents.');
977
978     $f = Html::normalize('test1<br class="test">test2');
979     $this->assertEqual($f, 'test1<br class="test" />test2', 'HTML corrector -- Automatically close single tags.');
980
981     $f = Html::normalize('line1<hr>line2');
982     $this->assertEqual($f, 'line1<hr />line2', 'HTML corrector -- Automatically close single tags.');
983
984     $f = Html::normalize('line1<HR>line2');
985     $this->assertEqual($f, 'line1<hr />line2', 'HTML corrector -- Automatically close single tags.');
986
987     $f = Html::normalize('<img src="http://example.com/test.jpg">test</img>');
988     $this->assertEqual($f, '<img src="http://example.com/test.jpg" />test', 'HTML corrector -- Automatically close single tags.');
989
990     $f = Html::normalize('<br></br>');
991     $this->assertEqual($f, '<br />', "HTML corrector -- Transform empty tags to a single closed tag if the tag's content model is EMPTY.");
992
993     $f = Html::normalize('<div></div>');
994     $this->assertEqual($f, '<div></div>', "HTML corrector -- Do not transform empty tags to a single closed tag if the tag's content model is not EMPTY.");
995
996     $f = Html::normalize('<p>line1<br/><hr/>line2</p>');
997     $this->assertEqual($f, '<p>line1<br /></p><hr />line2', 'HTML corrector -- Move non-inline elements outside of inline containers.');
998
999     $f = Html::normalize('<p>line1<div>line2</div></p>');
1000     $this->assertEqual($f, '<p>line1</p><div>line2</div>', 'HTML corrector -- Move non-inline elements outside of inline containers.');
1001
1002     $f = Html::normalize('<p>test<p>test</p>\n');
1003     $this->assertEqual($f, '<p>test</p><p>test</p>\n', 'HTML corrector -- Auto-close improperly nested tags.');
1004
1005     $f = Html::normalize('<p>Line1<br><STRONG>bold stuff</b>');
1006     $this->assertEqual($f, '<p>Line1<br /><strong>bold stuff</strong></p>', 'HTML corrector -- Properly close unclosed tags, and remove useless closing tags.');
1007
1008     $f = Html::normalize('test <!-- this is a comment -->');
1009     $this->assertEqual($f, 'test <!-- this is a comment -->', 'HTML corrector -- Do not touch HTML comments.');
1010
1011     $f = Html::normalize('test <!--this is a comment-->');
1012     $this->assertEqual($f, 'test <!--this is a comment-->', 'HTML corrector -- Do not touch HTML comments.');
1013
1014     $f = Html::normalize('test <!-- comment <p>another
1015     <strong>multiple</strong> line
1016     comment</p> -->');
1017     $this->assertEqual($f, 'test <!-- comment <p>another
1018     <strong>multiple</strong> line
1019     comment</p> -->', 'HTML corrector -- Do not touch HTML comments.');
1020
1021     $f = Html::normalize('test <!-- comment <p>another comment</p> -->');
1022     $this->assertEqual($f, 'test <!-- comment <p>another comment</p> -->', 'HTML corrector -- Do not touch HTML comments.');
1023
1024     $f = Html::normalize('test <!--break-->');
1025     $this->assertEqual($f, 'test <!--break-->', 'HTML corrector -- Do not touch HTML comments.');
1026
1027     $f = Html::normalize('<p>test\n</p>\n');
1028     $this->assertEqual($f, '<p>test\n</p>\n', 'HTML corrector -- New-lines are accepted and kept as-is.');
1029
1030     $f = Html::normalize('<p>دروبال');
1031     $this->assertEqual($f, '<p>دروبال</p>', 'HTML corrector -- Encoding is correctly kept.');
1032
1033     $f = Html::normalize('<script>alert("test")</script>');
1034     $this->assertEqual($f, '<script>
1035 <!--//--><![CDATA[// ><!--
1036 alert("test")
1037 //--><!]]>
1038 </script>', 'HTML corrector -- CDATA added to script element');
1039
1040     $f = Html::normalize('<p><script>alert("test")</script></p>');
1041     $this->assertEqual($f, '<p><script>
1042 <!--//--><![CDATA[// ><!--
1043 alert("test")
1044 //--><!]]>
1045 </script></p>', 'HTML corrector -- CDATA added to a nested script element');
1046
1047     $f = Html::normalize('<p><style> /* Styling */ body {color:red}</style></p>');
1048     $this->assertEqual($f, '<p><style>
1049 <!--/*--><![CDATA[/* ><!--*/
1050  /* Styling */ body {color:red}
1051 /*--><!]]>*/
1052 </style></p>', 'HTML corrector -- CDATA added to a style element.');
1053
1054     $filtered_data = Html::normalize('<p><style>
1055 /*<![CDATA[*/
1056 /* Styling */
1057 body {color:red}
1058 /*]]>*/
1059 </style></p>');
1060     $this->assertEqual($filtered_data, '<p><style>
1061 <!--/*--><![CDATA[/* ><!--*/
1062
1063 /*<![CDATA[*/
1064 /* Styling */
1065 body {color:red}
1066 /*]]]]><![CDATA[>*/
1067
1068 /*--><!]]>*/
1069 </style></p>',
1070       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '/*<![CDATA[*/'])
1071     );
1072
1073     $filtered_data = Html::normalize('<p><style>
1074   <!--/*--><![CDATA[/* ><!--*/
1075   /* Styling */
1076   body {color:red}
1077   /*--><!]]>*/
1078 </style></p>');
1079     $this->assertEqual($filtered_data, '<p><style>
1080 <!--/*--><![CDATA[/* ><!--*/
1081
1082   <!--/*--><![CDATA[/* ><!--*/
1083   /* Styling */
1084   body {color:red}
1085   /*--><!]]]]><![CDATA[>*/
1086
1087 /*--><!]]>*/
1088 </style></p>',
1089       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '<!--/*--><![CDATA[/* ><!--*/'])
1090     );
1091
1092     $filtered_data = Html::normalize('<p><script>
1093 <!--//--><![CDATA[// ><!--
1094   alert("test");
1095 //--><!]]>
1096 </script></p>');
1097     $this->assertEqual($filtered_data, '<p><script>
1098 <!--//--><![CDATA[// ><!--
1099
1100 <!--//--><![CDATA[// ><!--
1101   alert("test");
1102 //--><!]]]]><![CDATA[>
1103
1104 //--><!]]>
1105 </script></p>',
1106       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '<!--//--><![CDATA[// ><!--'])
1107     );
1108
1109     $filtered_data = Html::normalize('<p><script>
1110 // <![CDATA[
1111   alert("test");
1112 // ]]>
1113 </script></p>');
1114     $this->assertEqual($filtered_data, '<p><script>
1115 <!--//--><![CDATA[// ><!--
1116
1117 // <![CDATA[
1118   alert("test");
1119 // ]]]]><![CDATA[>
1120
1121 //--><!]]>
1122 </script></p>',
1123       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '// <![CDATA['])
1124     );
1125
1126   }
1127
1128   /**
1129    * Asserts that a text transformed to lowercase with HTML entities decoded does contains a given string.
1130    *
1131    * Otherwise fails the test with a given message, similar to all the
1132    * SimpleTest assert* functions.
1133    *
1134    * Note that this does not remove nulls, new lines and other characters that
1135    * could be used to obscure a tag or an attribute name.
1136    *
1137    * @param string $haystack
1138    *   Text to look in.
1139    * @param string $needle
1140    *   Lowercase, plain text to look for.
1141    * @param string $message
1142    *   (optional) Message to display if failed. Defaults to an empty string.
1143    * @param string $group
1144    *   (optional) The group this message belongs to. Defaults to 'Other'.
1145    *
1146    * @return bool
1147    *   TRUE on pass, FALSE on fail.
1148    */
1149   public function assertNormalized($haystack, $needle, $message = '', $group = 'Other') {
1150     return $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) !== FALSE, $message, $group);
1151   }
1152
1153   /**
1154    * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string.
1155    *
1156    * Otherwise fails the test with a given message, similar to all the
1157    * SimpleTest assert* functions.
1158    *
1159    * Note that this does not remove nulls, new lines, and other character that
1160    * could be used to obscure a tag or an attribute name.
1161    *
1162    * @param string $haystack
1163    *   Text to look in.
1164    * @param string $needle
1165    *   Lowercase, plain text to look for.
1166    * @param string $message
1167    *   (optional) Message to display if failed. Defaults to an empty string.
1168    * @param string $group
1169    *   (optional) The group this message belongs to. Defaults to 'Other'.
1170    *
1171    * @return bool
1172    *   TRUE on pass, FALSE on fail.
1173    */
1174   public function assertNoNormalized($haystack, $needle, $message = '', $group = 'Other') {
1175     return $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group);
1176   }
1177
1178 }