156da3a1f2ca438c14f4e18c3057fb3b66e1e474
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / DataHandling / DataHandlerTest.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests\Unit\DataHandling;
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 TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
19 use TYPO3\CMS\Core\Cache\CacheManager;
20 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
21 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
22 use TYPO3\CMS\Core\DataHandling\DataHandler;
23 use TYPO3\CMS\Core\Tests\Unit\DataHandling\Fixtures\AllowAccessHookFixture;
24 use TYPO3\CMS\Core\Tests\Unit\DataHandling\Fixtures\InvalidHookFixture;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
27 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
28
29 /**
30 * Test case
31 */
32 class DataHandlerTest extends UnitTestCase
33 {
34 /**
35 * Subject is not notice free, disable E_NOTICES
36 */
37 protected static $suppressNotices = true;
38
39 /**
40 * @var bool Reset singletons created by subject
41 */
42 protected $resetSingletonInstances = true;
43
44 /**
45 * @var array A backup of registered singleton instances
46 */
47 protected $singletonInstances = [];
48
49 /**
50 * @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
51 */
52 protected $subject;
53
54 /**
55 * @var BackendUserAuthentication a mock logged-in back-end user
56 */
57 protected $backEndUser;
58
59 /**
60 * Set up the tests
61 */
62 protected function setUp()
63 {
64 $GLOBALS['TCA'] = [];
65 $cacheManagerProphecy = $this->prophesize(CacheManager::class);
66 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
67 $cacheFrontendProphecy = $this->prophesize(FrontendInterface::class);
68 $cacheManagerProphecy->getCache('cache_runtime')->willReturn($cacheFrontendProphecy->reveal());
69 $this->backEndUser = $this->createMock(BackendUserAuthentication::class);
70 $this->subject = $this->getAccessibleMock(DataHandler::class, ['dummy']);
71 $this->subject->start([], '', $this->backEndUser);
72 }
73
74 /**
75 * @test
76 */
77 public function fixtureCanBeCreated()
78 {
79 $this->assertTrue($this->subject instanceof DataHandler);
80 }
81
82 //////////////////////////////////////////
83 // Test concerning checkModifyAccessList
84 //////////////////////////////////////////
85 /**
86 * @test
87 */
88 public function adminIsAllowedToModifyNonAdminTable()
89 {
90 $this->subject->admin = true;
91 $this->assertTrue($this->subject->checkModifyAccessList('tt_content'));
92 }
93
94 /**
95 * @test
96 */
97 public function nonAdminIsNorAllowedToModifyNonAdminTable()
98 {
99 $this->subject->admin = false;
100 $this->assertFalse($this->subject->checkModifyAccessList('tt_content'));
101 }
102
103 /**
104 * @test
105 */
106 public function nonAdminWithTableModifyAccessIsAllowedToModifyNonAdminTable()
107 {
108 $this->subject->admin = false;
109 $this->backEndUser->groupData['tables_modify'] = 'tt_content';
110 $this->assertTrue($this->subject->checkModifyAccessList('tt_content'));
111 }
112
113 /**
114 * @test
115 */
116 public function adminIsAllowedToModifyAdminTable()
117 {
118 $this->subject->admin = true;
119 $this->assertTrue($this->subject->checkModifyAccessList('be_users'));
120 }
121
122 /**
123 * @test
124 */
125 public function nonAdminIsNotAllowedToModifyAdminTable()
126 {
127 $this->subject->admin = false;
128 $this->assertFalse($this->subject->checkModifyAccessList('be_users'));
129 }
130
131 /**
132 * @test
133 */
134 public function nonAdminWithTableModifyAccessIsNotAllowedToModifyAdminTable()
135 {
136 $tableName = $this->getUniqueId('aTable');
137 $GLOBALS['TCA'] = [
138 $tableName => [
139 'ctrl' => [
140 'adminOnly' => true,
141 ],
142 ],
143 ];
144 $this->subject->admin = false;
145 $this->backEndUser->groupData['tables_modify'] = $tableName;
146 $this->assertFalse($this->subject->checkModifyAccessList($tableName));
147 }
148
149 /**
150 * @test
151 */
152 public function evalCheckValueDouble2()
153 {
154 $testData = [
155 '-0,5' => '-0.50',
156 '1000' => '1000.00',
157 '1000,10' => '1000.10',
158 '1000,0' => '1000.00',
159 '600.000.000,00' => '600000000.00',
160 '60aaa00' => '6000.00'
161 ];
162 foreach ($testData as $value => $expectedReturnValue) {
163 $returnValue = $this->subject->checkValue_input_Eval($value, ['double2'], '');
164 $this->assertSame($returnValue['value'], $expectedReturnValue);
165 }
166 }
167
168 public function dataProviderDatetime()
169 {
170 // Three elements: input, timezone of input, expected output (UTC)
171 return [
172 'timestamp is passed through, as it is UTC' => [
173 1457103519, 'Europe/Berlin', 1457103519
174 ],
175 'ISO date is interpreted as local date and is output as correct timestamp' => [
176 '2017-06-07T00:10:00Z', 'Europe/Berlin', 1496787000
177 ],
178 ];
179 }
180
181 /**
182 * @test
183 * @dataProvider dataProviderDatetime
184 */
185 public function evalCheckValueDatetime($input, $serverTimezone, $expectedOutput)
186 {
187 $oldTimezone = date_default_timezone_get();
188 date_default_timezone_set($serverTimezone);
189
190 $output = $this->subject->checkValue_input_Eval($input, ['datetime'], '');
191
192 // set before the assertion is performed, so it is restored even for failing tests
193 date_default_timezone_set($oldTimezone);
194
195 $this->assertEquals($expectedOutput, $output['value']);
196 }
197
198 /**
199 * Data provider for inputValueCheckRecognizesStringValuesAsIntegerValuesCorrectly
200 *
201 * @return array
202 */
203 public function inputValuesStringsDataProvider()
204 {
205 return [
206 '"0" returns zero as integer' => [
207 '0',
208 0
209 ],
210 '"-2000001" is interpreted correctly as -2000001 but is lower than -2000000 and set to -2000000' => [
211 '-2000001',
212 -2000000
213 ],
214 '"-2000000" is interpreted correctly as -2000000 and is equal to -2000000' => [
215 '-2000000',
216 -2000000
217 ],
218 '"2000000" is interpreted correctly as 2000000 and is equal to 2000000' => [
219 '2000000',
220 2000000
221 ],
222 '"2000001" is interpreted correctly as 2000001 but is greater then 2000000 and set to 2000000' => [
223 '2000001',
224 2000000
225 ],
226 ];
227 }
228
229 /**
230 * @test
231 * @dataProvider inputValuesStringsDataProvider
232 * @param string $value
233 * @param int $expectedReturnValue
234 */
235 public function inputValueCheckRecognizesStringValuesAsIntegerValuesCorrectly($value, $expectedReturnValue)
236 {
237 $tcaFieldConf = [
238 'input' => [],
239 'eval' => 'int',
240 'range' => [
241 'lower' => '-2000000',
242 'upper' => '2000000'
243 ]
244 ];
245 $returnValue = $this->subject->_call('checkValueForInput', $value, $tcaFieldConf, '', 0, 0, '');
246 $this->assertSame($returnValue['value'], $expectedReturnValue);
247 }
248
249 /**
250 * @return array
251 */
252 public function inputValuesDataTimeDataProvider()
253 {
254 return [
255 'undershot date adjusted' => [
256 '2018-02-28T00:00:00Z',
257 1519862400,
258 ],
259 'exact lower date accepted' => [
260 '2018-03-01T00:00:00Z',
261 1519862400,
262 ],
263 'exact upper date accepted' => [
264 '2018-03-31T23:59:59Z',
265 1522540799,
266 ],
267 'exceeded date adjusted' => [
268 '2018-04-01T00:00:00Z',
269 1522540799,
270 ],
271 ];
272 }
273
274 /**
275 * @param string $value
276 * @param int $expected
277 *
278 * @test
279 * @dataProvider inputValuesDataTimeDataProvider
280 */
281 public function inputValueCheckRecognizesDateTimeValuesAsIntegerValuesCorrectly($value, int $expected)
282 {
283 $tcaFieldConf = [
284 'input' => [],
285 'eval' => 'datetime',
286 'range' => [
287 // unix timestamp: 1519862400
288 'lower' => gmmktime(0, 0, 0, 3, 1, 2018),
289 // unix timestamp: 1522540799
290 'upper' => gmmktime(23, 59, 59, 3, 31, 2018),
291 ]
292 ];
293
294 // @todo Switch to UTC since otherwise DataHandler removes timezone offset
295 $previousTimezone = date_default_timezone_get();
296 date_default_timezone_set('UTC');
297
298 $returnValue = $this->subject->_call('checkValueForInput', $value, $tcaFieldConf, '', 0, 0, '');
299
300 date_default_timezone_set($previousTimezone);
301
302 $this->assertSame($returnValue['value'], $expected);
303 }
304
305 /**
306 * @return array
307 */
308 public function inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFieldsDataProvider()
309 {
310 return [
311 'tca without dbType' => [
312 [
313 'input' => []
314 ]
315 ],
316 'tca with dbType != date/datetime/time' => [
317 [
318 'input' => [],
319 'dbType' => 'foo'
320 ]
321 ]
322 ];
323 }
324
325 /**
326 * @test
327 * @param array $tcaFieldConf
328 * @dataProvider inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFieldsDataProvider
329 */
330 public function inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFields($tcaFieldConf)
331 {
332 $this->subject->_call('checkValueForInput', '', $tcaFieldConf, '', 0, 0, '');
333 }
334
335 ///////////////////////////////////////////
336 // Tests concerning checkModifyAccessList
337 ///////////////////////////////////////////
338 //
339 /**
340 * Tests whether a wrong interface on the 'checkModifyAccessList' hook throws an exception.
341 * @test
342 */
343 public function doesCheckModifyAccessListThrowExceptionOnWrongHookInterface()
344 {
345 $this->expectException(\UnexpectedValueException::class);
346 $this->expectExceptionCode(1251892472);
347
348 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = InvalidHookFixture::class;
349 $this->subject->checkModifyAccessList('tt_content');
350 }
351
352 /**
353 * Tests whether the 'checkModifyAccessList' hook is called correctly.
354 *
355 * @test
356 */
357 public function doesCheckModifyAccessListHookGetsCalled()
358 {
359 $hookClass = $this->getUniqueId('tx_coretest');
360 $hookMock = $this->getMockBuilder(\TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface::class)
361 ->setMethods(['checkModifyAccessList'])
362 ->setMockClassName($hookClass)
363 ->getMock();
364 $hookMock->expects($this->once())->method('checkModifyAccessList');
365 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = $hookClass;
366 GeneralUtility::addInstance($hookClass, $hookMock);
367 $this->subject->checkModifyAccessList('tt_content');
368 }
369
370 /**
371 * Tests whether the 'checkModifyAccessList' hook modifies the $accessAllowed variable.
372 *
373 * @test
374 */
375 public function doesCheckModifyAccessListHookModifyAccessAllowed()
376 {
377 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = AllowAccessHookFixture::class;
378 $this->assertTrue($this->subject->checkModifyAccessList('tt_content'));
379 }
380
381 /////////////////////////////////////
382 // Tests concerning process_datamap
383 /////////////////////////////////////
384 /**
385 * @test
386 */
387 public function processDatamapForFrozenNonZeroWorkspaceReturnsFalse()
388 {
389 /** @var DataHandler $subject */
390 $subject = $this->getMockBuilder(DataHandler::class)
391 ->setMethods(['newlog'])
392 ->getMock();
393 $this->backEndUser->workspace = 1;
394 $this->backEndUser->workspaceRec = ['freeze' => true];
395 $subject->BE_USER = $this->backEndUser;
396 $this->assertFalse($subject->process_datamap());
397 }
398
399 /**
400 * @test
401 */
402 public function processDatamapWhenEditingRecordInWorkspaceCreatesNewRecordInWorkspace()
403 {
404 $GLOBALS['TCA'] = [
405 'pages' => [
406 'columns' => [],
407 ],
408 ];
409
410 /** @var $subject DataHandler|\PHPUnit_Framework_MockObject_MockObject */
411 $subject = $this->getMockBuilder(DataHandler::class)
412 ->setMethods([
413 'newlog',
414 'checkModifyAccessList',
415 'tableReadOnly',
416 'checkRecordUpdateAccess',
417 'recordInfo',
418 'getCacheManager',
419 'registerElementsToBeDeleted',
420 'unsetElementsToBeDeleted',
421 'resetElementsToBeDeleted'
422 ])
423 ->disableOriginalConstructor()
424 ->getMock();
425
426 $subject->bypassWorkspaceRestrictions = false;
427 $subject->datamap = [
428 'pages' => [
429 '1' => [
430 'header' => 'demo'
431 ]
432 ]
433 ];
434
435 $cacheManagerMock = $this->getMockBuilder(CacheManager::class)
436 ->setMethods(['flushCachesInGroupByTags'])
437 ->getMock();
438 $cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', []);
439
440 $subject->expects($this->once())->method('getCacheManager')->willReturn($cacheManagerMock);
441 $subject->expects($this->once())->method('recordInfo')->will($this->returnValue(null));
442 $subject->expects($this->once())->method('checkModifyAccessList')->with('pages')->will($this->returnValue(true));
443 $subject->expects($this->once())->method('tableReadOnly')->with('pages')->will($this->returnValue(false));
444 $subject->expects($this->once())->method('checkRecordUpdateAccess')->will($this->returnValue(true));
445 $subject->expects($this->once())->method('unsetElementsToBeDeleted')->willReturnArgument(0);
446
447 /** @var BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject $backEndUser */
448 $backEndUser = $this->createMock(BackendUserAuthentication::class);
449 $backEndUser->workspace = 1;
450 $backEndUser->workspaceRec = ['freeze' => false];
451 $backEndUser->expects($this->once())->method('workspaceAllowAutoCreation')->will($this->returnValue(true));
452 $backEndUser->expects($this->once())->method('workspaceCannotEditRecord')->will($this->returnValue(true));
453 $backEndUser->expects($this->once())->method('recordEditAccessInternals')->with('pages', 1)->will($this->returnValue(true));
454 $subject->BE_USER = $backEndUser;
455 $createdDataHandler = $this->createMock(DataHandler::class);
456 $createdDataHandler->expects($this->once())->method('start')->with([], [
457 'pages' => [
458 1 => [
459 'version' => [
460 'action' => 'new',
461 'label' => 'Auto-created for WS #1'
462 ]
463 ]
464 ]
465 ]);
466 $createdDataHandler->expects($this->never())->method('process_datamap');
467 $createdDataHandler->expects($this->once())->method('process_cmdmap');
468 GeneralUtility::addInstance(DataHandler::class, $createdDataHandler);
469 $subject->process_datamap();
470 }
471
472 /**
473 * @test
474 */
475 public function doesCheckFlexFormValueHookGetsCalled()
476 {
477 $hookClass = $this->getUniqueId('tx_coretest');
478 $hookMock = $this->getMockBuilder($hookClass)
479 ->setMethods(['checkFlexFormValue_beforeMerge'])
480 ->getMock();
481 $hookMock->expects($this->once())->method('checkFlexFormValue_beforeMerge');
482 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'][] = $hookClass;
483 GeneralUtility::addInstance($hookClass, $hookMock);
484 $flexFormToolsProphecy = $this->prophesize(FlexFormTools::class);
485 $flexFormToolsProphecy->getDataStructureIdentifier(Argument::cetera())->willReturn('anIdentifier');
486 $flexFormToolsProphecy->parseDataStructureByIdentifier('anIdentifier')->willReturn([]);
487 GeneralUtility::addInstance(FlexFormTools::class, $flexFormToolsProphecy->reveal());
488 $this->subject->_call('checkValueForFlex', [], [], [], '', 0, '', '', 0, 0, 0, [], '');
489 }
490
491 /////////////////////////////////////
492 // Tests concerning log
493 /////////////////////////////////////
494 /**
495 * @test
496 */
497 public function logCallsWriteLogOfBackendUserIfLoggingIsEnabled()
498 {
499 $backendUser = $this->createMock(BackendUserAuthentication::class);
500 $backendUser->expects($this->once())->method('writelog');
501 $this->subject->enableLogging = true;
502 $this->subject->BE_USER = $backendUser;
503 $this->subject->log('', 23, 0, 42, 0, 'details');
504 }
505
506 /**
507 * @test
508 */
509 public function logDoesNotCallWriteLogOfBackendUserIfLoggingIsDisabled()
510 {
511 $backendUser = $this->createMock(BackendUserAuthentication::class);
512 $backendUser->expects($this->never())->method('writelog');
513 $this->subject->enableLogging = false;
514 $this->subject->BE_USER = $backendUser;
515 $this->subject->log('', 23, 0, 42, 0, 'details');
516 }
517
518 /**
519 * @test
520 */
521 public function logAddsEntryToLocalErrorLogArray()
522 {
523 $backendUser = $this->createMock(BackendUserAuthentication::class);
524 $this->subject->BE_USER = $backendUser;
525 $this->subject->enableLogging = true;
526 $this->subject->errorLog = [];
527 $logDetailsUnique = $this->getUniqueId('details');
528 $this->subject->log('', 23, 0, 42, 1, $logDetailsUnique);
529 $this->assertStringEndsWith($logDetailsUnique, $this->subject->errorLog[0]);
530 }
531
532 /**
533 * @test
534 */
535 public function logFormatsDetailMessageWithAdditionalDataInLocalErrorArray()
536 {
537 $backendUser = $this->createMock(BackendUserAuthentication::class);
538 $this->subject->BE_USER = $backendUser;
539 $this->subject->enableLogging = true;
540 $this->subject->errorLog = [];
541 $logDetails = $this->getUniqueId('details');
542 $this->subject->log('', 23, 0, 42, 1, '%1$s' . $logDetails . '%2$s', -1, ['foo', 'bar']);
543 $expected = 'foo' . $logDetails . 'bar';
544 $this->assertStringEndsWith($expected, $this->subject->errorLog[0]);
545 }
546
547 /**
548 * @param bool $expected
549 * @param string $submittedValue
550 * @param string $storedValue
551 * @param string $storedType
552 * @param bool $allowNull
553 *
554 * @dataProvider equalSubmittedAndStoredValuesAreDeterminedDataProvider
555 * @test
556 */
557 public function equalSubmittedAndStoredValuesAreDetermined($expected, $submittedValue, $storedValue, $storedType, $allowNull)
558 {
559 $result = $this->callInaccessibleMethod(
560 $this->subject,
561 'isSubmittedValueEqualToStoredValue',
562 $submittedValue,
563 $storedValue,
564 $storedType,
565 $allowNull
566 );
567 $this->assertEquals($expected, $result);
568 }
569
570 /**
571 * @return array
572 */
573 public function equalSubmittedAndStoredValuesAreDeterminedDataProvider()
574 {
575 return [
576 // String
577 'string value "" vs. ""' => [
578 true,
579 '', '', 'string', false
580 ],
581 'string value 0 vs. "0"' => [
582 true,
583 0, '0', 'string', false
584 ],
585 'string value 1 vs. "1"' => [
586 true,
587 1, '1', 'string', false
588 ],
589 'string value "0" vs. ""' => [
590 false,
591 '0', '', 'string', false
592 ],
593 'string value 0 vs. ""' => [
594 false,
595 0, '', 'string', false
596 ],
597 'string value null vs. ""' => [
598 true,
599 null, '', 'string', false
600 ],
601 // Integer
602 'integer value 0 vs. 0' => [
603 true,
604 0, 0, 'int', false
605 ],
606 'integer value "0" vs. "0"' => [
607 true,
608 '0', '0', 'int', false
609 ],
610 'integer value 0 vs. "0"' => [
611 true,
612 0, '0', 'int', false
613 ],
614 'integer value "" vs. "0"' => [
615 true,
616 '', '0', 'int', false
617 ],
618 'integer value "" vs. 0' => [
619 true,
620 '', 0, 'int', false
621 ],
622 'integer value "0" vs. 0' => [
623 true,
624 '0', 0, 'int', false
625 ],
626 'integer value 1 vs. 1' => [
627 true,
628 1, 1, 'int', false
629 ],
630 'integer value 1 vs. "1"' => [
631 true,
632 1, '1', 'int', false
633 ],
634 'integer value "1" vs. "1"' => [
635 true,
636 '1', '1', 'int', false
637 ],
638 'integer value "1" vs. 1' => [
639 true,
640 '1', 1, 'int', false
641 ],
642 'integer value "0" vs. "1"' => [
643 false,
644 '0', '1', 'int', false
645 ],
646 // String with allowed NULL values
647 'string with allowed null value "" vs. ""' => [
648 true,
649 '', '', 'string', true
650 ],
651 'string with allowed null value 0 vs. "0"' => [
652 true,
653 0, '0', 'string', true
654 ],
655 'string with allowed null value 1 vs. "1"' => [
656 true,
657 1, '1', 'string', true
658 ],
659 'string with allowed null value "0" vs. ""' => [
660 false,
661 '0', '', 'string', true
662 ],
663 'string with allowed null value 0 vs. ""' => [
664 false,
665 0, '', 'string', true
666 ],
667 'string with allowed null value null vs. ""' => [
668 false,
669 null, '', 'string', true
670 ],
671 'string with allowed null value "" vs. null' => [
672 false,
673 '', null, 'string', true
674 ],
675 'string with allowed null value null vs. null' => [
676 true,
677 null, null, 'string', true
678 ],
679 // Integer with allowed NULL values
680 'integer with allowed null value 0 vs. 0' => [
681 true,
682 0, 0, 'int', true
683 ],
684 'integer with allowed null value "0" vs. "0"' => [
685 true,
686 '0', '0', 'int', true
687 ],
688 'integer with allowed null value 0 vs. "0"' => [
689 true,
690 0, '0', 'int', true
691 ],
692 'integer with allowed null value "" vs. "0"' => [
693 true,
694 '', '0', 'int', true
695 ],
696 'integer with allowed null value "" vs. 0' => [
697 true,
698 '', 0, 'int', true
699 ],
700 'integer with allowed null value "0" vs. 0' => [
701 true,
702 '0', 0, 'int', true
703 ],
704 'integer with allowed null value 1 vs. 1' => [
705 true,
706 1, 1, 'int', true
707 ],
708 'integer with allowed null value "1" vs. "1"' => [
709 true,
710 '1', '1', 'int', true
711 ],
712 'integer with allowed null value "1" vs. 1' => [
713 true,
714 '1', 1, 'int', true
715 ],
716 'integer with allowed null value 1 vs. "1"' => [
717 true,
718 1, '1', 'int', true
719 ],
720 'integer with allowed null value "0" vs. "1"' => [
721 false,
722 '0', '1', 'int', true
723 ],
724 'integer with allowed null value null vs. ""' => [
725 false,
726 null, '', 'int', true
727 ],
728 'integer with allowed null value "" vs. null' => [
729 false,
730 '', null, 'int', true
731 ],
732 'integer with allowed null value null vs. null' => [
733 true,
734 null, null, 'int', true
735 ],
736 'integer with allowed null value null vs. "0"' => [
737 false,
738 null, '0', 'int', true
739 ],
740 'integer with allowed null value null vs. 0' => [
741 false,
742 null, 0, 'int', true
743 ],
744 'integer with allowed null value "0" vs. null' => [
745 false,
746 '0', null, 'int', true
747 ],
748 ];
749 }
750
751 /**
752 * @param bool $expected
753 * @param array $eval
754 * @dataProvider getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditionsDataProvider
755 * @test
756 */
757 public function getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditions($expected, $eval)
758 {
759 $table = 'phpunit_dummy';
760
761 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $subject */
762 $subject = $this->getAccessibleMock(
763 DataHandler::class,
764 ['dummy']
765 );
766
767 $backendUser = $this->createMock(BackendUserAuthentication::class);
768 $subject->BE_USER = $backendUser;
769 $subject->BE_USER->workspace = 1;
770
771 $GLOBALS['TCA'][$table] = [];
772 $GLOBALS['TCA'][$table]['ctrl'] = ['label' => 'dummy'];
773 $GLOBALS['TCA'][$table]['columns'] = [
774 'dummy' => [
775 'config' => [
776 'eval' => $eval
777 ]
778 ]
779 ];
780
781 $this->assertEquals($expected, $subject->_call('getPlaceholderTitleForTableLabel', $table));
782 }
783
784 /**
785 * @return array
786 */
787 public function getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditionsDataProvider()
788 {
789 return [
790 [
791 0.10,
792 'double2'
793 ],
794 [
795 0,
796 'int'
797 ],
798 [
799 '0',
800 'datetime'
801 ],
802 [
803 '[PLACEHOLDER, WS#1]',
804 ''
805 ]
806 ];
807 }
808
809 /**
810 * @test
811 */
812 public function deletePagesOnRootLevelIsDenied()
813 {
814 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $dataHandlerMock */
815 $dataHandlerMock = $this->getMockBuilder(DataHandler::class)
816 ->setMethods(['canDeletePage', 'log'])
817 ->getMock();
818 $dataHandlerMock
819 ->expects($this->never())
820 ->method('canDeletePage');
821 $dataHandlerMock
822 ->expects($this->once())
823 ->method('log')
824 ->with('pages', 0, 0, 0, 2, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
825
826 $dataHandlerMock->deletePages(0);
827 }
828
829 /**
830 * @test
831 */
832 public function deleteRecord_procBasedOnFieldTypeRespectsEnableCascadingDelete()
833 {
834 $table = $this->getUniqueId('foo_');
835 $conf = [
836 'type' => 'inline',
837 'foreign_table' => $this->getUniqueId('foreign_foo_'),
838 'behaviour' => [
839 'enableCascadingDelete' => 0,
840 ]
841 ];
842
843 /** @var \TYPO3\CMS\Core\Database\RelationHandler $mockRelationHandler */
844 $mockRelationHandler = $this->createMock(\TYPO3\CMS\Core\Database\RelationHandler::class);
845 $mockRelationHandler->itemArray = [
846 '1' => ['table' => $this->getUniqueId('bar_'), 'id' => 67]
847 ];
848
849 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $mockDataHandler */
850 $mockDataHandler = $this->getAccessibleMock(DataHandler::class, ['getInlineFieldType', 'deleteAction', 'createRelationHandlerInstance'], [], '', false);
851 $mockDataHandler->expects($this->once())->method('getInlineFieldType')->will($this->returnValue('field'));
852 $mockDataHandler->expects($this->once())->method('createRelationHandlerInstance')->will($this->returnValue($mockRelationHandler));
853 $mockDataHandler->expects($this->never())->method('deleteAction');
854 $mockDataHandler->deleteRecord_procBasedOnFieldType($table, 42, 'foo', 'bar', $conf);
855 }
856
857 /**
858 * @return array
859 */
860 public function checkValue_checkReturnsExpectedValuesDataProvider()
861 {
862 return [
863 'None item selected' => [
864 0,
865 0
866 ],
867 'All items selected' => [
868 7,
869 7
870 ],
871 'Item 1 and 2 are selected' => [
872 3,
873 3
874 ],
875 'Value is higher than allowed (all checkboxes checked)' => [
876 15,
877 7
878 ],
879 'Value is higher than allowed (some checkboxes checked)' => [
880 11,
881 3
882 ],
883 'Negative value' => [
884 -5,
885 0
886 ]
887 ];
888 }
889
890 /**
891 * @param string $value
892 * @param string $expectedValue
893 *
894 * @dataProvider checkValue_checkReturnsExpectedValuesDataProvider
895 * @test
896 */
897 public function checkValue_checkReturnsExpectedValues($value, $expectedValue)
898 {
899 $expectedResult = [
900 'value' => $expectedValue
901 ];
902 $result = [];
903 $tcaFieldConfiguration = [
904 'items' => [
905 ['Item 1', 0],
906 ['Item 2', 0],
907 ['Item 3', 0]
908 ]
909 ];
910 $this->assertSame($expectedResult, $this->subject->_call('checkValueForCheck', $result, $value, $tcaFieldConfiguration, '', 0, 0, ''));
911 }
912
913 /**
914 * @test
915 */
916 public function checkValueForInputConvertsNullToEmptyString()
917 {
918 $previousLanguageService = $GLOBALS['LANG'];
919 $GLOBALS['LANG'] = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class);
920 $GLOBALS['LANG']->init('default');
921 $expectedResult = ['value' => ''];
922 $this->assertSame($expectedResult, $this->subject->_call('checkValueForInput', null, ['type' => 'string', 'max' => 40], 'tt_content', 'NEW55c0e67f8f4d32.04974534', 89, 'table_caption'));
923 $GLOBALS['LANG'] = $previousLanguageService;
924 }
925
926 /**
927 * @param mixed $value
928 * @param array $configuration
929 * @param int|string $expected
930 * @test
931 * @dataProvider referenceValuesAreCastedDataProvider
932 */
933 public function referenceValuesAreCasted($value, array $configuration, $expected)
934 {
935 $this->assertEquals(
936 $expected,
937 $this->subject->_call('castReferenceValue', $value, $configuration)
938 );
939 }
940
941 /**
942 * @return array
943 */
944 public function referenceValuesAreCastedDataProvider()
945 {
946 return [
947 'all empty' => [
948 '', [], ''
949 ],
950 'cast zero with MM table' => [
951 '', ['MM' => 'table'], 0
952 ],
953 'cast zero with MM table with default value' => [
954 '', ['MM' => 'table', 'default' => 13], 0
955 ],
956 'cast zero with foreign field' => [
957 '', ['foreign_field' => 'table', 'default' => 13], 0
958 ],
959 'cast zero with foreign field with default value' => [
960 '', ['foreign_field' => 'table'], 0
961 ],
962 'pass zero' => [
963 '0', [], '0'
964 ],
965 'pass value' => [
966 '1', ['default' => 13], '1'
967 ],
968 'use default value' => [
969 '', ['default' => 13], 13
970 ],
971 ];
972 }
973
974 /**
975 * @return array
976 */
977 public function clearPrefixFromValueRemovesPrefixDataProvider(): array
978 {
979 return [
980 'normal case' => ['Test (copy 42)', 'Test'],
981 // all cases below look fishy and indicate bugs
982 'with double spaces before' => ['Test (copy 42)', 'Test '],
983 'with three spaces before' => ['Test (copy 42)', 'Test '],
984 'with space after' => ['Test (copy 42) ', 'Test (copy 42) '],
985 'with double spaces after' => ['Test (copy 42) ', 'Test (copy 42) '],
986 'with three spaces after' => ['Test (copy 42) ', 'Test (copy 42) '],
987 'with double tab before' => ['Test' . chr(9) . '(copy 42)', 'Test'],
988 'with double tab after' => ['Test (copy 42)' . chr(9), 'Test (copy 42)' . chr(9)],
989 ];
990 }
991
992 /**
993 * @test
994 * @dataProvider clearPrefixFromValueRemovesPrefixDataProvider
995 * @param string $input
996 * @param string $expected
997 */
998 public function clearPrefixFromValueRemovesPrefix(string $input, string $expected)
999 {
1000 $languageServiceProphecy = $this->prophesize(\TYPO3\CMS\Core\Localization\LanguageService::class);
1001 $languageServiceProphecy->sL('testLabel')->willReturn('(copy %s)');
1002 $GLOBALS['LANG'] = $languageServiceProphecy->reveal();
1003 $GLOBALS['TCA']['testTable']['ctrl']['prependAtCopy'] = 'testLabel';
1004 $this->assertEquals($expected, (new DataHandler())->clearPrefixFromValue('testTable', $input));
1005 }
1006 }