[BUGFIX] HTMLParser doesn't remove endtag when applying rmTagIfNoAttrib
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / Html / HtmlParserTest.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests\Unit\Html;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Testcase for \TYPO3\CMS\Core\Html\HtmlParser
19 */
20 class HtmlParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
21
22 /**
23 * @var \TYPO3\CMS\Core\Html\HtmlParser
24 */
25 protected $subject = NULL;
26
27 protected function setUp() {
28 $this->subject = new \TYPO3\CMS\Core\Html\HtmlParser();
29 }
30
31 /**
32 * Data provider for substituteMarkerAndSubpartArrayRecursiveResolvesMarkersAndSubpartsArray
33 */
34 public function substituteMarkerAndSubpartArrayRecursiveResolvesMarkersAndSubpartsArrayDataProvider() {
35 $template = '###SINGLEMARKER1###
36 <!-- ###FOO### begin -->
37 <!-- ###BAR### begin -->
38 ###SINGLEMARKER2###
39 <!-- ###BAR### end -->
40 <!-- ###FOOTER### begin -->
41 ###SINGLEMARKER3###
42 <!-- ###FOOTER### end -->
43 <!-- ###FOO### end -->';
44
45 $expected ='Value 1
46
47
48 Value 2.1
49
50 Value 2.2
51
52
53 Value 3.1
54
55 Value 3.2
56
57 ';
58
59 return array(
60 'Single marker' => array(
61 '###SINGLEMARKER###',
62 array(
63 '###SINGLEMARKER###' => 'Value 1'
64 ),
65 '',
66 FALSE,
67 FALSE,
68 'Value 1'
69 ),
70 'Subpart marker' => array(
71 $template,
72 array(
73 '###SINGLEMARKER1###' => 'Value 1',
74 '###FOO###' => array(
75 array(
76 '###BAR###' => array(
77 array(
78 '###SINGLEMARKER2###' => 'Value 2.1'
79 ),
80 array(
81 '###SINGLEMARKER2###' => 'Value 2.2'
82 )
83 ),
84 '###FOOTER###' => array(
85 array(
86 '###SINGLEMARKER3###' => 'Value 3.1'
87 ),
88 array(
89 '###SINGLEMARKER3###' => 'Value 3.2'
90 )
91 )
92 )
93 )
94 ),
95 '',
96 FALSE,
97 FALSE,
98 $expected
99 ),
100 'Subpart marker with wrap' => array(
101 $template,
102 array(
103 'SINGLEMARKER1' => 'Value 1',
104 'FOO' => array(
105 array(
106 'BAR' => array(
107 array(
108 'SINGLEMARKER2' => 'Value 2.1'
109 ),
110 array(
111 'SINGLEMARKER2' => 'Value 2.2'
112 )
113 ),
114 'FOOTER' => array(
115 array(
116 'SINGLEMARKER3' => 'Value 3.1'
117 ),
118 array(
119 'SINGLEMARKER3' => 'Value 3.2'
120 )
121 )
122 )
123 )
124 ),
125 '###|###',
126 FALSE,
127 FALSE,
128 $expected
129 ),
130 'Subpart marker with lower marker array keys' => array(
131 $template,
132 array(
133 '###singlemarker1###' => 'Value 1',
134 '###foo###' => array(
135 array(
136 '###bar###' => array(
137 array(
138 '###singlemarker2###' => 'Value 2.1'
139 ),
140 array(
141 '###singlemarker2###' => 'Value 2.2'
142 )
143 ),
144 '###footer###' => array(
145 array(
146 '###singlemarker3###' => 'Value 3.1'
147 ),
148 array(
149 '###singlemarker3###' => 'Value 3.2'
150 )
151 )
152 )
153 )
154 ),
155 '',
156 TRUE,
157 FALSE,
158 $expected
159 ),
160 'Subpart marker with unused markers' => array(
161 $template,
162 array(
163 '###FOO###' => array(
164 array(
165 '###BAR###' => array(
166 array(
167 '###SINGLEMARKER2###' => 'Value 2.1'
168 )
169 ),
170 '###FOOTER###' => array(
171 array(
172 '###SINGLEMARKER3###' => 'Value 3.1'
173 )
174 )
175 )
176 )
177 ),
178 '',
179 FALSE,
180 TRUE,
181 '
182
183
184 Value 2.1
185
186
187 Value 3.1
188
189 '
190 ),
191 'Subpart marker with empty subpart' => array(
192 $template,
193 array(
194 '###SINGLEMARKER1###' => 'Value 1',
195 '###FOO###' => array(
196 array(
197 '###BAR###' => array(
198 array(
199 '###SINGLEMARKER2###' => 'Value 2.1'
200 ),
201 array(
202 '###SINGLEMARKER2###' => 'Value 2.2'
203 )
204 ),
205 '###FOOTER###' => array()
206 )
207 )
208 ),
209 '',
210 FALSE,
211 FALSE,
212 'Value 1
213
214
215 Value 2.1
216
217 Value 2.2
218
219
220 '
221 )
222 );
223 }
224
225 /**
226 * @test
227 * @dataProvider substituteMarkerAndSubpartArrayRecursiveResolvesMarkersAndSubpartsArrayDataProvider
228 */
229 public function substituteMarkerAndSubpartArrayRecursiveResolvesMarkersAndSubpartsArray($template, $markersAndSubparts, $wrap, $uppercase, $deleteUnused, $expected) {
230 $this->assertSame($expected, $this->subject->substituteMarkerAndSubpartArrayRecursive($template, $markersAndSubparts, $wrap, $uppercase, $deleteUnused));
231 }
232
233 /**
234 * @return array
235 */
236 public function cDataWillRemainUnmodifiedDataProvider() {
237 return array(
238 'single-line CDATA' => array(
239 '/*<![CDATA[*/ <hello world> /*]]>*/',
240 '/*<![CDATA[*/ <hello world> /*]]>*/',
241 ),
242 'multi-line CDATA #1' => array(
243 '/*<![CDATA[*/' . LF . '<hello world> /*]]>*/',
244 '/*<![CDATA[*/' . LF . '<hello world> /*]]>*/',
245 ),
246 'multi-line CDATA #2' => array(
247 '/*<![CDATA[*/ <hello world>' . LF . '/*]]>*/',
248 '/*<![CDATA[*/ <hello world>' . LF . '/*]]>*/',
249 ),
250 'multi-line CDATA #3' => array(
251 '/*<![CDATA[*/' . LF . '<hello world>' . LF . '/*]]>*/',
252 '/*<![CDATA[*/' . LF . '<hello world>' . LF . '/*]]>*/',
253 ),
254 );
255 }
256
257 /**
258 * @test
259 * @param string $source
260 * @param string $expected
261 * @dataProvider cDataWillRemainUnmodifiedDataProvider
262 */
263 public function xHtmlCleaningDoesNotModifyCDATA($source, $expected) {
264 $result = $this->subject->XHTML_clean($source);
265 $this->assertSame($expected, $result);
266 }
267
268 /**
269 * Data provider for spanTagCorrectlyRemovedWhenRmTagIfNoAttribIsConfigured
270 */
271 public static function spanTagCorrectlyRemovedWhenRmTagIfNoAttribIsConfiguredDataProvider() {
272 return array(
273 'Span tag with no attrib' => array(
274 '<span>text</span>',
275 'text'
276 ),
277 'Span tag with allowed id attrib' => array(
278 '<span id="id">text</span>',
279 '<span id="id">text</span>'
280 ),
281 'Span tag with disallowed style attrib' => array(
282 '<span style="line-height: 12px;">text</span>',
283 'text'
284 )
285 );
286 }
287
288 /**
289 * @test
290 * @dataProvider spanTagCorrectlyRemovedWhenRmTagIfNoAttribIsConfiguredDataProvider
291 */
292 public function tagCorrectlyRemovedWhenRmTagIfNoAttribIsConfigured($content, $expectedResult) {
293 $tsConfig = array(
294 'allowTags' => 'span',
295 'tags.' => array(
296 'span.' => array(
297 'allowedAttribs' => 'id',
298 'rmTagIfNoAttrib' => 1
299 )
300 )
301 );
302 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
303 }
304
305 /**
306 * @test
307 */
308 public function rmTagIfNoAttribIsConfiguredDoesNotChangeNestingType() {
309 $tsConfig = array(
310 'allowTags' => 'div,span',
311 'rmTagIfNoAttrib' => 'span',
312 'globalNesting' => 'div,span'
313 );
314 $content = '<span></span><span id="test"><div></span></div>';
315 $expectedResult = '<span id="test"></span>';
316 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
317 }
318
319 /**
320 * Data provider for localNestingCorrectlyRemovesInvalidTags
321 */
322 public static function localNestingCorrectlyRemovesInvalidTagsDataProvider() {
323 return array(
324 'Valid nesting is untouched' => array(
325 '<B><I></B></I>',
326 '<B><I></B></I>'
327 ),
328 'Valid nesting with content is untouched' => array(
329 'testa<B>test1<I>test2</B>test3</I>testb',
330 'testa<B>test1<I>test2</B>test3</I>testb'
331 ),
332 'Superflous tags are removed' => array(
333 '</B><B><I></B></I></B>',
334 '<B><I></B></I>'
335 ),
336 'Superflous tags with content are removed' => array(
337 'test1</B>test2<B>test3<I>test4</B>test5</I>test6</B>test7',
338 'test1test2<B>test3<I>test4</B>test5</I>test6test7'
339 ),
340 'Another valid nesting test' => array(
341 '<span><div></span></div>',
342 '<span><div></span></div>',
343 ),
344 );
345 }
346
347 /**
348 * @test
349 * @dataProvider localNestingCorrectlyRemovesInvalidTagsDataProvider
350 * @param string $content
351 * @param string $expectedResult
352 */
353 public function localNestingCorrectlyRemovesInvalidTags($content, $expectedResult) {
354 $tsConfig = array(
355 'allowTags' => 'div,span,b,i',
356 'localNesting' => 'div,span,b,i',
357 );
358 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
359 }
360
361 /**
362 * Data provider for globalNestingCorrectlyRemovesInvalidTags
363 */
364 public static function globalNestingCorrectlyRemovesInvalidTagsDataProvider() {
365 return array(
366 'Valid nesting is untouched' => array(
367 '<B><I></I></B>',
368 '<B><I></I></B>'
369 ),
370 'Valid nesting with content is untouched' => array(
371 'testa<B>test1<I>test2</I>test3</B>testb',
372 'testa<B>test1<I>test2</I>test3</B>testb'
373 ),
374 'Invalid nesting is cleaned' => array(
375 '</B><B><I></B></I></B>',
376 '<B></B>'
377 ),
378 'Invalid nesting with content is cleaned' => array(
379 'test1</B>test2<B>test3<I>test4</B>test5</I>test6</B>test7',
380 'test1test2<B>test3test4</B>test5test6test7'
381 ),
382 'Another invalid nesting test' => array(
383 '<span><div></span></div>',
384 '<span></span>',
385 ),
386 );
387 }
388
389 /**
390 * @test
391 * @dataProvider globalNestingCorrectlyRemovesInvalidTagsDataProvider
392 * @param string $content
393 * @param string $expectedResult
394 */
395 public function globalNestingCorrectlyRemovesInvalidTags($content, $expectedResult) {
396 $tsConfig = array(
397 'allowTags' => 'span,div,b,i',
398 'globalNesting' => 'span,div,b,i',
399 );
400 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
401 }
402
403 /**
404 * @return array
405 */
406 public function emptyTagsDataProvider() {
407 return array(
408 array(0 , NULL, FALSE, '<h1></h1>', '<h1></h1>'),
409 array(1 , NULL, FALSE, '<h1></h1>', ''),
410 array(1 , NULL, FALSE, '<h1>hallo</h1>', '<h1>hallo</h1>'),
411 array(1 , NULL, FALSE, '<h1 class="something"></h1>', ''),
412 array(1 , NULL, FALSE, '<h1 class="something"></h1><h2></h2>', ''),
413 array(1 , 'h2', FALSE, '<h1 class="something"></h1><h2></h2>', '<h1 class="something"></h1>'),
414 array(1 , 'h2, h1', FALSE, '<h1 class="something"></h1><h2></h2>', ''),
415 array(1 , NULL, FALSE, '<div><p></p></div>', ''),
416 array(1 , NULL, FALSE, '<div><p>&nbsp;</p></div>', '<div><p>&nbsp;</p></div>'),
417 array(1 , NULL, TRUE, '<div><p>&nbsp;&nbsp;</p></div>', ''),
418 array(1 , NULL, TRUE, '<div>&nbsp;&nbsp;<p></p></div>', ''),
419 array(1 , NULL, FALSE, '<div>Some content<p></p></div>', '<div>Some content</div>'),
420 array(1 , NULL, TRUE, '<div>Some content<p></p></div>', '<div>Some content</div>'),
421 array(1 , NULL, FALSE, '<div>Some content</div>', '<div>Some content</div>'),
422 array(1 , NULL, TRUE, '<div>Some content</div>', '<div>Some content</div>'),
423 array(1 , NULL, FALSE, '<a href="#skiplinks">Skiplinks </a><b></b>', '<a href="#skiplinks">Skiplinks </a>'),
424 array(1 , NULL, TRUE, '<a href="#skiplinks">Skiplinks </a><b></b>', '<a href="#skiplinks">Skiplinks </a>'),
425 );
426 }
427
428 /**
429 * @test
430 * @dataProvider emptyTagsDataProvider
431 * @param bool $stripOn TRUE if stripping should be activated.
432 * @param string $tagList Comma seperated list of tags that should be stripped.
433 * @param bool $treatNonBreakingSpaceAsEmpty If TRUE &nbsp; will be considered empty.
434 * @param string $content The HTML code that should be modified.
435 * @param string $expectedResult The expected HTML code result.
436 */
437 public function stripEmptyTags($stripOn, $tagList, $treatNonBreakingSpaceAsEmpty, $content, $expectedResult) {
438 $tsConfig = array(
439 'keepNonMatchedTags' => 1,
440 'stripEmptyTags' => $stripOn,
441 'stripEmptyTags.' => array(
442 'tags' => $tagList,
443 'treatNonBreakingSpaceAsEmpty' => $treatNonBreakingSpaceAsEmpty
444 ),
445 );
446
447 $result = $this->parseConfigAndCleanHtml($tsConfig, $content);
448 $this->assertEquals($expectedResult, $result);
449 }
450
451 /**
452 * Calls HTMLparserConfig() and passes the generated config to the HTMLcleaner() method on the current subject.
453 *
454 * @param array $tsConfig The TypoScript that should be used to generate the HTML parser config.
455 * @param string $content The content that should be parsed by the HTMLcleaner.
456 * @return string The parsed content.
457 */
458 protected function parseConfigAndCleanHtml(array $tsConfig, $content) {
459 $config = $this->subject->HTMLparserConfig($tsConfig);
460 return $this->subject->HTMLcleaner($content, $config[0], $config[1], $config[2], $config[3]);
461 }
462 }