[TASK] Improve duplicate exception code check
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Tests / Testbase.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
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\DriverManager;
19 use TYPO3\CMS\Core\Core\Bootstrap;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
22 use TYPO3\CMS\Core\Database\Schema\SqlReader;
23 use TYPO3\CMS\Core\Utility\ArrayUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26 /**
27 * This is a helper class used by unit, functional and acceptance test
28 * environment builders.
29 * It contains methods to create test environments.
30 *
31 * This class is for internal use only and may change wihtout further notice.
32 *
33 * Use the classes "UnitTestCase", "FunctionalTestCase" or "AcceptanceCoreEnvironment"
34 * to indirectly benefit from this class in own extensions.
35 */
36 class Testbase
37 {
38 /**
39 * This class must be called in CLI environment as a security measure
40 * against path disclosures and other stuff. Check this within
41 * constructor to make sure this check can't be circumvented.
42 */
43 public function __construct()
44 {
45 // Ensure cli only as security measure
46 if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
47 die('This script supports command line usage only. Please check your command.');
48 }
49 }
50
51 /**
52 * Makes sure error messages during the tests get displayed no matter what is set in php.ini.
53 *
54 * @return void
55 */
56 public function enableDisplayErrors()
57 {
58 @ini_set('display_errors', 1);
59 }
60
61 /**
62 * Defines a list of basic constants that are used by GeneralUtility and other
63 * helpers during tests setup. Those are sanitized in SystemEnvironmentBuilder
64 * to be not defined again.
65 *
66 * @return void
67 * @see SystemEnvironmentBuilder::defineBaseConstants()
68 */
69 public function defineBaseConstants()
70 {
71 // A null, a tabulator, a linefeed, a carriage return, a substitution, a CR-LF combination
72 defined('NUL') ?: define('NUL', chr(0));
73 defined('TAB') ?: define('TAB', chr(9));
74 defined('LF') ?: define('LF', chr(10));
75 defined('CR') ?: define('CR', chr(13));
76 defined('SUB') ?: define('SUB', chr(26));
77 defined('CRLF') ?: define('CRLF', CR . LF);
78
79 if (!defined('TYPO3_OS')) {
80 // Operating system identifier
81 // Either "WIN" or empty string
82 $typoOs = '';
83 if (!stristr(PHP_OS, 'darwin') && !stristr(PHP_OS, 'cygwin') && stristr(PHP_OS, 'win')) {
84 $typoOs = 'WIN';
85 }
86 define('TYPO3_OS', $typoOs);
87 }
88 }
89
90 /**
91 * Defines the PATH_site and PATH_thisScript constant and sets $_SERVER['SCRIPT_NAME'].
92 * For unit tests only
93 *
94 * @return void
95 */
96 public function defineSitePath()
97 {
98 define('PATH_site', $this->getWebRoot());
99 define('PATH_thisScript', PATH_site . 'typo3/cli_dispatch.phpsh');
100 $_SERVER['SCRIPT_NAME'] = PATH_thisScript;
101
102 if (!file_exists(PATH_thisScript)) {
103 $this->exitWithMessage('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.');
104 }
105 }
106
107 /**
108 * Defines the constant ORIGINAL_ROOT for the path to the original TYPO3 document root.
109 * For functional / acceptance tests only
110 * If ORIGINAL_ROOT already is defined, this method is a no-op.
111 *
112 * @return void
113 */
114 public function defineOriginalRootPath()
115 {
116 if (!defined('ORIGINAL_ROOT')) {
117 define('ORIGINAL_ROOT', $this->getWebRoot());
118 }
119
120 if (!file_exists(ORIGINAL_ROOT . 'typo3/cli_dispatch.phpsh')) {
121 $this->exitWithMessage('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.');
122 }
123 }
124
125 /**
126 * Define TYPO3_MODE to BE
127 *
128 * @return void
129 */
130 public function defineTypo3ModeBe()
131 {
132 define('TYPO3_MODE', 'BE');
133 }
134
135 /**
136 * Sets the environment variable TYPO3_CONTEXT to testing.
137 *
138 * @return void
139 */
140 public function setTypo3TestingContext()
141 {
142 putenv('TYPO3_CONTEXT=Testing');
143 }
144
145 /**
146 * Creates directories, recursively if required.
147 *
148 * @param string $directory Absolute path to directories to create
149 * @return void
150 * @throws Exception
151 */
152 public function createDirectory($directory)
153 {
154 if (is_dir($directory)) {
155 return;
156 }
157 @mkdir($directory, 0777, true);
158 clearstatcache();
159 if (!is_dir($directory)) {
160 throw new Exception('Directory "' . $directory . '" could not be created', 1404038665);
161 }
162 }
163
164 /**
165 * Checks whether given test instance exists in path and is younger than some minutes.
166 * Used in functional tests
167 *
168 * @param string $instancePath Absolute path to test instance
169 * @return bool
170 */
171 public function recentTestInstanceExists($instancePath)
172 {
173 if (@file_get_contents($instancePath . '/last_run.txt') <= (time() - 300)) {
174 return false;
175 } else {
176 // Test instance exists and is pretty young -> re-use
177 return true;
178 }
179 }
180
181 /**
182 * Remove test instance folder structure if it exists.
183 * This may happen if a functional test before threw a fatal or is too old
184 *
185 * @param string $instancePath Absolute path to test instance
186 * @return void
187 * @throws Exception
188 */
189 public function removeOldInstanceIfExists($instancePath)
190 {
191 if (is_dir($instancePath)) {
192 $success = GeneralUtility::rmdir($instancePath, true);
193 if (!$success) {
194 throw new Exception(
195 'Can not remove folder: ' . $instancePath,
196 1376657210
197 );
198 }
199 }
200 }
201
202 /**
203 * Create last_run.txt file within instance path containing timestamp of "now".
204 * Used in functional tests to reuse an instance for multiple tests in one test case.
205 *
206 * @param string $instancePath Absolute path to test instance
207 * @return void
208 */
209 public function createLastRunTextfile($instancePath)
210 {
211 // Store the time instance was created
212 file_put_contents($instancePath . '/last_run.txt', time());
213 }
214
215 /**
216 * Link TYPO3 CMS core from "parent" instance.
217 * For functional and acceptance tests.
218 *
219 * @param string $instancePath Absolute path to test instance
220 * @throws Exception
221 * @return void
222 */
223 public function setUpInstanceCoreLinks($instancePath)
224 {
225 $linksToSet = [
226 ORIGINAL_ROOT . 'typo3' => $instancePath . '/typo3',
227 ORIGINAL_ROOT . 'index.php' => $instancePath . '/index.php'
228 ];
229 foreach ($linksToSet as $from => $to) {
230 $success = symlink($from, $to);
231 if (!$success) {
232 throw new Exception(
233 'Creating link failed: from ' . $from . ' to: ' . $to,
234 1376657199
235 );
236 }
237 }
238 }
239
240 /**
241 * Link test extensions to the typo3conf/ext folder of the instance.
242 * For functional and acceptance tests.
243 *
244 * @param string $instancePath Absolute path to test instance
245 * @param array $extensionPaths Contains paths to extensions relative to document root
246 * @throws Exception
247 * @return void
248 */
249 public function linkTestExtensionsToInstance($instancePath, array $extensionPaths)
250 {
251 foreach ($extensionPaths as $extensionPath) {
252 $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath;
253 if (!is_dir($absoluteExtensionPath)) {
254 throw new Exception(
255 'Test extension path ' . $absoluteExtensionPath . ' not found',
256 1376745645
257 );
258 }
259 $destinationPath = $instancePath . '/typo3conf/ext/' . basename($absoluteExtensionPath);
260 $success = symlink($absoluteExtensionPath, $destinationPath);
261 if (!$success) {
262 throw new Exception(
263 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
264 1376657142
265 );
266 }
267 }
268 }
269
270 /**
271 * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the
272 * test instance fileadmin folder.
273 * For functional and acceptance tests.
274 *
275 * @param string $instancePath Absolute path to test instance
276 * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root
277 * @throws Exception if a source path could not be found and on failing creating the symlink
278 * @return void
279 */
280 public function linkPathsInTestInstance($instancePath, array $pathsToLinkInTestInstance)
281 {
282 foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
283 $sourcePath = $instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/');
284 if (!file_exists($sourcePath)) {
285 throw new Exception(
286 'Path ' . $sourcePath . ' not found',
287 1476109221
288 );
289 }
290 $destinationPath = $instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/');
291 $success = symlink($sourcePath, $destinationPath);
292 if (!$success) {
293 throw new Exception(
294 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
295 1389969623
296 );
297 }
298 }
299 }
300
301 /**
302 * Database settings for functional and acceptance tests can be either set by
303 * environment variables (recommended), or from an existing LocalConfiguration as fallback.
304 * The method fetches these.
305 *
306 * An unique name will be added to the database name later.
307 *
308 * @throws Exception
309 * @return array [DB][host], [DB][username], ...
310 */
311 public function getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration()
312 {
313 $databaseName = trim(getenv('typo3DatabaseName'));
314 $databaseHost = trim(getenv('typo3DatabaseHost'));
315 $databaseUsername = trim(getenv('typo3DatabaseUsername'));
316 $databasePassword = trim(getenv('typo3DatabasePassword'));
317 $databasePort = trim(getenv('typo3DatabasePort'));
318 $databaseSocket = trim(getenv('typo3DatabaseSocket'));
319 if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) {
320 // Try to get database credentials from environment variables first
321 $originalConfigurationArray = [
322 'DB' => [
323 'Connections' => [
324 'Default' => [
325 'driver' => 'mysqli'
326 ],
327 ],
328 ],
329 ];
330 if ($databaseName) {
331 $originalConfigurationArray['DB']['Connections']['Default']['dbname'] = $databaseName;
332 }
333 if ($databaseHost) {
334 $originalConfigurationArray['DB']['Connections']['Default']['host'] = $databaseHost;
335 }
336 if ($databaseUsername) {
337 $originalConfigurationArray['DB']['Connections']['Default']['user'] = $databaseUsername;
338 }
339 if ($databasePassword) {
340 $originalConfigurationArray['DB']['Connections']['Default']['password'] = $databasePassword;
341 }
342 if ($databasePort) {
343 $originalConfigurationArray['DB']['Connections']['Default']['port'] = $databasePort;
344 }
345 if ($databaseSocket) {
346 $originalConfigurationArray['DB']['Connections']['Default']['unix_socket'] = $databaseSocket;
347 }
348 } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) {
349 // See if a LocalConfiguration file exists in "parent" instance to get db credentials from
350 $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php';
351 } else {
352 throw new Exception(
353 'Database credentials for tests are neither set through environment'
354 . ' variables, and can not be found in an existing LocalConfiguration file',
355 1397406356
356 );
357 }
358 return $originalConfigurationArray;
359 }
360
361 /**
362 * Maximum length of database names is 64 chars in mysql. Test this is not exceeded
363 * after a suffix has been added.
364 *
365 * @param string $originalDatabaseName Base name of the database
366 * @param array $configuration "LocalConfiguration" array with DB settings
367 * @throws Exception
368 */
369 public function testDatabaseNameIsNotTooLong($originalDatabaseName, array $configuration)
370 {
371 // Maximum database name length for mysql is 64 characters
372 if (strlen($configuration['DB']['Connections']['Default']['dbname']) > 64) {
373 $suffixLength = strlen($configuration['DB']['Connections']['Default']['dbname']) - strlen($originalDatabaseName);
374 $maximumOriginalDatabaseName = 64 - $suffixLength;
375 throw new Exception(
376 'The name of the database that is used for the functional test (' . $originalDatabaseName . ')' .
377 ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
378 ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
379 1377600104
380 );
381 }
382 }
383
384 /**
385 * Create LocalConfiguration.php file of the test instance.
386 * For functional and acceptance tests.
387 *
388 * @param string $instancePath Absolute path to test instance
389 * @param array $configuration Base configuration array
390 * @param array $overruleConfiguration Overrule factory and base configuration
391 * @throws Exception
392 * @return void
393 */
394 public function setUpLocalConfiguration($instancePath, array $configuration, array $overruleConfiguration)
395 {
396 // Base of final LocalConfiguration is core factory configuration
397 $finalConfigurationArray = require ORIGINAL_ROOT . 'typo3/sysext/core/Configuration/FactoryConfiguration.php';
398 ArrayUtility::mergeRecursiveWithOverrule($finalConfigurationArray, $configuration);
399 ArrayUtility::mergeRecursiveWithOverrule($finalConfigurationArray, $overruleConfiguration);
400 $result = $this->writeFile(
401 $instancePath . '/typo3conf/LocalConfiguration.php',
402 '<?php' . chr(10) .
403 'return ' .
404 ArrayUtility::arrayExport(
405 $finalConfigurationArray
406 ) .
407 ';'
408 );
409 if (!$result) {
410 throw new Exception('Can not write local configuration', 1376657277);
411 }
412 }
413
414 /**
415 * Compile typo3conf/PackageStates.php containing default packages like core,
416 * a test specific list of additional core extensions, and a list of
417 * test extensions.
418 * For functional and acceptance tests.
419 *
420 * @param string $instancePath Absolute path to test instance
421 * @param array $defaultCoreExtensionsToLoad Default list of core extensions to load
422 * @param array $additionalCoreExtensionsToLoad Additional core extensions to load
423 * @param array $testExtensionPaths Paths to extensions relative to document root
424 * @throws Exception
425 */
426 public function setUpPackageStates(
427 $instancePath,
428 array $defaultCoreExtensionsToLoad,
429 array $additionalCoreExtensionsToLoad,
430 array $testExtensionPaths
431 ) {
432 $packageStates = [
433 'packages' => [],
434 'version' => 5,
435 ];
436
437 // Register default list of extensions and set active
438 foreach ($defaultCoreExtensionsToLoad as $extensionName) {
439 $packageStates['packages'][$extensionName] = [
440 'packagePath' => 'typo3/sysext/' . $extensionName . '/'
441 ];
442 }
443
444 // Register additional core extensions and set active
445 foreach ($additionalCoreExtensionsToLoad as $extensionName) {
446 $packageStates['packages'][$extensionName] = [
447 'packagePath' => 'typo3/sysext/' . $extensionName . '/'
448 ];
449 }
450
451 // Activate test extensions that have been symlinked before
452 foreach ($testExtensionPaths as $extensionPath) {
453 $extensionName = basename($extensionPath);
454 $packageStates['packages'][$extensionName] = [
455 'packagePath' => 'typo3conf/ext/' . $extensionName . '/'
456 ];
457 }
458
459 $result = $this->writeFile(
460 $instancePath . '/typo3conf/PackageStates.php',
461 '<?php' . chr(10) .
462 'return ' .
463 ArrayUtility::arrayExport(
464 $packageStates
465 ) .
466 ';'
467 );
468
469 if (!$result) {
470 throw new Exception('Can not write PackageStates', 1381612729);
471 }
472 }
473
474 /**
475 * Create a low level connection to dbms, without selecting the target database.
476 * Drop existing database if it exists and create a new one.
477 *
478 * @param string $databaseName Database name of this test instance
479 * @param string $originalDatabaseName Original database name before suffix was added
480 * @throws \TYPO3\CMS\Core\Tests\Exception
481 * @return void
482 */
483 public function setUpTestDatabase($databaseName, $originalDatabaseName)
484 {
485 Bootstrap::getInstance()->initializeTypo3DbGlobal();
486
487 // Drop database if exists. Directly using the Doctrine DriverManager to
488 // work around connection caching in ConnectionPool
489 $connectionParameters = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default'];
490 unset($connectionParameters['dbname']);
491 $schemaManager = DriverManager::getConnection($connectionParameters)->getSchemaManager();
492
493 if (in_array($databaseName, $schemaManager->listDatabases(), true)) {
494 $schemaManager->dropDatabase($databaseName);
495 }
496
497 try {
498 $schemaManager->createDatabase($databaseName);
499 } catch (DBALException $e) {
500 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'];
501 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'];
502 throw new Exception(
503 'Unable to create database with name ' . $databaseName . '. This is probably a permission problem.'
504 . ' For this instance this could be fixed executing:'
505 . ' GRANT ALL ON `' . $originalDatabaseName . '_%`.* TO `' . $user . '`@`' . $host . '`;'
506 . ' Original message thrown by database layer: ' . $e->getMessage(),
507 1376579070
508 );
509 }
510 }
511
512 /**
513 * Bootstrap basic TYPO3. This bootstraps TYPO3 far enough to initialize database afterwards.
514 * For functional and acceptance tests.
515 *
516 * @param string $instancePath Absolute path to test instance
517 * @return void
518 */
519 public function setUpBasicTypo3Bootstrap($instancePath)
520 {
521 $_SERVER['PWD'] = $instancePath;
522 $_SERVER['argv'][0] = 'index.php';
523
524 $classLoader = require rtrim(realpath($instancePath . '/typo3'), '\\/') . '/../vendor/autoload.php';
525 Bootstrap::getInstance()
526 ->initializeClassLoader($classLoader)
527 ->setRequestType(TYPO3_REQUESTTYPE_BE | TYPO3_REQUESTTYPE_CLI)
528 ->baseSetup()
529 ->loadConfigurationAndInitialize(true)
530 ->loadTypo3LoadedExtAndExtLocalconf(true)
531 ->setFinalCachingFrameworkCacheConfiguration()
532 ->defineLoggingAndExceptionConstants()
533 ->unsetReservedGlobalVariables();
534 }
535
536 /**
537 * Truncate all tables.
538 * For functional and acceptance tests.
539 *
540 * @throws Exception
541 * @return void
542 */
543 public function initializeTestDatabaseAndTruncateTables()
544 {
545 Bootstrap::getInstance()->initializeTypo3DbGlobal();
546
547 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
548 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
549 $schemaManager = $connection->getSchemaManager();
550
551 foreach ($schemaManager->listTables() as $table) {
552 $connection->truncate($table->getName());
553 }
554 }
555
556 /**
557 * Load ext_tables.php files.
558 * For functional and acceptance tests.
559 *
560 * @return void
561 */
562 public function loadExtensionTables()
563 {
564 Bootstrap::getInstance()->loadExtensionTables();
565 }
566
567 /**
568 * Create tables and import static rows.
569 * For functional and acceptance tests.
570 *
571 * @return void
572 */
573 public function createDatabaseStructure()
574 {
575 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
576 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
577 $sqlCode = $sqlReader->getTablesDefinitionString(true);
578
579 $createTableStatements = $sqlReader->getCreateTableStatementArray($sqlCode);
580
581 $schemaMigrationService->install($createTableStatements);
582
583 $insertStatements = $sqlReader->getInsertStatementArray($sqlCode);
584 $schemaMigrationService->importStaticData($insertStatements);
585 }
586
587 /**
588 * Imports a data set represented as XML into the test database,
589 *
590 * @param string $path Absolute path to the XML file containing the data set to load
591 * @return void
592 * @throws Exception
593 */
594 public function importXmlDatabaseFixture($path)
595 {
596 if (!is_file($path)) {
597 throw new Exception(
598 'Fixture file ' . $path . ' not found',
599 1376746261
600 );
601 }
602
603 $fileContent = file_get_contents($path);
604 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
605 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
606 $xml = simplexml_load_string($fileContent);
607 libxml_disable_entity_loader($previousValueOfEntityLoader);
608 $foreignKeys = [];
609
610 /** @var $table \SimpleXMLElement */
611 foreach ($xml->children() as $table) {
612 $insertArray = [];
613
614 /** @var $column \SimpleXMLElement */
615 foreach ($table->children() as $column) {
616 $columnName = $column->getName();
617 $columnValue = null;
618
619 if (isset($column['ref'])) {
620 list($tableName, $elementId) = explode('#', $column['ref']);
621 $columnValue = $foreignKeys[$tableName][$elementId];
622 } elseif (isset($column['is-NULL']) && ($column['is-NULL'] === 'yes')) {
623 $columnValue = null;
624 } else {
625 $columnValue = (string)$table->$columnName;
626 }
627
628 $insertArray[$columnName] = $columnValue;
629 }
630
631 $tableName = $table->getName();
632 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
633 ->getConnectionForTable($tableName);
634 $connection->insert(
635 $tableName,
636 $insertArray
637 );
638 if (isset($table['id'])) {
639 $elementId = (string)$table['id'];
640 $foreignKeys[$tableName][$elementId] = $connection->lastInsertId($tableName);
641 }
642 }
643 }
644
645 /**
646 * Returns the absolute path the TYPO3 document root.
647 * This is the "original" document root, not the "instance" root for functional / acceptance tests.
648 *
649 * @return string the TYPO3 document root using Unix path separators
650 */
651 protected function getWebRoot()
652 {
653 if (getenv('TYPO3_PATH_WEB')) {
654 $webRoot = getenv('TYPO3_PATH_WEB');
655 } else {
656 $webRoot = getcwd();
657 }
658 return rtrim(strtr($webRoot, '\\', '/'), '/') . '/';
659 }
660
661 /**
662 * Send http headers, echo out a text message and exit with error code
663 *
664 * @param string $message
665 */
666 protected function exitWithMessage($message)
667 {
668 echo $message . LF;
669 exit(1);
670 }
671
672 /**
673 * Writes $content to the file $file. This is a simplified version
674 * of GeneralUtility::writeFile that does not fix permissions.
675 *
676 * @param string $file Filepath to write to
677 * @param string $content Content to write
678 * @return bool TRUE if the file was successfully opened and written to.
679 */
680 protected function writeFile($file, $content)
681 {
682 if ($fd = fopen($file, 'wb')) {
683 $res = fwrite($fd, $content);
684 fclose($fd);
685 if ($res === false) {
686 return false;
687 }
688 return true;
689 }
690 return false;
691 }
692 }