0eb322d4fbf5c93de31bf399a643f0d2cb15aaa1
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Tests / Unit / Form / FormDataProvider / TcaSelectItemsTest.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 Doctrine\DBAL\DBALException;
18 use Doctrine\DBAL\Driver\Statement;
19 use Prophecy\Argument;
20 use Prophecy\Prophecy\ObjectProphecy;
21 use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems;
22 use TYPO3\CMS\Backend\Module\ModuleLoader;
23 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
24 use TYPO3\CMS\Core\Cache\CacheManager;
25 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
26 use TYPO3\CMS\Core\Core\Environment;
27 use TYPO3\CMS\Core\Database\Connection;
28 use TYPO3\CMS\Core\Database\ConnectionPool;
29 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
30 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
31 use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
32 use TYPO3\CMS\Core\Database\RelationHandler;
33 use TYPO3\CMS\Core\Imaging\IconRegistry;
34 use TYPO3\CMS\Core\Localization\LanguageService;
35 use TYPO3\CMS\Core\Messaging\FlashMessage;
36 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
37 use TYPO3\CMS\Core\Messaging\FlashMessageService;
38 use TYPO3\CMS\Core\Resource\FileRepository;
39 use TYPO3\CMS\Core\Site\Entity\Site;
40 use TYPO3\CMS\Core\Site\SiteFinder;
41 use TYPO3\CMS\Core\Utility\ArrayUtility;
42 use TYPO3\CMS\Core\Utility\GeneralUtility;
43 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
44
45 /**
46 * Test case
47 */
48 class TcaSelectItemsTest extends UnitTestCase
49 {
50 /**
51 * Set up
52 */
53 protected function setUp(): void
54 {
55 parent::setUp();
56 // Default LANG prophecy just returns incoming value as label if calling ->sL()
57 $languageServiceProphecy = $this->prophesize(LanguageService::class);
58 $languageServiceProphecy->loadSingleTableDescription(Argument::cetera())->willReturn(null);
59 $languageServiceProphecy->sL(Argument::cetera())->willReturnArgument(0);
60 $GLOBALS['LANG'] = $languageServiceProphecy->reveal();
61
62 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
63 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
64
65 $cacheManagerProphecy = $this->prophesize(CacheManager::class);
66 $cacheProphecy = $this->prophesize(FrontendInterface::class);
67 $cacheManagerProphecy->getCache('runtime')->willReturn($cacheProphecy->reveal());
68 $cacheProphecy->get(Argument::cetera())->willReturn(false);
69 $cacheProphecy->set(Argument::cetera())->willReturn(false);
70 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
71 }
72
73 /**
74 * Tear down
75 */
76 protected function tearDown(): void
77 {
78 GeneralUtility::purgeInstances();
79 parent::tearDown();
80 }
81
82 /**
83 * Prepare a mock database setup for a Doctrine connection
84 * and return an array of all prophets to set expectations upon.
85 *
86 * @param string $tableName
87 * @return array
88 */
89 protected function mockDatabaseConnection($tableName = 'fTable')
90 {
91 $connectionProphet = $this->prophesize(Connection::class);
92 $connectionProphet->quote(Argument::cetera())->will(function ($arguments) {
93 return "'" . $arguments[0] . "'";
94 });
95 $connectionProphet->quoteIdentifier(Argument::cetera())->will(function ($arguments) {
96 return '`' . $arguments[0] . '`';
97 });
98
99 $restrictionProphet = $this->prophesize(DefaultRestrictionContainer::class);
100 $restrictionProphet->removeAll()->willReturn($restrictionProphet->reveal());
101 $restrictionProphet->add(Argument::cetera())->willReturn($restrictionProphet->reveal());
102
103 $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
104 $queryBuilderProphet->expr()->willReturn(
105 GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal())
106 );
107 $queryBuilderProphet->getRestrictions()->willReturn($restrictionProphet->reveal());
108 $queryBuilderProphet->quoteIdentifier(Argument::cetera())->will(function ($arguments) {
109 return '`' . $arguments[0] . '`';
110 });
111
112 $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
113 $connectionPoolProphet->getConnectionForTable($tableName)
114 ->willReturn($connectionProphet->reveal());
115 $connectionPoolProphet->getQueryBuilderForTable($tableName)
116 ->shouldBeCalled()
117 ->willReturn($queryBuilderProphet->reveal());
118
119 return [$queryBuilderProphet, $connectionPoolProphet, $connectionProphet, $restrictionProphet];
120 }
121
122 /**
123 * Mock a doctrine database connection with all expectations
124 * required for the processSelectField* tests.
125 */
126 protected function mockDatabaseConnectionForProcessSelectField()
127 {
128 list($queryBuilderProphet, $connectionPoolProphet) = $this->mockDatabaseConnection('foreignTable');
129
130 /** @var Statement|ObjectProphecy $statementProphet */
131 $statementProphet = $this->prophesize(Statement::class);
132 $statementProphet->fetch()->shouldBeCalled();
133
134 $queryBuilderProphet->select('foreignTable.uid')
135 ->shouldBeCalled()
136 ->willReturn($queryBuilderProphet->reveal());
137 $queryBuilderProphet->from('foreignTable')
138 ->shouldBeCalled()
139 ->willReturn($queryBuilderProphet->reveal());
140 $queryBuilderProphet->from('pages')
141 ->shouldBeCalled()
142 ->willReturn($queryBuilderProphet->reveal());
143 $queryBuilderProphet->where('')
144 ->shouldBeCalled()
145 ->willReturn($queryBuilderProphet->reveal());
146 $queryBuilderProphet->andWhere(' 1=1')
147 ->shouldBeCalled()
148 ->willReturn($queryBuilderProphet->reveal());
149 $queryBuilderProphet->andWhere('`pages.uid` = `foreignTable.pid`')
150 ->shouldBeCalled()
151 ->willReturn($queryBuilderProphet->reveal());
152 $queryBuilderProphet->execute()
153 ->shouldBeCalled()
154 ->willReturn($statementProphet->reveal());
155
156 // Two instances are needed due to the push/pop behavior of addInstance()
157 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
158 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
159 }
160
161 /**
162 * @test
163 */
164 public function addDataKeepExistingItems()
165 {
166 $input = [
167 'tableName' => 'aTable',
168 'processedTca' => [
169 'columns' => [
170 'aField' => [
171 'config' => [
172 'type' => 'radio',
173 'items' => [
174 0 => [
175 'foo',
176 'bar',
177 ],
178 ],
179 ],
180 ],
181 'anotherField' => [
182 'config' => [
183 'type' => 'group',
184 'items' => [
185 0 => [
186 'foo',
187 'bar',
188 ],
189 ],
190 ],
191 ],
192 ],
193 ],
194 ];
195
196 $expected = $input;
197 self::assertSame($expected, (new TcaSelectItems)->addData($input));
198 }
199
200 /**
201 * @test
202 */
203 public function addDataThrowsExceptionIfAnItemIsNotAnArray()
204 {
205 $input = [
206 'tableName' => 'aTable',
207 'processedTca' => [
208 'columns' => [
209 'aField' => [
210 'config' => [
211 'type' => 'select',
212 'renderType' => 'selectSingle',
213 'items' => [
214 0 => 'foo',
215 ],
216 ],
217 ],
218 ],
219 ],
220 ];
221
222 $this->expectException(\UnexpectedValueException::class);
223 $this->expectExceptionCode(1439288036);
224
225 (new TcaSelectItems)->addData($input);
226 }
227
228 /**
229 * @test
230 */
231 public function addDataTranslatesItemLabels()
232 {
233 $input = [
234 'tableName' => 'aTable',
235 'databaseRow' => [
236 'aField' => 'aValue',
237 ],
238 'processedTca' => [
239 'columns' => [
240 'aField' => [
241 'config' => [
242 'type' => 'select',
243 'renderType' => 'selectSingle',
244 'items' => [
245 0 => [
246 0 => 'aLabel',
247 1 => 'aValue',
248 ],
249 ],
250 'maxitems' => 99999,
251 ],
252 ],
253 ],
254 ],
255 ];
256
257 /** @var LanguageService|ObjectProphecy $languageService */
258 $languageService = $this->prophesize(LanguageService::class);
259 $GLOBALS['LANG'] = $languageService->reveal();
260 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue')->willReturn('INVALID VALUE "%s"');
261
262 $languageService->sL('aLabel')->shouldBeCalled()->willReturn('translated');
263
264 $expected = $input;
265 $expected['processedTca']['columns']['aField']['config']['items'][0][0] = 'translated';
266 $expected['processedTca']['columns']['aField']['config']['items'][0][2] = null;
267 $expected['processedTca']['columns']['aField']['config']['items'][0][3] = null;
268
269 $expected['databaseRow']['aField'] = ['aValue'];
270
271 self::assertSame($expected, (new TcaSelectItems)->addData($input));
272 }
273
274 /**
275 * @test
276 */
277 public function addDataKeepsIconFromItem()
278 {
279 $input = [
280 'tableName' => 'aTable',
281 'databaseRow' => [
282 'aField' => 'aValue',
283 ],
284 'processedTca' => [
285 'columns' => [
286 'aField' => [
287 'config' => [
288 'type' => 'select',
289 'renderType' => 'selectSingle',
290 'items' => [
291 0 => [
292 0 => 'aLabel',
293 1 => 'aValue',
294 2 => 'an-icon-reference',
295 3 => null,
296 ],
297 ],
298 'maxitems' => 99999,
299 ],
300 ],
301 ],
302 ],
303 ];
304
305 $expected = $input;
306 $expected['databaseRow']['aField'] = ['aValue'];
307
308 self::assertSame($expected, (new TcaSelectItems)->addData($input));
309 }
310
311 /**
312 * @test
313 */
314 public function addDataThrowsExceptionWithUnknownSpecialValue()
315 {
316 $input = [
317 'tableName' => 'aTable',
318 'processedTca' => [
319 'columns' => [
320 'aField' => [
321 'config' => [
322 'type' => 'select',
323 'renderType' => 'selectSingle',
324 'special' => 'anUnknownValue',
325 ],
326 ],
327 ],
328 ],
329 ];
330
331 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
332 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
333
334 $this->expectException(\UnexpectedValueException::class);
335 $this->expectExceptionCode(1439298496);
336
337 (new TcaSelectItems)->addData($input);
338 }
339
340 /**
341 * @test
342 */
343 public function addDataAddsTablesWithSpecialTables()
344 {
345 $input = [
346 'databaseRow' => [
347 'aField' => '',
348 ],
349 'tableName' => 'aTable',
350 'processedTca' => [
351 'columns' => [
352 'aField' => [
353 'config' => [
354 'type' => 'select',
355 'renderType' => 'selectSingle',
356 'special' => 'tables',
357 'maxitems' => 99999,
358 ],
359 ],
360 ],
361 ],
362 ];
363 $GLOBALS['TCA'] = [
364 'notInResult' => [
365 'ctrl' => [
366 'adminOnly' => true,
367 ],
368 ],
369 'aTable' => [
370 'ctrl' => [
371 'title' => 'aTitle',
372 ],
373 ],
374 ];
375 $GLOBALS['TCA_DESCR']['aTable']['columns']['']['description'] = 'aDescription';
376
377 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
378 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
379
380 /** @var LanguageService|ObjectProphecy $languageService */
381 $languageService = $this->prophesize(LanguageService::class);
382 $GLOBALS['LANG'] = $languageService->reveal();
383 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue')->willReturn('INVALID VALUE "%s"');
384 $languageService->sL(Argument::containingString('INVALID VALUE'))->willReturnArgument(0);
385
386 $languageService->sL('aTitle')->shouldBeCalled()->willReturnArgument(0);
387 $languageService->loadSingleTableDescription('aTable')->shouldBeCalled();
388
389 $expected = $input;
390 $expected['databaseRow']['aField'] = [];
391 $expected['processedTca']['columns']['aField']['config']['items'] = [
392 0 => [
393 0 => 'aTitle',
394 1 => 'aTable',
395 2 => null,
396 3 => [
397 'description' => 'aDescription',
398 ],
399 ]
400 ];
401
402 self::assertSame($expected, (new TcaSelectItems)->addData($input));
403 }
404
405 /**
406 * @test
407 */
408 public function addDataAddsTablesWithSpecialPageTypes()
409 {
410 $input = [
411 'databaseRow' => [
412 'aField' => 'aValue',
413 ],
414 'tableName' => 'aTable',
415 'processedTca' => [
416 'columns' => [
417 'aField' => [
418 'config' => [
419 'type' => 'select',
420 'renderType' => 'selectSingle',
421 'special' => 'pagetypes',
422 'items' => [],
423 'maxitems' => 99999,
424 ],
425 ],
426 ],
427 ],
428 ];
429 $GLOBALS['TCA'] = [
430 'pages' => [
431 'columns' => [
432 'doktype' => [
433 'config' => [
434 'items' => [
435 0 => [
436 0 => 'aLabel',
437 1 => 'aValue',
438 ],
439 ],
440 ],
441 ],
442 ],
443 ],
444 ];
445
446 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
447 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
448
449 /** @var LanguageService|ObjectProphecy $languageService */
450 $languageService = $this->prophesize(LanguageService::class);
451 $GLOBALS['LANG'] = $languageService->reveal();
452 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue')->willReturn('INVALID VALUE "%s"');
453
454 $languageService->sL('aLabel')->shouldBeCalled()->willReturnArgument(0);
455
456 $expected = $input;
457 $expected['databaseRow']['aField'] = ['aValue'];
458 $expected['processedTca']['columns']['aField']['config']['items'] = [
459 0 => [
460 0 => 'aLabel',
461 1 => 'aValue',
462 2 => null,
463 3 => null,
464 ]
465 ];
466
467 self::assertSame($expected, (new TcaSelectItems)->addData($input));
468 }
469
470 /**
471 * Data provider
472 */
473 public function addDataAddsExcludeFieldsWithSpecialExcludeDataProvider()
474 {
475 return [
476 'Table with exclude and non exclude field returns exclude item' => [
477 [
478 // input tca
479 'fooTable' => [
480 'ctrl' => [
481 'title' => 'fooTableTitle',
482 ],
483 'columns' => [
484 'bar' => [
485 'label' => 'barColumnTitle',
486 'exclude' => 1
487 ],
488 'baz' => [
489 'label' => 'bazColumnTitle',
490 ],
491 ],
492 ],
493 ],
494 [
495 // expected items
496 0 => [
497 0 => 'fooTableTitle',
498 1 => '--div--',
499 2 => null,
500 3 => null,
501 ],
502 1 => [
503 0 => 'barColumnTitle (bar)',
504 1 => 'fooTable:bar',
505 2 => 'empty-empty',
506 3 => null,
507 ],
508 ],
509 ],
510 'Root level table with ignored root level restriction returns exclude item' => [
511 [
512 // input tca
513 'fooTable' => [
514 'ctrl' => [
515 'title' => 'fooTableTitle',
516 'rootLevel' => 1,
517 'security' => [
518 'ignoreRootLevelRestriction' => true,
519 ],
520 ],
521 'columns' => [
522 'bar' => [
523 'label' => 'barColumnTitle',
524 'exclude' => true,
525 ],
526 ],
527 ],
528 ],
529 [
530 // expected items
531 0 => [
532 0 => 'fooTableTitle',
533 1 => '--div--',
534 2 => null,
535 3 => null,
536 ],
537 1 => [
538 0 => 'barColumnTitle (bar)',
539 1 => 'fooTable:bar',
540 2 => 'empty-empty',
541 3 => null,
542 ],
543 ],
544 ],
545 'Root level table without ignored root level restriction returns no item' => [
546 [
547 // input tca
548 'fooTable' => [
549 'ctrl' => [
550 'title' => 'fooTableTitle',
551 'rootLevel' => 1,
552 ],
553 'columns' => [
554 'bar' => [
555 'label' => 'barColumnTitle',
556 'exclude' => true,
557 ],
558 ],
559 ],
560 ],
561 [
562 // no items
563 ],
564 ],
565 'Admin table returns no item' => [
566 [
567 // input tca
568 'fooTable' => [
569 'ctrl' => [
570 'title' => 'fooTableTitle',
571 'adminOnly' => true,
572 ],
573 'columns' => [
574 'bar' => [
575 'label' => 'barColumnTitle',
576 'exclude' => true,
577 ],
578 ],
579 ],
580 ],
581 [
582 // no items
583 ],
584 ],
585 ];
586 }
587
588 /**
589 * @test
590 * @dataProvider addDataAddsExcludeFieldsWithSpecialExcludeDataProvider
591 */
592 public function addDataAddsExcludeFieldsWithSpecialExclude($tca, $expectedItems)
593 {
594 $input = [
595 'tableName' => 'aTable',
596 'databaseRow' => [],
597 'processedTca' => [
598 'columns' => [
599 'aField' => [
600 'config' => [
601 'type' => 'select',
602 'renderType' => 'selectSingle',
603 'special' => 'exclude',
604 ],
605 ],
606 ],
607 ],
608 ];
609 $GLOBALS['TCA'] = $tca;
610
611 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
612 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
613
614 $result = (new TcaSelectItems)->addData($input);
615
616 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
617 }
618
619 /**
620 * @test
621 */
622 public function addDataAddsExcludeFieldsFromFlexWithSpecialExclude()
623 {
624 $input = [
625 'tableName' => 'aTable',
626 'databaseRow' => [],
627 'processedTca' => [
628 'columns' => [
629 'aField' => [
630 'config' => [
631 'type' => 'select',
632 'renderType' => 'selectSingle',
633 'special' => 'exclude',
634 ],
635 ],
636 ],
637 ],
638 ];
639
640 $GLOBALS['TCA'] = [
641 'fooTable' => [
642 'ctrl' => [
643 'title' => 'fooTableTitle',
644 ],
645 'columns' => [
646 'aFlexField' => [
647 'label' => 'aFlexFieldTitle',
648 'config' => [
649 'type' => 'flex',
650 'title' => 'title',
651 'ds' => [
652 'dummy' => '
653 <T3DataStructure>
654 <ROOT>
655 <type>array</type>
656 <el>
657 <input1>
658 <TCEforms>
659 <label>flexInputLabel</label>
660 <exclude>1</exclude>
661 <config>
662 <type>input</type>
663 <size>23</size>
664 </config>
665 </TCEforms>
666 </input1>
667 </el>
668 </ROOT>
669 </T3DataStructure>
670 ',
671 ],
672 ],
673 ],
674 ],
675 ],
676 ];
677
678 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
679 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
680
681 $expectedItems = [
682 0 => [
683 0 => 'fooTableTitle aFlexFieldTitle dummy',
684 1 => '--div--',
685 2 => null,
686 3 => null,
687 ],
688 1 => [
689 0 => 'flexInputLabel (input1)',
690 1 => 'fooTable:aFlexField;dummy;sDEF;input1',
691 2 => 'empty-empty',
692 3 => null,
693 ],
694 ];
695
696 $result = (new TcaSelectItems)->addData($input);
697
698 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
699 }
700
701 /**
702 * @test
703 */
704 public function addDataAddsExplicitAllowFieldsWithSpecialExplicitValues()
705 {
706 $input = [
707 'tableName' => 'aTable',
708 'databaseRow' => [],
709 'processedTca' => [
710 'columns' => [
711 'aField' => [
712 'config' => [
713 'type' => 'select',
714 'renderType' => 'selectSingle',
715 'special' => 'explicitValues',
716 ],
717 ],
718 ],
719 ],
720 ];
721
722 $GLOBALS['TCA'] = [
723 'fooTable' => [
724 'ctrl' => [
725 'title' => 'fooTableTitle',
726 ],
727 'columns' => [
728 'aField' => [
729 'label' => 'aFieldTitle',
730 'config' => [
731 'type' => 'select',
732 'renderType' => 'selectSingle',
733 'authMode' => 'explicitAllow',
734 'items' => [
735 0 => [
736 'anItemTitle',
737 'anItemValue',
738 ],
739 ]
740 ],
741 ],
742 ],
743 ],
744 ];
745
746 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
747 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
748
749 /** @var LanguageService|ObjectProphecy $languageService */
750 $languageService = $this->prophesize(LanguageService::class);
751 $GLOBALS['LANG'] = $languageService->reveal();
752 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.allow')->shouldBeCalled()->willReturn('allowMe');
753 $languageService->sL(Argument::cetera())->willReturnArgument(0);
754
755 $expectedItems = [
756 0 => [
757 0 => 'fooTableTitle: aFieldTitle',
758 1 => '--div--',
759 2 => null,
760 3 => null,
761 ],
762 1 => [
763 0 => '[allowMe] anItemTitle',
764 1 => 'fooTable:aField:anItemValue:ALLOW',
765 2 => 'status-status-permission-granted',
766 3 => null,
767 ],
768 ];
769
770 $result = (new TcaSelectItems)->addData($input);
771
772 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
773 }
774
775 /**
776 * @test
777 */
778 public function addDataAddsExplicitDenyFieldsWithSpecialExplicitValues()
779 {
780 $input = [
781 'tableName' => 'aTable',
782 'databaseRow' => [],
783 'processedTca' => [
784 'columns' => [
785 'aField' => [
786 'config' => [
787 'type' => 'select',
788 'renderType' => 'selectSingle',
789 'special' => 'explicitValues',
790 ],
791 ],
792 ],
793 ],
794 ];
795
796 $GLOBALS['TCA'] = [
797 'fooTable' => [
798 'ctrl' => [
799 'title' => 'fooTableTitle',
800 ],
801 'columns' => [
802 'aField' => [
803 'label' => 'aFieldTitle',
804 'config' => [
805 'type' => 'select',
806 'renderType' => 'selectSingle',
807 'authMode' => 'explicitDeny',
808 'items' => [
809 0 => [
810 'anItemTitle',
811 'anItemValue',
812 ],
813 ]
814 ],
815 ],
816 ],
817 ],
818 ];
819
820 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
821 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
822
823 /** @var LanguageService|ObjectProphecy $languageService */
824 $languageService = $this->prophesize(LanguageService::class);
825 $GLOBALS['LANG'] = $languageService->reveal();
826 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deny')->shouldBeCalled()->willReturn('denyMe');
827 $languageService->sL(Argument::cetera())->willReturnArgument(0);
828
829 $expectedItems = [
830 0 => [
831 0 => 'fooTableTitle: aFieldTitle',
832 1 => '--div--',
833 2 => null,
834 3 => null,
835 ],
836 1 => [
837 0 => '[denyMe] anItemTitle',
838 1 => 'fooTable:aField:anItemValue:DENY',
839 2 => 'status-status-permission-denied',
840 3 => null,
841 ],
842 ];
843
844 $result = (new TcaSelectItems)->addData($input);
845
846 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
847 }
848
849 /**
850 * @test
851 */
852 public function addDataAddsExplicitIndividualAllowFieldsWithSpecialExplicitValues()
853 {
854 $input = [
855 'tableName' => 'aTable',
856 'databaseRow' => [],
857 'processedTca' => [
858 'columns' => [
859 'aField' => [
860 'config' => [
861 'type' => 'select',
862 'renderType' => 'selectSingle',
863 'special' => 'explicitValues',
864 ],
865 ],
866 ],
867 ],
868 ];
869
870 $GLOBALS['TCA'] = [
871 'fooTable' => [
872 'ctrl' => [
873 'title' => 'fooTableTitle',
874 ],
875 'columns' => [
876 'aField' => [
877 'label' => 'aFieldTitle',
878 'config' => [
879 'type' => 'select',
880 'renderType' => 'selectSingle',
881 'authMode' => 'individual',
882 'items' => [
883 0 => [
884 'aItemTitle',
885 'aItemValue',
886 null,
887 null,
888 'EXPL_ALLOW',
889 ],
890 // 1 is not selectable as allow and is always allowed
891 1 => [
892 'bItemTitle',
893 'bItemValue',
894 ],
895 2 => [
896 'cItemTitle',
897 'cItemValue',
898 null,
899 null,
900 'EXPL_ALLOW',
901 ],
902 ]
903 ],
904 ],
905 ],
906 ],
907 ];
908
909 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
910 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
911
912 /** @var LanguageService|ObjectProphecy $languageService */
913 $languageService = $this->prophesize(LanguageService::class);
914 $GLOBALS['LANG'] = $languageService->reveal();
915 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.allow')->shouldBeCalled()->willReturn('allowMe');
916 $languageService->sL(Argument::cetera())->willReturnArgument(0);
917
918 $expectedItems = [
919 0 => [
920 0 => 'fooTableTitle: aFieldTitle',
921 1 => '--div--',
922 2 => null,
923 3 => null,
924 ],
925 1 => [
926 0 => '[allowMe] aItemTitle',
927 1 => 'fooTable:aField:aItemValue:ALLOW',
928 2 => 'status-status-permission-granted',
929 3 => null,
930 ],
931 2 => [
932 0 => '[allowMe] cItemTitle',
933 1 => 'fooTable:aField:cItemValue:ALLOW',
934 2 => 'status-status-permission-granted',
935 3 => null,
936 ],
937 ];
938
939 $result = (new TcaSelectItems)->addData($input);
940
941 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
942 }
943
944 /**
945 * @test
946 */
947 public function addDataAddsExplicitIndividualDenyFieldsWithSpecialExplicitValues()
948 {
949 $input = [
950 'tableName' => 'aTable',
951 'databaseRow' => [],
952 'processedTca' => [
953 'columns' => [
954 'aField' => [
955 'config' => [
956 'type' => 'select',
957 'renderType' => 'selectSingle',
958 'special' => 'explicitValues',
959 ],
960 ],
961 ],
962 ],
963 ];
964
965 $GLOBALS['TCA'] = [
966 'fooTable' => [
967 'ctrl' => [
968 'title' => 'fooTableTitle',
969 ],
970 'columns' => [
971 'aField' => [
972 'label' => 'aFieldTitle',
973 'config' => [
974 'type' => 'select',
975 'renderType' => 'selectSingle',
976 'authMode' => 'individual',
977 'items' => [
978 0 => [
979 'aItemTitle',
980 'aItemValue',
981 null,
982 null,
983 'EXPL_DENY',
984 ],
985 // 1 is not selectable as allow and is always allowed
986 1 => [
987 'bItemTitle',
988 'bItemValue',
989 ],
990 2 => [
991 'cItemTitle',
992 'cItemValue',
993 null,
994 null,
995 'EXPL_DENY',
996 ],
997 ]
998 ],
999 ],
1000 ],
1001 ],
1002 ];
1003
1004 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
1005 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
1006
1007 /** @var LanguageService|ObjectProphecy $languageService */
1008 $languageService = $this->prophesize(LanguageService::class);
1009 $GLOBALS['LANG'] = $languageService->reveal();
1010 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deny')->shouldBeCalled()->willReturn('denyMe');
1011 $languageService->sL(Argument::cetera())->willReturnArgument(0);
1012
1013 $expectedItems = [
1014 0 => [
1015 0 => 'fooTableTitle: aFieldTitle',
1016 1 => '--div--',
1017 2 => null,
1018 3 => null,
1019 ],
1020 1 => [
1021 0 => '[denyMe] aItemTitle',
1022 1 => 'fooTable:aField:aItemValue:DENY',
1023 2 => 'status-status-permission-denied',
1024 3 => null,
1025 ],
1026 2 => [
1027 0 => '[denyMe] cItemTitle',
1028 1 => 'fooTable:aField:cItemValue:DENY',
1029 2 => 'status-status-permission-denied',
1030 3 => null,
1031 ],
1032 ];
1033
1034 $result = (new TcaSelectItems)->addData($input);
1035
1036 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
1037 }
1038
1039 /**
1040 * @test
1041 */
1042 public function addDataAddsLanguagesWithSpecialLanguages()
1043 {
1044 $input = [
1045 'tableName' => 'aTable',
1046 'databaseRow' => [],
1047 'processedTca' => [
1048 'columns' => [
1049 'aField' => [
1050 'config' => [
1051 'type' => 'select',
1052 'renderType' => 'selectSingle',
1053 'special' => 'languages',
1054 ],
1055 ],
1056 ],
1057 ],
1058 ];
1059
1060 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
1061 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
1062
1063 $siteFinder = $this->prophesize(SiteFinder::class);
1064 $siteFinder->getAllSites()->willReturn([
1065 new Site('test', 13, [
1066 'base' => '/',
1067 'languages' => [
1068 [
1069 'title' => 'French',
1070 'languageId' => 13,
1071 'base' => '/fr/',
1072 'locale' => 'fr_FR',
1073 'flag' => 'aFlag.gif'
1074 ]
1075 ]
1076 ])
1077 ]);
1078 GeneralUtility::addInstance(SiteFinder::class, $siteFinder->reveal());
1079
1080 $expectedItems = [
1081 0 => [
1082 0 => 'French [Site: test]',
1083 1 => 13,
1084 2 => 'flags-aFlag.gif',
1085 3 => null,
1086 ],
1087 ];
1088
1089 $result = (new TcaSelectItems)->addData($input);
1090
1091 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
1092 }
1093
1094 /**
1095 * @test
1096 */
1097 public function addDataAddsCustomOptionsWithSpecialCustom()
1098 {
1099 $input = [
1100 'tableName' => 'aTable',
1101 'databaseRow' => [],
1102 'processedTca' => [
1103 'columns' => [
1104 'aField' => [
1105 'config' => [
1106 'type' => 'select',
1107 'renderType' => 'selectSingle',
1108 'special' => 'custom',
1109 ],
1110 ],
1111 ],
1112 ],
1113 ];
1114
1115 $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'] = [
1116 'aKey' => [
1117 'header' => 'aHeader',
1118 'items' => [
1119 'anItemKey' => [
1120 0 => 'anItemTitle',
1121 ],
1122 'anotherKey' => [
1123 0 => 'anotherTitle',
1124 1 => 'status-status-permission-denied',
1125 2 => 'aDescription',
1126 ],
1127 ],
1128 ]
1129 ];
1130
1131 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
1132 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
1133
1134 $expectedItems = [
1135 0 => [
1136 0 => 'aHeader',
1137 1 => '--div--',
1138 null,
1139 null,
1140 ],
1141 1 => [
1142 0 => 'anItemTitle',
1143 1 => 'aKey:anItemKey',
1144 2 => 'empty-empty',
1145 3 => null,
1146 ],
1147 2 => [
1148 0 => 'anotherTitle',
1149 1 => 'aKey:anotherKey',
1150 2 => 'empty-empty',
1151 3 => [ 'description' => 'aDescription' ],
1152 ],
1153 ];
1154
1155 $result = (new TcaSelectItems)->addData($input);
1156
1157 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
1158 }
1159
1160 /**
1161 * @test
1162 */
1163 public function addDataAddsGroupItemsWithSpecialModListGroup()
1164 {
1165 $input = [
1166 'tableName' => 'aTable',
1167 'databaseRow' => [],
1168 'processedTca' => [
1169 'columns' => [
1170 'aField' => [
1171 'config' => [
1172 'type' => 'select',
1173 'renderType' => 'selectSingle',
1174 'special' => 'modListGroup',
1175 ],
1176 ],
1177 ],
1178 ],
1179 ];
1180
1181 $GLOBALS['TBE_MODULES'] = [];
1182
1183 $iconFactoryProphecy = $this->prophesize(IconRegistry::class);
1184 GeneralUtility::setSingletonInstance(IconRegistry::class, $iconFactoryProphecy->reveal());
1185
1186 /** @var ModuleLoader|ObjectProphecy $moduleLoaderProphecy */
1187 $moduleLoaderProphecy = $this->prophesize(ModuleLoader::class);
1188 GeneralUtility::addInstance(ModuleLoader::class, $moduleLoaderProphecy->reveal());
1189 $moduleLoaderProphecy->load([])->shouldBeCalled();
1190 $moduleLoaderProphecy->modListGroup = [
1191 'aModule',
1192 ];
1193 $moduleLoaderProphecy->modules = [
1194 'aModule' => [
1195 'iconIdentifier' => 'empty-empty'
1196 ]
1197 ];
1198 $moduleLoaderProphecy->getLabelsForModule('aModule')->shouldBeCalled()->willReturn([
1199 'shortdescription' => 'aModuleTabLabel',
1200 'description' => 'aModuleTabDescription',
1201 'title' => 'aModuleLabel'
1202 ]);
1203
1204 $expectedItems = [
1205 0 => [
1206 0 => 'aModuleLabel',
1207 1 => 'aModule',
1208 2 => 'empty-empty',
1209 3 => [
1210 'title' => 'aModuleTabLabel',
1211 'description' => 'aModuleTabDescription',
1212 ],
1213 ],
1214 ];
1215
1216 $result = (new TcaSelectItems)->addData($input);
1217
1218 $result['processedTca']['columns']['aField']['config']['items'][0][2] = str_replace([CR, LF, "\t"], '', $result['processedTca']['columns']['aField']['config']['items'][0][2]);
1219 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
1220 }
1221
1222 /**
1223 * @test
1224 */
1225 public function addDataAddsFileItemsWithConfiguredFileFolder()
1226 {
1227 $directory = $this->getUniqueId('test-') . '/';
1228 $input = [
1229 'tableName' => 'aTable',
1230 'databaseRow' => [],
1231 'processedTca' => [
1232 'columns' => [
1233 'aField' => [
1234 'config' => [
1235 'type' => 'select',
1236 'renderType' => 'selectSingle',
1237 // use absolute path here to avoid fallback to public path as prefix
1238 'fileFolder' => Environment::getVarPath() . '/' . $directory,
1239 'fileFolder_extList' => 'gif',
1240 'fileFolder_recursions' => 1,
1241 ],
1242 ],
1243 ],
1244 ],
1245 ];
1246
1247 mkdir(Environment::getVarPath() . '/' . $directory);
1248 $this->testFilesToDelete[] = Environment::getVarPath() . '/' . $directory;
1249 touch(Environment::getVarPath() . '/' . $directory . 'anImage.gif');
1250 touch(Environment::getVarPath() . '/' . $directory . 'aFile.txt');
1251 mkdir(Environment::getVarPath() . '/' . $directory . '/subdir');
1252 touch(Environment::getVarPath() . '/' . $directory . '/subdir/anotherImage.gif');
1253
1254 $expectedItems = [
1255 0 => [
1256 0 => 'anImage.gif',
1257 1 => 'anImage.gif',
1258 2 => Environment::getVarPath() . '/' . $directory . 'anImage.gif',
1259 3 => null,
1260 ],
1261 1 => [
1262 0 => 'subdir/anotherImage.gif',
1263 1 => 'subdir/anotherImage.gif',
1264 2 => Environment::getVarPath() . '/' . $directory . 'subdir/anotherImage.gif',
1265 3 => null,
1266 ],
1267 ];
1268
1269 $result = (new TcaSelectItems)->addData($input);
1270
1271 self::assertSame($expectedItems, $result['processedTca']['columns']['aField']['config']['items']);
1272 }
1273
1274 /**
1275 * @test
1276 */
1277 public function addDataThrowsExceptionForInvalidFileFolder()
1278 {
1279 $input = [
1280 'tableName' => 'aTable',
1281 'databaseRow' => [],
1282 'processedTca' => [
1283 'columns' => [
1284 'aField' => [
1285 'config' => [
1286 'type' => 'select',
1287 'renderType' => 'selectSingle',
1288 'fileFolder' => 'EXT:non_existing/Resources/Public/',
1289 ],
1290 ],
1291 ],
1292 ],
1293 ];
1294
1295 $this->expectException(\RuntimeException::class);
1296 $this->expectExceptionCode(1479399227);
1297 (new TcaSelectItems)->addData($input);
1298 }
1299
1300 /**
1301 * @test
1302 */
1303 public function addDataAddsItemsByAddItemsFromPageTsConfig()
1304 {
1305 $input = [
1306 'databaseRow' => [
1307 'aField' => '',
1308 ],
1309 'tableName' => 'aTable',
1310 'processedTca' => [
1311 'columns' => [
1312 'aField' => [
1313 'config' => [
1314 'type' => 'select',
1315 'renderType' => 'selectSingle',
1316 'items' => [
1317 0 => [
1318 0 => 'keepMe',
1319 1 => 'keep',
1320 null,
1321 null,
1322 ],
1323 ],
1324 'maxitems' => 99999,
1325 ],
1326 ],
1327 ]
1328 ],
1329 'pageTsConfig' => [
1330 'TCEFORM.' => [
1331 'aTable.' => [
1332 'aField.' => [
1333 'addItems.' => [
1334 '1' => 'addMe'
1335 ],
1336 ],
1337 ],
1338 ],
1339 ],
1340 ];
1341
1342 $expected = $input;
1343 $expected['databaseRow']['aField'] = [];
1344 $expected['processedTca']['columns']['aField']['config']['items'][1] = [
1345 0 => 'addMe',
1346 1 => '1',
1347 null,
1348 null,
1349 ];
1350
1351 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
1352 }
1353
1354 /**
1355 * @test
1356 */
1357 public function addDataAddsItemsByAddItemsWithDuplicateValuesFromPageTsConfig()
1358 {
1359 $input = [
1360 'databaseRow' => [
1361 'aField' => '',
1362 ],
1363 'tableName' => 'aTable',
1364 'processedTca' => [
1365 'columns' => [
1366 'aField' => [
1367 'config' => [
1368 'type' => 'select',
1369 'renderType' => 'selectSingle',
1370 'items' => [
1371 0 => [
1372 0 => 'keepMe',
1373 1 => 'keep',
1374 null,
1375 null,
1376 ],
1377 ],
1378 'maxitems' => 99999,
1379 ],
1380 ],
1381 ]
1382 ],
1383 'pageTsConfig' => [
1384 'TCEFORM.' => [
1385 'aTable.' => [
1386 'aField.' => [
1387 'addItems.' => [
1388 'keep' => 'addMe'
1389 ],
1390 ],
1391 ],
1392 ],
1393 ],
1394 ];
1395
1396 $expected = $input;
1397 $expected['databaseRow']['aField'] = [];
1398 $expected['processedTca']['columns']['aField']['config']['items'][1] = [
1399 0 => 'addMe',
1400 1 => 'keep',
1401 null,
1402 null,
1403 ];
1404
1405 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
1406 }
1407
1408 /**
1409 * Data provider
1410 */
1411 public function addDataReplacesMarkersInForeignTableClauseDataProvider()
1412 {
1413 return [
1414 'replace REC_FIELD' => [
1415 'AND fTable.title=\'###REC_FIELD_rowField###\'',
1416 [
1417 ['fTable.title=\'rowFieldValue\''],
1418 [' 1=1'],
1419 ['`pages.uid` = `fTable.pid`']
1420 ],
1421 [],
1422 ],
1423 'replace REC_FIELD within FlexForm' => [
1424 'AND fTable.title=###REC_FIELD_rowFieldFlexForm###',
1425 [
1426 ['fTable.title=\'rowFieldFlexFormValue\''],
1427 [' 1=1'],
1428 ['`pages.uid` = `fTable.pid`']
1429 ],
1430 [
1431 'databaseRow' => [
1432 'rowFieldThree' => [
1433 0 => 'rowFieldThreeValue'
1434 ]
1435 ],
1436 'flexParentDatabaseRow' => [
1437 'rowFieldFlexForm' => [
1438 0 => 'rowFieldFlexFormValue'
1439 ]
1440 ],
1441 ],
1442 ],
1443 'replace REC_FIELD fullQuote' => [
1444 'AND fTable.title=###REC_FIELD_rowField###',
1445 [
1446 ['fTable.title=\'rowFieldValue\''],
1447 [' 1=1'],
1448 ['`pages.uid` = `fTable.pid`']
1449 ],
1450 [],
1451 ],
1452 'replace REC_FIELD fullQuoteWithArray' => [
1453 'AND fTable.title=###REC_FIELD_rowFieldThree###',
1454 [
1455 ['fTable.title=\'rowFieldThreeValue\''],
1456 [' 1=1'],
1457 ['`pages.uid` = `fTable.pid`']
1458 ],
1459 [
1460 'databaseRow' => [
1461 'rowFieldThree' => [
1462 0 => 'rowFieldThreeValue'
1463 ]
1464 ],
1465 ],
1466 ],
1467 'replace REC_FIELD multiple markers' => [
1468 'AND fTable.title=\'###REC_FIELD_rowField###\' AND fTable.pid=###REC_FIELD_rowFieldTwo###',
1469 [
1470 ['fTable.title=\'rowFieldValue\' AND fTable.pid=\'rowFieldTwoValue\''],
1471 [' 1=1'],
1472 ['`pages.uid` = `fTable.pid`']
1473 ],
1474 [],
1475 ],
1476 'replace CURRENT_PID' => [
1477 'AND fTable.uid=###CURRENT_PID###',
1478 [
1479 ['fTable.uid=43'],
1480 [' 1=1'],
1481 ['`pages.uid` = `fTable.pid`']
1482 ],
1483 [],
1484 ],
1485 'replace CURRENT_PID within FlexForm' => [
1486 'AND fTable.uid=###CURRENT_PID###',
1487 [
1488 ['fTable.uid=77'],
1489 [' 1=1'],
1490 ['`pages.uid` = `fTable.pid`']
1491 ],
1492 [
1493 'flexParentDatabaseRow' => [
1494 'pid' => '77',
1495 ],
1496 ],
1497 ],
1498 'replace CURRENT_PID integer cast' => [
1499 'AND fTable.uid=###CURRENT_PID###',
1500 [
1501 ['fTable.uid=431'],
1502 [' 1=1'],
1503 ['`pages.uid` = `fTable.pid`']
1504 ],
1505 [
1506 'effectivePid' => '431string',
1507 ],
1508 ],
1509 'replace THIS_UID' => [
1510 'AND fTable.uid=###THIS_UID###',
1511 [
1512 ['fTable.uid=42'],
1513 [' 1=1'],
1514 ['`pages.uid` = `fTable.pid`']
1515 ],
1516 [],
1517 ],
1518 'replace THIS_UID integer cast' => [
1519 'AND fTable.uid=###THIS_UID###',
1520 [
1521 ['fTable.uid=421'],
1522 [' 1=1'],
1523 ['`pages.uid` = `fTable.pid`']
1524 ],
1525 [
1526 'databaseRow' => [
1527 'uid' => '421string',
1528 ],
1529 ],
1530 ],
1531 'replace SITEROOT' => [
1532 'AND fTable.uid=###SITEROOT###',
1533 [
1534 ['fTable.uid=44'],
1535 [' 1=1'],
1536 ['`pages.uid` = `fTable.pid`']
1537 ],
1538 [],
1539 ],
1540 'replace SITEROOT integer cast' => [
1541 'AND fTable.uid=###SITEROOT###',
1542 [
1543 ['fTable.uid=441'],
1544 [' 1=1'],
1545 ['`pages.uid` = `fTable.pid`']
1546 ],
1547 [
1548 'rootline' => [
1549 1 => [
1550 'uid' => '441string',
1551 ],
1552 ],
1553 ],
1554 ],
1555 'replace PAGE_TSCONFIG_ID' => [
1556 'AND fTable.uid=###PAGE_TSCONFIG_ID###',
1557 [
1558 ['fTable.uid=45'],
1559 [' 1=1'],
1560 ['`pages.uid` = `fTable.pid`']
1561 ],
1562 [
1563 'pageTsConfig' => [
1564 'TCEFORM.' => [
1565 'aTable.' => [
1566 'aField.' => [
1567 'PAGE_TSCONFIG_ID' => '45',
1568 ],
1569 ],
1570 ],
1571 ],
1572 ],
1573 ],
1574 'replace PAGE_TSCONFIG_ID integer cast' => [
1575 'AND fTable.uid=###PAGE_TSCONFIG_ID###',
1576 [
1577 ['fTable.uid=451'],
1578 [' 1=1'],
1579 ['`pages.uid` = `fTable.pid`']
1580 ],
1581 [
1582 'pageTsConfig' => [
1583 'TCEFORM.' => [
1584 'aTable.' => [
1585 'aField.' => [
1586 'PAGE_TSCONFIG_ID' => '451string'
1587 ],
1588 ],
1589 ],
1590 ],
1591 ],
1592 ],
1593 'replace PAGE_TSCONFIG_STR' => [
1594 'AND fTable.uid=\'###PAGE_TSCONFIG_STR###\'',
1595 [
1596 ['fTable.uid=\'46\''],
1597 [' 1=1'],
1598 ['`pages.uid` = `fTable.pid`']
1599 ],
1600 [
1601 'pageTsConfig' => [
1602 'TCEFORM.' => [
1603 'aTable.' => [
1604 'aField.' => [
1605 'PAGE_TSCONFIG_STR' => '46',
1606 ],
1607 ],
1608 ],
1609 ],
1610 ],
1611 ],
1612 'replace PAGE_TSCONFIG_IDLIST' => [
1613 'AND fTable.uid IN (###PAGE_TSCONFIG_IDLIST###)',
1614 [
1615 ['fTable.uid IN (47,48)'],
1616 [' 1=1'],
1617 ['`pages.uid` = `fTable.pid`']
1618 ],
1619 [
1620 'pageTsConfig' => [
1621 'TCEFORM.' => [
1622 'aTable.' => [
1623 'aField.' => [
1624 'PAGE_TSCONFIG_IDLIST' => '47,48',
1625 ],
1626 ],
1627 ],
1628 ],
1629 ],
1630 ],
1631 'replace PAGE_TSCONFIG_IDLIST cleans list' => [
1632 'AND fTable.uid IN (###PAGE_TSCONFIG_IDLIST###)',
1633 [
1634 ['fTable.uid IN (471,481)'],
1635 [' 1=1'],
1636 ['`pages.uid` = `fTable.pid`']
1637 ],
1638 [
1639 'pageTsConfig' => [
1640 'TCEFORM.' => [
1641 'aTable.' => [
1642 'aField.' => [
1643 'PAGE_TSCONFIG_IDLIST' => 'a, 471, b, 481, c',
1644 ],
1645 ],
1646 ],
1647 ],
1648 ],
1649 ],
1650 ];
1651 }
1652
1653 /**
1654 * @test
1655 * @dataProvider addDataReplacesMarkersInForeignTableClauseDataProvider
1656 */
1657 public function addDataReplacesMarkersInForeignTableClause($foreignTableWhere, $expectedWhere, array $inputOverride)
1658 {
1659 $input = [
1660 'tableName' => 'aTable',
1661 'effectivePid' => 43,
1662 'databaseRow' => [
1663 'uid' => 42,
1664 'rowField' => 'rowFieldValue',
1665 'rowFieldTwo' => 'rowFieldTwoValue',
1666 ],
1667 'processedTca' => [
1668 'columns' => [
1669 'aField' => [
1670 'config' => [
1671 'type' => 'select',
1672 'renderType' => 'selectSingle',
1673 'foreign_table' => 'fTable',
1674 'foreign_table_where' => $foreignTableWhere,
1675 ],
1676 ],
1677 ]
1678 ],
1679 'rootline' => [
1680 2 => [
1681 'uid' => 999,
1682 'is_siteroot' => 0,
1683 ],
1684 1 => [
1685 'uid' => 44,
1686 'is_siteroot' => 1,
1687 ],
1688 0 => [
1689 'uid' => 0,
1690 'is_siteroot' => null,
1691 ],
1692 ],
1693 'pageTsConfig' => [],
1694 ];
1695 ArrayUtility::mergeRecursiveWithOverrule($input, $inputOverride);
1696
1697 $GLOBALS['TCA']['fTable'] = [];
1698
1699 $fileRepositoryProphecy = $this->prophesize(FileRepository::class);
1700 $fileRepositoryProphecy->findByRelation(Argument::cetera())->shouldNotBeCalled();
1701 GeneralUtility::setSingletonInstance(FileRepository::class, $fileRepositoryProphecy->reveal());
1702
1703 list($queryBuilderProphet, $connectionPoolProphet) = $this->mockDatabaseConnection();
1704
1705 /** @var Statement|ObjectProphecy $statementProphet */
1706 $statementProphet = $this->prophesize(Statement::class);
1707
1708 $queryBuilderProphet->select('fTable.uid')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1709 $queryBuilderProphet->from('fTable')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1710 $queryBuilderProphet->from('pages')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1711 $queryBuilderProphet->where(...array_shift($expectedWhere))->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1712 $queryBuilderProphet->execute()->shouldBeCalled()->willReturn($statementProphet->reveal());
1713
1714 while ($constraint = array_shift($expectedWhere)) {
1715 $queryBuilderProphet->andWhere(...$constraint)
1716 ->shouldBeCalled()
1717 ->willReturn($queryBuilderProphet->reveal());
1718 }
1719
1720 // Two instances are needed due to the push/pop behavior of addInstance()
1721 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1722 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1723
1724 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
1725 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
1726 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
1727 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
1728
1729 (new TcaSelectItems)->addData($input);
1730 }
1731
1732 /**
1733 * @test
1734 */
1735 public function addDataThrowsExceptionIfForeignTableIsNotDefinedInTca()
1736 {
1737 $input = [
1738 'tableName' => 'aTable',
1739 'processedTca' => [
1740 'columns' => [
1741 'aField' => [
1742 'config' => [
1743 'type' => 'select',
1744 'renderType' => 'selectSingle',
1745 'foreign_table' => 'fTable',
1746 ],
1747 ],
1748 ]
1749 ],
1750 ];
1751
1752 $this->expectException(\UnexpectedValueException::class);
1753 $this->expectExceptionCode(1439569743);
1754
1755 (new TcaSelectItems)->addData($input);
1756 }
1757
1758 /**
1759 * @test
1760 */
1761 public function addDataForeignTableSplitsGroupOrderAndLimit()
1762 {
1763 $input = [
1764 'tableName' => 'aTable',
1765 'effectivePid' => 42,
1766 'databaseRow' => [
1767 'uid' => 23,
1768 ],
1769 'processedTca' => [
1770 'columns' => [
1771 'aField' => [
1772 'config' => [
1773 'type' => 'select',
1774 'renderType' => 'selectSingle',
1775 'foreign_table' => 'fTable',
1776 'foreign_table_where' => '
1777 AND ftable.uid=1
1778 GROUP BY groupField1, groupField2
1779 ORDER BY orderField
1780 LIMIT 1,2',
1781 ],
1782 ],
1783 ]
1784 ],
1785 'rootline' => [],
1786 ];
1787
1788 $GLOBALS['TCA']['fTable'] = [];
1789
1790 $fileRepositoryProphecy = $this->prophesize(FileRepository::class);
1791 $fileRepositoryProphecy->findByRelation(Argument::cetera())->shouldNotBeCalled();
1792 GeneralUtility::setSingletonInstance(FileRepository::class, $fileRepositoryProphecy->reveal());
1793
1794 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
1795 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
1796 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
1797 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
1798
1799 list($queryBuilderProphet, $connectionPoolProphet) = $this->mockDatabaseConnection();
1800
1801 /** @var Statement|ObjectProphecy $statementProphet */
1802 $statementProphet = $this->prophesize(Statement::class);
1803
1804 $queryBuilderProphet->select('fTable.uid')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1805 $queryBuilderProphet->from('fTable')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1806 $queryBuilderProphet->from('pages')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1807 $queryBuilderProphet->groupBy('groupField1', 'groupField2')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1808 $queryBuilderProphet->addOrderBy('orderField', null)->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1809 $queryBuilderProphet->setFirstResult(1)->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1810 $queryBuilderProphet->setMaxResults(2)->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1811 $queryBuilderProphet->where('ftable.uid=1')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1812 $queryBuilderProphet->andWhere(' 1=1')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1813 $queryBuilderProphet->andWhere('`pages.uid` = `fTable.pid`')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1814 $queryBuilderProphet->execute()->shouldBeCalled()->willReturn($statementProphet->reveal());
1815
1816 // Two instances are needed due to the push/pop behavior of addInstance()
1817 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1818 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1819
1820 (new TcaSelectItems)->addData($input);
1821 }
1822
1823 /**
1824 * @test
1825 */
1826 public function addDataForeignTableQueuesFlashMessageOnDatabaseError()
1827 {
1828 $input = [
1829 'databaseRow' => [
1830 'uid' => 23,
1831 'aField' => '',
1832 ],
1833 'tableName' => 'aTable',
1834 'effectivePid' => 42,
1835 'processedTca' => [
1836 'columns' => [
1837 'aField' => [
1838 'config' => [
1839 'type' => 'select',
1840 'renderType' => 'selectSingle',
1841 'foreign_table' => 'fTable',
1842 'items' => [
1843 0 => [
1844 0 => 'itemLabel',
1845 1 => 'itemValue',
1846 2 => null,
1847 3 => null,
1848 ],
1849 ],
1850 'maxitems' => 99999,
1851 ],
1852 ],
1853 ]
1854 ],
1855 'rootline' => [],
1856 ];
1857
1858 $GLOBALS['TCA']['fTable'] = [];
1859
1860 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
1861 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
1862 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
1863 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
1864
1865 list($queryBuilderProphet, $connectionPoolProphet) = $this->mockDatabaseConnection();
1866
1867 /** @var Statement|ObjectProphecy $statementProphet */
1868 $statementProphet = $this->prophesize(Statement::class);
1869
1870 $queryBuilderProphet->select('fTable.uid')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1871 $queryBuilderProphet->from('fTable')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1872 $queryBuilderProphet->from('pages')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1873 $queryBuilderProphet->where('')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1874 $queryBuilderProphet->andWhere(' 1=1')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1875 $queryBuilderProphet->andWhere('`pages.uid` = `fTable.pid`')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1876
1877 $prevException = new DBALException('Invalid table name', 1476045274);
1878 $exception = new DBALException('Driver error', 1476045971, $prevException);
1879
1880 $queryBuilderProphet->execute()->shouldBeCalled()->willThrow($exception);
1881
1882 // Two instances are needed due to the push/pop behavior of addInstance()
1883 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1884 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1885
1886 /** @var FlashMessage|ObjectProphecy $flashMessage */
1887 $flashMessage = $this->prophesize(FlashMessage::class);
1888 GeneralUtility::addInstance(FlashMessage::class, $flashMessage->reveal());
1889 /** @var FlashMessageService|ObjectProphecy $flashMessageService */
1890 $flashMessageService = $this->prophesize(FlashMessageService::class);
1891 GeneralUtility::setSingletonInstance(FlashMessageService::class, $flashMessageService->reveal());
1892 /** @var FlashMessageQueue|ObjectProphecy $flashMessageQueue */
1893 $flashMessageQueue = $this->prophesize(FlashMessageQueue::class);
1894 $flashMessageService->getMessageQueueByIdentifier(Argument::cetera())->willReturn($flashMessageQueue->reveal());
1895
1896 $flashMessageQueue->enqueue($flashMessage)->shouldBeCalled();
1897
1898 $expected = $input;
1899 $expected['databaseRow']['aField'] = [];
1900
1901 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
1902 }
1903
1904 /**
1905 * @test
1906 */
1907 public function addDataForeignTableHandlesForeignTableRows()
1908 {
1909 $input = [
1910 'databaseRow' => [
1911 'uid' => 5,
1912 'aField' => '',
1913 ],
1914 'tableName' => 'aTable',
1915 'effectivePid' => 42,
1916 'processedTca' => [
1917 'columns' => [
1918 'aField' => [
1919 'config' => [
1920 'type' => 'select',
1921 'renderType' => 'selectSingle',
1922 'foreign_table' => 'fTable',
1923 'foreign_table_prefix' => 'aPrefix',
1924 'items' => [],
1925 'maxitems' => 99999,
1926 ],
1927 ],
1928 ]
1929 ],
1930 'rootline' => [],
1931 ];
1932
1933 $GLOBALS['TCA']['fTable'] = [
1934 'ctrl' => [
1935 'label' => 'labelField',
1936 ],
1937 'columns' => [],
1938 ];
1939
1940 $fileRepositoryProphecy = $this->prophesize(FileRepository::class);
1941 $fileRepositoryProphecy->findByRelation(Argument::cetera())->shouldNotBeCalled();
1942 GeneralUtility::setSingletonInstance(FileRepository::class, $fileRepositoryProphecy->reveal());
1943
1944 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
1945 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
1946 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
1947 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
1948
1949 list($queryBuilderProphet, $connectionPoolProphet) = $this->mockDatabaseConnection();
1950
1951 /** @var Statement|ObjectProphecy $statementProphet */
1952 $statementProphet = $this->prophesize(Statement::class);
1953
1954 $queryBuilderProphet->select('fTable.uid', 'fTable.labelField')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1955 $queryBuilderProphet->from('fTable')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1956 $queryBuilderProphet->from('pages')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1957 $queryBuilderProphet->where('')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1958 $queryBuilderProphet->andWhere(' 1=1')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1959 $queryBuilderProphet->andWhere('`pages.uid` = `fTable.pid`')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
1960 $queryBuilderProphet->execute()->shouldBeCalled()->willReturn($statementProphet->reveal());
1961
1962 // Two instances are needed due to the push/pop behavior of addInstance()
1963 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1964 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
1965
1966 $counter = 0;
1967 $statementProphet->fetch()->shouldBeCalled()->will(function ($args) use (&$counter) {
1968 $counter++;
1969 if ($counter >= 3) {
1970 return false;
1971 }
1972 return [
1973 'uid' => $counter,
1974 'pid' => 23,
1975 'labelField' => 'aLabel',
1976 'aValue' => 'bar,',
1977 ];
1978 });
1979
1980 $expected = $input;
1981 $expected['processedTca']['columns']['aField']['config']['items'] = [
1982 0 => [
1983 0 => 'aPrefix[LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title]',
1984 1 => 1,
1985 2 => null,
1986 3 => null,
1987 ],
1988 1 => [
1989 0 => 'aPrefix[LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title]',
1990 1 => 2,
1991 2 => null,
1992 3 => null,
1993 ],
1994 ];
1995
1996 $expected['databaseRow']['aField'] = [];
1997
1998 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
1999 }
2000
2001 /**
2002 * @test
2003 */
2004 public function addDataForeignTableResolvesIconFromSelicon()
2005 {
2006 $input = [
2007 'databaseRow' => [
2008 'uid' => 5,
2009 'aField' => '',
2010 ],
2011 'tableName' => 'aTable',
2012 'effectivePid' => 42,
2013 'processedTca' => [
2014 'columns' => [
2015 'aField' => [
2016 'config' => [
2017 'type' => 'select',
2018 'renderType' => 'selectSingle',
2019 'foreign_table' => 'fTable',
2020 'maxitems' => 99999,
2021 ],
2022 ],
2023 ]
2024 ],
2025 'rootline' => [],
2026 ];
2027
2028 // Fake the foreign_table
2029 $GLOBALS['TCA']['fTable'] = [
2030 'ctrl' => [
2031 'label' => 'icon',
2032 'selicon_field' => 'icon',
2033 ],
2034 'columns' =>[
2035 'icon' => [],
2036 ],
2037 ];
2038
2039 $fileRepositoryProphecy = $this->prophesize(FileRepository::class);
2040 $fileRepositoryProphecy->findByRelation(Argument::cetera())->shouldNotBeCalled();
2041 GeneralUtility::setSingletonInstance(FileRepository::class, $fileRepositoryProphecy->reveal());
2042
2043 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2044 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2045 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2046 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
2047
2048 list($queryBuilderProphet, $connectionPoolProphet) = $this->mockDatabaseConnection();
2049
2050 /** @var Statement|ObjectProphecy $statementProphet */
2051 $statementProphet = $this->prophesize(Statement::class);
2052
2053 $queryBuilderProphet->select('fTable.uid', 'fTable.icon')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
2054 $queryBuilderProphet->from('fTable')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
2055 $queryBuilderProphet->from('pages')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
2056 $queryBuilderProphet->where('')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
2057 $queryBuilderProphet->andWhere(' 1=1')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
2058 $queryBuilderProphet->andWhere('`pages.uid` = `fTable.pid`')->shouldBeCalled()->willReturn($queryBuilderProphet->reveal());
2059 $queryBuilderProphet->execute()->shouldBeCalled()->willReturn($statementProphet->reveal());
2060
2061 // Two instances are needed due to the push/pop behavior of addInstance()
2062 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
2063 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
2064
2065 // Query returns one row, then false on second call
2066 $foreignTableRowResultOne = [
2067 'uid' => 1,
2068 'pid' => 23,
2069 'icon' => 'foo.jpg',
2070 ];
2071 $statementProphet->fetch()->shouldBeCalled()->willReturn($foreignTableRowResultOne, false);
2072
2073 $expected = $input;
2074 $expected['processedTca']['columns']['aField']['config']['items'] = [
2075 0 => [
2076 0 => 'foo.jpg',
2077 1 => 1,
2078 2 => null,
2079 3 => null,
2080 ],
2081 ];
2082 $expected['databaseRow']['aField'] = [];
2083
2084 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2085 }
2086
2087 /**
2088 * @test
2089 */
2090 public function addDataRemovesItemsByKeepItemsPageTsConfig()
2091 {
2092 $input = [
2093 'databaseRow' => [
2094 'aField' => '',
2095 ],
2096 'tableName' => 'aTable',
2097 'processedTca' => [
2098 'columns' => [
2099 'aField' => [
2100 'config' => [
2101 'type' => 'select',
2102 'renderType' => 'selectSingle',
2103 'items' => [
2104 0 => [
2105 0 => 'keepMe',
2106 1 => 'keep',
2107 null,
2108 null,
2109 ],
2110 1 => [
2111 0 => 'removeMe',
2112 1 => 'remove',
2113 ],
2114 2 => [
2115 0 => 'removeMe',
2116 1 => 0,
2117 ],
2118 ],
2119 'maxitems' => 99999,
2120 ],
2121 ],
2122 ]
2123 ],
2124 'pageTsConfig' => [
2125 'TCEFORM.' => [
2126 'aTable.' => [
2127 'aField.' => [
2128 'keepItems' => 'keep',
2129 ],
2130 ],
2131 ],
2132 ],
2133 ];
2134
2135 $expected = $input;
2136 $expected['databaseRow']['aField'] = [];
2137 unset(
2138 $expected['processedTca']['columns']['aField']['config']['items'][1],
2139 $expected['processedTca']['columns']['aField']['config']['items'][2]
2140 );
2141
2142 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2143 }
2144
2145 /**
2146 * @test
2147 */
2148 public function addDataRemovesAllItemsByEmptyKeepItemsPageTsConfig()
2149 {
2150 $input = [
2151 'databaseRow' => [
2152 'aField' => '',
2153 ],
2154 'tableName' => 'aTable',
2155 'processedTca' => [
2156 'columns' => [
2157 'aField' => [
2158 'config' => [
2159 'type' => 'select',
2160 'renderType' => 'selectSingle',
2161 'items' => [
2162 0 => [
2163 0 => 'keepMe',
2164 1 => 'keep',
2165 null,
2166 null,
2167 ],
2168 1 => [
2169 0 => 'removeMe',
2170 1 => 'remove',
2171 ],
2172 ],
2173 'maxitems' => 99999,
2174 ],
2175 ],
2176 ]
2177 ],
2178 'pageTsConfig' => [
2179 'TCEFORM.' => [
2180 'aTable.' => [
2181 'aField.' => [
2182 'keepItems' => '',
2183 ],
2184 ],
2185 ],
2186 ],
2187 ];
2188
2189 $expected = $input;
2190 $expected['databaseRow']['aField'] = [];
2191 $expected['processedTca']['columns']['aField']['config']['items'] = [];
2192
2193 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2194 }
2195
2196 /**
2197 * @test
2198 */
2199 public function addDataEvaluatesKeepItemsBeforeAddItemsFromPageTsConfig()
2200 {
2201 $input = [
2202 'databaseRow' => [
2203 'aField' => '',
2204 ],
2205 'tableName' => 'aTable',
2206 'processedTca' => [
2207 'columns' => [
2208 'aField' => [
2209 'config' => [
2210 'type' => 'select',
2211 'renderType' => 'selectSingle',
2212 'items' => [
2213 0 => [
2214 0 => 'keepMe',
2215 1 => '1',
2216 null,
2217 null,
2218 ],
2219 1 => [
2220 0 => 'removeMe',
2221 1 => 'remove',
2222 ],
2223 ],
2224 'maxitems' => 99999,
2225 ],
2226 ],
2227 ]
2228 ],
2229 'pageTsConfig' => [
2230 'TCEFORM.' => [
2231 'aTable.' => [
2232 'aField.' => [
2233 'keepItems' => '1',
2234 'addItems.' => [
2235 '1' => 'addItem #1',
2236 '12' => 'addItem #12',
2237 ],
2238 ],
2239 ],
2240 ],
2241 ],
2242 ];
2243
2244 $expected = $input;
2245 $expected['databaseRow']['aField'] = [];
2246 $expected['processedTca']['columns']['aField']['config']['items'] = [
2247 0 => [
2248 0 => 'keepMe',
2249 1 => '1',
2250 null,
2251 null,
2252 ],
2253 1 => [
2254 0 => 'addItem #1',
2255 1 => '1',
2256 null,
2257 null,
2258 ],
2259 2 => [
2260 0 => 'addItem #12',
2261 1 => '12',
2262 null,
2263 null,
2264 ],
2265 ];
2266
2267 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2268 }
2269
2270 /**
2271 * @test
2272 */
2273 public function addDataRemovesItemsByRemoveItemsPageTsConfig()
2274 {
2275 $input = [
2276 'databaseRow' => [
2277 'aField' => ''
2278 ],
2279 'tableName' => 'aTable',
2280 'processedTca' => [
2281 'columns' => [
2282 'aField' => [
2283 'config' => [
2284 'type' => 'select',
2285 'renderType' => 'selectSingle',
2286 'items' => [
2287 0 => [
2288 0 => 'keepMe',
2289 1 => 'keep',
2290 null,
2291 null,
2292 ],
2293 1 => [
2294 0 => 'removeMe',
2295 1 => 'remove',
2296 ],
2297 2 => [
2298 0 => 'keep me',
2299 1 => 0,
2300 null,
2301 null,
2302 ],
2303 ],
2304 'maxitems' => 99999,
2305 ],
2306 ],
2307 ]
2308 ],
2309 'pageTsConfig' => [
2310 'TCEFORM.' => [
2311 'aTable.' => [
2312 'aField.' => [
2313 'removeItems' => 'remove',
2314 ],
2315 ],
2316 ],
2317 ],
2318 ];
2319
2320 $expected = $input;
2321 $expected['databaseRow']['aField'] = [];
2322 unset($expected['processedTca']['columns']['aField']['config']['items'][1]);
2323 $expected['processedTca']['columns']['aField']['config']['items'] = array_values($expected['processedTca']['columns']['aField']['config']['items']);
2324 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2325 }
2326
2327 /**
2328 * @test
2329 */
2330 public function addDataRemovesItemsByZeroValueRemoveItemsPageTsConfig()
2331 {
2332 $input = [
2333 'databaseRow' => [
2334 'aField' => ''
2335 ],
2336 'tableName' => 'aTable',
2337 'processedTca' => [
2338 'columns' => [
2339 'aField' => [
2340 'config' => [
2341 'type' => 'select',
2342 'renderType' => 'selectSingle',
2343 'items' => [
2344 0 => [
2345 0 => 'keepMe',
2346 1 => 'keep',
2347 null,
2348 null,
2349 ],
2350 1 => [
2351 0 => 'keepMe',
2352 1 => 'keepMe2',
2353 null,
2354 null,
2355 ],
2356 2 => [
2357 0 => 'remove me',
2358 1 => 0,
2359 ],
2360 ],
2361 'maxitems' => 99999,
2362 ],
2363 ],
2364 ]
2365 ],
2366 'pageTsConfig' => [
2367 'TCEFORM.' => [
2368 'aTable.' => [
2369 'aField.' => [
2370 'removeItems' => '0',
2371 ],
2372 ],
2373 ],
2374 ],
2375 ];
2376
2377 $expected = $input;
2378 $expected['databaseRow']['aField'] = [];
2379 unset($expected['processedTca']['columns']['aField']['config']['items'][2]);
2380 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2381 }
2382
2383 /**
2384 * @test
2385 */
2386 public function addDataRemovesItemsAddedByAddItemsFromPageTsConfigByRemoveItemsPageTsConfig()
2387 {
2388 $input = [
2389 'databaseRow' => [
2390 'aField' => ''
2391 ],
2392 'tableName' => 'aTable',
2393 'processedTca' => [
2394 'columns' => [
2395 'aField' => [
2396 'config' => [
2397 'type' => 'select',
2398 'renderType' => 'selectSingle',
2399 'items' => [
2400 0 => [
2401 0 => 'keepMe',
2402 1 => 'keep',
2403 null,
2404 null,
2405 ],
2406 1 => [
2407 0 => 'removeMe',
2408 1 => 'remove',
2409 ],
2410 ],
2411 'maxitems' => 99999,
2412 ],
2413 ],
2414 ]
2415 ],
2416 'pageTsConfig' => [
2417 'TCEFORM.' => [
2418 'aTable.' => [
2419 'aField.' => [
2420 'removeItems' => 'remove,add',
2421 'addItems.' => [
2422 'add' => 'addMe'
2423 ]
2424 ],
2425 ],
2426 ],
2427 ],
2428 ];
2429
2430 $expected = $input;
2431 $expected['databaseRow']['aField'] = [];
2432 unset($expected['processedTca']['columns']['aField']['config']['items'][1]);
2433
2434 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2435 }
2436
2437 /**
2438 * @test
2439 */
2440 public function addDataRemovesItemsByLanguageFieldUserRestriction()
2441 {
2442 $input = [
2443 'databaseRow' => [
2444 'aField' => 'aValue,remove'
2445 ],
2446 'tableName' => 'aTable',
2447 'processedTca' => [
2448 'ctrl' => [
2449 'languageField' => 'aField',
2450 ],
2451 'columns' => [
2452 'aField' => [
2453 'config' => [
2454 'type' => 'select',
2455 'renderType' => 'selectSingle',
2456 'items' => [
2457 0 => [
2458 0 => 'keepMe',
2459 1 => 'keep',
2460 null,
2461 null,
2462 ],
2463 1 => [
2464 0 => 'removeMe',
2465 1 => 'remove',
2466 ],
2467 ],
2468 'maxitems' => 99999,
2469 ],
2470 ],
2471 ]
2472 ],
2473 ];
2474
2475 /** @var LanguageService|ObjectProphecy $languageService */
2476 $languageService = $this->prophesize(LanguageService::class);
2477 $GLOBALS['LANG'] = $languageService->reveal();
2478 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue')->willReturn('INVALID VALUE "%s"');
2479 $languageService->sL(Argument::cetera())->willReturnArgument(0);
2480
2481 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2482 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2483 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2484 $backendUserProphecy->checkLanguageAccess('keep')->shouldBeCalled()->willReturn(true);
2485 $backendUserProphecy->checkLanguageAccess('remove')->shouldBeCalled()->willReturn(false);
2486
2487 $expected = $input;
2488 $expected['databaseRow']['aField'] = [];
2489 $expected['processedTca']['columns']['aField']['config']['items'] = [
2490 [ '[ INVALID VALUE "aValue" ]', 'aValue', null, null ],
2491 [ 'keepMe', 'keep', null, null ],
2492 ];
2493
2494 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2495 }
2496
2497 /**
2498 * @test
2499 */
2500 public function addDataRemovesItemsByUserAuthModeRestriction()
2501 {
2502 $input = [
2503 'databaseRow' => [
2504 'aField' => 'keep,remove'
2505 ],
2506 'tableName' => 'aTable',
2507 'processedTca' => [
2508 'columns' => [
2509 'aField' => [
2510 'config' => [
2511 'type' => 'select',
2512 'renderType' => 'selectSingle',
2513 'authMode' => 'explicitAllow',
2514 'items' => [
2515 0 => [
2516 0 => 'keepMe',
2517 1 => 'keep',
2518 null,
2519 null,
2520 ],
2521 1 => [
2522 0 => 'removeMe',
2523 1 => 'remove',
2524 ],
2525 ],
2526 'maxitems' => 99999,
2527 ],
2528 ],
2529 ]
2530 ],
2531 ];
2532
2533 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2534 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2535 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2536 $backendUserProphecy->checkAuthMode('aTable', 'aField', 'keep', 'explicitAllow')->shouldBeCalled()->willReturn(true);
2537 $backendUserProphecy->checkAuthMode('aTable', 'aField', 'remove', 'explicitAllow')->shouldBeCalled()->willReturn(false);
2538
2539 $expected = $input;
2540 $expected['databaseRow']['aField'] = ['keep'];
2541 unset($expected['processedTca']['columns']['aField']['config']['items'][1]);
2542
2543 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2544 }
2545
2546 /**
2547 * @test
2548 */
2549 public function addDataKeepsAllPagesDoktypesForAdminUser()
2550 {
2551 $input = [
2552 'databaseRow' => [
2553 'doktype' => 'keep'
2554 ],
2555 'tableName' => 'pages',
2556 'processedTca' => [
2557 'columns' => [
2558 'doktype' => [
2559 'config' => [
2560 'type' => 'select',
2561 'renderType' => 'selectSingle',
2562 'items' => [
2563 0 => [
2564 0 => 'keepMe',
2565 1 => 'keep',
2566 null,
2567 null,
2568 ],
2569 ],
2570 'maxitems' => 99999,
2571 ],
2572 ],
2573 ],
2574 ],
2575 ];
2576
2577 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2578 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2579 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2580 $backendUserProphecy->isAdmin()->shouldBeCalled()->willReturn(true);
2581
2582 $expected = $input;
2583 $expected['databaseRow']['doktype'] = ['keep'];
2584
2585 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2586 }
2587
2588 /**
2589 * @test
2590 */
2591 public function addDataKeepsAllowedPageTypesForNonAdminUser()
2592 {
2593 $input = [
2594 'databaseRow' => [
2595 'doktype' => 'keep',
2596 ],
2597 'tableName' => 'pages',
2598 'processedTca' => [
2599 'columns' => [
2600 'doktype' => [
2601 'config' => [
2602 'type' => 'select',
2603 'renderType' => 'selectSingle',
2604 'items' => [
2605 0 => [
2606 0 => 'keepMe',
2607 1 => 'keep',
2608 null,
2609 null,
2610 ],
2611 1 => [
2612 0 => 'removeMe',
2613 1 => 'remove',
2614 ],
2615 ],
2616 'maxitems' => 99999,
2617 ],
2618 ],
2619 ],
2620 ],
2621 ];
2622
2623 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2624 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2625 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2626 $backendUserProphecy->isAdmin()->shouldBeCalled()->willReturn(false);
2627 $backendUserProphecy->groupData = [
2628 'pagetypes_select' => 'foo,keep,anotherAllowedDoktype',
2629 ];
2630
2631 $expected = $input;
2632 $expected['databaseRow']['doktype'] = ['keep'];
2633 unset($expected['processedTca']['columns']['doktype']['config']['items'][1]);
2634
2635 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2636 }
2637
2638 /**
2639 * @test
2640 */
2641 public function addDataCallsItemsProcFunc()
2642 {
2643 $input = [
2644 'tableName' => 'aTable',
2645 'databaseRow' => [
2646 'aField' => 'aValue'
2647 ],
2648 'processedTca' => [
2649 'columns' => [
2650 'aField' => [
2651 'config' => [
2652 'type' => 'select',
2653 'renderType' => 'selectSingle',
2654 'items' => [],
2655 'itemsProcFunc' => function (array $parameters, $pObj) {
2656 $parameters['items'] = [
2657 0 => [
2658 0 => 'aLabel',
2659 1 => 'aValue',
2660 2 => null,
2661 3 => null,
2662 ],
2663 ];
2664 },
2665 ],
2666 ],
2667 ],
2668 ],
2669 ];
2670
2671 $expected = $input;
2672 $expected['databaseRow']['aField'] = ['aValue'];
2673 $expected['processedTca']['columns']['aField']['config'] = [
2674 'type' => 'select',
2675 'renderType' => 'selectSingle',
2676 'items' => [
2677 0 => [
2678 0 => 'aLabel',
2679 1 => 'aValue',
2680 2 => null,
2681 3 => null,
2682 ],
2683 ],
2684 'maxitems' => 99999,
2685 ];
2686
2687 self::assertSame($expected, (new TcaSelectItems)->addData($input));
2688 }
2689
2690 /**
2691 * @test
2692 */
2693 public function addDataItemsProcFuncReceivesParameters()
2694 {
2695 $input = [
2696 'tableName' => 'aTable',
2697 'databaseRow' => [
2698 'aField' => 'aValue',
2699 ],
2700 'pageTsConfig' => [
2701 'TCEFORM.' => [
2702 'aTable.' => [
2703 'aField.' => [
2704 'itemsProcFunc.' => [
2705 'itemParamKey' => 'itemParamValue',
2706 ],
2707 ]
2708 ],
2709 ],
2710 ],
2711 'processedTca' => [
2712 'columns' => [
2713 'aField' => [
2714 'config' => [
2715 'type' => 'select',
2716 'renderType' => 'selectSingle',
2717 'aKey' => 'aValue',
2718 'items' => [
2719 0 => [
2720 0 => 'aLabel',
2721 1 => 'aValue',
2722 ],
2723 ],
2724 'itemsProcFunc' => function (array $parameters, $pObj) {
2725 if ($parameters['items'] !== [ 0 => [ 'aLabel', 'aValue'] ]
2726 || $parameters['config']['aKey'] !== 'aValue'
2727 || $parameters['TSconfig'] !== [ 'itemParamKey' => 'itemParamValue' ]
2728 || $parameters['table'] !== 'aTable'
2729 || $parameters['row'] !== [ 'aField' => 'aValue' ]
2730 || $parameters['field'] !== 'aField'
2731 ) {
2732 throw new \UnexpectedValueException('broken', 1476109436);
2733 }
2734 },
2735 ],
2736 ],
2737 ],
2738 ],
2739 ];
2740
2741 $languageService = $this->prophesize(LanguageService::class);
2742 $GLOBALS['LANG'] = $languageService->reveal();
2743 $languageService->sL(Argument::cetera())->willReturnArgument(0);
2744 /** @var FlashMessage|ObjectProphecy $flashMessage */
2745 $flashMessage = $this->prophesize(FlashMessage::class);
2746 GeneralUtility::addInstance(FlashMessage::class, $flashMessage->reveal());
2747 /** @var FlashMessageService|ObjectProphecy $flashMessageService */
2748 $flashMessageService = $this->prophesize(FlashMessageService::class);
2749 GeneralUtility::setSingletonInstance(FlashMessageService::class, $flashMessageService->reveal());
2750 /** @var FlashMessageQueue|ObjectProphecy $flashMessageQueue */
2751 $flashMessageQueue = $this->prophesize(FlashMessageQueue::class);
2752 $flashMessageService->getMessageQueueByIdentifier(Argument::cetera())->willReturn($flashMessageQueue->reveal());
2753
2754 // itemsProcFunc must NOT have raised an exception
2755 $flashMessageQueue->enqueue($flashMessage)->shouldNotBeCalled();
2756
2757 (new TcaSelectItems)->addData($input);
2758 }
2759
2760 /**
2761 * @test
2762 */
2763 public function addDataItemsProcFuncEnqueuesFlashMessageOnException()
2764 {
2765 $input = [
2766 'tableName' => 'aTable',
2767 'databaseRow' => [
2768 'aField' => 'aValue',
2769 ],
2770 'pageTsConfig' => [
2771 'TCEFORM.' => [
2772 'aTable.' => [
2773 'aField.' => [
2774 'itemsProcFunc.' => [
2775 'itemParamKey' => 'itemParamValue',
2776 ],
2777 ]
2778 ],
2779 ],
2780 ],
2781 'processedTca' => [
2782 'columns' => [
2783 'aField' => [
2784 'config' => [
2785 'type' => 'select',
2786 'renderType' => 'selectSingle',
2787 'aKey' => 'aValue',
2788 'items' => [
2789 0 => [
2790 0 => 'aLabel',
2791 1 => 'aValue',
2792 ],
2793 ],
2794 'itemsProcFunc' => function (array $parameters, $pObj) {
2795 throw new \UnexpectedValueException('anException', 1476109437);
2796 },
2797 ],
2798 ],
2799 ],
2800 ],
2801 ];
2802
2803 $languageService = $this->prophesize(LanguageService::class);
2804 $GLOBALS['LANG'] = $languageService->reveal();
2805 /** @var FlashMessage|ObjectProphecy $flashMessage */
2806 $flashMessage = $this->prophesize(FlashMessage::class);
2807 GeneralUtility::addInstance(FlashMessage::class, $flashMessage->reveal());
2808 /** @var FlashMessageService|ObjectProphecy $flashMessageService */
2809 $flashMessageService = $this->prophesize(FlashMessageService::class);
2810 GeneralUtility::setSingletonInstance(FlashMessageService::class, $flashMessageService->reveal());
2811 /** @var FlashMessageQueue|ObjectProphecy $flashMessageQueue */
2812 $flashMessageQueue = $this->prophesize(FlashMessageQueue::class);
2813 $flashMessageService->getMessageQueueByIdentifier(Argument::cetera())->willReturn($flashMessageQueue->reveal());
2814
2815 $flashMessageQueue->enqueue($flashMessage)->shouldBeCalled();
2816
2817 (new TcaSelectItems)->addData($input);
2818 }
2819
2820 /**
2821 * @test
2822 */
2823 public function addDataTranslatesItemLabelsFromPageTsConfig()
2824 {
2825 $input = [
2826 'databaseRow' => [
2827 'aField' => 'aValue',
2828 ],
2829 'tableName' => 'aTable',
2830 'processedTca' => [
2831 'columns' => [
2832 'aField' => [
2833 'config' => [
2834 'type' => 'select',
2835 'renderType' => 'selectSingle',
2836 'items' => [
2837 0 => [
2838 0 => 'aLabel',
2839 1 => 'aValue',
2840 null,
2841 null,
2842 ],
2843 ],
2844 'maxitems' => 99999,
2845 ],
2846 ],
2847 ],
2848 ],
2849 'pageTsConfig' => [
2850 'TCEFORM.' => [
2851 'aTable.' => [
2852 'aField.' => [
2853 'altLabels.' => [
2854 'aValue' => 'labelOverride',
2855 ],
2856 ]
2857 ],
2858 ],
2859 ],
2860 ];
2861
2862 /** @var LanguageService|ObjectProphecy $languageService */
2863 $languageService = $this->prophesize(LanguageService::class);
2864 $GLOBALS['LANG'] = $languageService->reveal();
2865 $languageService->sL('aLabel')->willReturnArgument(0);
2866 $languageService->sL('labelOverride')->shouldBeCalled()->willReturnArgument(0);
2867 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue')->willReturn('INVALID VALUE "%s"');
2868
2869 $expected = $input;
2870 $expected['databaseRow']['aField'] = ['aValue'];
2871 $expected['processedTca']['columns']['aField']['config']['items'][0][0] = 'labelOverride';
2872
2873 self::assertSame($expected, (new TcaSelectItems)->addData($input));
2874 }
2875
2876 /**
2877 * @test
2878 */
2879 public function processSelectFieldValueSetsMmForeignRelationValues()
2880 {
2881 $GLOBALS['TCA']['foreignTable'] = [];
2882
2883 $fileRepositoryProphecy = $this->prophesize(FileRepository::class);
2884 $fileRepositoryProphecy->findByRelation(Argument::cetera())->shouldNotBeCalled();
2885 GeneralUtility::setSingletonInstance(FileRepository::class, $fileRepositoryProphecy->reveal());
2886
2887 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2888 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2889 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2890 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
2891
2892 $this->mockDatabaseConnectionForProcessSelectField();
2893
2894 $input = [
2895 'command' => 'edit',
2896 'tableName' => 'aTable',
2897 'effectivePid' => 23,
2898 'databaseRow' => [
2899 'uid' => 42,
2900 // Two connected rows
2901 'aField' => 2,
2902 ],
2903 'processedTca' => [
2904 'columns' => [
2905 'aField' => [
2906 'config' => [
2907 'type' => 'select',
2908 'renderType' => 'selectSingle',
2909 'maxitems' => 999,
2910 'foreign_table' => 'foreignTable',
2911 'MM' => 'aTable_foreignTable_mm',
2912 'items' => [],
2913 ],
2914 ],
2915 ],
2916 ],
2917 ];
2918 $fieldConfig = $input['processedTca']['columns']['aField']['config'];
2919 /** @var RelationHandler|ObjectProphecy $relationHandlerProphecy */
2920 $relationHandlerProphecy = $this->prophesize(RelationHandler::class);
2921 GeneralUtility::addInstance(RelationHandler::class, $relationHandlerProphecy->reveal());
2922
2923 $relationHandlerUids = [
2924 23,
2925 24
2926 ];
2927
2928 $relationHandlerProphecy->start(2, 'foreignTable', 'aTable_foreignTable_mm', 42, 'aTable', $fieldConfig)->shouldBeCalled();
2929 $relationHandlerProphecy->getValueArray()->shouldBeCalled()->willReturn($relationHandlerUids);
2930
2931 $expected = $input;
2932 $expected['databaseRow']['aField'] = $relationHandlerUids;
2933
2934 self::assertEquals($expected, (new TcaSelectItems)->addData($input));
2935 }
2936
2937 /**
2938 * @test
2939 */
2940 public function processSelectFieldValueSetsForeignRelationValues()
2941 {
2942 $GLOBALS['TCA']['foreignTable'] = [];
2943
2944 $fileRepositoryProphecy = $this->prophesize(FileRepository::class);
2945 $fileRepositoryProphecy->findByRelation(Argument::cetera())->shouldNotBeCalled();
2946 GeneralUtility::setSingletonInstance(FileRepository::class, $fileRepositoryProphecy->reveal());
2947
2948 /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
2949 $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
2950 $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
2951 $backendUserProphecy->getPagePermsClause(1)->shouldBeCalled()->willReturn(' 1=1');
2952
2953 $this->mockDatabaseConnectionForProcessSelectField();
2954
2955 $input = [
2956 'tableName' => 'aTable',
2957 'effectivePid' => 23,
2958 'databaseRow' => [
2959 'uid' => 42,
2960 // Two connected rows
2961 'aField' => '22,23,24,25',
2962 ],
2963