[SECURITY] XSS in TCA type inline
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Tests / Unit / Form / FormDataProvider / TcaRecordTitleTest.php
1 <?php
2 namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;
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 Prophecy\Argument;
18 use Prophecy\Prophecy\ObjectProphecy;
19 use TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle;
20 use TYPO3\CMS\Core\Tests\UnitTestCase;
21 use TYPO3\CMS\Lang\LanguageService;
22
23 /**
24 * Test case
25 */
26 class TcaRecordTitleTest extends UnitTestCase
27 {
28 /**
29 * @var TcaRecordTitle
30 */
31 protected $subject;
32
33 /**
34 * @var string
35 */
36 protected $timeZone;
37
38 public function setUp()
39 {
40 $this->subject = new TcaRecordTitle();
41 $this->timeZone = date_default_timezone_get();
42 date_default_timezone_set('UTC');
43 }
44
45 protected function tearDown()
46 {
47 date_default_timezone_set($this->timeZone);
48 }
49
50 /**
51 * @test
52 */
53 public function addDataThrowsExceptionWithMissingLabel()
54 {
55 $input = [
56 'tableName' => 'aTable',
57 'databaseRew' => [],
58 'processedTca' => [
59 'ctrl' => [],
60 ],
61 ];
62 $this->setExpectedException(\UnexpectedValueException::class, $this->any(), 1443706103);
63 $this->subject->addData($input);
64 }
65
66 /**
67 * @test
68 */
69 public function addDataReturnsRecordTitleForLabelUserFunction()
70 {
71 $input = [
72 'tableName' => 'aTable',
73 'databaseRow' => [],
74 'processedTca' => [
75 'ctrl' => [
76 'label' => 'uid',
77 'label_userFunc' => function (&$parameters) {
78 $parameters['title'] = 'Test';
79 }
80 ],
81 'columns' => [],
82 ],
83 ];
84
85 $expected = $input;
86 $expected['recordTitle'] = 'Test';
87
88 $this->assertSame($expected, $this->subject->addData($input));
89 }
90
91 /**
92 * @test
93 */
94 public function addDataReturnsRecordTitleForFormattedLabelUserFunction()
95 {
96 $input = [
97 'tableName' => 'aTable',
98 'databaseRow' => [],
99 'isInlineChild' => true,
100 'processedTca' => [
101 'ctrl' => [
102 'label' => 'uid',
103 'formattedLabel_userFunc' => function (&$parameters) {
104 $parameters['title'] = 'Test';
105 }
106 ],
107 'columns' => [],
108 ],
109 ];
110
111 $expected = $input;
112 $expected['recordTitle'] = 'Test';
113
114 $this->assertSame($expected, $this->subject->addData($input));
115 }
116
117 /**
118 * @test
119 */
120 public function addDataReturnsRecordTitleForInlineChildWithForeignLabel()
121 {
122 $input = [
123 'tableName' => 'aTable',
124 'databaseRow' => [
125 'aField' => 'aValue',
126 ],
127 'processedTca' => [
128 'ctrl' => [
129 'label' => 'foo',
130 'label_userFunc' => function (&$parameters) {
131 $parameters['title'] = 'Value that MUST NOT be used, otherwise the code is broken.';
132 }
133 ],
134 'columns' => [
135 'aField' => [
136 'config' => [
137 'type' => 'input',
138 ],
139 ],
140 ],
141 ],
142 'isInlineChild' => true,
143 'inlineParentConfig' => [
144 'foreign_label' => 'aField',
145 ],
146 ];
147 $expected = $input;
148 $expected['recordTitle'] = 'aValue';
149 $this->assertSame($expected, $this->subject->addData($input));
150 }
151
152 /**
153 * @test
154 */
155 public function addDataOverridesRecordTitleWithFormattedLabelUserFuncForInlineChildWithForeignLabel()
156 {
157 $input = [
158 'tableName' => 'aTable',
159 'databaseRow' => [
160 'aField' => 'aValue',
161 ],
162 'processedTca' => [
163 'ctrl' => [
164 'label' => 'foo',
165 'formattedLabel_userFunc' => function (&$parameters) {
166 $parameters['title'] = 'aFormattedLabel';
167 },
168 ],
169 'columns' => [
170 'aField' => [
171 'config' => [
172 'type' => 'input',
173 ],
174 ],
175 ],
176 ],
177 'isInlineChild' => true,
178 'inlineParentConfig' => [
179 'foreign_label' => 'aField',
180 ],
181 ];
182 $expected = $input;
183 $expected['recordTitle'] = 'aFormattedLabel';
184 $this->assertSame($expected, $this->subject->addData($input));
185 }
186
187 /**
188 * @test
189 */
190 public function addDataReturnsRecordTitleForInlineChildWithSymmetricLabel()
191 {
192 $input = [
193 'tableName' => 'aTable',
194 'databaseRow' => [
195 'aField' => 'aValue',
196 ],
197 'processedTca' => [
198 'ctrl' => [
199 'label' => 'foo',
200 ],
201 'columns' => [
202 'aField' => [
203 'config' => [
204 'type' => 'input',
205 ],
206 ],
207 ],
208 ],
209 'isInlineChild' => true,
210 'inlineParentConfig' => [
211 'symmetric_label' => 'aField',
212 ],
213 'isOnSymmetricSide' => true,
214 ];
215 $expected = $input;
216 $expected['recordTitle'] = 'aValue';
217 $this->assertSame($expected, $this->subject->addData($input));
218 }
219
220 /**
221 * @test
222 */
223 public function addDataReturnsRecordTitleForUid()
224 {
225 $input = [
226 'tableName' => 'aTable',
227 'databaseRow' => [
228 'uid' => 'NEW56017ee37d10e587251374',
229 ],
230 'processedTca' => [
231 'ctrl' => [
232 'label' => 'uid'
233 ],
234 'columns' => [],
235 ]
236 ];
237
238 /** @var LanguageService|ObjectProphecy $languageService */
239 $languageService = $this->prophesize(LanguageService::class);
240 $GLOBALS['LANG'] = $languageService->reveal();
241 $languageService->sL(Argument::cetera())->willReturnArgument(0);
242
243 $expected = $input;
244 $expected['recordTitle'] = 'NEW56017ee37d10e587251374';
245 $this->assertSame($expected, $this->subject->addData($input));
246 }
247
248 /**
249 * Data provider for addDataReturnsRecordTitleForInputType
250 * Each data set is an array with the following elements:
251 * - TCA field ['config'] section
252 * - Database value for field
253 * - expected title to be generated
254 *
255 * @returns array
256 */
257 public function addDataReturnsRecordTitleForInputTypeDataProvider()
258 {
259 return [
260 'new record' => [
261 [
262 'type' => 'input',
263 ],
264 '',
265 '',
266 ],
267 'plain text input' => [
268 [
269 'type' => 'input',
270 ],
271 'aValue',
272 'aValue',
273 ],
274 'html is escaped' => [
275 [
276 'type' => 'input',
277 ],
278 '<foo>',
279 '&lt;foo&gt;',
280 ],
281 'date input' => [
282 [
283 'type' => 'input',
284 'eval' => 'date'
285 ],
286 '978307261',
287 '01-01-01 (-7 days)',
288 ],
289 'date input (dbType: date)' => [
290 [
291 'type' => 'input',
292 'eval' => 'date',
293 'dbType' => 'date'
294 ],
295 '2001-01-01',
296 '01-01-01 (-7 days)',
297 ],
298 'date input (disableAgeDisplay: TRUE)' => [
299 [
300 'type' => 'input',
301 'eval' => 'date',
302 'disableAgeDisplay' => true
303 ],
304 '978307261',
305 '01-01-01',
306 ],
307 'time input' => [
308 [
309 'type' => 'input',
310 'eval' => 'time',
311 ],
312 '44100',
313 '12:15',
314 ],
315 'timesec input' => [
316 [
317 'type' => 'input',
318 'eval' => 'timesec',
319 ],
320 '44130',
321 '12:15:30',
322 ],
323 'datetime input' => [
324 [
325 'type' => 'input',
326 'eval' => 'datetime',
327 'dbType' => 'date'
328 ],
329 '978307261',
330 '01-01-01 00:01',
331 ],
332 'datetime input (dbType: datetime)' => [
333 [
334 'type' => 'input',
335 'eval' => 'datetime',
336 'dbType' => 'datetime'
337 ],
338 '2014-12-31 23:59:59',
339 '31-12-14 23:59',
340 ],
341 ];
342 }
343
344 /**
345 * @test
346 * @dataProvider addDataReturnsRecordTitleForInputTypeDataProvider
347 *
348 * @param array $fieldConfig
349 * @param string $fieldValue
350 * @param string $expectedTitle
351 */
352 public function addDataReturnsRecordTitleForInputType($fieldConfig, $fieldValue, $expectedTitle)
353 {
354 $input = [
355 'tableName' => 'aTable',
356 'databaseRow' => [
357 'uid' => '1',
358 'aField' => $fieldValue,
359 ],
360 'processedTca' => [
361 'ctrl' => [
362 'label' => 'aField'
363 ],
364 'columns' => [
365 'aField' => [
366 'config' => $fieldConfig,
367 ]
368 ],
369 ]
370 ];
371
372 /** @var LanguageService|ObjectProphecy $languageService */
373 $languageService = $this->prophesize(LanguageService::class);
374 $GLOBALS['LANG'] = $languageService->reveal();
375 $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')
376 ->willReturn(' min| hrs| days| yrs| min| hour| day| year');
377 $languageService->sL(Argument::cetera())->willReturnArgument(0);
378 $GLOBALS['EXEC_TIME'] = 978912061;
379
380 $expected = $input;
381 $expected['recordTitle'] = $expectedTitle;
382 $this->assertSame($expected, $this->subject->addData($input));
383 }
384
385 /**
386 * @test
387 */
388 public function addDataReturnsRecordTitleWithAlternativeLabel()
389 {
390 $input = [
391 'tableName' => 'aTable',
392 'databaseRow' => [
393 'uid' => '1',
394 'aField' => '',
395 'anotherField' => 'anotherValue',
396 ],
397 'processedTca' => [
398 'ctrl' => [
399 'label' => 'aField',
400 'label_alt' => 'anotherField',
401 ],
402 'columns' => [
403 'aField' => [
404 'config' => [
405 'type' => 'input'
406 ]
407 ],
408 'anotherField' => [
409 'config' => [
410 'type' => 'input'
411 ]
412 ]
413 ],
414 ]
415 ];
416
417 $expected = $input;
418 $expected['recordTitle'] = 'anotherValue';
419 $this->assertSame($expected, $this->subject->addData($input));
420 }
421
422 /**
423 * @test
424 */
425 public function addDataReturnsRecordTitleWithMultipleAlternativeLabels()
426 {
427 $input = [
428 'tableName' => 'aTable',
429 'databaseRow' => [
430 'uid' => '1',
431 'aField' => '',
432 'anotherField' => '',
433 'additionalField' => 'additionalValue'
434 ],
435 'processedTca' => [
436 'ctrl' => [
437 'label' => 'aField',
438 'label_alt' => 'anotherField,additionalField',
439 ],
440 'columns' => [
441 'aField' => [
442 'config' => [
443 'type' => 'input'
444 ]
445 ],
446 'anotherField' => [
447 'config' => [
448 'type' => 'input'
449 ]
450 ],
451 'additionalField' => [
452 'config' => [
453 'type' => 'input'
454 ]
455 ],
456 ],
457 ]
458 ];
459
460 $expected = $input;
461 $expected['recordTitle'] = 'additionalValue';
462 $this->assertSame($expected, $this->subject->addData($input));
463 }
464
465 /**
466 * @test
467 */
468 public function addDataReturnsRecordTitleWithForcedAlternativeLabel()
469 {
470 $input = [
471 'tableName' => 'aTable',
472 'databaseRow' => [
473 'uid' => '1',
474 'aField' => 'aField',
475 'anotherField' => 'anotherField'
476 ],
477 'processedTca' => [
478 'ctrl' => [
479 'label' => 'aField',
480 'label_alt' => 'anotherField',
481 'label_alt_force' => true,
482 ],
483 'columns' => [
484 'aField' => [
485 'config' => [
486 'type' => 'input'
487 ]
488 ],
489 'anotherField' => [
490 'config' => [
491 'type' => 'input'
492 ]
493 ],
494 ],
495 ]
496 ];
497
498 $expected = $input;
499 $expected['recordTitle'] = 'aField, anotherField';
500 $this->assertSame($expected, $this->subject->addData($input));
501 }
502
503 /**
504 * @test
505 */
506 public function addDataReturnsRecordTitleWithMultipleForcedAlternativeLabels()
507 {
508 $input = [
509 'tableName' => 'aTable',
510 'databaseRow' => [
511 'uid' => '1',
512 'aField' => 'aField',
513 'anotherField' => 'anotherField',
514 'additionalField' => 'additionalValue'
515 ],
516 'processedTca' => [
517 'ctrl' => [
518 'label' => 'aField',
519 'label_alt' => 'anotherField,additionalField',
520 'label_alt_force' => true,
521 ],
522 'columns' => [
523 'aField' => [
524 'config' => [
525 'type' => 'input'
526 ]
527 ],
528 'anotherField' => [
529 'config' => [
530 'type' => 'input'
531 ]
532 ],
533 'additionalField' => [
534 'config' => [
535 'type' => 'input'
536 ]
537 ],
538 ],
539 ]
540 ];
541
542 $expected = $input;
543 $expected['recordTitle'] = 'aField, anotherField, additionalValue';
544 $this->assertSame($expected, $this->subject->addData($input));
545 }
546
547 /**
548 * @test
549 */
550 public function addDataReturnsRecordTitleIgnoresEmptyAlternativeLabels()
551 {
552 $input = [
553 'tableName' => 'aTable',
554 'databaseRow' => [
555 'uid' => '1',
556 'aField' => 'aField',
557 'anotherField' => '',
558 'additionalField' => 'additionalValue'
559 ],
560 'processedTca' => [
561 'ctrl' => [
562 'label' => 'aField',
563 'label_alt' => 'anotherField,additionalField',
564 'label_alt_force' => true,
565 ],
566 'columns' => [
567 'aField' => [
568 'config' => [
569 'type' => 'input'
570 ]
571 ],
572 'anotherField' => [
573 'config' => [
574 'type' => 'input'
575 ]
576 ],
577 'additionalField' => [
578 'config' => [
579 'type' => 'input'
580 ]
581 ],
582 ],
583 ]
584 ];
585
586 $expected = $input;
587 $expected['recordTitle'] = 'aField, additionalValue';
588 $this->assertSame($expected, $this->subject->addData($input));
589 }
590
591 /**
592 * @test
593 */
594 public function addDataReturnsRecordTitleForRadioType()
595 {
596 $input = [
597 'tableName' => 'aTable',
598 'databaseRow' => [
599 'uid' => '1',
600 'aField' => '2',
601 ],
602 'processedTca' => [
603 'ctrl' => [
604 'label' => 'aField'
605 ],
606 'columns' => [
607 'aField' => [
608 'config' => [
609 'type' => 'radio',
610 'items' => [
611 ['foo', 1],
612 ['bar', 2],
613 ['baz', 3],
614 ]
615 ]
616 ]
617 ],
618 ]
619 ];
620
621 $expected = $input;
622 $expected['recordTitle'] = 'bar';
623 $this->assertSame($expected, $this->subject->addData($input));
624 }
625
626 /**
627 * Data provider for addDataReturnsRecordTitleForGroupType
628 * Each data set is an array with the following elements:
629 * - TCA field configuration (merged with base config)
630 * - Database value for field
631 * - expected title to be generated
632 *
633 * @returns array
634 */
635 public function addDataReturnsRecordTitleForGroupTypeDataProvider()
636 {
637 return [
638 'new record' => [
639 [
640 'internal_type' => 'db',
641 ],
642 '',
643 ''
644 ],
645 'internal_type: file' => [
646 [
647 'internal_type' => 'file',
648 ],
649 'somePath/aFile.jpg,someOtherPath/anotherFile.png',
650 'somePath/aFile.jpg, someOtherPath/anotherFile.png',
651 ],
652 'internal_type: db, single table, single record' => [
653 [
654 'internal_type' => 'db',
655 'allowed' => 'aTable'
656 ],
657 '1|aValue',
658 'aValue',
659 ],
660 'internal_type: db, single table, multiple records' => [
661 [
662 'internal_type' => 'db',
663 'allowed' => 'aTable'
664 ],
665 '1|aValue,3|anotherValue',
666 'aValue, anotherValue',
667 ],
668 'internal_type: db, multiple tables, single record' => [
669 [
670 'internal_type' => 'db',
671 'allowed' => 'aTable,anotherTable'
672 ],
673 'anotherTable_1|anotherValue',
674 'anotherValue',
675 ],
676 'internal_type: db, multiple tables, multiple records' => [
677 [
678 'internal_type' => 'db',
679 'allowed' => 'aTable,anotherTable'
680 ],
681 'anotherTable_1|anotherValue,aTable_1|aValue',
682 'aValue, anotherValue',
683 ],
684 ];
685 }
686
687 /**
688 * @test
689 * @dataProvider addDataReturnsRecordTitleForGroupTypeDataProvider
690 *
691 * @param array $fieldConfig
692 * @param string $fieldValue
693 * @param string $expectedTitle
694 */
695 public function addDataReturnsRecordTitleForGroupType($fieldConfig, $fieldValue, $expectedTitle)
696 {
697 $input = [
698 'tableName' => 'aTable',
699 'databaseRow' => [
700 'uid' => '1',
701 'aField' => $fieldValue,
702 ],
703 'processedTca' => [
704 'ctrl' => [
705 'label' => 'aField'
706 ],
707 'columns' => [
708 'aField' => [
709 'config' => array_merge(
710 [
711 'type' => 'group',
712 ],
713 $fieldConfig
714 ),
715 ]
716 ],
717 ]
718 ];
719
720 /** @var LanguageService|ObjectProphecy $languageService */
721 $languageService = $this->prophesize(LanguageService::class);
722 $GLOBALS['LANG'] = $languageService->reveal();
723 $languageService->sL(Argument::cetera())->willReturnArgument(0);
724
725 $expected = $input;
726 $expected['recordTitle'] = $expectedTitle;
727 $this->assertSame($expected, $this->subject->addData($input));
728 }
729
730 /**
731 * @test
732 */
733 public function addDataReturnsRecordTitleForGroupTypeWithInternalTypeDb()
734 {
735 $input = [
736 'tableName' => 'aTable',
737 'databaseRow' => [
738 'uid' => '1',
739 'aField' => 'aTable_1|aValue,anotherTable_2|anotherValue',
740 ],
741 'processedTca' => [
742 'ctrl' => [
743 'label' => 'aField'
744 ],
745 'columns' => [
746 'aField' => [
747 'config' => [
748 'type' => 'group',
749 'internal_type' => 'db',
750 'allowed' => 'aTable,anotherTable',
751 ]
752 ]
753 ],
754 ]
755 ];
756
757 $expected = $input;
758 $expected['recordTitle'] = 'aValue, anotherValue';
759 $this->assertSame($expected, $this->subject->addData($input));
760 }
761
762 /**
763 * @test
764 */
765 public function addDataReturnsRecordTitleForSingleCheckboxType()
766 {
767 $input = [
768 'tableName' => 'aTable',
769 'databaseRow' => [
770 'aField' => 1,
771 ],
772 'processedTca' => [
773 'ctrl' => [
774 'label' => 'aField'
775 ],
776 'columns' => [
777 'aField' => [
778 'config' => [
779 'type' => 'check',
780 ]
781 ]
782 ],
783 ]
784 ];
785
786 /** @var LanguageService|ObjectProphecy $languageService */
787 $languageService = $this->prophesize(LanguageService::class);
788 $GLOBALS['LANG'] = $languageService->reveal();
789 $languageService->sL(Argument::cetera())->willReturnArgument(0)->shouldBeCalled();
790
791 $expected = $input;
792 $expected['recordTitle'] = 'LLL:EXT:lang/locallang_common.xlf:yes';
793 $this->assertSame($expected, $this->subject->addData($input));
794 }
795
796 /**
797 * @test
798 */
799 public function addDataReturnsRecordTitleForArrayCheckboxType()
800 {
801 $input = [
802 'tableName' => 'aTable',
803 'databaseRow' => [
804 'aField' => '5'
805 ],
806 'processedTca' => [
807 'ctrl' => [
808 'label' => 'aField'
809 ],
810 'columns' => [
811 'aField' => [
812 'config' => [
813 'type' => 'check',
814 'items' => [
815 ['foo', ''],
816 ['bar', ''],
817 ['baz', ''],
818 ]
819 ]
820 ]
821 ],
822 ]
823 ];
824
825 $expected = $input;
826 $expected['recordTitle'] = 'foo, baz';
827 $this->assertSame($expected, $this->subject->addData($input));
828 }
829
830 /**
831 * @test
832 */
833 public function addDataReturnsEmptyRecordTitleForFlexType()
834 {
835 $input = [
836 'tableName' => 'aTable',
837 'databaseRow' => [
838 'aField' => [
839 'data' => [
840 'sDEF' => [
841 'lDEF' => [
842 'aFlexField' => [
843 'vDEF' => 'aFlexValue',
844 ]
845 ]
846 ]
847 ]
848 ]
849 ],
850 'processedTca' => [
851 'ctrl' => [
852 'label' => 'aField'
853 ],
854 'columns' => [
855 'aField' => [
856 'config' => [
857 'type' => 'flex',
858 'ds' => [
859 'sheets' => [
860 'sDEF' => [
861 'ROOT' => [
862 'type' => 'array',
863 'el' => [
864 'aFlexField' => [
865 'label' => 'Some input field',
866 'config' => [
867 'type' => 'input',
868 ],
869 ],
870 ],
871 ],
872 ],
873 ],
874 ]
875
876 ]
877 ]
878 ],
879 ]
880 ];
881
882 $expected = $input;
883 $expected['recordTitle'] = '';
884 $this->assertSame($expected, $this->subject->addData($input));
885 }
886
887 /**
888 * @test
889 */
890 public function addDataReturnsRecordTitleForSelectType()
891 {
892 $input = [
893 'tableName' => 'aTable',
894 'databaseRow' => [
895 'aField' => [
896 '1',
897 '2'
898 ]
899 ],
900 'processedTca' => [
901 'ctrl' => [
902 'label' => 'aField'
903 ],
904 'columns' => [
905 'aField' => [
906 'config' => [
907 'type' => 'select',
908 'items' => [
909 ['foo', 1, null, null],
910 ['bar', 2, null, null],
911 ['baz', 4, null, null],
912 ]
913 ]
914 ]
915 ],
916 ]
917 ];
918
919 $expected = $input;
920 $expected['recordTitle'] = 'foo, bar';
921 $this->assertSame($expected, $this->subject->addData($input));
922 }
923
924 /**
925 * @test
926 */
927 public function addDataReturnsStrippedAndTrimmedValueForTextType()
928 {
929 $input = [
930 'tableName' => 'aTable',
931 'databaseRow' => [
932 'aField' => '<p> text </p>',
933 ],
934 'processedTca' => [
935 'ctrl' => [
936 'label' => 'aField',
937 ],
938 'columns' => [
939 'aField' => [
940 'config' => [
941 'type' => 'text',
942 ],
943 ],
944 ],
945 ],
946 ];
947
948 $expected = $input;
949 $expected['recordTitle'] = 'text';
950 $this->assertSame($expected, $this->subject->addData($input));
951 }
952 }