2 namespace TYPO3\CMS\Core\Tests\Unit\DataHandler
;
5 * This file is part of the TYPO3 CMS project.
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.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication
;
18 use TYPO3\CMS\Core\Database\DatabaseConnection
;
19 use TYPO3\CMS\Core\DataHandling\DataHandler
;
20 use TYPO3\CMS\Core\Tests\AccessibleObjectInterface
;
21 use TYPO3\CMS\Core\Tests\Unit\DataHandling\Fixtures\AllowAccessHookFixture
;
22 use TYPO3\CMS\Core\Tests\Unit\DataHandling\Fixtures\InvalidHookFixture
;
23 use TYPO3\CMS\Core\Utility\GeneralUtility
;
28 class DataHandlerTest
extends \TYPO3\CMS\Core\Tests\UnitTestCase
31 * @var array A backup of registered singleton instances
33 protected $singletonInstances = array();
36 * @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
41 * @var BackendUserAuthentication a mock logged-in back-end user
43 protected $backEndUser;
46 * @var DatabaseConnection|\PHPUnit_Framework_MockObject_MockObject
48 protected $mockDatabaseConnection;
53 protected function setUp()
55 $GLOBALS['TCA'] = array();
56 $this->singletonInstances
= GeneralUtility
::getSingletonInstances();
57 $this->backEndUser
= $this->getMock(BackendUserAuthentication
::class);
58 $this->mockDatabaseConnection
= $this->getMock(DatabaseConnection
::class, array(), array(), '', false);
59 $GLOBALS['TYPO3_DB'] = $this->mockDatabaseConnection
;
60 $this->subject
= $this->getAccessibleMock(DataHandler
::class, ['dummy']);
61 $this->subject
->start(array(), '', $this->backEndUser
);
67 protected function tearDown()
69 GeneralUtility
::resetSingletonInstances($this->singletonInstances
);
73 //////////////////////////////////////
74 // Tests for the basic functionality
75 //////////////////////////////////////
79 public function fixtureCanBeCreated()
81 $this->assertTrue($this->subject
instanceof DataHandler
);
84 //////////////////////////////////////////
85 // Test concerning checkModifyAccessList
86 //////////////////////////////////////////
90 public function adminIsAllowedToModifyNonAdminTable()
92 $this->subject
->admin
= true;
93 $this->assertTrue($this->subject
->checkModifyAccessList('tt_content'));
99 public function nonAdminIsNorAllowedToModifyNonAdminTable()
101 $this->subject
->admin
= false;
102 $this->assertFalse($this->subject
->checkModifyAccessList('tt_content'));
108 public function nonAdminWithTableModifyAccessIsAllowedToModifyNonAdminTable()
110 $this->subject
->admin
= false;
111 $this->backEndUser
->groupData
['tables_modify'] = 'tt_content';
112 $this->assertTrue($this->subject
->checkModifyAccessList('tt_content'));
118 public function adminIsAllowedToModifyAdminTable()
120 $this->subject
->admin
= true;
121 $this->assertTrue($this->subject
->checkModifyAccessList('be_users'));
127 public function nonAdminIsNotAllowedToModifyAdminTable()
129 $this->subject
->admin
= false;
130 $this->assertFalse($this->subject
->checkModifyAccessList('be_users'));
136 public function nonAdminWithTableModifyAccessIsNotAllowedToModifyAdminTable()
138 $tableName = $this->getUniqueId('aTable');
139 $GLOBALS['TCA'] = array(
146 $this->subject
->admin
= false;
147 $this->backEndUser
->groupData
['tables_modify'] = $tableName;
148 $this->assertFalse($this->subject
->checkModifyAccessList($tableName));
154 public function evalCheckValueDouble2()
159 '1000,10' => '1000.10',
160 '1000,0' => '1000.00',
161 '600.000.000,00' => '600000000.00',
162 '60aaa00' => '6000.00'
164 foreach ($testData as $value => $expectedReturnValue) {
165 $returnValue = $this->subject
->checkValue_input_Eval($value, array('double2'), '');
166 $this->assertSame($returnValue['value'], $expectedReturnValue);
170 public function dataProviderDatetime()
172 // Three elements: input, timezone of input, expected output (UTC)
174 // German standard time (without DST) is one hour ahead of UTC
175 'date in 2016 in German timezone' => [
176 1457103519, 'Europe/Berlin', 1457103519 - 3600
178 'date in 1969 in German timezone' => [
179 -7200, 'Europe/Berlin', -10800
181 // Los Angeles is 8 hours behind UTC
182 'date in 2016 in Los Angeles timezone' => [
183 1457103519, 'America/Los_Angeles', 1457103519 +
28800
186 1457103519, 'UTC', 1457103519
193 * @dataProvider dataProviderDatetime
195 public function evalCheckValueDatetime($input, $serverTimezone, $expectedOutput)
197 $oldTimezone = date_default_timezone_get();
198 date_default_timezone_set($serverTimezone);
200 $output = $this->subject
->checkValue_input_Eval($input, ['datetime'], '');
202 // set before the assertion is performed, so it is restored even for failing tests
203 date_default_timezone_set($oldTimezone);
205 $this->assertEquals($expectedOutput, $output['value']);
209 * Data provider for inputValueCheckRecognizesStringValuesAsIntegerValuesCorrectly
213 public function inputValuesStringsDataProvider()
216 '"0" returns zero as integer' => array(
220 '"-1999999" is interpreted correctly as -1999999 and is lot lower than -200000' => array(
224 '"3000000" is interpreted correctly as 3000000 but is higher then 200000 and set to 200000' => array(
233 * @dataProvider inputValuesStringsDataProvider
234 * @param string $value
235 * @param int $expectedReturnValue
237 public function inputValueCheckRecognizesStringValuesAsIntegerValuesCorrectly($value, $expectedReturnValue)
239 $tcaFieldConf = array(
243 'lower' => '-2000000',
247 $returnValue = $this->subject
->_call('checkValueForInput', $value, $tcaFieldConf, '', 0, 0, '');
248 $this->assertSame($returnValue['value'], $expectedReturnValue);
254 public function inputValueCheckCallsGetDateTimeFormatsForDatetimeFieldsDataProvider()
257 'dbType = date' => array(
260 'dbType = datetime' => array(
268 * @dataProvider inputValueCheckCallsGetDateTimeFormatsForDatetimeFieldsDataProvider
269 * @param string $dbType
271 public function inputValueCheckCallsNotGetDateTimeFormatsForDatetimeFieldsWithEmptyValue($dbType)
273 $tcaFieldConf = array(
277 $this->mockDatabaseConnection
->expects($this->never())->method('getDateTimeFormats');
278 $this->subject
->_call('checkValueForInput', '', $tcaFieldConf, '', 0, 0, '');
283 * @dataProvider inputValueCheckCallsGetDateTimeFormatsForDatetimeFieldsDataProvider
284 * @param string $dbType
286 public function inputValueCheckCallsGetDateTimeFormatsForDatetimeFieldsWithNonEmptyValue($dbType)
290 'empty' => '0000-00-00',
294 'empty' => '0000-00-00 00:00:00',
295 'format' => 'Y-m-d H:i:s'
298 $tcaFieldConf = array(
302 $this->mockDatabaseConnection
->expects($this->once())->method('getDateTimeFormats')->willReturn($dateTimeFormats);
303 $this->subject
->_call('checkValueForInput', $dateTimeFormats[$dbType]['empty'], $tcaFieldConf, '', 0, 0, '');
309 public function inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFieldsDataProvider()
312 'tca without dbType' => array(
317 'tca with dbType != date/datetime' => array(
328 * @param array $tcaFieldConf
329 * @dataProvider inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFieldsDataProvider
331 public function inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFields($tcaFieldConf)
333 $this->mockDatabaseConnection
->expects($this->never())->method('getDateTimeFormats');
334 $this->subject
->_call('checkValueForInput', '', $tcaFieldConf, '', 0, 0, '');
337 ///////////////////////////////////////////
338 // Tests concerning checkModifyAccessList
339 ///////////////////////////////////////////
342 * Tests whether a wrong interface on the 'checkModifyAccessList' hook throws an exception.
345 * @expectedException \UnexpectedValueException
347 public function doesCheckModifyAccessListThrowExceptionOnWrongHookInterface()
349 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = InvalidHookFixture
::class;
350 $this->subject
->checkModifyAccessList('tt_content');
354 * Tests whether the 'checkModifyAccessList' hook is called correctly.
358 public function doesCheckModifyAccessListHookGetsCalled()
360 $hookClass = $this->getUniqueId('tx_coretest');
361 $hookMock = $this->getMock(\TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface
::class, array('checkModifyAccessList'), array(), $hookClass);
362 $hookMock->expects($this->once())->method('checkModifyAccessList');
363 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = $hookClass;
364 GeneralUtility
::addInstance($hookClass, $hookMock);
365 $this->subject
->checkModifyAccessList('tt_content');
369 * Tests whether the 'checkModifyAccessList' hook modifies the $accessAllowed variable.
373 public function doesCheckModifyAccessListHookModifyAccessAllowed()
375 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = AllowAccessHookFixture
::class;
376 $this->assertTrue($this->subject
->checkModifyAccessList('tt_content'));
379 /////////////////////////////////////
380 // Tests concerning process_datamap
381 /////////////////////////////////////
385 public function processDatamapForFrozenNonZeroWorkspaceReturnsFalse()
387 /** @var DataHandler $subject */
388 $subject = $this->getMock(DataHandler
::class, array('newlog'));
389 $this->backEndUser
->workspace
= 1;
390 $this->backEndUser
->workspaceRec
= array('freeze' => true);
391 $subject->BE_USER
= $this->backEndUser
;
392 $this->assertFalse($subject->process_datamap());
398 public function processDatamapWhenEditingRecordInWorkspaceCreatesNewRecordInWorkspace()
400 // Unset possible hooks on method under test
401 // @TODO: Can be removed if unit test boostrap is fixed to not load LocalConfiguration anymore
402 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] = array();
404 $GLOBALS['TCA'] = array(
406 'columns' => array(),
410 /** @var $subject DataHandler|\PHPUnit_Framework_MockObject_MockObject */
411 $subject = $this->getMock(
413 array('newlog', 'checkModifyAccessList', 'tableReadOnly', 'checkRecordUpdateAccess')
416 $subject->bypassWorkspaceRestrictions
= false;
417 $subject->datamap
= array(
424 $subject->expects($this->once())->method('checkModifyAccessList')->with('pages')->will($this->returnValue(true));
425 $subject->expects($this->once())->method('tableReadOnly')->with('pages')->will($this->returnValue(false));
426 $subject->expects($this->once())->method('checkRecordUpdateAccess')->will($this->returnValue(true));
428 /** @var BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject $backEndUser */
429 $backEndUser = $this->getMock(BackendUserAuthentication
::class);
430 $backEndUser->workspace
= 1;
431 $backEndUser->workspaceRec
= array('freeze' => false);
432 $backEndUser->expects($this->once())->method('workspaceAllowAutoCreation')->will($this->returnValue(true));
433 $backEndUser->expects($this->once())->method('workspaceCannotEditRecord')->will($this->returnValue(true));
434 $backEndUser->expects($this->once())->method('recordEditAccessInternals')->with('pages', 1)->will($this->returnValue(true));
435 $subject->BE_USER
= $backEndUser;
436 $createdTceMain = $this->getMock(DataHandler
::class, array());
437 $createdTceMain->expects($this->once())->method('start')->with(array(), array(
443 'label' => 'Auto-created for WS #1'
448 $createdTceMain->expects($this->never())->method('process_datamap');
449 $createdTceMain->expects($this->once())->method('process_cmdmap');
450 GeneralUtility
::addInstance(DataHandler
::class, $createdTceMain);
451 $subject->process_datamap();
457 public function doesCheckFlexFormValueHookGetsCalled()
459 $hookClass = $this->getUniqueId('tx_coretest');
460 $hookMock = $this->getMock($hookClass, array('checkFlexFormValue_beforeMerge'));
461 $hookMock->expects($this->once())->method('checkFlexFormValue_beforeMerge');
462 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'][] = $hookClass;
463 GeneralUtility
::addInstance($hookClass, $hookMock);
464 $this->subject
->_call('checkValueForFlex', [], [], [], '', 0, '', '', 0, 0, 0, [], '');
467 /////////////////////////////////////
468 // Tests concerning log
469 /////////////////////////////////////
473 public function logCallsWriteLogOfBackendUserIfLoggingIsEnabled()
475 $backendUser = $this->getMock(BackendUserAuthentication
::class);
476 $backendUser->expects($this->once())->method('writelog');
477 $this->subject
->enableLogging
= true;
478 $this->subject
->BE_USER
= $backendUser;
479 $this->subject
->log('', 23, 0, 42, 0, 'details');
485 public function logDoesNotCallWriteLogOfBackendUserIfLoggingIsDisabled()
487 $backendUser = $this->getMock(BackendUserAuthentication
::class);
488 $backendUser->expects($this->never())->method('writelog');
489 $this->subject
->enableLogging
= false;
490 $this->subject
->BE_USER
= $backendUser;
491 $this->subject
->log('', 23, 0, 42, 0, 'details');
497 public function logAddsEntryToLocalErrorLogArray()
499 $backendUser = $this->getMock(BackendUserAuthentication
::class);
500 $this->subject
->BE_USER
= $backendUser;
501 $this->subject
->enableLogging
= true;
502 $this->subject
->errorLog
= array();
503 $logDetailsUnique = $this->getUniqueId('details');
504 $this->subject
->log('', 23, 0, 42, 1, $logDetailsUnique);
505 $this->assertStringEndsWith($logDetailsUnique, $this->subject
->errorLog
[0]);
511 public function logFormatsDetailMessageWithAdditionalDataInLocalErrorArray()
513 $backendUser = $this->getMock(BackendUserAuthentication
::class);
514 $this->subject
->BE_USER
= $backendUser;
515 $this->subject
->enableLogging
= true;
516 $this->subject
->errorLog
= array();
517 $logDetails = $this->getUniqueId('details');
518 $this->subject
->log('', 23, 0, 42, 1, '%1$s' . $logDetails . '%2$s', -1, array('foo', 'bar'));
519 $expected = 'foo' . $logDetails . 'bar';
520 $this->assertStringEndsWith($expected, $this->subject
->errorLog
[0]);
524 * @param bool $expected
525 * @param string $submittedValue
526 * @param string $storedValue
527 * @param string $storedType
528 * @param bool $allowNull
530 * @dataProvider equalSubmittedAndStoredValuesAreDeterminedDataProvider
533 public function equalSubmittedAndStoredValuesAreDetermined($expected, $submittedValue, $storedValue, $storedType, $allowNull)
535 $result = $this->callInaccessibleMethod(
537 'isSubmittedValueEqualToStoredValue',
538 $submittedValue, $storedValue, $storedType, $allowNull
540 $this->assertEquals($expected, $result);
546 public function equalSubmittedAndStoredValuesAreDeterminedDataProvider()
550 'string value "" vs. ""' => array(
552 '', '', 'string', false
554 'string value 0 vs. "0"' => array(
556 0, '0', 'string', false
558 'string value 1 vs. "1"' => array(
560 1, '1', 'string', false
562 'string value "0" vs. ""' => array(
564 '0', '', 'string', false
566 'string value 0 vs. ""' => array(
568 0, '', 'string', false
570 'string value null vs. ""' => array(
572 null, '', 'string', false
575 'integer value 0 vs. 0' => array(
579 'integer value "0" vs. "0"' => array(
581 '0', '0', 'int', false
583 'integer value 0 vs. "0"' => array(
587 'integer value "" vs. "0"' => array(
589 '', '0', 'int', false
591 'integer value "" vs. 0' => array(
595 'integer value "0" vs. 0' => array(
599 'integer value 1 vs. 1' => array(
603 'integer value 1 vs. "1"' => array(
607 'integer value "1" vs. "1"' => array(
609 '1', '1', 'int', false
611 'integer value "1" vs. 1' => array(
615 'integer value "0" vs. "1"' => array(
617 '0', '1', 'int', false
619 // String with allowed NULL values
620 'string with allowed null value "" vs. ""' => array(
622 '', '', 'string', true
624 'string with allowed null value 0 vs. "0"' => array(
626 0, '0', 'string', true
628 'string with allowed null value 1 vs. "1"' => array(
630 1, '1', 'string', true
632 'string with allowed null value "0" vs. ""' => array(
634 '0', '', 'string', true
636 'string with allowed null value 0 vs. ""' => array(
638 0, '', 'string', true
640 'string with allowed null value null vs. ""' => array(
642 null, '', 'string', true
644 'string with allowed null value "" vs. null' => array(
646 '', null, 'string', true
648 'string with allowed null value null vs. null' => array(
650 null, null, 'string', true
652 // Integer with allowed NULL values
653 'integer with allowed null value 0 vs. 0' => array(
657 'integer with allowed null value "0" vs. "0"' => array(
659 '0', '0', 'int', true
661 'integer with allowed null value 0 vs. "0"' => array(
665 'integer with allowed null value "" vs. "0"' => array(
669 'integer with allowed null value "" vs. 0' => array(
673 'integer with allowed null value "0" vs. 0' => array(
677 'integer with allowed null value 1 vs. 1' => array(
681 'integer with allowed null value "1" vs. "1"' => array(
683 '1', '1', 'int', true
685 'integer with allowed null value "1" vs. 1' => array(
689 'integer with allowed null value 1 vs. "1"' => array(
693 'integer with allowed null value "0" vs. "1"' => array(
695 '0', '1', 'int', true
697 'integer with allowed null value null vs. ""' => array(
699 null, '', 'int', true
701 'integer with allowed null value "" vs. null' => array(
703 '', null, 'int', true
705 'integer with allowed null value null vs. null' => array(
707 null, null, 'int', true
709 'integer with allowed null value null vs. "0"' => array(
711 null, '0', 'int', true
713 'integer with allowed null value null vs. 0' => array(
717 'integer with allowed null value "0" vs. null' => array(
719 '0', null, 'int', true
725 * @param bool $expected
727 * @dataProvider getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditionsDataProvider
730 public function getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditions($expected, $eval)
732 $table = 'phpunit_dummy';
734 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $subject */
735 $subject = $this->getAccessibleMock(
740 $backendUser = $this->getMock(BackendUserAuthentication
::class);
741 $subject->BE_USER
= $backendUser;
742 $subject->BE_USER
->workspace
= 1;
744 $GLOBALS['TCA'][$table] = array();
745 $GLOBALS['TCA'][$table]['ctrl'] = array('label' => 'dummy');
746 $GLOBALS['TCA'][$table]['columns'] = array(
754 $this->assertEquals($expected, $subject->_call('getPlaceholderTitleForTableLabel', $table));
760 public function getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditionsDataProvider()
776 '[PLACEHOLDER, WS#1]',
785 public function deletePagesOnRootLevelIsDenied()
787 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $dataHandlerMock */
788 $dataHandlerMock = $this->getMock(DataHandler
::class, ['canDeletePage', 'newlog2']);
790 ->expects($this->never())
791 ->method('canDeletePage');
793 ->expects($this->once())
795 ->with('Deleting all pages starting from the root-page is disabled.', 'pages', 0, 0, 2);
797 $dataHandlerMock->deletePages(0);
803 public function deleteRecord_procBasedOnFieldTypeRespectsEnableCascadingDelete()
805 $table = $this->getUniqueId('foo_');
808 'foreign_table' => $this->getUniqueId('foreign_foo_'),
809 'behaviour' => array(
810 'enableCascadingDelete' => 0,
814 /** @var \TYPO3\CMS\Core\Database\RelationHandler $mockRelationHandler */
815 $mockRelationHandler = $this->getMock(\TYPO3\CMS\Core\Database\RelationHandler
::class, array(), array(), '', false);
816 $mockRelationHandler->itemArray
= array(
817 '1' => array('table' => $this->getUniqueId('bar_'), 'id' => 67)
820 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $mockDataHandler */
821 $mockDataHandler = $this->getAccessibleMock(DataHandler
::class, array('getInlineFieldType', 'deleteAction', 'createRelationHandlerInstance'), array(), '', false);
822 $mockDataHandler->expects($this->once())->method('getInlineFieldType')->will($this->returnValue('field'));
823 $mockDataHandler->expects($this->once())->method('createRelationHandlerInstance')->will($this->returnValue($mockRelationHandler));
824 $mockDataHandler->expects($this->never())->method('deleteAction');
825 $mockDataHandler->deleteRecord_procBasedOnFieldType($table, 42, 'foo', 'bar', $conf);
831 public function checkValue_checkReturnsExpectedValuesDataProvider()
834 'None item selected' => array(
838 'All items selected' => array(
842 'Item 1 and 2 are selected' => array(
846 'Value is higher than allowed' => array(
850 'Negative value' => array(
858 * @param string $value
859 * @param string $expectedValue
861 * @dataProvider checkValue_checkReturnsExpectedValuesDataProvider
864 public function checkValue_checkReturnsExpectedValues($value, $expectedValue)
866 $expectedResult = array(
867 'value' => $expectedValue
870 $tcaFieldConfiguration = array(
877 $this->assertSame($expectedResult, $this->subject
->_call('checkValueForCheck', $result, $value, $tcaFieldConfiguration, '', 0, 0, ''));
883 public function checkValueForInputConvertsNullToEmptyString()
885 $previousLanguageService = $GLOBALS['LANG'];
886 $GLOBALS['LANG'] = GeneralUtility
::makeInstance(\TYPO3\CMS\Lang\LanguageService
::class);
887 $GLOBALS['LANG']->init('default');
888 $expectedResult = array('value' => '');
889 $this->assertSame($expectedResult, $this->subject
->_call('checkValueForInput', null, array('type' => 'string', 'max' => 40), 'tt_content', 'NEW55c0e67f8f4d32.04974534', 89, 'table_caption'));
890 $GLOBALS['LANG'] = $previousLanguageService;
894 * @param mixed $value
895 * @param array $configuration
896 * @param int|string $expected
898 * @dataProvider referenceValuesAreCastedDataProvider
900 public function referenceValuesAreCasted($value, array $configuration, $expected)
904 $this->subject
->_call('castReferenceValue', $value, $configuration)
911 public function referenceValuesAreCastedDataProvider()
914 'all empty' => array(
917 'cast zero with MM table' => array(
918 '', array('MM' => 'table'), 0
920 'cast zero with MM table with default value' => array(
921 '', array('MM' => 'table', 'default' => 13), 0
923 'cast zero with foreign field' => array(
924 '', array('foreign_field' => 'table', 'default' => 13), 0
926 'cast zero with foreign field with default value' => array(
927 '', array('foreign_field' => 'table'), 0
929 'pass zero' => array(
932 'pass value' => array(
933 '1', array('default' => 13), '1'
935 'use default value' => array(
936 '', array('default' => 13), 13