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