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