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