b8626aedae4268c6911f9eb473d5a7f956aaa67a
[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    * Tests filter settings, defaults, access restrictions and similar.
393    *
394    * @todo This is for functions like filter_filter and check_markup, whose
395    *   functionality is not completely focused on filtering. Some ideas:
396    *   restricting formats according to user permissions, proper cache
397    *   handling, defaults -- allowed tags/attributes/protocols.
398    *
399    * @todo It is possible to add script, iframe etc. to allowed tags, but this
400    *   makes HTML filter completely ineffective.
401    *
402    * @todo Class, id, name and xmlns should be added to disallowed attributes,
403    *   or better a whitelist approach should be used for that too.
404    */
405   public function testHtmlFilter() {
406     // Get FilterHtml object.
407     $filter = $this->filters['filter_html'];
408     $filter->setConfiguration([
409       'settings' => [
410         'allowed_html' => '<a> <p> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
411         'filter_html_help' => 1,
412         'filter_html_nofollow' => 0,
413       ],
414     ]);
415
416     // HTML filter is not able to secure some tags, these should never be
417     // allowed.
418     $f = (string) $filter->process('<script />', Language::LANGCODE_NOT_SPECIFIED);
419     $this->assertIdentical($f, '', 'HTML filter should remove script tags.');
420
421     $f = (string) $filter->process('<iframe />', Language::LANGCODE_NOT_SPECIFIED);
422     $this->assertIdentical($f, '', 'HTML filter should remove iframe tags.');
423
424     $f = (string) $filter->process('<object />', Language::LANGCODE_NOT_SPECIFIED);
425     $this->assertIdentical($f, '', 'HTML filter should remove object tags.');
426
427     $f = (string) $filter->process('<style />', Language::LANGCODE_NOT_SPECIFIED);
428     $this->assertIdentical($f, '', 'HTML filter should remove style tags.');
429
430     // Some tags make CSRF attacks easier, let the user take the risk herself.
431     $f = (string) $filter->process('<img />', Language::LANGCODE_NOT_SPECIFIED);
432     $this->assertIdentical($f, '', 'HTML filter should remove img tags by default.');
433
434     $f = (string) $filter->process('<input />', Language::LANGCODE_NOT_SPECIFIED);
435     $this->assertIdentical($f, '', 'HTML filter should remove input tags by default.');
436
437     // Filtering content of some attributes is infeasible, these shouldn't be
438     // allowed too.
439     $f = (string) $filter->process('<p style="display: none;" />', Language::LANGCODE_NOT_SPECIFIED);
440     $this->assertNoNormalized($f, 'style', 'HTML filter should remove style attributes.');
441     $this->assertIdentical($f, '<p></p>');
442
443     $f = (string) $filter->process('<p onerror="alert(0);"></p>', Language::LANGCODE_NOT_SPECIFIED);
444     $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove on* attributes.');
445     $this->assertIdentical($f, '<p></p>');
446
447     $f = (string) $filter->process('<code onerror>&nbsp;</code>', Language::LANGCODE_NOT_SPECIFIED);
448     $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove empty on* attributes.');
449     // Note - this string has a decoded &nbsp; character.
450     $this->assertIdentical($f, '<code> </code>');
451
452     $f = (string) $filter->process('<br>', Language::LANGCODE_NOT_SPECIFIED);
453     $this->assertNormalized($f, '<br />', 'HTML filter should allow line breaks.');
454
455     $f = (string) $filter->process('<br />', Language::LANGCODE_NOT_SPECIFIED);
456     $this->assertNormalized($f, '<br />', 'HTML filter should allow self-closing line breaks.');
457
458     // All attributes of whitelisted tags are stripped by default.
459     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
460     $this->assertNormalized($f, '<a>link</a>', 'HTML filter should remove attributes that are not explicitly allowed.');
461
462     // Now whitelist the "llama" attribute on <a>.
463     $filter->setConfiguration([
464       'settings' => [
465         'allowed_html' => '<a href llama> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
466         'filter_html_help' => 1,
467         'filter_html_nofollow' => 0,
468       ],
469     ]);
470     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
471     $this->assertNormalized($f, '<a llama="awesome">link</a>', 'HTML filter keeps explicitly allowed attributes, and removes attributes that are not explicitly allowed.');
472
473     // Restrict the whitelisted "llama" attribute on <a> to only allow the value
474     // "majestical", or "epic".
475     $filter->setConfiguration([
476       'settings' => [
477         'allowed_html' => '<a href llama="majestical epic"> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
478         'filter_html_help' => 1,
479         'filter_html_nofollow' => 0,
480       ],
481     ]);
482     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
483     $this->assertIdentical($f, '<a>link</a>', 'HTML filter removes allowed attributes that do not have an explicitly allowed value.');
484     $f = (string) $filter->process('<a kitten="cute" llama="majestical">link</a>', Language::LANGCODE_NOT_SPECIFIED);
485     $this->assertIdentical($f, '<a llama="majestical">link</a>', 'HTML filter keeps explicitly allowed attributes with an attribute value that is also explicitly allowed.');
486     $f = (string) $filter->process('<a kitten="cute" llama="awesome">link</a>', Language::LANGCODE_NOT_SPECIFIED);
487     $this->assertNormalized($f, '<a>link</a>', 'HTML filter removes allowed attributes that have a not explicitly allowed value.');
488     $f = (string) $filter->process('<a href="/beautiful-animals" kitten="cute" llama="epic majestical">link</a>', Language::LANGCODE_NOT_SPECIFIED);
489     $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.');
490   }
491
492   /**
493    * Tests the spam deterrent.
494    */
495   public function testNoFollowFilter() {
496     // Get FilterHtml object.
497     $filter = $this->filters['filter_html'];
498     $filter->setConfiguration([
499       'settings' => [
500         'allowed_html' => '<a href>',
501         'filter_html_help' => 1,
502         'filter_html_nofollow' => 1,
503       ],
504     ]);
505
506     // Test if the rel="nofollow" attribute is added, even if we try to prevent
507     // it.
508     $f = (string) $filter->process('<a href="http://www.example.com/">text</a>', Language::LANGCODE_NOT_SPECIFIED);
509     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent -- no evasion.');
510
511     $f = (string) $filter->process('<A href="http://www.example.com/">text</a>', Language::LANGCODE_NOT_SPECIFIED);
512     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- capital A.');
513
514     $f = (string) $filter->process("<a/href=\"http://www.example.com/\">text</a>", Language::LANGCODE_NOT_SPECIFIED);
515     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- non whitespace character after tag name.');
516
517     $f = (string) $filter->process("<\0a\0 href=\"http://www.example.com/\">text</a>", Language::LANGCODE_NOT_SPECIFIED);
518     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- some nulls.');
519
520     $f = (string) $filter->process('<a href="http://www.example.com/" rel="follow">text</a>', Language::LANGCODE_NOT_SPECIFIED);
521     $this->assertNoNormalized($f, 'rel="follow"', 'Spam deterrent evasion -- with rel set - rel="follow" removed.');
522     $this->assertNormalized($f, 'rel="nofollow"', 'Spam deterrent evasion -- with rel set - rel="nofollow" added.');
523   }
524
525   /**
526    * Tests the HTML escaping filter.
527    */
528   public function testHtmlEscapeFilter() {
529     // Get FilterHtmlEscape object.
530     $filter = $this->filters['filter_html_escape'];
531
532     $tests = [
533       "   One. <!-- \"comment\" --> Two'.\n<p>Three.</p>\n    " => [
534         "One. &lt;!-- &quot;comment&quot; --&gt; Two&#039;.\n&lt;p&gt;Three.&lt;/p&gt;" => TRUE,
535         '   One.' => FALSE,
536         "</p>\n    " => FALSE,
537       ],
538     ];
539     $this->assertFilteredString($filter, $tests);
540   }
541
542   /**
543    * Tests the URL filter.
544    */
545   public function testUrlFilter() {
546     // Get FilterUrl object.
547     $filter = $this->filters['filter_url'];
548     $filter->setConfiguration([
549       'settings' => [
550         'filter_url_length' => 496,
551       ],
552     ]);
553
554     // @todo Possible categories:
555     // - absolute, mail, partial
556     // - characters/encoding, surrounding markup, security
557
558     // Create a email that is too long.
559     $long_email = str_repeat('a', 254) . '@example.com';
560     $too_long_email = str_repeat('b', 255) . '@example.com';
561     $email_with_plus_sign = 'one+two@example.com';
562
563     // Filter selection/pattern matching.
564     $tests = [
565       // HTTP URLs.
566       '
567 http://example.com or www.example.com
568 ' => [
569         '<a href="http://example.com">http://example.com</a>' => TRUE,
570         '<a href="http://www.example.com">www.example.com</a>' => TRUE,
571       ],
572       // MAILTO URLs.
573       '
574 person@example.com or mailto:person2@example.com or ' . $email_with_plus_sign . ' or ' . $long_email . ' but not ' . $too_long_email . '
575 ' => [
576         '<a href="mailto:person@example.com">person@example.com</a>' => TRUE,
577         '<a href="mailto:person2@example.com">mailto:person2@example.com</a>' => TRUE,
578         '<a href="mailto:' . $long_email . '">' . $long_email . '</a>' => TRUE,
579         '<a href="mailto:' . $too_long_email . '">' . $too_long_email . '</a>' => FALSE,
580         '<a href="mailto:' . $email_with_plus_sign . '">' . $email_with_plus_sign . '</a>' => TRUE,
581       ],
582       // URI parts and special characters.
583       '
584 http://trailingslash.com/ or www.trailingslash.com/
585 http://host.com/some/path?query=foo&bar[baz]=beer#fragment or www.host.com/some/path?query=foo&bar[baz]=beer#fragment
586 http://twitter.com/#!/example/status/22376963142324226
587 http://example.com/@user/
588 ftp://user:pass@ftp.example.com/~home/dir1
589 sftp://user@nonstandardport:222/dir
590 ssh://192.168.0.100/srv/git/drupal.git
591 ' => [
592         '<a href="http://trailingslash.com/">http://trailingslash.com/</a>' => TRUE,
593         '<a href="http://www.trailingslash.com/">www.trailingslash.com/</a>' => TRUE,
594         '<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,
595         '<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,
596         '<a href="http://twitter.com/#!/example/status/22376963142324226">http://twitter.com/#!/example/status/22376963142324226</a>' => TRUE,
597         '<a href="http://example.com/@user/">http://example.com/@user/</a>' => TRUE,
598         '<a href="ftp://user:pass@ftp.example.com/~home/dir1">ftp://user:pass@ftp.example.com/~home/dir1</a>' => TRUE,
599         '<a href="sftp://user@nonstandardport:222/dir">sftp://user@nonstandardport:222/dir</a>' => TRUE,
600         '<a href="ssh://192.168.0.100/srv/git/drupal.git">ssh://192.168.0.100/srv/git/drupal.git</a>' => TRUE,
601       ],
602       // International Unicode characters.
603       '
604 http://пример.испытание/
605 http://مثال.إختبار/
606 http://例子.測試/
607 http://12345.中国/
608 http://例え.テスト/
609 http://dréißig-bücher.de/
610 http://méxico-mañana.es/
611 ' => [
612         '<a href="http://пример.испытание/">http://пример.испытание/</a>' => TRUE,
613         '<a href="http://مثال.إختبار/">http://مثال.إختبار/</a>' => TRUE,
614         '<a href="http://例子.測試/">http://例子.測試/</a>' => TRUE,
615         '<a href="http://12345.中国/">http://12345.中国/</a>' => TRUE,
616         '<a href="http://例え.テスト/">http://例え.テスト/</a>' => TRUE,
617         '<a href="http://dréißig-bücher.de/">http://dréißig-bücher.de/</a>' => TRUE,
618         '<a href="http://méxico-mañana.es/">http://méxico-mañana.es/</a>' => TRUE,
619       ],
620       // Encoding.
621       '
622 http://ampersand.com/?a=1&b=2
623 http://encoded.com/?a=1&amp;b=2
624 ' => [
625         '<a href="http://ampersand.com/?a=1&amp;b=2">http://ampersand.com/?a=1&amp;b=2</a>' => TRUE,
626         '<a href="http://encoded.com/?a=1&amp;b=2">http://encoded.com/?a=1&amp;b=2</a>' => TRUE,
627       ],
628       // Domain name length.
629       '
630 www.ex.ex or www.example.example or www.toolongdomainexampledomainexampledomainexampledomainexampledomain or
631 me@me.tv
632 ' => [
633         '<a href="http://www.ex.ex">www.ex.ex</a>' => TRUE,
634         '<a href="http://www.example.example">www.example.example</a>' => TRUE,
635         'http://www.toolong' => FALSE,
636         '<a href="mailto:me@me.tv">me@me.tv</a>' => TRUE,
637       ],
638       // Absolute URL protocols.
639       // The list to test is found in the beginning of _filter_url() at
640       // $protocols = \Drupal::getContainer()->getParameter('filter_protocols').
641       '
642 https://example.com,
643 ftp://ftp.example.com,
644 news://example.net,
645 telnet://example,
646 irc://example.host,
647 ssh://odd.geek,
648 sftp://secure.host?,
649 webcal://calendar,
650 rtsp://127.0.0.1,
651 not foo://disallowed.com.
652 ' => [
653         'href="https://example.com"' => TRUE,
654         'href="ftp://ftp.example.com"' => TRUE,
655         'href="news://example.net"' => TRUE,
656         'href="telnet://example"' => TRUE,
657         'href="irc://example.host"' => TRUE,
658         'href="ssh://odd.geek"' => TRUE,
659         'href="sftp://secure.host"' => TRUE,
660         'href="webcal://calendar"' => TRUE,
661         'href="rtsp://127.0.0.1"' => TRUE,
662         'href="foo://disallowed.com"' => FALSE,
663         'not foo://disallowed.com.' => TRUE,
664       ],
665     ];
666     $this->assertFilteredString($filter, $tests);
667
668     // Surrounding text/punctuation.
669     $tests = [
670       '
671 Partial URL with trailing period www.partial.com.
672 Email with trailing comma person@example.com,
673 Absolute URL with trailing question http://www.absolute.com?
674 Query string with trailing exclamation www.query.com/index.php?a=!
675 Partial URL with 3 trailing www.partial.periods...
676 Email with 3 trailing exclamations@example.com!!!
677 Absolute URL and query string with 2 different punctuation characters (http://www.example.com/q=abc).
678 Partial URL with brackets in the URL as well as surrounded brackets (www.foo.com/more_(than)_one_(parens)).
679 Absolute URL with square brackets in the URL as well as surrounded brackets [https://www.drupal.org/?class[]=1]
680 Absolute URL with quotes "https://www.drupal.org/sample"
681
682 ' => [
683         'period <a href="http://www.partial.com">www.partial.com</a>.' => TRUE,
684         'comma <a href="mailto:person@example.com">person@example.com</a>,' => TRUE,
685         'question <a href="http://www.absolute.com">http://www.absolute.com</a>?' => TRUE,
686         'exclamation <a href="http://www.query.com/index.php?a=">www.query.com/index.php?a=</a>!' => TRUE,
687         'trailing <a href="http://www.partial.periods">www.partial.periods</a>...' => TRUE,
688         'trailing <a href="mailto:exclamations@example.com">exclamations@example.com</a>!!!' => TRUE,
689         'characters (<a href="http://www.example.com/q=abc">http://www.example.com/q=abc</a>).' => TRUE,
690         'brackets (<a href="http://www.foo.com/more_(than)_one_(parens)">www.foo.com/more_(than)_one_(parens)</a>).' => TRUE,
691         'brackets [<a href="https://www.drupal.org/?class[]=1">https://www.drupal.org/?class[]=1</a>]' => TRUE,
692         'quotes "<a href="https://www.drupal.org/sample">https://www.drupal.org/sample</a>"' => TRUE,
693       ],
694       '
695 (www.parenthesis.com/dir?a=1&b=2#a)
696 ' => [
697         '(<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,
698       ],
699     ];
700     $this->assertFilteredString($filter, $tests);
701
702     // Surrounding markup.
703     $tests = [
704       '
705 <p xmlns="www.namespace.com" />
706 <p xmlns="http://namespace.com">
707 An <a href="http://example.com" title="Read more at www.example.info...">anchor</a>.
708 </p>
709 ' => [
710         '<p xmlns="www.namespace.com" />' => TRUE,
711         '<p xmlns="http://namespace.com">' => TRUE,
712         'href="http://www.namespace.com"' => FALSE,
713         'href="http://namespace.com"' => FALSE,
714         'An <a href="http://example.com" title="Read more at www.example.info...">anchor</a>.' => TRUE,
715       ],
716       '
717 Not <a href="foo">www.relative.com</a> or <a href="http://absolute.com">www.absolute.com</a>
718 but <strong>http://www.strong.net</strong> or <em>www.emphasis.info</em>
719 ' => [
720         '<a href="foo">www.relative.com</a>' => TRUE,
721         'href="http://www.relative.com"' => FALSE,
722         '<a href="http://absolute.com">www.absolute.com</a>' => TRUE,
723         '<strong><a href="http://www.strong.net">http://www.strong.net</a></strong>' => TRUE,
724         '<em><a href="http://www.emphasis.info">www.emphasis.info</a></em>' => TRUE,
725       ],
726       '
727 Test <code>using www.example.com the code tag</code>.
728 ' => [
729         'href' => FALSE,
730         'http' => FALSE,
731       ],
732       '
733 Intro.
734 <blockquote>
735 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>.
736 </blockquote>
737
738 Outro.
739 ' => [
740         'href="http://www.example.com"' => TRUE,
741         'href="mailto:person@example.com"' => TRUE,
742         'href="http://origin.example.com"' => TRUE,
743         'http://www.usage.example.com' => FALSE,
744         'http://www.example.info' => FALSE,
745         'Intro.' => TRUE,
746         'Outro.' => TRUE,
747       ],
748       '
749 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>
750 ' => [
751         'href="http://www.example.com"' => TRUE,
752         'href="http://www.example.pooh"' => TRUE,
753       ],
754       '
755 <p>Test &lt;br/&gt;: This is a www.example17.com example <strong>with</strong> various http://www.example18.com tags. *<br/>
756  It is important www.example19.com to *<br/>test different URLs and http://www.example20.com in the same paragraph. *<br>
757 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.
758 ' => [
759         'href="http://www.example17.com"' => TRUE,
760         'href="http://www.example18.com"' => TRUE,
761         'href="http://www.example19.com"' => TRUE,
762         'href="http://www.example20.com"' => TRUE,
763         'href="http://www.example21.com"' => TRUE,
764         'href="mailto:person@example22.com"' => TRUE,
765         'href="http://www.example23.com"' => TRUE,
766         'href="http://www.example24.com"' => TRUE,
767         'href="http://www.example25.com"' => TRUE,
768         'href="http://www.example26.com"' => TRUE,
769         'href="mailto:person@example27.com"' => TRUE,
770         'href="http://www.example28.com"' => TRUE,
771       ],
772       '
773 <script>
774 <!--
775   // @see www.example.com
776   var exampleurl = "http://example.net";
777 -->
778 <!--//--><![CDATA[//><!--
779   // @see www.example.com
780   var exampleurl = "http://example.net";
781 //--><!]]>
782 </script>
783 ' => [
784         'href="http://www.example.com"' => FALSE,
785         'href="http://example.net"' => FALSE,
786       ],
787       '
788 <style>body {
789   background: url(http://example.com/pixel.gif);
790 }</style>
791 ' => [
792         'href' => FALSE,
793       ],
794       '
795 <!-- Skip any URLs like www.example.com in comments -->
796 ' => [
797         'href' => FALSE,
798       ],
799       '
800 <!-- Skip any URLs like
801 www.example.com with a newline in comments -->
802 ' => [
803         'href' => FALSE,
804       ],
805       '
806 <!-- Skip any URLs like www.comment.com in comments. <p>Also ignore http://commented.out/markup.</p> -->
807 ' => [
808         'href' => FALSE,
809       ],
810       '
811 <dl>
812 <dt>www.example.com</dt>
813 <dd>http://example.com</dd>
814 <dd>person@example.com</dd>
815 <dt>Check www.example.net</dt>
816 <dd>Some text around http://www.example.info by person@example.info?</dd>
817 </dl>
818 ' => [
819         'href="http://www.example.com"' => TRUE,
820         'href="http://example.com"' => TRUE,
821         'href="mailto:person@example.com"' => TRUE,
822         'href="http://www.example.net"' => TRUE,
823         'href="http://www.example.info"' => TRUE,
824         'href="mailto:person@example.info"' => TRUE,
825       ],
826       '
827 <div>www.div.com</div>
828 <ul>
829 <li>http://listitem.com</li>
830 <li class="odd">www.class.listitem.com</li>
831 </ul>
832 ' => [
833         '<div><a href="http://www.div.com">www.div.com</a></div>' => TRUE,
834         '<li><a href="http://listitem.com">http://listitem.com</a></li>' => TRUE,
835         '<li class="odd"><a href="http://www.class.listitem.com">www.class.listitem.com</a></li>' => TRUE,
836       ],
837     ];
838     $this->assertFilteredString($filter, $tests);
839
840     // URL trimming.
841     $filter->setConfiguration([
842       'settings' => [
843         'filter_url_length' => 20,
844       ],
845     ]);
846     $tests = [
847       'www.trimmed.com/d/ff.ext?a=1&b=2#a1' => [
848         '<a href="http://www.trimmed.com/d/ff.ext?a=1&amp;b=2#a1">www.trimmed.com/d/f…</a>' => TRUE,
849       ],
850     ];
851     $this->assertFilteredString($filter, $tests);
852   }
853
854   /**
855    * Asserts multiple filter output expectations for multiple input strings.
856    *
857    * @param FilterInterface $filter
858    *   A input filter object.
859    * @param array $tests
860    *   An associative array, whereas each key is an arbitrary input string and
861    *   each value is again an associative array whose keys are filter output
862    *   strings and whose values are Booleans indicating whether the output is
863    *   expected or not. For example:
864    *   @code
865    *   $tests = array(
866    *     'Input string' => array(
867    *       '<p>Input string</p>' => TRUE,
868    *       'Input string<br' => FALSE,
869    *     ),
870    *   );
871    *   @endcode
872    */
873   public function assertFilteredString($filter, $tests) {
874     foreach ($tests as $source => $tasks) {
875       $result = $filter->process($source, $filter)->getProcessedText();
876       foreach ($tasks as $value => $is_expected) {
877         // Not using assertIdentical, since combination with strpos() is hard to grok.
878         if ($is_expected) {
879           $success = $this->assertTrue(strpos($result, $value) !== FALSE, format_string('@source: @value found. Filtered result: @result.', [
880             '@source' => var_export($source, TRUE),
881             '@value' => var_export($value, TRUE),
882             '@result' => var_export($result, TRUE),
883           ]));
884         }
885         else {
886           $success = $this->assertTrue(strpos($result, $value) === FALSE, format_string('@source: @value not found. Filtered result: @result.', [
887             '@source' => var_export($source, TRUE),
888             '@value' => var_export($value, TRUE),
889             '@result' => var_export($result, TRUE),
890           ]));
891         }
892         if (!$success) {
893           $this->verbose('Source:<pre>' . Html::escape(var_export($source, TRUE)) . '</pre>'
894             . '<hr />' . 'Result:<pre>' . Html::escape(var_export($result, TRUE)) . '</pre>'
895             . '<hr />' . ($is_expected ? 'Expected:' : 'Not expected:')
896             . '<pre>' . Html::escape(var_export($value, TRUE)) . '</pre>'
897           );
898         }
899       }
900     }
901   }
902
903   /**
904    * Tests URL filter on longer content.
905    *
906    * Filters based on regular expressions should also be tested with a more
907    * complex content than just isolated test lines.
908    * The most common errors are:
909    * - accidental '*' (greedy) match instead of '*?' (minimal) match.
910    * - only matching first occurrence instead of all.
911    * - newlines not matching '.*'.
912    *
913    * This test covers:
914    * - Document with multiple newlines and paragraphs (two newlines).
915    * - Mix of several HTML tags, invalid non-HTML tags, tags to ignore and HTML
916    *   comments.
917    * - Empty HTML tags (BR, IMG).
918    * - Mix of absolute and partial URLs, and email addresses in one content.
919    */
920   public function testUrlFilterContent() {
921     // Get FilterUrl object.
922     $filter = $this->filters['filter_url'];
923     $filter->setConfiguration([
924       'settings' => [
925         'filter_url_length' => 496,
926       ],
927     ]);
928     $path = __DIR__ . '/../..';
929
930     $input = file_get_contents($path . '/filter.url-input.txt');
931     $expected = file_get_contents($path . '/filter.url-output.txt');
932     $result = _filter_url($input, $filter);
933     $this->assertIdentical($result, $expected, 'Complex HTML document was correctly processed.');
934   }
935
936   /**
937    * Tests the HTML corrector filter.
938    *
939    * @todo This test could really use some validity checking function.
940    */
941   public function testHtmlCorrectorFilter() {
942     // Tag closing.
943     $f = Html::normalize('<p>text');
944     $this->assertEqual($f, '<p>text</p>', 'HTML corrector -- tag closing at the end of input.');
945
946     $f = Html::normalize('<p>text<p><p>text');
947     $this->assertEqual($f, '<p>text</p><p></p><p>text</p>', 'HTML corrector -- tag closing.');
948
949     $f = Html::normalize("<ul><li>e1<li>e2");
950     $this->assertEqual($f, "<ul><li>e1</li><li>e2</li></ul>", 'HTML corrector -- unclosed list tags.');
951
952     $f = Html::normalize('<div id="d">content');
953     $this->assertEqual($f, '<div id="d">content</div>', 'HTML corrector -- unclosed tag with attribute.');
954
955     // XHTML slash for empty elements.
956     $f = Html::normalize('<hr><br>');
957     $this->assertEqual($f, '<hr /><br />', 'HTML corrector -- XHTML closing slash.');
958
959     $f = Html::normalize('<P>test</P>');
960     $this->assertEqual($f, '<p>test</p>', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
961
962     $f = Html::normalize('<P>test</p>');
963     $this->assertEqual($f, '<p>test</p>', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
964
965     $f = Html::normalize('test<hr />');
966     $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through.');
967
968     $f = Html::normalize('test<hr/>');
969     $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through, but ensure there is a single space before the closing slash.');
970
971     $f = Html::normalize('test<hr    />');
972     $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through, but ensure there are not too many spaces before the closing slash.');
973
974     $f = Html::normalize('<span class="test" />');
975     $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.');
976
977     $f = Html::normalize('test1<br class="test">test2');
978     $this->assertEqual($f, 'test1<br class="test" />test2', 'HTML corrector -- Automatically close single tags.');
979
980     $f = Html::normalize('line1<hr>line2');
981     $this->assertEqual($f, 'line1<hr />line2', 'HTML corrector -- Automatically close single tags.');
982
983     $f = Html::normalize('line1<HR>line2');
984     $this->assertEqual($f, 'line1<hr />line2', 'HTML corrector -- Automatically close single tags.');
985
986     $f = Html::normalize('<img src="http://example.com/test.jpg">test</img>');
987     $this->assertEqual($f, '<img src="http://example.com/test.jpg" />test', 'HTML corrector -- Automatically close single tags.');
988
989     $f = Html::normalize('<br></br>');
990     $this->assertEqual($f, '<br />', "HTML corrector -- Transform empty tags to a single closed tag if the tag's content model is EMPTY.");
991
992     $f = Html::normalize('<div></div>');
993     $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.");
994
995     $f = Html::normalize('<p>line1<br/><hr/>line2</p>');
996     $this->assertEqual($f, '<p>line1<br /></p><hr />line2', 'HTML corrector -- Move non-inline elements outside of inline containers.');
997
998     $f = Html::normalize('<p>line1<div>line2</div></p>');
999     $this->assertEqual($f, '<p>line1</p><div>line2</div>', 'HTML corrector -- Move non-inline elements outside of inline containers.');
1000
1001     $f = Html::normalize('<p>test<p>test</p>\n');
1002     $this->assertEqual($f, '<p>test</p><p>test</p>\n', 'HTML corrector -- Auto-close improperly nested tags.');
1003
1004     $f = Html::normalize('<p>Line1<br><STRONG>bold stuff</b>');
1005     $this->assertEqual($f, '<p>Line1<br /><strong>bold stuff</strong></p>', 'HTML corrector -- Properly close unclosed tags, and remove useless closing tags.');
1006
1007     $f = Html::normalize('test <!-- this is a comment -->');
1008     $this->assertEqual($f, 'test <!-- this is a comment -->', 'HTML corrector -- Do not touch HTML comments.');
1009
1010     $f = Html::normalize('test <!--this is a comment-->');
1011     $this->assertEqual($f, 'test <!--this is a comment-->', 'HTML corrector -- Do not touch HTML comments.');
1012
1013     $f = Html::normalize('test <!-- comment <p>another
1014     <strong>multiple</strong> line
1015     comment</p> -->');
1016     $this->assertEqual($f, 'test <!-- comment <p>another
1017     <strong>multiple</strong> line
1018     comment</p> -->', 'HTML corrector -- Do not touch HTML comments.');
1019
1020     $f = Html::normalize('test <!-- comment <p>another comment</p> -->');
1021     $this->assertEqual($f, 'test <!-- comment <p>another comment</p> -->', 'HTML corrector -- Do not touch HTML comments.');
1022
1023     $f = Html::normalize('test <!--break-->');
1024     $this->assertEqual($f, 'test <!--break-->', 'HTML corrector -- Do not touch HTML comments.');
1025
1026     $f = Html::normalize('<p>test\n</p>\n');
1027     $this->assertEqual($f, '<p>test\n</p>\n', 'HTML corrector -- New-lines are accepted and kept as-is.');
1028
1029     $f = Html::normalize('<p>دروبال');
1030     $this->assertEqual($f, '<p>دروبال</p>', 'HTML corrector -- Encoding is correctly kept.');
1031
1032     $f = Html::normalize('<script>alert("test")</script>');
1033     $this->assertEqual($f, '<script>
1034 <!--//--><![CDATA[// ><!--
1035 alert("test")
1036 //--><!]]>
1037 </script>', 'HTML corrector -- CDATA added to script element');
1038
1039     $f = Html::normalize('<p><script>alert("test")</script></p>');
1040     $this->assertEqual($f, '<p><script>
1041 <!--//--><![CDATA[// ><!--
1042 alert("test")
1043 //--><!]]>
1044 </script></p>', 'HTML corrector -- CDATA added to a nested script element');
1045
1046     $f = Html::normalize('<p><style> /* Styling */ body {color:red}</style></p>');
1047     $this->assertEqual($f, '<p><style>
1048 <!--/*--><![CDATA[/* ><!--*/
1049  /* Styling */ body {color:red}
1050 /*--><!]]>*/
1051 </style></p>', 'HTML corrector -- CDATA added to a style element.');
1052
1053     $filtered_data = Html::normalize('<p><style>
1054 /*<![CDATA[*/
1055 /* Styling */
1056 body {color:red}
1057 /*]]>*/
1058 </style></p>');
1059     $this->assertEqual($filtered_data, '<p><style>
1060 <!--/*--><![CDATA[/* ><!--*/
1061
1062 /*<![CDATA[*/
1063 /* Styling */
1064 body {color:red}
1065 /*]]]]><![CDATA[>*/
1066
1067 /*--><!]]>*/
1068 </style></p>',
1069       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '/*<![CDATA[*/'])
1070     );
1071
1072     $filtered_data = Html::normalize('<p><style>
1073   <!--/*--><![CDATA[/* ><!--*/
1074   /* Styling */
1075   body {color:red}
1076   /*--><!]]>*/
1077 </style></p>');
1078     $this->assertEqual($filtered_data, '<p><style>
1079 <!--/*--><![CDATA[/* ><!--*/
1080
1081   <!--/*--><![CDATA[/* ><!--*/
1082   /* Styling */
1083   body {color:red}
1084   /*--><!]]]]><![CDATA[>*/
1085
1086 /*--><!]]>*/
1087 </style></p>',
1088       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '<!--/*--><![CDATA[/* ><!--*/'])
1089     );
1090
1091     $filtered_data = Html::normalize('<p><script>
1092 <!--//--><![CDATA[// ><!--
1093   alert("test");
1094 //--><!]]>
1095 </script></p>');
1096     $this->assertEqual($filtered_data, '<p><script>
1097 <!--//--><![CDATA[// ><!--
1098
1099 <!--//--><![CDATA[// ><!--
1100   alert("test");
1101 //--><!]]]]><![CDATA[>
1102
1103 //--><!]]>
1104 </script></p>',
1105       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '<!--//--><![CDATA[// ><!--'])
1106     );
1107
1108     $filtered_data = Html::normalize('<p><script>
1109 // <![CDATA[
1110   alert("test");
1111 // ]]>
1112 </script></p>');
1113     $this->assertEqual($filtered_data, '<p><script>
1114 <!--//--><![CDATA[// ><!--
1115
1116 // <![CDATA[
1117   alert("test");
1118 // ]]]]><![CDATA[>
1119
1120 //--><!]]>
1121 </script></p>',
1122       format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', ['@pattern_name' => '// <![CDATA['])
1123     );
1124
1125   }
1126
1127   /**
1128    * Asserts that a text transformed to lowercase with HTML entities decoded does contains a given string.
1129    *
1130    * Otherwise fails the test with a given message, similar to all the
1131    * SimpleTest assert* functions.
1132    *
1133    * Note that this does not remove nulls, new lines and other characters that
1134    * could be used to obscure a tag or an attribute name.
1135    *
1136    * @param string $haystack
1137    *   Text to look in.
1138    * @param string $needle
1139    *   Lowercase, plain text to look for.
1140    * @param string $message
1141    *   (optional) Message to display if failed. Defaults to an empty string.
1142    * @param string $group
1143    *   (optional) The group this message belongs to. Defaults to 'Other'.
1144    *
1145    * @return bool
1146    *   TRUE on pass, FALSE on fail.
1147    */
1148   public function assertNormalized($haystack, $needle, $message = '', $group = 'Other') {
1149     return $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) !== FALSE, $message, $group);
1150   }
1151
1152   /**
1153    * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string.
1154    *
1155    * Otherwise fails the test with a given message, similar to all the
1156    * SimpleTest assert* functions.
1157    *
1158    * Note that this does not remove nulls, new lines, and other character that
1159    * could be used to obscure a tag or an attribute name.
1160    *
1161    * @param string $haystack
1162    *   Text to look in.
1163    * @param string $needle
1164    *   Lowercase, plain text to look for.
1165    * @param string $message
1166    *   (optional) Message to display if failed. Defaults to an empty string.
1167    * @param string $group
1168    *   (optional) The group this message belongs to. Defaults to 'Other'.
1169    *
1170    * @return bool
1171    *   TRUE on pass, FALSE on fail.
1172    */
1173   public function assertNoNormalized($haystack, $needle, $message = '', $group = 'Other') {
1174     return $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group);
1175   }
1176
1177 }