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