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