1c665b2b7ebea2da4b8bf84033abd33d3454d345
[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();
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 = null)
453 {
454 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
455 if ($rawDefinitions !== null) {
456 $statements = $sqlReader->getCreateTableStatementArray($rawDefinitions);
457 } else {
458 // Read all ext_tables.sql files including new loaded extension
459 $statements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
460 }
461 if (count($statements) !== 0) {
462 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
463 $schemaMigrationService->install($statements, true);
464 }
465 }
466
467 /**
468 * Import static SQL data (normally used for ext_tables_static+adt.sql)
469 *
470 * @param string $rawDefinitions
471 */
472 public function importStaticSql($rawDefinitions)
473 {
474 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
475 $statements = $sqlReader->getStatementArray($rawDefinitions);
476
477 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
478 $schemaMigrationService->importStaticData($statements, true);
479 }
480
481 /**
482 * Remove an extension (delete the directory)
483 *
484 * @param string $extension
485 * @throws ExtensionManagerException
486 */
487 public function removeExtension($extension)
488 {
489 $absolutePath = $this->fileHandlingUtility->getAbsoluteExtensionPath($extension);
490 if ($this->fileHandlingUtility->isValidExtensionPath($absolutePath)) {
491 if ($this->packageManager->isPackageAvailable($extension)) {
492 // Package manager deletes the extension and removes the entry from PackageStates.php
493 $this->packageManager->deletePackage($extension);
494 } else {
495 // The extension is not listed in PackageStates.php, we can safely remove it
496 $this->fileHandlingUtility->removeDirectory($absolutePath);
497 }
498 } else {
499 throw new ExtensionManagerException('No valid extension path given.', 1342875724);
500 }
501 }
502
503 /**
504 * Checks if an update for an extension is available which also resolves dependencies.
505 *
506 * @internal
507 * @param Extension $extensionData
508 * @return bool
509 */
510 public function isUpdateAvailable(Extension $extensionData)
511 {
512 return (bool)$this->getUpdateableVersion($extensionData);
513 }
514
515 /**
516 * Returns the updateable version for an extension which also resolves dependencies.
517 *
518 * @internal
519 * @param Extension $extensionData
520 * @return bool|Extension FALSE if no update available otherwise latest possible update
521 */
522 public function getUpdateableVersion(Extension $extensionData)
523 {
524 // Only check for update for TER extensions
525 $version = $extensionData->getIntegerVersion();
526
527 /** @var $extensionUpdates[] \TYPO3\CMS\Extensionmanager\Domain\Model\Extension */
528 $extensionUpdates = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
529 $extensionData->getExtensionKey(),
530 $version,
531 0,
532 false
533 );
534 if ($extensionUpdates->count() > 0) {
535 foreach ($extensionUpdates as $extensionUpdate) {
536 try {
537 $this->dependencyUtility->checkDependencies($extensionUpdate);
538 if (!$this->dependencyUtility->hasDependencyErrors()) {
539 return $extensionUpdate;
540 }
541 } catch (ExtensionManagerException $e) {
542 }
543 }
544 }
545 return false;
546 }
547
548 /**
549 * Uses the export import extension to import a T3D or XML file to PID 0
550 * Execution state is saved in the this->registry, so it only happens once
551 *
552 * @param string $extensionSiteRelPath
553 */
554 protected function importT3DFile($extensionSiteRelPath)
555 {
556 $registryKeysToCheck = [
557 $extensionSiteRelPath . 'Initialisation/data.t3d',
558 $extensionSiteRelPath . 'Initialisation/dataImported',
559 ];
560 foreach ($registryKeysToCheck as $registryKeyToCheck) {
561 if ($this->registry->get('extensionDataImport', $registryKeyToCheck)) {
562 // Data was imported before => early return
563 return;
564 }
565 }
566 $importFileToUse = null;
567 $possibleImportFiles = [
568 $extensionSiteRelPath . 'Initialisation/data.t3d',
569 $extensionSiteRelPath . 'Initialisation/data.xml'
570 ];
571 foreach ($possibleImportFiles as $possibleImportFile) {
572 if (!file_exists(PATH_site . $possibleImportFile)) {
573 continue;
574 }
575 $importFileToUse = $possibleImportFile;
576 }
577 if ($importFileToUse !== null) {
578 /** @var ImportExportUtility $importExportUtility */
579 $importExportUtility = $this->objectManager->get(ImportExportUtility::class);
580 try {
581 $importResult = $importExportUtility->importT3DFile(PATH_site . $importFileToUse, 0);
582 $this->registry->set('extensionDataImport', $extensionSiteRelPath . 'Initialisation/dataImported', 1);
583 $this->emitAfterExtensionT3DImportSignal($importFileToUse, $importResult);
584 } catch (\ErrorException $e) {
585 /** @var \TYPO3\CMS\Core\Log\Logger $logger */
586 $logger = $this->objectManager->get(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger(__CLASS__);
587 $logger->log(\TYPO3\CMS\Core\Log\LogLevel::WARNING, $e->getMessage());
588 }
589 }
590 }
591
592 /**
593 * Emits a signal after an t3d file was imported
594 *
595 * @param string $importFileToUse
596 * @param int $importResult
597 */
598 protected function emitAfterExtensionT3DImportSignal($importFileToUse, $importResult)
599 {
600 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionT3DImport', [$importFileToUse, $importResult, $this]);
601 }
602
603 /**
604 * Imports a static tables SQL File (ext_tables_static+adt)
605 * Execution state is saved in the this->registry, so it only happens once
606 *
607 * @param string $extensionSiteRelPath
608 */
609 public function importStaticSqlFile($extensionSiteRelPath)
610 {
611 $extTablesStaticSqlRelFile = $extensionSiteRelPath . 'ext_tables_static+adt.sql';
612 if (!$this->registry->get('extensionDataImport', $extTablesStaticSqlRelFile)) {
613 $extTablesStaticSqlFile = PATH_site . $extTablesStaticSqlRelFile;
614 $shortFileHash = '';
615 if (file_exists($extTablesStaticSqlFile)) {
616 $extTablesStaticSqlContent = file_get_contents($extTablesStaticSqlFile);
617 $shortFileHash = md5($extTablesStaticSqlContent);
618 $this->importStaticSql($extTablesStaticSqlContent);
619 }
620 $this->registry->set('extensionDataImport', $extTablesStaticSqlRelFile, $shortFileHash);
621 $this->emitAfterExtensionStaticSqlImportSignal($extTablesStaticSqlRelFile);
622 }
623 }
624
625 /**
626 * Emits a signal after a static sql file was imported
627 *
628 * @param string $extTablesStaticSqlRelFile
629 */
630 protected function emitAfterExtensionStaticSqlImportSignal($extTablesStaticSqlRelFile)
631 {
632 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionStaticSqlImport', [$extTablesStaticSqlRelFile, $this]);
633 }
634
635 /**
636 * Imports files from Initialisation/Files to fileadmin
637 * via lowlevel copy directory method
638 *
639 * @param string $extensionSiteRelPath relative path to extension dir
640 * @param string $extensionKey
641 */
642 protected function importInitialFiles($extensionSiteRelPath, $extensionKey)
643 {
644 $importRelFolder = $extensionSiteRelPath . 'Initialisation/Files';
645 if (!$this->registry->get('extensionDataImport', $importRelFolder)) {
646 $importFolder = PATH_site . $importRelFolder;
647 if (file_exists($importFolder)) {
648 $destinationRelPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . $extensionKey;
649 $destinationAbsolutePath = PATH_site . $destinationRelPath;
650 if (!file_exists($destinationAbsolutePath) &&
651 GeneralUtility::isAllowedAbsPath($destinationAbsolutePath)
652 ) {
653 GeneralUtility::mkdir($destinationAbsolutePath);
654 }
655 GeneralUtility::copyDirectory($importRelFolder, $destinationRelPath);
656 $this->registry->set('extensionDataImport', $importRelFolder, 1);
657 $this->emitAfterExtensionFileImportSignal($destinationAbsolutePath);
658 }
659 }
660 }
661
662 /**
663 * Emits a signal after extension files were imported
664 *
665 * @param string $destinationAbsolutePath
666 */
667 protected function emitAfterExtensionFileImportSignal($destinationAbsolutePath)
668 {
669 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterExtensionFileImport', [$destinationAbsolutePath, $this]);
670 }
671 }