a82b4dac3a8d4f40327dd8d8ff86a462f3b168a0
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Package / PackageManager.php
1 <?php
2 namespace TYPO3\CMS\Core\Package;
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 Symfony\Component\Finder\Finder;
18 use Symfony\Component\Finder\SplFileInfo;
19 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
20 use TYPO3\CMS\Core\Compatibility\LoadedExtensionArrayElement;
21 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
22 use TYPO3\CMS\Core\Core\Environment;
23 use TYPO3\CMS\Core\Service\DependencyOrderingService;
24 use TYPO3\CMS\Core\Service\OpcodeCacheService;
25 use TYPO3\CMS\Core\SingletonInterface;
26 use TYPO3\CMS\Core\Utility\ArrayUtility;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\PathUtility;
29 use TYPO3\CMS\Core\Utility\StringUtility;
30
31 /**
32 * The default TYPO3 Package Manager
33 */
34 class PackageManager implements SingletonInterface
35 {
36 /**
37 * @var DependencyOrderingService
38 */
39 protected $dependencyOrderingService;
40
41 /**
42 * @var FrontendInterface
43 */
44 protected $coreCache;
45
46 /**
47 * @var string
48 */
49 protected $cacheIdentifier;
50
51 /**
52 * @var array
53 */
54 protected $packagesBasePaths = [];
55
56 /**
57 * @var array
58 */
59 protected $packageAliasMap = [];
60
61 /**
62 * @var array
63 */
64 protected $runtimeActivatedPackages = [];
65
66 /**
67 * Absolute path leading to the various package directories
68 * @var string
69 */
70 protected $packagesBasePath;
71
72 /**
73 * Array of available packages, indexed by package key
74 * @var PackageInterface[]
75 */
76 protected $packages = [];
77
78 /**
79 * @var bool
80 */
81 protected $availablePackagesScanned = false;
82
83 /**
84 * A map between ComposerName and PackageKey, only available when scanAvailablePackages is run
85 * @var array
86 */
87 protected $composerNameToPackageKeyMap = [];
88
89 /**
90 * List of active packages as package key => package object
91 * @var array
92 */
93 protected $activePackages = [];
94
95 /**
96 * @var string
97 */
98 protected $packageStatesPathAndFilename;
99
100 /**
101 * Package states configuration as stored in the PackageStates.php file
102 * @var array
103 */
104 protected $packageStatesConfiguration = [];
105
106 /**
107 * @param DependencyOrderingService $dependencyOrderingService
108 */
109 public function __construct(DependencyOrderingService $dependencyOrderingService = null)
110 {
111 $this->packagesBasePath = Environment::getPublicPath() . '/';
112 $this->packageStatesPathAndFilename = Environment::getPublicPath() . '/typo3conf/PackageStates.php';
113 if ($dependencyOrderingService === null) {
114 trigger_error(self::class . ' without constructor based dependency injection has been deprecated in v9.2 and will not work in TYPO3 v10.', E_USER_DEPRECATED);
115 $dependencyOrderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);
116 }
117 $this->dependencyOrderingService = $dependencyOrderingService;
118 }
119
120 /**
121 * @param FrontendInterface $coreCache
122 */
123 public function injectCoreCache(FrontendInterface $coreCache)
124 {
125 $this->coreCache = $coreCache;
126 }
127
128 /**
129 * @param DependencyResolver $dependencyResolver
130 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.
131 */
132 public function injectDependencyResolver(DependencyResolver $dependencyResolver)
133 {
134 trigger_error(self::class . '::injectDependencyResolver() has been deprecated with v9.2 and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
135 }
136
137 /**
138 * Initializes the package manager
139 */
140 public function initialize()
141 {
142 try {
143 $this->loadPackageManagerStatesFromCache();
144 } catch (Exception\PackageManagerCacheUnavailableException $exception) {
145 $this->loadPackageStates();
146 $this->initializePackageObjects();
147 $this->initializeCompatibilityLoadedExtArray();
148 $this->saveToPackageCache();
149 }
150 }
151
152 /**
153 * @return string
154 */
155 protected function getCacheIdentifier()
156 {
157 if ($this->cacheIdentifier === null) {
158 $mTime = @filemtime($this->packageStatesPathAndFilename);
159 if ($mTime !== false) {
160 $this->cacheIdentifier = md5($this->packageStatesPathAndFilename . $mTime);
161 } else {
162 $this->cacheIdentifier = null;
163 }
164 }
165 return $this->cacheIdentifier;
166 }
167
168 /**
169 * @return string
170 */
171 protected function getCacheEntryIdentifier()
172 {
173 $cacheIdentifier = $this->getCacheIdentifier();
174 return $cacheIdentifier !== null ? 'PackageManager_' . $cacheIdentifier : null;
175 }
176
177 /**
178 * Saves the current state of all relevant information to the TYPO3 Core Cache
179 */
180 protected function saveToPackageCache()
181 {
182 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
183 if ($cacheEntryIdentifier !== null && !$this->coreCache->has($cacheEntryIdentifier)) {
184 // Package objects get their own cache entry, so PHP does not have to parse the serialized string
185 $packageObjectsCacheEntryIdentifier = StringUtility::getUniqueId('PackageObjects_');
186 // Build cache file
187 $packageCache = [
188 'packageStatesConfiguration' => $this->packageStatesConfiguration,
189 'packageAliasMap' => $this->packageAliasMap,
190 'loadedExtArray' => $GLOBALS['TYPO3_LOADED_EXT'],
191 'composerNameToPackageKeyMap' => $this->composerNameToPackageKeyMap,
192 'packageObjectsCacheEntryIdentifier' => $packageObjectsCacheEntryIdentifier
193 ];
194 $this->coreCache->set($packageObjectsCacheEntryIdentifier, serialize($this->packages));
195 $this->coreCache->set(
196 $cacheEntryIdentifier,
197 'return ' . PHP_EOL . var_export($packageCache, true) . ';'
198 );
199 }
200 }
201
202 /**
203 * Attempts to load the package manager states from cache
204 *
205 * @throws Exception\PackageManagerCacheUnavailableException
206 */
207 protected function loadPackageManagerStatesFromCache()
208 {
209 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
210 if ($cacheEntryIdentifier === null || !$this->coreCache->has($cacheEntryIdentifier) || !($packageCache = $this->coreCache->require($cacheEntryIdentifier))) {
211 throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883342);
212 }
213 $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
214 if ($this->packageStatesConfiguration['version'] < 5) {
215 throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883341);
216 }
217 $this->packageAliasMap = $packageCache['packageAliasMap'];
218 $this->composerNameToPackageKeyMap = $packageCache['composerNameToPackageKeyMap'];
219 $GLOBALS['TYPO3_LOADED_EXT'] = $packageCache['loadedExtArray'];
220 $GLOBALS['TYPO3_currentPackageManager'] = $this;
221 // Strip off PHP Tags from Php Cache Frontend
222 $packageObjects = substr(substr($this->coreCache->get($packageCache['packageObjectsCacheEntryIdentifier']), 6), 0, -2);
223 $this->packages = unserialize($packageObjects);
224 unset($GLOBALS['TYPO3_currentPackageManager']);
225 }
226
227 /**
228 * Loads the states of available packages from the PackageStates.php file.
229 * The result is stored in $this->packageStatesConfiguration.
230 *
231 * @throws Exception\PackageStatesUnavailableException
232 */
233 protected function loadPackageStates()
234 {
235 $forcePackageStatesRewrite = false;
236 $this->packageStatesConfiguration = @include $this->packageStatesPathAndFilename ?: [];
237 if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
238 $this->packageStatesConfiguration = [];
239 } elseif ($this->packageStatesConfiguration['version'] === 4) {
240 // Convert to v5 format which only includes a list of active packages.
241 // Deprecated since version 8, will be removed in version 10.
242 $activePackages = [];
243 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfiguration) {
244 if ($packageConfiguration['state'] !== 'active') {
245 continue;
246 }
247 $activePackages[$packageKey] = ['packagePath' => $packageConfiguration['packagePath']];
248 }
249 $this->packageStatesConfiguration['packages'] = $activePackages;
250 $this->packageStatesConfiguration['version'] = 5;
251 $forcePackageStatesRewrite = true;
252 }
253 if ($this->packageStatesConfiguration !== []) {
254 $this->registerPackagesFromConfiguration($this->packageStatesConfiguration['packages'], false, $forcePackageStatesRewrite);
255 } else {
256 throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
257 }
258 }
259
260 /**
261 * Initializes activePackages property
262 *
263 * Saves PackageStates.php if list of required extensions has changed.
264 */
265 protected function initializePackageObjects()
266 {
267 $requiredPackages = [];
268 $activePackages = [];
269 foreach ($this->packages as $packageKey => $package) {
270 if ($package->isProtected()) {
271 $requiredPackages[$packageKey] = $package;
272 }
273 if (isset($this->packageStatesConfiguration['packages'][$packageKey])) {
274 $activePackages[$packageKey] = $package;
275 }
276 }
277 $previousActivePackages = $activePackages;
278 $activePackages = array_merge($requiredPackages, $activePackages);
279
280 if ($activePackages != $previousActivePackages) {
281 foreach ($requiredPackages as $requiredPackageKey => $package) {
282 $this->registerActivePackage($package);
283 }
284 $this->sortAndSavePackageStates();
285 }
286 }
287
288 /**
289 * @param PackageInterface $package
290 */
291 protected function registerActivePackage(PackageInterface $package)
292 {
293 // reset the active packages so they are rebuilt.
294 $this->activePackages = [];
295 $this->packageStatesConfiguration['packages'][$package->getPackageKey()] = ['packagePath' => str_replace($this->packagesBasePath, '', $package->getPackagePath())];
296 }
297
298 /**
299 * Initializes a backwards compatibility $GLOBALS['TYPO3_LOADED_EXT'] array
300 */
301 protected function initializeCompatibilityLoadedExtArray()
302 {
303 $loadedExtObj = new \TYPO3\CMS\Core\Compatibility\LoadedExtensionsArray($this);
304 $GLOBALS['TYPO3_LOADED_EXT'] = $loadedExtObj->toArray();
305 }
306
307 /**
308 * Scans all directories in the packages directories for available packages.
309 * For each package a Package object is created and stored in $this->packages.
310 */
311 public function scanAvailablePackages()
312 {
313 $packagePaths = $this->scanPackagePathsForExtensions();
314 $packages = [];
315 foreach ($packagePaths as $packageKey => $packagePath) {
316 try {
317 $composerManifest = $this->getComposerManifest($packagePath);
318 $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath);
319 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
320 $packages[$packageKey] = ['packagePath' => str_replace($this->packagesBasePath, '', $packagePath)];
321 } catch (Exception\MissingPackageManifestException $exception) {
322 if (!$this->isPackageKeyValid($packageKey)) {
323 continue;
324 }
325 } catch (Exception\InvalidPackageKeyException $exception) {
326 continue;
327 }
328 }
329
330 $this->availablePackagesScanned = true;
331 $registerOnlyNewPackages = !empty($this->packages);
332 $this->registerPackagesFromConfiguration($packages, $registerOnlyNewPackages);
333 }
334
335 /**
336 * Scans all directories for a certain package.
337 *
338 * @param string $packageKey
339 * @return PackageInterface
340 */
341 protected function registerPackageDuringRuntime($packageKey)
342 {
343 $packagePaths = $this->scanPackagePathsForExtensions();
344 $packagePath = $packagePaths[$packageKey];
345 $composerManifest = $this->getComposerManifest($packagePath);
346 $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath);
347 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
348 $packagePath = PathUtility::sanitizeTrailingSeparator($packagePath);
349 $package = new Package($this, $packageKey, $packagePath);
350 $this->registerPackage($package);
351 return $package;
352 }
353
354 /**
355 * Fetches all directories from sysext/global/local locations and checks if the extension contains an ext_emconf.php
356 *
357 * @return array
358 */
359 protected function scanPackagePathsForExtensions()
360 {
361 $collectedExtensionPaths = [];
362 foreach ($this->getPackageBasePaths() as $packageBasePath) {
363 // Only add the extension if we have an EMCONF and the extension is not yet registered.
364 // This is crucial in order to allow overriding of system extension by local extensions
365 // and strongly depends on the order of paths defined in $this->packagesBasePaths.
366 $finder = new Finder();
367 $finder
368 ->name('ext_emconf.php')
369 ->followLinks()
370 ->depth(0)
371 ->ignoreUnreadableDirs()
372 ->in($packageBasePath);
373
374 /** @var SplFileInfo $fileInfo */
375 foreach ($finder as $fileInfo) {
376 $path = PathUtility::dirname($fileInfo->getPathname());
377 $extensionName = PathUtility::basename($path);
378 // Fix Windows backslashes
379 // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
380 $currentPath = str_replace('\\', '/', $path) . '/';
381 if (!isset($collectedExtensionPaths[$extensionName])) {
382 $collectedExtensionPaths[$extensionName] = $currentPath;
383 }
384 }
385 }
386 return $collectedExtensionPaths;
387 }
388
389 /**
390 * Requires and registers all packages which were defined in packageStatesConfiguration
391 *
392 * @param array $packages
393 * @param bool $registerOnlyNewPackages
394 * @param bool $packageStatesHasChanged
395 * @throws Exception\InvalidPackageStateException
396 * @throws Exception\PackageStatesFileNotWritableException
397 */
398 protected function registerPackagesFromConfiguration(array $packages, $registerOnlyNewPackages = false, $packageStatesHasChanged = false)
399 {
400 foreach ($packages as $packageKey => $stateConfiguration) {
401 if ($registerOnlyNewPackages && $this->isPackageRegistered($packageKey)) {
402 continue;
403 }
404
405 if (!isset($stateConfiguration['packagePath'])) {
406 $this->unregisterPackageByPackageKey($packageKey);
407 $packageStatesHasChanged = true;
408 continue;
409 }
410
411 try {
412 $packagePath = PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
413 $package = new Package($this, $packageKey, $packagePath);
414 } catch (Exception\InvalidPackagePathException $exception) {
415 $this->unregisterPackageByPackageKey($packageKey);
416 $packageStatesHasChanged = true;
417 continue;
418 } catch (Exception\InvalidPackageKeyException $exception) {
419 $this->unregisterPackageByPackageKey($packageKey);
420 $packageStatesHasChanged = true;
421 continue;
422 } catch (Exception\InvalidPackageManifestException $exception) {
423 $this->unregisterPackageByPackageKey($packageKey);
424 $packageStatesHasChanged = true;
425 continue;
426 }
427
428 $this->registerPackage($package);
429 }
430 if ($packageStatesHasChanged) {
431 $this->sortAndSavePackageStates();
432 }
433 }
434
435 /**
436 * Register a native TYPO3 package
437 *
438 * @param PackageInterface $package The Package to be registered
439 * @return PackageInterface
440 * @throws Exception\InvalidPackageStateException
441 */
442 public function registerPackage(PackageInterface $package)
443 {
444 $packageKey = $package->getPackageKey();
445 if ($this->isPackageRegistered($packageKey)) {
446 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
447 }
448
449 $this->packages[$packageKey] = $package;
450
451 if ($package instanceof PackageInterface) {
452 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
453 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
454 }
455 }
456 return $package;
457 }
458
459 /**
460 * Unregisters a package from the list of available packages
461 *
462 * @param string $packageKey Package Key of the package to be unregistered
463 */
464 protected function unregisterPackageByPackageKey($packageKey)
465 {
466 try {
467 $package = $this->getPackage($packageKey);
468 if ($package instanceof PackageInterface) {
469 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
470 unset($this->packageAliasMap[strtolower($packageToReplace)]);
471 }
472 }
473 } catch (Exception\UnknownPackageException $e) {
474 }
475 unset($this->packages[$packageKey]);
476 unset($this->packageStatesConfiguration['packages'][$packageKey]);
477 }
478
479 /**
480 * Resolves a TYPO3 package key from a composer package name.
481 *
482 * @param string $composerName
483 * @return string
484 */
485 public function getPackageKeyFromComposerName($composerName)
486 {
487 $lowercasedComposerName = strtolower($composerName);
488 if (isset($this->packageAliasMap[$lowercasedComposerName])) {
489 return $this->packageAliasMap[$lowercasedComposerName];
490 }
491 if (isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
492 return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
493 }
494 return $composerName;
495 }
496
497 /**
498 * Returns a PackageInterface object for the specified package.
499 * A package is available, if the package directory contains valid MetaData information.
500 *
501 * @param string $packageKey
502 * @return PackageInterface The requested package object
503 * @throws Exception\UnknownPackageException if the specified package is not known
504 * @api
505 */
506 public function getPackage($packageKey)
507 {
508 if (!$this->isPackageRegistered($packageKey) && !$this->isPackageAvailable($packageKey)) {
509 throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available. Please check if the package exists and that the package key is correct (package keys are case sensitive).', 1166546734);
510 }
511 return $this->packages[$packageKey];
512 }
513
514 /**
515 * Returns TRUE if a package is available (the package's files exist in the packages directory)
516 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
517 *
518 * @param string $packageKey The key of the package to check
519 * @return bool TRUE if the package is available, otherwise FALSE
520 * @api
521 */
522 public function isPackageAvailable($packageKey)
523 {
524 if ($this->isPackageRegistered($packageKey)) {
525 return true;
526 }
527
528 // If activePackages is empty, the PackageManager is currently initializing
529 // thus packages should not be scanned
530 if (!$this->availablePackagesScanned && !empty($this->activePackages)) {
531 $this->scanAvailablePackages();
532 }
533
534 return $this->isPackageRegistered($packageKey);
535 }
536
537 /**
538 * Returns TRUE if a package is activated or FALSE if it's not.
539 *
540 * @param string $packageKey The key of the package to check
541 * @return bool TRUE if package is active, otherwise FALSE
542 * @api
543 */
544 public function isPackageActive($packageKey)
545 {
546 $packageKey = $this->getPackageKeyFromComposerName($packageKey);
547
548 return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->packageStatesConfiguration['packages'][$packageKey]);
549 }
550
551 /**
552 * Deactivates a package and updates the packagestates configuration
553 *
554 * @param string $packageKey
555 * @throws Exception\PackageStatesFileNotWritableException
556 * @throws Exception\ProtectedPackageKeyException
557 * @throws Exception\UnknownPackageException
558 */
559 public function deactivatePackage($packageKey)
560 {
561 $packagesWithDependencies = $this->sortActivePackagesByDependencies();
562
563 foreach ($packagesWithDependencies as $packageStateKey => $packageStateConfiguration) {
564 if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies'])) {
565 continue;
566 }
567 if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
568 $this->deactivatePackage($packageStateKey);
569 }
570 }
571
572 if (!$this->isPackageActive($packageKey)) {
573 return;
574 }
575
576 $package = $this->getPackage($packageKey);
577 if ($package->isProtected()) {
578 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
579 }
580
581 $this->activePackages = [];
582 unset($this->packageStatesConfiguration['packages'][$packageKey]);
583 $this->sortAndSavePackageStates();
584 }
585
586 /**
587 * @param string $packageKey
588 */
589 public function activatePackage($packageKey)
590 {
591 $package = $this->getPackage($packageKey);
592 $this->registerTransientClassLoadingInformationForPackage($package);
593
594 if ($this->isPackageActive($packageKey)) {
595 return;
596 }
597
598 $this->registerActivePackage($package);
599 $this->sortAndSavePackageStates();
600 }
601
602 /**
603 * Enables packages during runtime, but no class aliases will be available
604 *
605 * @param string $packageKey
606 * @api
607 */
608 public function activatePackageDuringRuntime($packageKey)
609 {
610 $package = $this->registerPackageDuringRuntime($packageKey);
611 $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
612 if (!isset($GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()])) {
613 $loadedExtArrayElement = new LoadedExtensionArrayElement($package);
614 $GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()] = $loadedExtArrayElement->toArray();
615 }
616 $this->registerTransientClassLoadingInformationForPackage($package);
617 }
618
619 /**
620 * @param PackageInterface $package
621 * @throws \TYPO3\CMS\Core\Exception
622 */
623 protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
624 {
625 if (Environment::isComposerMode()) {
626 return;
627 }
628 ClassLoadingInformation::registerTransientClassLoadingInformationForPackage($package);
629 }
630
631 /**
632 * Removes a package from the file system.
633 *
634 * @param string $packageKey
635 * @throws Exception
636 * @throws Exception\ProtectedPackageKeyException
637 * @throws Exception\UnknownPackageException
638 */
639 public function deletePackage($packageKey)
640 {
641 if (!$this->isPackageAvailable($packageKey)) {
642 throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
643 }
644
645 $package = $this->getPackage($packageKey);
646 if ($package->isProtected()) {
647 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
648 }
649
650 if ($this->isPackageActive($packageKey)) {
651 $this->deactivatePackage($packageKey);
652 }
653
654 $this->unregisterPackage($package);
655 $this->sortAndSavePackageStates();
656
657 $packagePath = $package->getPackagePath();
658 $deletion = GeneralUtility::rmdir($packagePath, true);
659 if ($deletion === false) {
660 throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
661 }
662 }
663
664 /**
665 * Returns an array of \TYPO3\CMS\Core\Package objects of all active packages.
666 * A package is active, if it is available and has been activated in the package
667 * manager settings. This method returns runtime activated packages too
668 *
669 * @return PackageInterface[]
670 * @api
671 */
672 public function getActivePackages()
673 {
674 if (empty($this->activePackages)) {
675 if (!empty($this->packageStatesConfiguration['packages'])) {
676 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfig) {
677 $this->activePackages[$packageKey] = $this->getPackage($packageKey);
678 }
679 }
680 }
681 return array_merge($this->activePackages, $this->runtimeActivatedPackages);
682 }
683
684 /**
685 * Returns TRUE if a package was already registered or FALSE if it's not.
686 *
687 * @param string $packageKey
688 * @return bool
689 */
690 protected function isPackageRegistered($packageKey)
691 {
692 $packageKey = $this->getPackageKeyFromComposerName($packageKey);
693
694 return isset($this->packages[$packageKey]);
695 }
696
697 /**
698 * Orders all active packages by comparing their dependencies. By this, the packages
699 * and package configurations arrays holds all packages in the correct
700 * initialization order.
701 *
702 * @return array
703 */
704 protected function sortActivePackagesByDependencies()
705 {
706 $packagesWithDependencies = $this->resolvePackageDependencies($this->packageStatesConfiguration['packages']);
707
708 // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
709 ksort($packagesWithDependencies);
710 $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
711
712 // Reorder the packages according to the loading order
713 $this->packageStatesConfiguration['packages'] = [];
714 foreach ($sortedPackageKeys as $packageKey) {
715 $this->registerActivePackage($this->packages[$packageKey]);
716 }
717 return $packagesWithDependencies;
718 }
719
720 /**
721 * Resolves the dependent packages from the meta data of all packages recursively. The
722 * resolved direct or indirect dependencies of each package will put into the package
723 * states configuration array.
724 *
725 * @param $packageConfig
726 * @return array
727 */
728 protected function resolvePackageDependencies($packageConfig)
729 {
730 $packagesWithDependencies = [];
731 foreach ($packageConfig as $packageKey => $_) {
732 $packagesWithDependencies[$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
733 $packagesWithDependencies[$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
734 }
735 return $packagesWithDependencies;
736 }
737
738 /**
739 * Returns an array of suggested package keys for the given package.
740 *
741 * @param string $packageKey The package key to fetch the suggestions for
742 * @return array|null An array of directly suggested packages
743 */
744 protected function getSuggestionArrayForPackage($packageKey)
745 {
746 if (!isset($this->packages[$packageKey])) {
747 return null;
748 }
749 $suggestedPackageKeys = [];
750 $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_SUGGESTS);
751 foreach ($suggestedPackageConstraints as $constraint) {
752 if ($constraint instanceof MetaData\PackageConstraint) {
753 $suggestedPackageKey = $constraint->getValue();
754 if (isset($this->packages[$suggestedPackageKey])) {
755 $suggestedPackageKeys[] = $suggestedPackageKey;
756 }
757 }
758 }
759 return array_reverse($suggestedPackageKeys);
760 }
761
762 /**
763 * Saves the current content of $this->packageStatesConfiguration to the
764 * PackageStates.php file.
765 *
766 * @throws Exception\PackageStatesFileNotWritableException
767 */
768 protected function sortAndSavePackageStates()
769 {
770 $this->sortActivePackagesByDependencies();
771
772 $this->packageStatesConfiguration['version'] = 5;
773
774 $fileDescription = "# PackageStates.php\n\n";
775 $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
776 $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
777 $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
778 $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
779
780 if (!@is_writable($this->packageStatesPathAndFilename)) {
781 // If file does not exists try to create it
782 $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
783 if (!$fileHandle) {
784 throw new Exception\PackageStatesFileNotWritableException(
785 sprintf('We could not update the list of installed packages because the file %s is not writable. Please, check the file system permissions for this file and make sure that the web server can update it.', $this->packageStatesPathAndFilename),
786 1382449759
787 );
788 }
789 fclose($fileHandle);
790 }
791 $packageStatesCode = "<?php\n$fileDescription\nreturn " . ArrayUtility::arrayExport($this->packageStatesConfiguration) . ";\n";
792 GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
793
794 $this->initializeCompatibilityLoadedExtArray();
795
796 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
797 }
798
799 /**
800 * Check the conformance of the given package key
801 *
802 * @param string $packageKey The package key to validate
803 * @return bool If the package key is valid, returns TRUE otherwise FALSE
804 * @api
805 */
806 public function isPackageKeyValid($packageKey)
807 {
808 return preg_match(PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
809 }
810
811 /**
812 * Returns an array of \TYPO3\CMS\Core\Package objects of all available packages.
813 * A package is available, if the package directory contains valid meta information.
814 *
815 * @return PackageInterface[] Array of PackageInterface
816 * @api
817 */
818 public function getAvailablePackages()
819 {
820 if ($this->availablePackagesScanned === false) {
821 $this->scanAvailablePackages();
822 }
823
824 return $this->packages;
825 }
826
827 /**
828 * Unregisters a package from the list of available packages
829 *
830 * @param PackageInterface $package The package to be unregistered
831 * @throws Exception\InvalidPackageStateException
832 */
833 public function unregisterPackage(PackageInterface $package)
834 {
835 $packageKey = $package->getPackageKey();
836 if (!$this->isPackageRegistered($packageKey)) {
837 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
838 }
839 $this->unregisterPackageByPackageKey($packageKey);
840 }
841
842 /**
843 * Reloads a package and its information
844 *
845 * @param string $packageKey
846 * @throws Exception\InvalidPackageStateException if the package isn't available
847 */
848 public function reloadPackageInformation($packageKey)
849 {
850 if (!$this->isPackageRegistered($packageKey)) {
851 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
852 }
853
854 /** @var PackageInterface $package */
855 $package = $this->packages[$packageKey];
856 $packagePath = $package->getPackagePath();
857 $newPackage = new Package($this, $packageKey, $packagePath);
858 $this->packages[$packageKey] = $newPackage;
859 unset($package);
860 }
861
862 /**
863 * Returns contents of Composer manifest as a stdObject
864 *
865 * @param string $manifestPath
866 * @return \stdClass
867 * @throws Exception\InvalidPackageManifestException
868 */
869 public function getComposerManifest($manifestPath)
870 {
871 $composerManifest = null;
872 if (file_exists($manifestPath . 'composer.json')) {
873 $json = file_get_contents($manifestPath . 'composer.json');
874 $composerManifest = json_decode($json);
875 if (!$composerManifest instanceof \stdClass) {
876 throw new Exception\InvalidPackageManifestException('The composer.json found for extension "' . PathUtility::basename($manifestPath) . '" is invalid!', 1439555561);
877 }
878 }
879
880 $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath);
881 $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
882 PathUtility::basename($manifestPath),
883 $extensionManagerConfiguration,
884 $composerManifest ?: new \stdClass()
885 );
886
887 return $composerManifest;
888 }
889
890 /**
891 * Fetches MetaData information from ext_emconf.php, used for
892 * resolving dependencies as well.
893 *
894 * @param string $packagePath
895 * @return array
896 * @throws Exception\InvalidPackageManifestException
897 */
898 protected function getExtensionEmConf($packagePath)
899 {
900 $packageKey = PathUtility::basename($packagePath);
901 $_EXTKEY = $packageKey;
902 $path = $packagePath . 'ext_emconf.php';
903 $EM_CONF = null;
904 if (@file_exists($path)) {
905 include $path;
906 if (is_array($EM_CONF[$_EXTKEY])) {
907 return $EM_CONF[$_EXTKEY];
908 }
909 }
910 throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
911 }
912
913 /**
914 * Fetches information from ext_emconf.php and maps it so it is treated as it would come from composer.json
915 *
916 * @param string $packageKey
917 * @param array $extensionManagerConfiguration
918 * @param \stdClass $composerManifest
919 * @return \stdClass
920 * @throws Exception\InvalidPackageManifestException
921 */
922 protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
923 {
924 $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
925 $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
926 $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title'] ?? '');
927 $this->setComposerManifestValueIfEmpty($composerManifest, 'authors', [['name' => $extensionManagerConfiguration['author'] ?? '', 'email' => $extensionManagerConfiguration['author_email'] ?? '']]);
928 $composerManifest->version = $extensionManagerConfiguration['version'] ?? '';
929 if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
930 $composerManifest->require = new \stdClass();
931 foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
932 if (!empty($requiredPackageKey)) {
933 if ($requiredPackageKey === 'typo3') {
934 // Add implicit dependency to 'core'
935 $composerManifest->require->core = $requiredPackageVersion;
936 } elseif ($requiredPackageKey !== 'php') {
937 // Skip php dependency
938 $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
939 }
940 } else {
941 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
942 }
943 }
944 }
945 if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
946 $composerManifest->conflict = new \stdClass();
947 foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
948 if (!empty($conflictingPackageKey)) {
949 $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
950 } else {
951 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
952 }
953 }
954 }
955 if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
956 $composerManifest->suggest = new \stdClass();
957 foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
958 if (!empty($suggestedPackageKey)) {
959 $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
960 } else {
961 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
962 }
963 }
964 }
965 if (isset($extensionManagerConfiguration['autoload'])) {
966 $composerManifest->autoload = json_decode(json_encode($extensionManagerConfiguration['autoload']));
967 }
968 // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
969 unset($composerManifest->{'autoload-dev'});
970 if (isset($extensionManagerConfiguration['autoload-dev'])) {
971 $composerManifest->{'autoload-dev'} = json_decode(json_encode($extensionManagerConfiguration['autoload-dev']));
972 }
973
974 return $composerManifest;
975 }
976
977 /**
978 * @param \stdClass $manifest
979 * @param string $property
980 * @param mixed $value
981 * @return \stdClass
982 */
983 protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
984 {
985 if (empty($manifest->{$property})) {
986 $manifest->{$property} = $value;
987 }
988
989 return $manifest;
990 }
991
992 /**
993 * Returns an array of dependent package keys for the given package. It will
994 * do this recursively, so dependencies of dependent packages will also be
995 * in the result.
996 *
997 * @param string $packageKey The package key to fetch the dependencies for
998 * @param array $dependentPackageKeys
999 * @param array $trace An array of already visited package keys, to detect circular dependencies
1000 * @return array|null An array of direct or indirect dependent packages
1001 * @throws Exception\InvalidPackageKeyException
1002 */
1003 protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = [], array $trace = [])
1004 {
1005 if (!isset($this->packages[$packageKey])) {
1006 return null;
1007 }
1008 if (in_array($packageKey, $trace, true) !== false) {
1009 return $dependentPackageKeys;
1010 }
1011 $trace[] = $packageKey;
1012 $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1013 foreach ($dependentPackageConstraints as $constraint) {
1014 if ($constraint instanceof MetaData\PackageConstraint) {
1015 $dependentPackageKey = $constraint->getValue();
1016 if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1017 $dependentPackageKeys[] = $dependentPackageKey;
1018 }
1019 $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1020 }
1021 }
1022 return array_reverse($dependentPackageKeys);
1023 }
1024
1025 /**
1026 * Resolves package key from Composer manifest
1027 *
1028 * If it is a TYPO3 package the name of the containing directory will be used.
1029 *
1030 * Else if the composer name of the package matches the first part of the lowercased namespace of the package, the mixed
1031 * case version of the composer name / namespace will be used, with backslashes replaced by dots.
1032 *
1033 * Else the composer name will be used with the slash replaced by a dot
1034 *
1035 * @param object $manifest
1036 * @param string $packagePath
1037 * @throws Exception\InvalidPackageManifestException
1038 * @return string
1039 */
1040 protected function getPackageKeyFromManifest($manifest, $packagePath)
1041 {
1042 if (!is_object($manifest)) {
1043 throw new Exception\InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1044 }
1045 if (isset($manifest->type) && strpos($manifest->type, 'typo3-cms-') === 0) {
1046 $packageKey = PathUtility::basename($packagePath);
1047 return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
1048 }
1049 $packageKey = str_replace('/', '.', $manifest->name);
1050 return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
1051 }
1052
1053 /**
1054 * The order of paths is crucial for allowing overriding of system extension by local extensions.
1055 * Pay attention if you change order of the paths here.
1056 *
1057 * @return array
1058 */
1059 protected function getPackageBasePaths()
1060 {
1061 if (count($this->packagesBasePaths) < 3) {
1062 // Check if the directory even exists and if it is not empty
1063 if (is_dir(Environment::getPublicPath() . '/typo3conf/ext') && $this->hasSubDirectories(Environment::getPublicPath() . '/typo3conf/ext')) {
1064 $this->packagesBasePaths['local'] = Environment::getPublicPath() . '/typo3conf/ext/*/';
1065 }
1066 if (is_dir(Environment::getPublicPath() . '/typo3/ext') && $this->hasSubDirectories(Environment::getPublicPath() . '/typo3/ext')) {
1067 $this->packagesBasePaths['global'] = Environment::getPublicPath() . '/typo3/ext/*/';
1068 }
1069 $this->packagesBasePaths['system'] = Environment::getPublicPath() . '/typo3/sysext/*/';
1070 }
1071 return $this->packagesBasePaths;
1072 }
1073
1074 /**
1075 * Returns true if the given path has valid subdirectories, false otherwise.
1076 *
1077 * @param string $path
1078 * @return bool
1079 */
1080 protected function hasSubDirectories(string $path): bool
1081 {
1082 return !empty(glob(rtrim($path, '/\\') . '/*', GLOB_ONLYDIR));
1083 }
1084
1085 /**
1086 * @param array $packageStatesConfiguration
1087 * @return array Returns the packageStatesConfiguration sorted by dependencies
1088 * @throws \UnexpectedValueException
1089 */
1090 protected function sortPackageStatesConfigurationByDependency(array $packageStatesConfiguration)
1091 {
1092 return $this->dependencyOrderingService->calculateOrder($this->buildDependencyGraph($packageStatesConfiguration));
1093 }
1094
1095 /**
1096 * Convert the package configuration into a dependency definition
1097 *
1098 * This converts "dependencies" and "suggestions" to "after" syntax for the usage in DependencyOrderingService
1099 *
1100 * @param array $packageStatesConfiguration
1101 * @param array $packageKeys
1102 * @return array
1103 * @throws \UnexpectedValueException
1104 */
1105 protected function convertConfigurationForGraph(array $packageStatesConfiguration, array $packageKeys)
1106 {
1107 $dependencies = [];
1108 foreach ($packageKeys as $packageKey) {
1109 if (!isset($packageStatesConfiguration[$packageKey]['dependencies']) && !isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1110 continue;
1111 }
1112 $dependencies[$packageKey] = [
1113 'after' => []
1114 ];
1115 if (isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
1116 foreach ($packageStatesConfiguration[$packageKey]['dependencies'] as $dependentPackageKey) {
1117 if (!in_array($dependentPackageKey, $packageKeys, true)) {
1118 throw new \UnexpectedValueException(
1119 'The package "' . $packageKey . '" depends on "'
1120 . $dependentPackageKey . '" which is not present in the system.',
1121 1519931815
1122 );
1123 }
1124 $dependencies[$packageKey]['after'][] = $dependentPackageKey;
1125 }
1126 }
1127 if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1128 foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
1129 // skip suggestions on not existing packages
1130 if (in_array($suggestedPackageKey, $packageKeys, true)) {
1131 // Suggestions actually have never been meant to influence loading order.
1132 // We misuse this currently, as there is no other way to influence the loading order
1133 // for not-required packages (soft-dependency).
1134 // When considering suggestions for the loading order, we might create a cyclic dependency
1135 // if the suggested package already has a real dependency on this package, so the suggestion
1136 // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
1137 $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
1138 }
1139 }
1140 }
1141 }
1142 return $dependencies;
1143 }
1144
1145 /**
1146 * Adds all root packages of current dependency graph as dependency to all extensions
1147 *
1148 * This ensures that the framework extensions (aka sysext) are
1149 * always loaded first, before any other external extension.
1150 *
1151 * @param array $packageStateConfiguration
1152 * @param array $rootPackageKeys
1153 * @return array
1154 */
1155 protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys)
1156 {
1157 $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1158 $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
1159 foreach ($extensionPackageKeys as $packageKey) {
1160 // Remove framework packages from list
1161 $packageKeysWithoutFramework = array_diff(
1162 $packageStateConfiguration[$packageKey]['dependencies'],
1163 $frameworkPackageKeys
1164 );
1165 // The order of the array_merge is crucial here,
1166 // we want the framework first
1167 $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
1168 $rootPackageKeys,
1169 $packageKeysWithoutFramework
1170 );
1171 }
1172 return $packageStateConfiguration;
1173 }
1174
1175 /**
1176 * Builds the dependency graph for all packages
1177 *
1178 * This method also introduces dependencies among the dependencies
1179 * to ensure the loading order is exactly as specified in the list.
1180 *
1181 * @param array $packageStateConfiguration
1182 * @return array
1183 */
1184 protected function buildDependencyGraph(array $packageStateConfiguration)
1185 {
1186 $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1187 $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
1188 $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
1189
1190 $packageKeys = array_keys($packageStateConfiguration);
1191 return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
1192 }
1193
1194 /**
1195 * @param array $packageStateConfiguration
1196 * @return array
1197 */
1198 protected function findFrameworkPackages(array $packageStateConfiguration)
1199 {
1200 $frameworkPackageKeys = [];
1201 foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
1202 $package = $this->getPackage($packageKey);
1203 if ($package->getValueFromComposerManifest('type') === 'typo3-cms-framework') {
1204 $frameworkPackageKeys[] = $packageKey;
1205 }
1206 }
1207
1208 return $frameworkPackageKeys;
1209 }
1210 }