[BUGFIX] typolink: Ensure explode() is using string
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Functional / ContentObject / ContentObjectRendererTest.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Tests\Functional\ContentObject;
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\Database\ConnectionPool;
18 use TYPO3\CMS\Core\TypoScript\TemplateService;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
21 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
22 use TYPO3\CMS\Frontend\Page\PageRepository;
23
24 /**
25 * Testcase for TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
26 */
27 class ContentObjectRendererTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase
28 {
29 /**
30 * @var ContentObjectRenderer
31 */
32 protected $subject;
33
34 /**
35 * @var string
36 */
37 protected $quoteChar;
38
39 protected function setUp()
40 {
41 parent::setUp();
42
43 $typoScriptFrontendController = GeneralUtility::makeInstance(
44 TypoScriptFrontendController::class,
45 null,
46 1,
47 0
48 );
49 $typoScriptFrontendController->sys_page = GeneralUtility::makeInstance(PageRepository::class);
50 $typoScriptFrontendController->tmpl = GeneralUtility::makeInstance(TemplateService::class);
51 $GLOBALS['TSFE'] = $typoScriptFrontendController;
52
53 $this->subject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
54 $this->quoteChar = GeneralUtility::makeInstance(ConnectionPool::class)
55 ->getConnectionForTable('tt_content')
56 ->getDatabasePlatform()
57 ->getIdentifierQuoteCharacter();
58 }
59
60 /**
61 * Data provider for the getQuery test
62 *
63 * @return array multi-dimensional array with the second level like this:
64 * @see getQuery
65 */
66 public function getQueryDataProvider(): array
67 {
68 $data = [
69 'testing empty conf' => [
70 'tt_content',
71 [],
72 [
73 'SELECT' => '*'
74 ]
75 ],
76 'testing #17284: adding uid/pid for workspaces' => [
77 'tt_content',
78 [
79 'selectFields' => 'header,bodytext'
80 ],
81 [
82 'SELECT' => 'header,bodytext, `tt_content`.`uid` AS `uid`, `tt_content`.`pid` AS `pid`, `tt_content`.`t3ver_state` AS `t3ver_state`'
83 ]
84 ],
85 'testing #17284: no need to add' => [
86 'tt_content',
87 [
88 'selectFields' => 'tt_content.*'
89 ],
90 [
91 'SELECT' => 'tt_content.*'
92 ]
93 ],
94 'testing #17284: no need to add #2' => [
95 'tt_content',
96 [
97 'selectFields' => '*'
98 ],
99 [
100 'SELECT' => '*'
101 ]
102 ],
103 'testing #29783: joined tables, prefix tablename' => [
104 'tt_content',
105 [
106 'selectFields' => 'tt_content.header,be_users.username',
107 'join' => 'be_users ON tt_content.cruser_id = be_users.uid'
108 ],
109 [
110 'SELECT' => 'tt_content.header,be_users.username, `tt_content`.`uid` AS `uid`, `tt_content`.`pid` AS `pid`, `tt_content`.`t3ver_state` AS `t3ver_state`'
111 ]
112 ],
113 'testing #34152: single count(*), add nothing' => [
114 'tt_content',
115 [
116 'selectFields' => 'count(*)'
117 ],
118 [
119 'SELECT' => 'count(*)'
120 ]
121 ],
122 'testing #34152: single max(crdate), add nothing' => [
123 'tt_content',
124 [
125 'selectFields' => 'max(crdate)'
126 ],
127 [
128 'SELECT' => 'max(crdate)'
129 ]
130 ],
131 'testing #34152: single min(crdate), add nothing' => [
132 'tt_content',
133 [
134 'selectFields' => 'min(crdate)'
135 ],
136 [
137 'SELECT' => 'min(crdate)'
138 ]
139 ],
140 'testing #34152: single sum(is_siteroot), add nothing' => [
141 'tt_content',
142 [
143 'selectFields' => 'sum(is_siteroot)'
144 ],
145 [
146 'SELECT' => 'sum(is_siteroot)'
147 ]
148 ],
149 'testing #34152: single avg(crdate), add nothing' => [
150 'tt_content',
151 [
152 'selectFields' => 'avg(crdate)'
153 ],
154 [
155 'SELECT' => 'avg(crdate)'
156 ]
157 ]
158 ];
159
160 return $data;
161 }
162
163 /**
164 * Check if sanitizeSelectPart works as expected
165 *
166 * @dataProvider getQueryDataProvider
167 * @test
168 * @param string $table
169 * @param array $conf
170 * @param array $expected
171 */
172 public function getQuery(string $table, array $conf, array $expected)
173 {
174 $GLOBALS['TCA'] = [
175 'pages' => [
176 'ctrl' => [
177 'enablecolumns' => [
178 'disabled' => 'hidden'
179 ]
180 ]
181 ],
182 'tt_content' => [
183 'ctrl' => [
184 'enablecolumns' => [
185 'disabled' => 'hidden'
186 ],
187 'versioningWS' => true
188 ]
189 ],
190 ];
191
192 $result = $this->subject->getQuery($table, $conf, true);
193 foreach ($expected as $field => $value) {
194 // Replace the MySQL backtick quote character with the actual quote character for the DBMS
195 if ($field === 'SELECT') {
196 $value = str_replace('`', $this->quoteChar, $value);
197 }
198 $this->assertEquals($value, $result[$field]);
199 }
200 }
201
202 /**
203 * @test
204 */
205 public function getQueryCallsGetTreeListWithNegativeValuesIfRecursiveIsSet()
206 {
207 $this->subject = $this->getAccessibleMock(ContentObjectRenderer::class, ['getTreeList']);
208 $this->subject->start([], 'tt_content');
209
210 $conf = [
211 'recursive' => '15',
212 'pidInList' => '16, -35'
213 ];
214
215 $this->subject->expects($this->at(0))
216 ->method('getTreeList')
217 ->with(-16, 15)
218 ->will($this->returnValue('15,16'));
219 $this->subject->expects($this->at(1))
220 ->method('getTreeList')
221 ->with(-35, 15)
222 ->will($this->returnValue('15,35'));
223
224 $this->subject->getQuery('tt_content', $conf, true);
225 }
226
227 /**
228 * @test
229 */
230 public function getQueryCallsGetTreeListWithCurrentPageIfThisIsSet()
231 {
232 $GLOBALS['TSFE']->id = 27;
233
234 $this->subject = $this->getAccessibleMock(ContentObjectRenderer::class, ['getTreeList']);
235 $this->subject->start([], 'tt_content');
236
237 $conf = [
238 'pidInList' => 'this',
239 'recursive' => '4'
240 ];
241
242 $this->subject->expects($this->once())
243 ->method('getTreeList')
244 ->with(-27)
245 ->will($this->returnValue('27'));
246
247 $this->subject->getQuery('tt_content', $conf, true);
248 }
249
250 /**
251 * @return array
252 */
253 public function getWhereReturnCorrectQueryDataProvider()
254 {
255 return [
256 [
257 [
258 'tt_content' => [
259 'ctrl' => [
260 ],
261 'columns' => [
262 ]
263 ],
264 ],
265 'tt_content',
266 [
267 'uidInList' => '42',
268 'pidInList' => 43,
269 'where' => 'tt_content.cruser_id=5',
270 'groupBy' => 'tt_content.title',
271 'orderBy' => 'tt_content.sorting',
272 ],
273 'WHERE (`tt_content`.`uid` IN (42)) AND (`tt_content`.`pid` IN (43)) AND (tt_content.cruser_id=5) GROUP BY `tt_content`.`title` ORDER BY `tt_content`.`sorting`',
274 ],
275 [
276 [
277 'tt_content' => [
278 'ctrl' => [
279 'delete' => 'deleted',
280 'enablecolumns' => [
281 'disabled' => 'hidden',
282 'starttime' => 'startdate',
283 'endtime' => 'enddate',
284 ],
285 'languageField' => 'sys_language_uid',
286 'transOrigPointerField' => 'l18n_parent',
287 ],
288 'columns' => [
289 ]
290 ],
291 ],
292 'tt_content',
293 [
294 'uidInList' => 42,
295 'pidInList' => 43,
296 'where' => 'tt_content.cruser_id=5',
297 'groupBy' => 'tt_content.title',
298 'orderBy' => 'tt_content.sorting',
299 ],
300 'WHERE (`tt_content`.`uid` IN (42)) AND (`tt_content`.`pid` IN (43)) AND (tt_content.cruser_id=5) AND (`tt_content`.`sys_language_uid` = 13) AND ((`tt_content`.`deleted` = 0) AND (`tt_content`.`hidden` = 0) AND (`tt_content`.`startdate` <= 4242) AND ((`tt_content`.`enddate` = 0) OR (`tt_content`.`enddate` > 4242))) GROUP BY `tt_content`.`title` ORDER BY `tt_content`.`sorting`',
301 ],
302 [
303 [
304 'tt_content' => [
305 'ctrl' => [
306 'languageField' => 'sys_language_uid',
307 'transOrigPointerField' => 'l18n_parent',
308 ],
309 'columns' => [
310 ]
311 ],
312 ],
313 'tt_content',
314 [
315 'uidInList' => 42,
316 'pidInList' => 43,
317 'where' => 'tt_content.cruser_id=5',
318 'languageField' => 0,
319 ],
320 'WHERE (`tt_content`.`uid` IN (42)) AND (`tt_content`.`pid` IN (43)) AND (tt_content.cruser_id=5)',
321 ],
322 ];
323 }
324
325 /**
326 * @test
327 * @param array $tca
328 * @param string $table
329 * @param array $configuration
330 * @param string $expectedResult
331 * @dataProvider getWhereReturnCorrectQueryDataProvider
332 */
333 public function getWhereReturnCorrectQuery(array $tca, string $table, array $configuration, string $expectedResult)
334 {
335 $GLOBALS['TCA'] = $tca;
336 $GLOBALS['SIM_ACCESS_TIME'] = '4242';
337 $GLOBALS['TSFE']->sys_language_content = 13;
338 /** @var \PHPUnit_Framework_MockObject_MockObject|ContentObjectRenderer $contentObjectRenderer */
339 $contentObjectRenderer = $this->getMockBuilder(ContentObjectRenderer::class)
340 ->setMethods(['checkPidArray'])
341 ->getMock();
342 $contentObjectRenderer->expects($this->any())
343 ->method('checkPidArray')
344 ->willReturn(explode(',', $configuration['pidInList']));
345
346 // Replace the MySQL backtick quote character with the actual quote character for the DBMS
347 $expectedResult = str_replace('`', $this->quoteChar, $expectedResult);
348
349 // Embed the enable fields string into the expected result as the database
350 // connection is still unconfigured when the data provider is being run.
351 $expectedResult = sprintf($expectedResult, $GLOBALS['TSFE']->sys_page->enableFields($table));
352
353 $this->assertSame($expectedResult, $contentObjectRenderer->getWhere($table, $configuration));
354 }
355
356 /**
357 * @return array
358 */
359 public function typolinkReturnsCorrectLinksForPagesDataProvider()
360 {
361 return [
362 'Link to page' => [
363 'My page',
364 [
365 'parameter' => 42,
366 ],
367 [
368 'uid' => 42,
369 'title' => 'Page title',
370 ],
371 '<a href="index.php?id=42">My page</a>',
372 ],
373 'Link to page without link text' => [
374 '',
375 [
376 'parameter' => 42,
377 ],
378 [
379 'uid' => 42,
380 'title' => 'Page title',
381 ],
382 '<a href="index.php?id=42">Page title</a>',
383 ],
384 'Link to page with attributes' => [
385 'My page',
386 [
387 'parameter' => '42',
388 'ATagParams' => 'class="page-class"',
389 'target' => '_self',
390 'title' => 'Link to internal page',
391 ],
392 [
393 'uid' => 42,
394 'title' => 'Page title',
395 ],
396 '<a href="index.php?id=42" title="Link to internal page" target="_self" class="page-class">My page</a>',
397 ],
398 'Link to page with attributes in parameter' => [
399 'My page',
400 [
401 'parameter' => '42 _self page-class "Link to internal page"',
402 ],
403 [
404 'uid' => 42,
405 'title' => 'Page title',
406 ],
407 '<a href="index.php?id=42" title="Link to internal page" target="_self" class="page-class">My page</a>',
408 ],
409 'Link to page with bold tag in title' => [
410 '',
411 [
412 'parameter' => 42,
413 ],
414 [
415 'uid' => 42,
416 'title' => 'Page <b>title</b>',
417 ],
418 '<a href="index.php?id=42">Page <b>title</b></a>',
419 ],
420 'Link to page with script tag in title' => [
421 '',
422 [
423 'parameter' => 42,
424 ],
425 [
426 'uid' => 42,
427 'title' => '<script>alert(123)</script>Page title',
428 ],
429 '<a href="index.php?id=42">&lt;script&gt;alert(123)&lt;/script&gt;Page title</a>',
430 ],
431 ];
432 }
433
434 /**
435 * @test
436 * @param string $linkText
437 * @param array $configuration
438 * @param array $pageArray
439 * @param string $expectedResult
440 * @dataProvider typolinkReturnsCorrectLinksForPagesDataProvider
441 */
442 public function typolinkReturnsCorrectLinksForPages($linkText, $configuration, $pageArray, $expectedResult)
443 {
444 $pageRepositoryMockObject = $this->getMockBuilder(PageRepository::class)
445 ->setMethods(['getPage'])
446 ->getMock();
447 $pageRepositoryMockObject->expects($this->any())->method('getPage')->willReturn($pageArray);
448
449 $typoScriptFrontendController = GeneralUtility::makeInstance(
450 TypoScriptFrontendController::class,
451 null,
452 1,
453 0
454 );
455 $typoScriptFrontendController->config = [
456 'config' => [],
457 'mainScript' => 'index.php',
458 ];
459 $typoScriptFrontendController->sys_page = $pageRepositoryMockObject;
460 $typoScriptFrontendController->tmpl = GeneralUtility::makeInstance(TemplateService::class);
461 $typoScriptFrontendController->tmpl->setup = [
462 'lib.' => [
463 'parseFunc.' => $this->getLibParseFunc(),
464 ],
465 ];
466 $GLOBALS['TSFE'] = $typoScriptFrontendController;
467
468 $subject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
469 $this->assertEquals($expectedResult, $subject->typoLink($linkText, $configuration));
470 }
471
472 /**
473 * @test
474 */
475 public function typolinkReturnsCorrectLinkForSectionToHomePageWithUrlRewriting()
476 {
477 $pageRepositoryMockObject = $this->getMockBuilder(PageRepository::class)
478 ->setMethods(['getPage'])
479 ->getMock();
480 $pageRepositoryMockObject->expects($this->any())->method('getPage')->willReturn([
481 'uid' => 1,
482 'title' => 'Page title',
483 ]);
484
485 $templateServiceMockObject = $this->getMockBuilder(TemplateService::class)
486 ->setMethods(['linkData'])
487 ->getMock();
488 $templateServiceMockObject->setup = [
489 'lib.' => [
490 'parseFunc.' => $this->getLibParseFunc(),
491 ],
492 ];
493 $templateServiceMockObject->expects($this->once())->method('linkData')->willReturn([
494 'url' => '/index.php?id=1',
495 'target' => '',
496 'type' => '',
497 'orig_type' => '',
498 'no_cache' => '',
499 'linkVars' => '',
500 'sectionIndex' => '',
501 'totalURL' => '/',
502 ]);
503
504 $typoScriptFrontendController = GeneralUtility::makeInstance(
505 TypoScriptFrontendController::class,
506 null,
507 1,
508 0
509 );
510 $typoScriptFrontendController->config = [
511 'config' => [],
512 'mainScript' => 'index.php',
513 ];
514 $typoScriptFrontendController->sys_page = $pageRepositoryMockObject;
515 $typoScriptFrontendController->tmpl = $templateServiceMockObject;
516 $GLOBALS['TSFE'] = $typoScriptFrontendController;
517
518 $configuration = [
519 'parameter' => 1,
520 'section' => 'content',
521 ];
522
523 $subject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
524 $this->assertEquals('<a href="#content">Page title</a>', $subject->typoLink('', $configuration));
525 }
526
527 /**
528 * @return array
529 */
530 protected function getLibParseTarget()
531 {
532 return [
533 'override' => '',
534 'override.' => [
535 'if.' => [
536 'isTrue.' => [
537 'data' => 'TSFE:dtdAllowsFrames',
538 ],
539 ],
540 ],
541 ];
542 }
543
544 /**
545 * @return array
546 */
547 protected function getLibParseFunc()
548 {
549 return [
550 'makelinks' => '1',
551 'makelinks.' => [
552 'http.' => [
553 'keep' => '{$styles.content.links.keep}',
554 'extTarget' => '',
555 'extTarget.' => $this->getLibParseTarget(),
556 'mailto.' => [
557 'keep' => 'path',
558 ],
559 ],
560 ],
561 'tags' => [
562 'link' => 'TEXT',
563 'link.' => [
564 'current' => '1',
565 'typolink.' => [
566 'parameter.' => [
567 'data' => 'parameters : allParams',
568 ],
569 'extTarget.' => $this->getLibParseTarget(),
570 'target.' => $this->getLibParseTarget(),
571 ],
572 'parseFunc.' => [
573 'constants' => '1',
574 ],
575 ],
576 ],
577
578 'allowTags' => 'a, abbr, acronym, address, article, aside, b, bdo, big, blockquote, br, caption, center, cite, code, col, colgroup, dd, del, dfn, dl, div, dt, em, font, footer, header, h1, h2, h3, h4, h5, h6, hr, i, img, ins, kbd, label, li, link, meta, nav, ol, p, pre, q, samp, sdfield, section, small, span, strike, strong, style, sub, sup, table, thead, tbody, tfoot, td, th, tr, title, tt, u, ul, var',
579 'denyTags' => '*',
580 'sword' => '<span class="csc-sword">|</span>',
581 'constants' => '1',
582 'nonTypoTagStdWrap.' => [
583 'HTMLparser' => '1',
584 'HTMLparser.' => [
585 'keepNonMatchedTags' => '1',
586 'htmlSpecialChars' => '2',
587 ],
588 ],
589 ];
590 }
591 }