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