[BUGFIX] Reload classAliasMap after extension installation
[Packages/TYPO3.CMS.git] / typo3 / sysext / extensionmanager / Classes / Utility / InstallUtility.php
1 <?php
2 namespace TYPO3\CMS\Extensionmanager\Utility;
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 TYPO3\CMS\Impexp\Utility\ImportExportUtility;
18
19 /**
20 * Extension Manager Install Utility
21 *
22 * @author Susanne Moog <susanne.moog@typo3.org>
23 */
24 class InstallUtility implements \TYPO3\CMS\Core\SingletonInterface {
25
26 /**
27 * @var \TYPO3\CMS\Extbase\Object\ObjectManager
28 * @inject
29 */
30 public $objectManager;
31
32 /**
33 * @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService
34 * @inject
35 */
36 public $installToolSqlParser;
37
38 /**
39 * @var \TYPO3\CMS\Extensionmanager\Utility\DependencyUtility
40 * @inject
41 */
42 protected $dependencyUtility;
43
44 /**
45 * @var \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility
46 * @inject
47 */
48 protected $fileHandlingUtility;
49
50 /**
51 * @var \TYPO3\CMS\Extensionmanager\Utility\ListUtility
52 * @inject
53 */
54 protected $listUtility;
55
56 /**
57 * @var \TYPO3\CMS\Extensionmanager\Utility\DatabaseUtility
58 * @inject
59 */
60 protected $databaseUtility;
61
62 /**
63 * @var \TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository
64 * @inject
65 */
66 public $extensionRepository;
67
68 /**
69 * @var \TYPO3\CMS\Core\Package\PackageManager
70 * @inject
71 */
72 protected $packageManager;
73
74 /**
75 * @var \TYPO3\CMS\Core\Cache\CacheManager
76 * @inject
77 */
78 protected $cacheManager;
79
80 /**
81 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
82 * @inject
83 */
84 protected $signalSlotDispatcher;
85
86 /**
87 * @var \TYPO3\CMS\Core\Registry
88 * @inject
89 */
90 protected $registry;
91
92 /**
93 * Helper function to install an extension
94 * also processes db updates and clears the cache if the extension asks for it
95 *
96 * @param string $extensionKey
97 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
98 * @return void
99 */
100 public function install($extensionKey) {
101 $extension = $this->enrichExtensionWithDetails($extensionKey);
102 $this->ensureConfiguredDirectoriesExist($extension);
103 if (!$this->isLoaded($extensionKey)) {
104 $this->loadExtension($extensionKey);
105 }
106 if (!empty($extension['clearcacheonload']) || !empty($extension['clearCacheOnLoad'])) {
107 $this->cacheManager->flushCaches();
108 } else {
109 $this->cacheManager->flushCachesInGroup('system');
110 }
111 // Reload class aliases defined in Migrations/Code/ClassAliasMap.php
112 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->getEarlyInstance(\TYPO3\CMS\Core\Core\ClassLoader::class)
113 ->setPackages($this->packageManager->getActivePackages());
114 $this->reloadCaches();
115
116 $this->importInitialFiles($extension['siteRelPath'], $extensionKey);
117 $this->processDatabaseUpdates($extension);
118 $this->processRuntimeDatabaseUpdates($extensionKey);
119 $this->saveDefaultConfiguration($extension['key']);
120
121 $this->emitAfterExtensionInstallSignal($extensionKey);
122 }
123
124 /**
125 * Helper function to uninstall an extension
126 *
127 * @param string $extensionKey
128 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
129 * @return void
130 */
131 public function uninstall($extensionKey) {
132 $dependentExtensions = $this->dependencyUtility->findInstalledExtensionsThatDependOnMe($extensionKey);
133 if (is_array($dependentExtensions) && count($dependentExtensions) > 0) {
134 throw new \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException(
135 \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate(
136 'extensionList.uninstall.dependencyError',
137 'extensionmanager',
138 array($extensionKey, implode(',', $dependentExtensions))
139 ),
140 1342554622
141 );
142 } else {
143 $this->unloadExtension($extensionKey);
144 }
145 }
146
147 /**
148 * Wrapper function to check for loaded extensions
149 *
150 * @param string $extensionKey
151 * @return bool TRUE if extension is loaded
152 */
153 public function isLoaded($extensionKey) {
154 return $this->packageManager->isPackageActive($extensionKey);
155 }
156
157 /**
158 * Wrapper function for loading extensions
159 *
160 * @param string $extensionKey
161 * @return void
162 */
163 protected function loadExtension($extensionKey) {
164 $this->packageManager->activatePackage($extensionKey);
165 }
166
167 /**
168 * Wrapper function for unloading extensions
169 *
170 * @param string $extensionKey
171 * @return void
172 */
173 protected function unloadExtension($extensionKey) {
174 $this->packageManager->deactivatePackage($extensionKey);
175 $this->emitAfterExtensionUninstallSignal($extensionKey);
176 $this->cacheManager->flushCachesInGroup('system');
177 }
178
179 /**
180 * Emits a signal after an extension has been installed
181 *
182 * @param string $extensionKey
183 */
184 protected function emitAfterExtensionInstallSignal($extensionKey) {
185 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionInstall', array($extensionKey, $this));
186 }
187
188 /**
189 * Emits a signal after an extension has been uninstalled
190 *
191 * @param string $extensionKey
192 */
193 protected function emitAfterExtensionUninstallSignal($extensionKey) {
194 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionUninstall', array($extensionKey, $this));
195 }
196
197 /**
198 * Checks if an extension is available in the system
199 *
200 * @param string $extensionKey
201 * @return bool
202 */
203 public function isAvailable($extensionKey) {
204 return $this->packageManager->isPackageAvailable($extensionKey);
205 }
206
207 /**
208 * Fetch additional information for an extension key
209 *
210 * @param string $extensionKey
211 * @access private
212 * @return array
213 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
214 */
215 public function enrichExtensionWithDetails($extensionKey) {
216 $availableExtensions = $this->listUtility->getAvailableExtensions();
217 if (isset($availableExtensions[$extensionKey])) {
218 $extension = $availableExtensions[$extensionKey];
219 } else {
220 throw new \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException('Extension ' . $extensionKey . ' is not available', 1342864081);
221 }
222 $availableAndInstalledExtensions = $this->listUtility->enrichExtensionsWithEmConfAndTerInformation(array($extensionKey => $extension));
223
224 if (!isset($availableAndInstalledExtensions[$extensionKey])) {
225 throw new \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException(
226 'Please check your uploaded extension "' . $extensionKey . '". The configuration file "ext_emconf.php" seems to be invalid.',
227 1391432222
228 );
229 }
230
231 return $availableAndInstalledExtensions[$extensionKey];
232 }
233
234 /**
235 * Creates directories as requested in ext_emconf.php
236 *
237 * @param array $extension
238 */
239 protected function ensureConfiguredDirectoriesExist(array $extension) {
240 $this->fileHandlingUtility->ensureConfiguredDirectoriesExist($extension);
241 }
242
243 /**
244 * Gets the content of the ext_tables.sql and ext_tables_static+adt.sql files
245 * Additionally adds the table definitions for the cache tables
246 *
247 * @param array $extension
248 */
249 public function processDatabaseUpdates(array $extension) {
250 $extTablesSqlFile = PATH_site . $extension['siteRelPath'] . 'ext_tables.sql';
251 $extTablesSqlContent = '';
252 if (file_exists($extTablesSqlFile)) {
253 $extTablesSqlContent .= \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($extTablesSqlFile);
254 }
255 if ($extTablesSqlContent !== '') {
256 $this->updateDbWithExtTablesSql($extTablesSqlContent);
257 }
258
259 $this->importStaticSqlFile($extension['siteRelPath']);
260 $this->importT3DFile($extension['siteRelPath']);
261 }
262
263 /**
264 * Gets all database updates due to runtime configuration, like caching framework or
265 * category api for example
266 *
267 * @param string $extensionKey
268 */
269 protected function processRuntimeDatabaseUpdates($extensionKey) {
270 $sqlString = $this->emitTablesDefinitionIsBeingBuiltSignal($extensionKey);
271 if (!empty($sqlString)) {
272 $this->updateDbWithExtTablesSql(implode(LF . LF . LF . LF, $sqlString));
273 }
274 }
275
276 /**
277 * Emits a signal to manipulate the tables definitions
278 *
279 * @param string $extensionKey
280 * @return mixed
281 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
282 */
283 protected function emitTablesDefinitionIsBeingBuiltSignal($extensionKey) {
284 $signalReturn = $this->signalSlotDispatcher->dispatch(__CLASS__, 'tablesDefinitionIsBeingBuilt', array(array(), $extensionKey));
285 // This is important to support old associated returns
286 $signalReturn = array_values($signalReturn);
287 $sqlString = $signalReturn[0];
288 if (!is_array($sqlString)) {
289 throw new \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException(
290 sprintf(
291 'The signal %s of class %s returned a value of type %s, but array was expected.',
292 'tablesDefinitionIsBeingBuilt',
293 __CLASS__,
294 gettype($sqlString)
295 ),
296 1382360258
297 );
298 }
299 return $sqlString;
300 }
301
302 /**
303 * Reload Cache files and Typo3LoadedExtensions
304 *
305 * @return void
306 */
307 public function reloadCaches() {
308 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::loadExtLocalconf(FALSE);
309 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(FALSE);
310 }
311
312 /**
313 * Save default configuration of an extension
314 *
315 * @param string $extensionKey
316 * @return void
317 */
318 protected function saveDefaultConfiguration($extensionKey) {
319 /** @var $configUtility \TYPO3\CMS\Extensionmanager\Utility\ConfigurationUtility */
320 $configUtility = $this->objectManager->get(\TYPO3\CMS\Extensionmanager\Utility\ConfigurationUtility::class);
321 $configUtility->saveDefaultConfiguration($extensionKey);
322 }
323
324 /**
325 * Update database / process db updates from ext_tables
326 *
327 * @param string $rawDefinitions The raw SQL statements from ext_tables.sql
328 * @return void
329 */
330 public function updateDbWithExtTablesSql($rawDefinitions) {
331 $fieldDefinitionsFromFile = $this->installToolSqlParser->getFieldDefinitions_fileContent($rawDefinitions);
332 if (count($fieldDefinitionsFromFile)) {
333 $fieldDefinitionsFromCurrentDatabase = $this->installToolSqlParser->getFieldDefinitions_database();
334 $diff = $this->installToolSqlParser->getDatabaseExtra($fieldDefinitionsFromFile, $fieldDefinitionsFromCurrentDatabase);
335 $updateStatements = $this->installToolSqlParser->getUpdateSuggestions($diff);
336 $db = $this->getDatabaseConnection();
337 foreach ((array)$updateStatements['add'] as $string) {
338 $db->admin_query($string);
339 }
340 foreach ((array)$updateStatements['change'] as $string) {
341 $db->admin_query($string);
342 }
343 foreach ((array)$updateStatements['create_table'] as $string) {
344 $db->admin_query($string);
345 }
346 }
347 }
348
349 /**
350 * Import static SQL data (normally used for ext_tables_static+adt.sql)
351 *
352 * @param string $rawDefinitions
353 * @return void
354 */
355 public function importStaticSql($rawDefinitions) {
356 $statements = $this->installToolSqlParser->getStatementarray($rawDefinitions, 1);
357 list($statementsPerTable, $insertCount) = $this->installToolSqlParser->getCreateTables($statements, 1);
358 $db = $this->getDatabaseConnection();
359 // Traverse the tables
360 foreach ($statementsPerTable as $table => $query) {
361 $db->admin_query('DROP TABLE IF EXISTS ' . $table);
362 $db->admin_query($query);
363 if ($insertCount[$table]) {
364 $insertStatements = $this->installToolSqlParser->getTableInsertStatements($statements, $table);
365 foreach ($insertStatements as $statement) {
366 $db->admin_query($statement);
367 }
368 }
369 }
370 }
371
372 /**
373 * Remove an extension (delete the directory)
374 *
375 * @param string $extension
376 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
377 * @return void
378 */
379 public function removeExtension($extension) {
380 $absolutePath = $this->fileHandlingUtility->getAbsoluteExtensionPath($extension);
381 if ($this->fileHandlingUtility->isValidExtensionPath($absolutePath)) {
382 if ($this->packageManager->isPackageAvailable($extension)) {
383 // Package manager deletes the extension and removes the entry from PackageStates.php
384 $this->packageManager->deletePackage($extension);
385 } else {
386 // The extension is not listed in PackageStates.php, we can safely remove it
387 $this->fileHandlingUtility->removeDirectory($absolutePath);
388 }
389 } else {
390 throw new \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException('No valid extension path given.', 1342875724);
391 }
392 }
393
394 /**
395 * Get the data dump for an extension
396 *
397 * @param string $extension
398 * @return array
399 */
400 public function getExtensionSqlDataDump($extension) {
401 $extension = $this->enrichExtensionWithDetails($extension);
402 $filePrefix = PATH_site . $extension['siteRelPath'];
403 $sqlData['extTables'] = $this->getSqlDataDumpForFile($filePrefix . 'ext_tables.sql');
404 $sqlData['staticSql'] = $this->getSqlDataDumpForFile($filePrefix . 'ext_tables_static+adt.sql');
405 return $sqlData;
406 }
407
408 /**
409 * Gets the sql data dump for a specific sql file (for example ext_tables.sql)
410 *
411 * @param string $sqlFile
412 * @return string
413 */
414 protected function getSqlDataDumpForFile($sqlFile) {
415 $sqlData = '';
416 if (file_exists($sqlFile)) {
417 $sqlContent = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($sqlFile);
418 $fieldDefinitions = $this->installToolSqlParser->getFieldDefinitions_fileContent($sqlContent);
419 $sqlData = $this->databaseUtility->dumpStaticTables($fieldDefinitions);
420 }
421 return $sqlData;
422 }
423
424
425 /**
426 * Checks if an update for an extension is available which also resolves dependencies.
427 *
428 * @internal
429 * @param \TYPO3\CMS\Extensionmanager\Domain\Model\Extension $extensionData
430 * @return bool
431 */
432 public function isUpdateAvailable(\TYPO3\CMS\Extensionmanager\Domain\Model\Extension $extensionData) {
433 return (bool)$this->getUpdateableVersion($extensionData);
434 }
435
436 /**
437 * Returns the updateable version for an extension which also resolves dependencies.
438 *
439 * @internal
440 * @param \TYPO3\CMS\Extensionmanager\Domain\Model\Extension $extensionData
441 * @return bool|\TYPO3\CMS\Extensionmanager\Domain\Model\Extension FALSE if no update available otherwise latest
442 * possible update
443 */
444 public function getUpdateableVersion(\TYPO3\CMS\Extensionmanager\Domain\Model\Extension $extensionData) {
445 // Only check for update for TER extensions
446 $version = $extensionData->getIntegerVersion();
447
448 /** @var $extensionUpdates[] \TYPO3\CMS\Extensionmanager\Domain\Model\Extension */
449 $extensionUpdates = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
450 $extensionData->getExtensionKey(),
451 $version + 1
452 );
453 if ($extensionUpdates->count() > 0) {
454 foreach ($extensionUpdates as $extensionUpdate) {
455 $this->dependencyUtility->checkDependencies($extensionUpdate);
456 if (!$this->dependencyUtility->hasDependencyErrors()) {
457 return $extensionUpdate;
458 }
459 }
460 }
461 return FALSE;
462 }
463
464 /**
465 * Uses the export import extension to import a T3D or XML file to PID 0
466 * Execution state is saved in the this->registry, so it only happens once
467 *
468 * @param string $extensionSiteRelPath
469 * @return void
470 */
471 protected function importT3DFile($extensionSiteRelPath) {
472 $registryKeysToCheck = array(
473 $extensionSiteRelPath . 'Initialisation/data.t3d',
474 $extensionSiteRelPath . 'Initialisation/dataImported',
475 );
476 foreach ($registryKeysToCheck as $registryKeyToCheck) {
477 if ($this->registry->get('extensionDataImport', $registryKeyToCheck)) {
478 // Data was imported before => early return
479 return;
480 }
481 }
482 $importFileToUse = NULL;
483 $possibleImportFiles = array(
484 $extensionSiteRelPath . 'Initialisation/data.t3d',
485 $extensionSiteRelPath . 'Initialisation/data.xml'
486 );
487 foreach ($possibleImportFiles as $possibleImportFile) {
488 if (!file_exists(PATH_site . $possibleImportFile)) {
489 continue;
490 }
491 $importFileToUse = $possibleImportFile;
492 }
493 if ($importFileToUse !== NULL) {
494 /** @var ImportExportUtility $importExportUtility */
495 $importExportUtility = $this->objectManager->get(\TYPO3\CMS\Impexp\Utility\ImportExportUtility::class);
496 try {
497 $importResult = $importExportUtility->importT3DFile(PATH_site . $importFileToUse, 0);
498 $this->registry->set('extensionDataImport', $extensionSiteRelPath . 'Initialisation/dataImported', 1);
499 $this->emitAfterExtensionT3DImportSignal($importFileToUse, $importResult);
500 } catch (\ErrorException $e) {
501 /** @var \TYPO3\CMS\Core\Log\Logger $logger */
502 $logger = $this->objectManager->get(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger(__CLASS__);
503 $logger->log(\TYPO3\CMS\Core\Log\LogLevel::WARNING, $e->getMessage());
504 }
505 }
506 }
507
508 /**
509 * Emits a signal after an t3d file was imported
510 *
511 * @param string $importFileToUse
512 * @param int $importResult
513 */
514 protected function emitAfterExtensionT3DImportSignal($importFileToUse, $importResult) {
515 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionT3DImport', array($importFileToUse, $importResult, $this));
516 }
517
518 /**
519 * Imports a static tables SQL File (ext_tables_static+adt)
520 * Execution state is saved in the this->registry, so it only happens once
521 *
522 * @param string $extensionSiteRelPath
523 * @return void
524 */
525 protected function importStaticSqlFile($extensionSiteRelPath) {
526 $extTablesStaticSqlRelFile = $extensionSiteRelPath . 'ext_tables_static+adt.sql';
527 if (!$this->registry->get('extensionDataImport', $extTablesStaticSqlRelFile)) {
528 $extTablesStaticSqlFile = PATH_site . $extTablesStaticSqlRelFile;
529 if (file_exists($extTablesStaticSqlFile)) {
530 $extTablesStaticSqlContent = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($extTablesStaticSqlFile);
531 $this->importStaticSql($extTablesStaticSqlContent);
532 }
533 $this->registry->set('extensionDataImport', $extTablesStaticSqlRelFile, 1);
534 $this->emitAfterExtensionStaticSqlImportSignal($extTablesStaticSqlRelFile);
535 }
536 }
537
538 /**
539 * Emits a signal after a static sql file was imported
540 *
541 * @param string $extTablesStaticSqlRelFile
542 */
543 protected function emitAfterExtensionStaticSqlImportSignal($extTablesStaticSqlRelFile) {
544 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionStaticSqlImport', array($extTablesStaticSqlRelFile, $this));
545 }
546
547 /**
548 * Imports files from Initialisation/Files to fileadmin
549 * via lowlevel copy directory method
550 *
551 * @param string $extensionSiteRelPath relative path to extension dir
552 * @param string $extensionKey
553 */
554 protected function importInitialFiles($extensionSiteRelPath, $extensionKey) {
555 $importRelFolder = $extensionSiteRelPath . 'Initialisation/Files';
556 if (!$this->registry->get('extensionDataImport', $importRelFolder)) {
557 $importFolder = PATH_site . $importRelFolder;
558 if (file_exists($importFolder)) {
559 $destinationRelPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . $extensionKey;
560 $destinationAbsolutePath = PATH_site . $destinationRelPath;
561 if (!file_exists($destinationAbsolutePath) &&
562 \TYPO3\CMS\Core\Utility\GeneralUtility::isAllowedAbsPath($destinationAbsolutePath)
563 ) {
564 \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir($destinationAbsolutePath);
565 }
566 \TYPO3\CMS\Core\Utility\GeneralUtility::copyDirectory($importRelFolder, $destinationRelPath);
567 $this->registry->set('extensionDataImport', $importRelFolder, 1);
568 $this->emitAfterExtensionFileImportSignal($destinationAbsolutePath);
569 }
570 }
571 }
572
573 /**
574 * Emits a signal after extension files were imported
575 *
576 * @param string $destinationAbsolutePath
577 */
578 protected function emitAfterExtensionFileImportSignal($destinationAbsolutePath) {
579 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionFileImport', array($destinationAbsolutePath, $this));
580 }
581
582 /**
583 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
584 */
585 protected function getDatabaseConnection() {
586 return $GLOBALS['TYPO3_DB'];
587 }
588
589 }