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