36bb3ceaa56854466b8e94b07a95197bbc4af9a6
[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 use TYPO3\CMS\Core\Html\HtmlParser;
18
19 /**
20 * Testcase for \TYPO3\CMS\Core\Html\HtmlParser
21 */
22 class HtmlParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
23
24 /**
25 * @var \TYPO3\CMS\Core\Html\HtmlParser
26 */
27 protected $subject = NULL;
28
29 protected function setUp() {
30 $this->subject = new HtmlParser();
31 }
32
33 /**
34 * @return array
35 */
36 public function cDataWillRemainUnmodifiedDataProvider() {
37 return array(
38 'single-line CDATA' => array(
39 '/*<![CDATA[*/ <hello world> /*]]>*/',
40 '/*<![CDATA[*/ <hello world> /*]]>*/',
41 ),
42 'multi-line CDATA #1' => array(
43 '/*<![CDATA[*/' . LF . '<hello world> /*]]>*/',
44 '/*<![CDATA[*/' . LF . '<hello world> /*]]>*/',
45 ),
46 'multi-line CDATA #2' => array(
47 '/*<![CDATA[*/ <hello world>' . LF . '/*]]>*/',
48 '/*<![CDATA[*/ <hello world>' . LF . '/*]]>*/',
49 ),
50 'multi-line CDATA #3' => array(
51 '/*<![CDATA[*/' . LF . '<hello world>' . LF . '/*]]>*/',
52 '/*<![CDATA[*/' . LF . '<hello world>' . LF . '/*]]>*/',
53 ),
54 );
55 }
56
57 /**
58 * Data provider for splitIntoBlock
59 *
60 * @return array
61 */
62 public function splitIntoBlockDataProvider() {
63 return array(
64 'splitBlock' => array(
65 'h1,span',
66 '<body><h1>Title</h1><span>Note</span></body>',
67 FALSE,
68 array('<body>',
69 '<h1>Title</h1>',
70 '',
71 '<span>Note</span>',
72 '</body>')
73 ),
74 'splitBlock br' => array(
75 'h1,span',
76 '<body><h1>Title</h1><br /><span>Note</span><br /></body>',
77 FALSE,
78 array('<body>',
79 '<h1>Title</h1>',
80 '<br />',
81 '<span>Note</span>',
82 '<br /></body>')
83 ),
84 'splitBlock with attribute' => array(
85 'h1,span',
86 '<body><h1 class="title">Title</h1><span>Note</span></body>',
87 FALSE,
88 array('<body>',
89 '<h1 class="title">Title</h1>',
90 '',
91 '<span>Note</span>',
92 '</body>')
93 ),
94 'splitBlock span with attribute' => array(
95 'span',
96 '<body><h1>Title</h1><span class="title">Note</span></body>',
97 FALSE,
98 array('<body><h1>Title</h1>',
99 '<span class="title">Note</span>',
100 '</body>')
101 ),
102 'splitBlock without extra end tags' => array(
103 'h1,span,div',
104 '<body><h1>Title</h1><span>Note</span></body></div>',
105 TRUE,
106 array('<body>',
107 '<h1>Title</h1>',
108 '',
109 '<span>Note</span>',
110 '</body>')
111 ),
112 );
113 }
114
115 /**
116 * @test
117 * @param string $tag List of tags, comma separated.
118 * @param string $content HTML-content
119 * @param bool $eliminateExtraEndTags If set, excessive end tags are ignored - you should probably set this in most cases.
120 * @param array $expected The expected result
121 * @dataProvider splitIntoBlockDataProvider
122 */
123 public function splitIntoBlock($tag, $content, $eliminateExtraEndTags, $expected) {
124 $this->assertSame($expected, $this->subject->splitIntoBlock($tag, $content, $eliminateExtraEndTags));
125 }
126
127 /**
128 * @test
129 * @param string $source
130 * @param string $expected
131 * @dataProvider cDataWillRemainUnmodifiedDataProvider
132 */
133 public function xHtmlCleaningDoesNotModifyCDATA($source, $expected) {
134 $result = $this->subject->XHTML_clean($source);
135 $this->assertSame($expected, $result);
136 }
137
138 /**
139 * Data provider for spanTagCorrectlyRemovedWhenRmTagIfNoAttribIsConfigured
140 */
141 public static function spanTagCorrectlyRemovedWhenRmTagIfNoAttribIsConfiguredDataProvider() {
142 return array(
143 'Span tag with no attrib' => array(
144 '<span>text</span>',
145 'text'
146 ),
147 'Span tag with allowed id attrib' => array(
148 '<span id="id">text</span>',
149 '<span id="id">text</span>'
150 ),
151 'Span tag with disallowed style attrib' => array(
152 '<span style="line-height: 12px;">text</span>',
153 'text'
154 )
155 );
156 }
157
158 /**
159 * @test
160 * @param string $content
161 * @param string $expectedResult
162 * @dataProvider spanTagCorrectlyRemovedWhenRmTagIfNoAttribIsConfiguredDataProvider
163 */
164 public function tagCorrectlyRemovedWhenRmTagIfNoAttribIsConfigured($content, $expectedResult) {
165 $tsConfig = array(
166 'allowTags' => 'span',
167 'tags.' => array(
168 'span.' => array(
169 'allowedAttribs' => 'id',
170 'rmTagIfNoAttrib' => 1
171 )
172 )
173 );
174 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
175 }
176
177 /**
178 * @test
179 */
180 public function rmTagIfNoAttribIsConfiguredDoesNotChangeNestingType() {
181 $tsConfig = array(
182 'allowTags' => 'div,span',
183 'rmTagIfNoAttrib' => 'span',
184 'globalNesting' => 'div,span'
185 );
186 $content = '<span></span><span id="test"><div></span></div>';
187 $expectedResult = '<span id="test"></span>';
188 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
189 }
190
191 /**
192 * Data provider for localNestingCorrectlyRemovesInvalidTags
193 *
194 * @return array
195 */
196 public static function localNestingCorrectlyRemovesInvalidTagsDataProvider() {
197 return array(
198 'Valid nesting is untouched' => array(
199 '<B><I></B></I>',
200 '<B><I></B></I>'
201 ),
202 'Valid nesting with content is untouched' => array(
203 'testa<B>test1<I>test2</B>test3</I>testb',
204 'testa<B>test1<I>test2</B>test3</I>testb'
205 ),
206 'Superflous tags are removed' => array(
207 '</B><B><I></B></I></B>',
208 '<B><I></B></I>'
209 ),
210 'Superflous tags with content are removed' => array(
211 'test1</B>test2<B>test3<I>test4</B>test5</I>test6</B>test7',
212 'test1test2<B>test3<I>test4</B>test5</I>test6test7'
213 ),
214 'Another valid nesting test' => array(
215 '<span><div></span></div>',
216 '<span><div></span></div>',
217 ),
218 );
219 }
220
221 /**
222 * @test
223 * @dataProvider localNestingCorrectlyRemovesInvalidTagsDataProvider
224 * @param string $content
225 * @param string $expectedResult
226 */
227 public function localNestingCorrectlyRemovesInvalidTags($content, $expectedResult) {
228 $tsConfig = array(
229 'allowTags' => 'div,span,b,i',
230 'localNesting' => 'div,span,b,i',
231 );
232 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
233 }
234
235 /**
236 * Data provider for globalNestingCorrectlyRemovesInvalidTags
237 *
238 * @return array
239 */
240 public static function globalNestingCorrectlyRemovesInvalidTagsDataProvider() {
241 return array(
242 'Valid nesting is untouched' => array(
243 '<B><I></I></B>',
244 '<B><I></I></B>'
245 ),
246 'Valid nesting with content is untouched' => array(
247 'testa<B>test1<I>test2</I>test3</B>testb',
248 'testa<B>test1<I>test2</I>test3</B>testb'
249 ),
250 'Invalid nesting is cleaned' => array(
251 '</B><B><I></B></I></B>',
252 '<B></B>'
253 ),
254 'Invalid nesting with content is cleaned' => array(
255 'test1</B>test2<B>test3<I>test4</B>test5</I>test6</B>test7',
256 'test1test2<B>test3test4</B>test5test6test7'
257 ),
258 'Another invalid nesting test' => array(
259 '<span><div></span></div>',
260 '<span></span>',
261 ),
262 );
263 }
264
265 /**
266 * @test
267 * @dataProvider globalNestingCorrectlyRemovesInvalidTagsDataProvider
268 * @param string $content
269 * @param string $expectedResult
270 */
271 public function globalNestingCorrectlyRemovesInvalidTags($content, $expectedResult) {
272 $tsConfig = array(
273 'allowTags' => 'span,div,b,i',
274 'globalNesting' => 'span,div,b,i',
275 );
276 $this->assertEquals($expectedResult, $this->parseConfigAndCleanHtml($tsConfig, $content));
277 }
278
279 /**
280 * @return array
281 */
282 public function emptyTagsDataProvider() {
283 return array(
284 array(0 , NULL, FALSE, '<h1></h1>', '<h1></h1>'),
285 array(1 , NULL, FALSE, '<h1></h1>', ''),
286 array(1 , NULL, FALSE, '<h1>hallo</h1>', '<h1>hallo</h1>'),
287 array(1 , NULL, FALSE, '<h1 class="something"></h1>', ''),
288 array(1 , NULL, FALSE, '<h1 class="something"></h1><h2></h2>', ''),
289 array(1 , 'h2', FALSE, '<h1 class="something"></h1><h2></h2>', '<h1 class="something"></h1>'),
290 array(1 , 'h2, h1', FALSE, '<h1 class="something"></h1><h2></h2>', ''),
291 array(1 , NULL, FALSE, '<div><p></p></div>', ''),
292 array(1 , NULL, FALSE, '<div><p>&nbsp;</p></div>', '<div><p>&nbsp;</p></div>'),
293 array(1 , NULL, TRUE, '<div><p>&nbsp;&nbsp;</p></div>', ''),
294 array(1 , NULL, TRUE, '<div>&nbsp;&nbsp;<p></p></div>', ''),
295 array(1 , NULL, FALSE, '<div>Some content<p></p></div>', '<div>Some content</div>'),
296 array(1 , NULL, TRUE, '<div>Some content<p></p></div>', '<div>Some content</div>'),
297 array(1 , NULL, FALSE, '<div>Some content</div>', '<div>Some content</div>'),
298 array(1 , NULL, TRUE, '<div>Some content</div>', '<div>Some content</div>'),
299 array(1 , NULL, FALSE, '<a href="#skiplinks">Skiplinks </a><b></b>', '<a href="#skiplinks">Skiplinks </a>'),
300 array(1 , NULL, TRUE, '<a href="#skiplinks">Skiplinks </a><b></b>', '<a href="#skiplinks">Skiplinks </a>'),
301 );
302 }
303
304 /**
305 * @test
306 * @dataProvider emptyTagsDataProvider
307 * @param bool $stripOn TRUE if stripping should be activated.
308 * @param string $tagList Comma seperated list of tags that should be stripped.
309 * @param bool $treatNonBreakingSpaceAsEmpty If TRUE &nbsp; will be considered empty.
310 * @param string $content The HTML code that should be modified.
311 * @param string $expectedResult The expected HTML code result.
312 */
313 public function stripEmptyTags($stripOn, $tagList, $treatNonBreakingSpaceAsEmpty, $content, $expectedResult) {
314 $tsConfig = array(
315 'keepNonMatchedTags' => 1,
316 'stripEmptyTags' => $stripOn,
317 'stripEmptyTags.' => array(
318 'tags' => $tagList,
319 'treatNonBreakingSpaceAsEmpty' => $treatNonBreakingSpaceAsEmpty
320 ),
321 );
322
323 $result = $this->parseConfigAndCleanHtml($tsConfig, $content);
324 $this->assertEquals($expectedResult, $result);
325 }
326
327 /**
328 * Calls HTMLparserConfig() and passes the generated config to the HTMLcleaner() method on the current subject.
329 *
330 * @param array $tsConfig The TypoScript that should be used to generate the HTML parser config.
331 * @param string $content The content that should be parsed by the HTMLcleaner.
332 * @return string The parsed content.
333 */
334 protected function parseConfigAndCleanHtml(array $tsConfig, $content) {
335 $config = $this->subject->HTMLparserConfig($tsConfig);
336 return $this->subject->HTMLcleaner($content, $config[0], $config[1], $config[2], $config[3]);
337 }
338
339 /**
340 * Data provider for getFirstTag
341 *
342 * @return array
343 */
344 public function getFirstTagDataProvider() {
345 return array(
346 array('<body><span></span></body>',
347 '<body>'),
348 );
349 }
350
351 /**
352 * Returns the first tag in $str
353 * Actually everything from the beginning of the $str is returned, so you better make sure the tag is the first thing...
354 *
355 * @test
356 * @dataProvider getFirstTagDataProvider
357 *
358 * @param string $str HTML string with tags
359 * @param string $expected The expected result.
360 */
361 public function getFirstTag($str, $expected) {
362 $this->assertEquals($expected, $this->subject->getFirstTag($str));
363 }
364
365 /**
366 * Data provider for getFirstTagName
367 *
368 * @return array
369 */
370 public function getFirstTagNameDataProvider() {
371 return array(
372 array('<body><span></span></body>',
373 FALSE,
374 'BODY'),
375 array('<body><span></span></body>',
376 TRUE,
377 'body'),
378 array('<div class="test"><span></span></div>',
379 FALSE,
380 'DIV'),
381 array('<div><span class="test"></span></div>',
382 FALSE,
383 'DIV'),
384 array('<br /><span class="test"></span>',
385 FALSE,
386 'BR'),
387 array('<img src="test.jpg" />',
388 FALSE,
389 'IMG'),
390 );
391 }
392
393 /**
394 * Returns the NAME of the first tag in $str
395 *
396 * @test
397 * @dataProvider getFirstTagNameDataProvider
398 *
399 * @param string $str HTML tag (The element name MUST be separated from the attributes by a space character! Just *whitespace* will not do)
400 * @param bool $preserveCase If set, then the tag is NOT converted to uppercase by case is preserved.
401 * @param string $expected The expected result.
402 */
403 public function getFirstTagName($str, $preserveCase, $expected) {
404 $this->assertEquals($expected, $this->subject->getFirstTagName($str, $preserveCase));
405 }
406
407 /**
408 * @return array
409 */
410 public function removeFirstAndLastTagDataProvider() {
411 return array(
412 array('<span>Wrapper<div>Some content</div></span>', 'Wrapper<div>Some content</div>'),
413 array('<td><tr>Some content</tr></td>', '<tr>Some content</tr>'),
414 array('Something before<span>Wrapper<div>Some content</div></span>Something after', 'Wrapper<div>Some content</div>'),
415 array('<span class="hidden">Wrapper<div>Some content</div></span>', 'Wrapper<div>Some content</div>'),
416 array('<span>Wrapper<div class="hidden">Some content</div></span>', 'Wrapper<div class="hidden">Some content</div>'),
417 );
418 }
419
420 /**
421 * Removes the first and last tag in the string
422 * Anything before the first and after the last tags respectively is also removed
423 *
424 * @test
425 * @dataProvider removeFirstAndLastTagDataProvider
426 * @param string $str String to process
427 * @param string $expectedResult
428 */
429 public function removeFirstAndLastTag($str, $expectedResult) {
430 $this->assertEquals($expectedResult, $this->subject->removeFirstAndLastTag($str));
431 }
432 }