[CLEANUP] Streamline EXT:about
[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\Compatibility\LoadedExtensionArrayElement;
20 use TYPO3\CMS\Core\Core\Bootstrap;
21 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
22 use TYPO3\CMS\Core\Service\OpcodeCacheService;
23 use TYPO3\CMS\Core\Utility\ArrayUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Core\Utility\PathUtility;
26 use TYPO3\CMS\Core\Utility\StringUtility;
27
28 /**
29 * The default TYPO3 Package Manager
30 * Adapted from FLOW for TYPO3 CMS
31 */
32 class PackageManager implements \TYPO3\CMS\Core\SingletonInterface
33 {
34 /**
35 * @var \TYPO3\CMS\Core\Package\DependencyResolver
36 */
37 protected $dependencyResolver;
38
39 /**
40 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
41 */
42 protected $coreCache;
43
44 /**
45 * @var string
46 */
47 protected $cacheIdentifier;
48
49 /**
50 * @var array
51 */
52 protected $packagesBasePaths = [];
53
54 /**
55 * @var array
56 */
57 protected $packageAliasMap = [];
58
59 /**
60 * @var array
61 */
62 protected $runtimeActivatedPackages = [];
63
64 /**
65 * Absolute path leading to the various package directories
66 * @var string
67 */
68 protected $packagesBasePath = PATH_site;
69
70 /**
71 * Array of available packages, indexed by package key
72 * @var PackageInterface[]
73 */
74 protected $packages = [];
75
76 /**
77 * @var bool
78 */
79 protected $availablePackagesScanned = false;
80
81 /**
82 * A map between ComposerName and PackageKey, only available when scanAvailablePackages is run
83 * @var array
84 */
85 protected $composerNameToPackageKeyMap = [];
86
87 /**
88 * List of active packages as package key => package object
89 * @var array
90 */
91 protected $activePackages = [];
92
93 /**
94 * @var string
95 */
96 protected $packageStatesPathAndFilename;
97
98 /**
99 * Package states configuration as stored in the PackageStates.php file
100 * @var array
101 */
102 protected $packageStatesConfiguration = [];
103
104 /**
105 * Constructor
106 */
107 public function __construct()
108 {
109 $this->packageStatesPathAndFilename = PATH_typo3conf . 'PackageStates.php';
110 }
111
112 /**
113 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
114 */
115 public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache)
116 {
117 $this->coreCache = $coreCache;
118 }
119
120 /**
121 * @param DependencyResolver $dependencyResolver
122 */
123 public function injectDependencyResolver(DependencyResolver $dependencyResolver)
124 {
125 $this->dependencyResolver = $dependencyResolver;
126 }
127
128 /**
129 * Initializes the package manager
130 *
131 * @return void
132 */
133 public function initialize()
134 {
135 try {
136 $this->loadPackageManagerStatesFromCache();
137 } catch (Exception\PackageManagerCacheUnavailableException $exception) {
138 $this->loadPackageStates();
139 $this->initializePackageObjects();
140 $this->initializeCompatibilityLoadedExtArray();
141 $this->saveToPackageCache();
142 }
143 }
144
145 /**
146 * @return string
147 */
148 protected function getCacheIdentifier()
149 {
150 if ($this->cacheIdentifier === null) {
151 $mTime = @filemtime($this->packageStatesPathAndFilename);
152 if ($mTime !== false) {
153 $this->cacheIdentifier = md5($this->packageStatesPathAndFilename . $mTime);
154 } else {
155 $this->cacheIdentifier = null;
156 }
157 }
158 return $this->cacheIdentifier;
159 }
160
161 /**
162 * @return string
163 */
164 protected function getCacheEntryIdentifier()
165 {
166 $cacheIdentifier = $this->getCacheIdentifier();
167 return $cacheIdentifier !== null ? 'PackageManager_' . $cacheIdentifier : null;
168 }
169
170 /**
171 * Saves the current state of all relevant information to the TYPO3 Core Cache
172 */
173 protected function saveToPackageCache()
174 {
175 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
176 if ($cacheEntryIdentifier !== null && !$this->coreCache->has($cacheEntryIdentifier)) {
177 // Package objects get their own cache entry, so PHP does not have to parse the serialized string
178 $packageObjectsCacheEntryIdentifier = StringUtility::getUniqueId('PackageObjects_');
179 // Build cache file
180 $packageCache = [
181 'packageStatesConfiguration' => $this->packageStatesConfiguration,
182 'packageAliasMap' => $this->packageAliasMap,
183 'loadedExtArray' => $GLOBALS['TYPO3_LOADED_EXT'],
184 'composerNameToPackageKeyMap' => $this->composerNameToPackageKeyMap,
185 'packageObjectsCacheEntryIdentifier' => $packageObjectsCacheEntryIdentifier
186 ];
187 $this->coreCache->set($packageObjectsCacheEntryIdentifier, serialize($this->packages));
188 $this->coreCache->set(
189 $cacheEntryIdentifier,
190 'return ' . PHP_EOL . var_export($packageCache, true) . ';'
191 );
192 }
193 }
194
195 /**
196 * Attempts to load the package manager states from cache
197 *
198 * @throws Exception\PackageManagerCacheUnavailableException
199 */
200 protected function loadPackageManagerStatesFromCache()
201 {
202 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
203 if ($cacheEntryIdentifier === null || !$this->coreCache->has($cacheEntryIdentifier) || !($packageCache = $this->coreCache->requireOnce($cacheEntryIdentifier))) {
204 throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883342);
205 }
206 $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
207 if ($this->packageStatesConfiguration['version'] < 5) {
208 throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883341);
209 }
210 $this->packageAliasMap = $packageCache['packageAliasMap'];
211 $this->composerNameToPackageKeyMap = $packageCache['composerNameToPackageKeyMap'];
212 $GLOBALS['TYPO3_LOADED_EXT'] = $packageCache['loadedExtArray'];
213 $GLOBALS['TYPO3_currentPackageManager'] = $this;
214 // Strip off PHP Tags from Php Cache Frontend
215 $packageObjects = substr(substr($this->coreCache->get($packageCache['packageObjectsCacheEntryIdentifier']), 6), 0, -2);
216 $this->packages = unserialize($packageObjects);
217 unset($GLOBALS['TYPO3_currentPackageManager']);
218 }
219
220 /**
221 * Loads the states of available packages from the PackageStates.php file.
222 * The result is stored in $this->packageStatesConfiguration.
223 *
224 * @throws Exception\PackageStatesUnavailableException
225 * @return void
226 */
227 protected function loadPackageStates()
228 {
229 $forcePackageStatesRewrite = false;
230 $this->packageStatesConfiguration = @include($this->packageStatesPathAndFilename) ?: [];
231 if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
232 $this->packageStatesConfiguration = [];
233 } elseif ($this->packageStatesConfiguration['version'] === 4) {
234 // Convert to v5 format which only includes a list of active packages.
235 // Deprecated since version 8, will be removed in version 9.
236 $activePackages = [];
237 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfiguration) {
238 if ($packageConfiguration['state'] !== 'active') {
239 continue;
240 }
241 $activePackages[$packageKey] = ['packagePath' => $packageConfiguration['packagePath']];
242 }
243 $this->packageStatesConfiguration['packages'] = $activePackages;
244 $this->packageStatesConfiguration['version'] = 5;
245 $forcePackageStatesRewrite = true;
246 }
247 if ($this->packageStatesConfiguration !== []) {
248 $this->registerPackagesFromConfiguration($this->packageStatesConfiguration['packages'], false, $forcePackageStatesRewrite);
249 } else {
250 throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
251 }
252 }
253
254 /**
255 * Initializes activePackages property
256 *
257 * Saves PackageStates.php if list of required extensions has changed.
258 *
259 * @return void
260 */
261 protected function initializePackageObjects()
262 {
263 $requiredPackages = [];
264 $activePackages = [];
265 foreach ($this->packages as $packageKey => $package) {
266 if ($package->isProtected()) {
267 $requiredPackages[$packageKey] = $package;
268 }
269 if (isset($this->packageStatesConfiguration['packages'][$packageKey])) {
270 $activePackages[$packageKey] = $package;
271 }
272 }
273 $previousActivePackages = $activePackages;
274 $activePackages = array_merge($requiredPackages, $activePackages);
275
276 if ($activePackages != $previousActivePackages) {
277 foreach ($requiredPackages as $requiredPackageKey => $package) {
278 $this->registerActivePackage($package);
279 }
280 $this->sortAndSavePackageStates();
281 }
282 }
283
284 /**
285 * @param PackageInterface $package
286 */
287 protected function registerActivePackage(PackageInterface $package)
288 {
289 // reset the active packages so they are rebuilt.
290 $this->activePackages = [];
291 $this->packageStatesConfiguration['packages'][$package->getPackageKey()] = ['packagePath' => str_replace($this->packagesBasePath, '', $package->getPackagePath())];
292 }
293
294 /**
295 * Initializes a backwards compatibility $GLOBALS['TYPO3_LOADED_EXT'] array
296 *
297 * @return void
298 */
299 protected function initializeCompatibilityLoadedExtArray()
300 {
301 $loadedExtObj = new \TYPO3\CMS\Core\Compatibility\LoadedExtensionsArray($this);
302 $GLOBALS['TYPO3_LOADED_EXT'] = $loadedExtObj->toArray();
303 }
304
305 /**
306 * Scans all directories in the packages directories for available packages.
307 * For each package a Package object is created and stored in $this->packages.
308 *
309 * @return void
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 = dirname($fileInfo->getPathname());
377 $extensionName = 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->isPackageAvailable($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 * @throws Exception\PackageStatesFileNotWritableException
442 */
443 public function registerPackage(PackageInterface $package)
444 {
445 $packageKey = $package->getPackageKey();
446 if ($this->isPackageAvailable($packageKey)) {
447 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
448 }
449
450 $this->packages[$packageKey] = $package;
451
452 if ($package instanceof PackageInterface) {
453 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
454 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
455 }
456 }
457 return $package;
458 }
459
460 /**
461 * Unregisters a package from the list of available packages
462 *
463 * @param string $packageKey Package Key of the package to be unregistered
464 * @return void
465 */
466 protected function unregisterPackageByPackageKey($packageKey)
467 {
468 try {
469 $package = $this->getPackage($packageKey);
470 if ($package instanceof PackageInterface) {
471 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
472 unset($this->packageAliasMap[strtolower($packageToReplace)]);
473 }
474 }
475 } catch (Exception\UnknownPackageException $e) {
476 }
477 unset($this->packages[$packageKey]);
478 unset($this->packageStatesConfiguration['packages'][$packageKey]);
479 }
480
481 /**
482 * Resolves a TYPO3 package key from a composer package name.
483 *
484 * @param string $composerName
485 * @return string
486 */
487 public function getPackageKeyFromComposerName($composerName)
488 {
489 if (isset($this->packageAliasMap[$composerName])) {
490 return $this->packageAliasMap[$composerName];
491 }
492 $lowercasedComposerName = strtolower($composerName);
493 if (isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
494 return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
495 } else {
496 return $composerName;
497 }
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 (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
512 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
513 }
514 if (!$this->isPackageAvailable($packageKey)) {
515 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);
516 }
517 return $this->packages[$packageKey];
518 }
519
520 /**
521 * Returns TRUE if a package is available (the package's files exist in the packages directory)
522 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
523 *
524 * @param string $packageKey The key of the package to check
525 * @return bool TRUE if the package is available, otherwise FALSE
526 * @api
527 */
528 public function isPackageAvailable($packageKey)
529 {
530 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
531 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
532 }
533 return isset($this->packages[$packageKey]);
534 }
535
536 /**
537 * Returns TRUE if a package is activated or FALSE if it's not.
538 *
539 * @param string $packageKey The key of the package to check
540 * @return bool TRUE if package is active, otherwise FALSE
541 * @api
542 */
543 public function isPackageActive($packageKey)
544 {
545 return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->packageStatesConfiguration['packages'][$packageKey]);
546 }
547
548 /**
549 * Deactivates a package and updates the packagestates configuration
550 *
551 * @param string $packageKey
552 * @throws Exception\PackageStatesFileNotWritableException
553 * @throws Exception\ProtectedPackageKeyException
554 * @throws Exception\UnknownPackageException
555 */
556 public function deactivatePackage($packageKey)
557 {
558 $packagesWithDependencies = $this->sortActivePackagesByDependencies();
559
560 foreach ($packagesWithDependencies as $packageStateKey => $packageStateConfiguration) {
561 if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies'])) {
562 continue;
563 }
564 if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
565 $this->deactivatePackage($packageStateKey);
566 }
567 }
568
569 if (!$this->isPackageActive($packageKey)) {
570 return;
571 }
572
573 $package = $this->getPackage($packageKey);
574 if ($package->isProtected()) {
575 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
576 }
577
578 $this->activePackages = [];
579 unset($this->packageStatesConfiguration['packages'][$packageKey]);
580 $this->sortAndSavePackageStates();
581 }
582
583 /**
584 * @param string $packageKey
585 */
586 public function activatePackage($packageKey)
587 {
588 $package = $this->getPackage($packageKey);
589 $this->registerTransientClassLoadingInformationForPackage($package);
590
591 if ($this->isPackageActive($packageKey)) {
592 return;
593 }
594
595 $this->registerActivePackage($package);
596 $this->sortAndSavePackageStates();
597 }
598
599 /**
600 * Enables packages during runtime, but no class aliases will be available
601 *
602 * @param string $packageKey
603 * @api
604 */
605 public function activatePackageDuringRuntime($packageKey)
606 {
607 $package = $this->registerPackageDuringRuntime($packageKey);
608 $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
609 if (!isset($GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()])) {
610 $loadedExtArrayElement = new LoadedExtensionArrayElement($package);
611 $GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()] = $loadedExtArrayElement->toArray();
612 }
613 $this->registerTransientClassLoadingInformationForPackage($package);
614 }
615
616 /**
617 * @param PackageInterface $package
618 * @throws \TYPO3\CMS\Core\Exception
619 */
620 protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
621 {
622 if (Bootstrap::usesComposerClassLoading()) {
623 return;
624 }
625 ClassLoadingInformation::registerTransientClassLoadingInformationForPackage($package);
626 }
627
628 /**
629 * Removes a package from the file system.
630 *
631 * @param string $packageKey
632 * @throws Exception
633 * @throws Exception\InvalidPackageStateException
634 * @throws Exception\ProtectedPackageKeyException
635 * @throws Exception\UnknownPackageException
636 */
637 public function deletePackage($packageKey)
638 {
639 if (!$this->isPackageAvailable($packageKey)) {
640 throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
641 }
642
643 $package = $this->getPackage($packageKey);
644 if ($package->isProtected()) {
645 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
646 }
647
648 if ($this->isPackageActive($packageKey)) {
649 $this->deactivatePackage($packageKey);
650 }
651
652 $this->unregisterPackage($package);
653 $this->sortAndSavePackageStates();
654
655 $packagePath = $package->getPackagePath();
656 $deletion = GeneralUtility::rmdir($packagePath, true);
657 if ($deletion === false) {
658 throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
659 }
660 }
661
662 /**
663 * Returns an array of \TYPO3\CMS\Core\Package objects of all active packages.
664 * A package is active, if it is available and has been activated in the package
665 * manager settings. This method returns runtime activated packages too
666 *
667 * @return PackageInterface[]
668 * @api
669 */
670 public function getActivePackages()
671 {
672 if (empty($this->activePackages)) {
673 if (!empty($this->packageStatesConfiguration['packages'])) {
674 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfig) {
675 $this->activePackages[$packageKey] = $this->getPackage($packageKey);
676 }
677 }
678 }
679 return array_merge($this->activePackages, $this->runtimeActivatedPackages);
680 }
681
682 /**
683 * Orders all active packages by comparing their dependencies. By this, the packages
684 * and package configurations arrays holds all packages in the correct
685 * initialization order.
686 *
687 * @return array
688 */
689 protected function sortActivePackagesByDependencies()
690 {
691 $packagesWithDependencies = $this->resolvePackageDependencies($this->packageStatesConfiguration['packages']);
692
693 // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
694 ksort($packagesWithDependencies);
695 $sortedPackageKeys = $this->dependencyResolver->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
696
697 // Reorder the packages according to the loading order
698 $this->packageStatesConfiguration['packages'] = [];
699 foreach ($sortedPackageKeys as $packageKey) {
700 $this->registerActivePackage($this->packages[$packageKey]);
701 }
702 return $packagesWithDependencies;
703 }
704
705 /**
706 * Resolves the dependent packages from the meta data of all packages recursively. The
707 * resolved direct or indirect dependencies of each package will put into the package
708 * states configuration array.
709 *
710 * @param $packageConfig
711 * @return array
712 */
713 protected function resolvePackageDependencies($packageConfig)
714 {
715 $packagesWithDependencies = [];
716 foreach ($packageConfig as $packageKey => $_) {
717 $packagesWithDependencies[$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
718 $packagesWithDependencies[$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
719 }
720 return $packagesWithDependencies;
721 }
722
723 /**
724 * Returns an array of suggested package keys for the given package.
725 *
726 * @param string $packageKey The package key to fetch the suggestions for
727 * @return array|NULL An array of directly suggested packages
728 */
729 protected function getSuggestionArrayForPackage($packageKey)
730 {
731 if (!isset($this->packages[$packageKey])) {
732 return null;
733 }
734 $suggestedPackageKeys = [];
735 $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_SUGGESTS);
736 foreach ($suggestedPackageConstraints as $constraint) {
737 if ($constraint instanceof MetaData\PackageConstraint) {
738 $suggestedPackageKey = $constraint->getValue();
739 if (isset($this->packages[$suggestedPackageKey])) {
740 $suggestedPackageKeys[] = $suggestedPackageKey;
741 }
742 }
743 }
744 return array_reverse($suggestedPackageKeys);
745 }
746
747 /**
748 * Saves the current content of $this->packageStatesConfiguration to the
749 * PackageStates.php file.
750 *
751 * @throws Exception\PackageStatesFileNotWritableException
752 */
753 protected function sortAndSavePackageStates()
754 {
755 $this->sortActivePackagesByDependencies();
756
757 $this->packageStatesConfiguration['version'] = 5;
758
759 $fileDescription = "# PackageStates.php\n\n";
760 $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
761 $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
762 $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
763 $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
764
765 if (!@is_writable($this->packageStatesPathAndFilename)) {
766 // If file does not exists try to create it
767 $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
768 if (!$fileHandle) {
769 throw new Exception\PackageStatesFileNotWritableException(
770 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),
771 1382449759
772 );
773 }
774 fclose($fileHandle);
775 }
776 $packageStatesCode = "<?php\n$fileDescription\nreturn " . ArrayUtility::arrayExport($this->packageStatesConfiguration) . ";\n";
777 GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
778
779 $this->initializeCompatibilityLoadedExtArray();
780
781 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
782 }
783
784 /**
785 * Check the conformance of the given package key
786 *
787 * @param string $packageKey The package key to validate
788 * @return bool If the package key is valid, returns TRUE otherwise FALSE
789 * @api
790 */
791 public function isPackageKeyValid($packageKey)
792 {
793 return preg_match(PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
794 }
795
796 /**
797 * Returns an array of \TYPO3\CMS\Core\Package objects of all available packages.
798 * A package is available, if the package directory contains valid meta information.
799 *
800 * @return PackageInterface[] Array of PackageInterface
801 * @api
802 */
803 public function getAvailablePackages()
804 {
805 if ($this->availablePackagesScanned === false) {
806 $this->scanAvailablePackages();
807 }
808
809 return $this->packages;
810 }
811
812 /**
813 * Unregisters a package from the list of available packages
814 *
815 * @param PackageInterface $package The package to be unregistered
816 * @return void
817 * @throws Exception\InvalidPackageStateException
818 */
819 public function unregisterPackage(PackageInterface $package)
820 {
821 $packageKey = $package->getPackageKey();
822 if (!$this->isPackageAvailable($packageKey)) {
823 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
824 }
825 $this->unregisterPackageByPackageKey($packageKey);
826 }
827
828 /**
829 * Reloads a package and its information
830 *
831 * @param string $packageKey
832 * @throws Exception\InvalidPackageStateException if the package isn't available
833 * @throws Exception\InvalidPackageKeyException if an invalid package key was passed
834 * @throws Exception\InvalidPackagePathException if an invalid package path was passed
835 * @throws Exception\InvalidPackageManifestException if no extension configuration file could be found
836 */
837 public function reloadPackageInformation($packageKey)
838 {
839 if (!$this->isPackageAvailable($packageKey)) {
840 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
841 }
842
843 /** @var PackageInterface $package */
844 $package = $this->packages[$packageKey];
845 $packagePath = $package->getPackagePath();
846 $newPackage = new Package($this, $packageKey, $packagePath);
847 $this->packages[$packageKey] = $newPackage;
848 unset($package);
849 }
850
851 /**
852 * Returns contents of Composer manifest as a stdObject
853 *
854 * @param string $manifestPath
855 * @return \stdClass
856 * @throws Exception\InvalidPackageManifestException
857 */
858 public function getComposerManifest($manifestPath)
859 {
860 $composerManifest = null;
861 if (file_exists($manifestPath . 'composer.json')) {
862 $json = file_get_contents($manifestPath . 'composer.json');
863 $composerManifest = json_decode($json);
864 if (!$composerManifest instanceof \stdClass) {
865 throw new Exception\InvalidPackageManifestException('The composer.json found for extension "' . basename($manifestPath) . '" is invalid!', 1439555561);
866 }
867 }
868
869 $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath);
870 $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
871 basename($manifestPath),
872 $extensionManagerConfiguration,
873 $composerManifest ?: new \stdClass()
874 );
875
876 return $composerManifest;
877 }
878
879 /**
880 * Fetches MetaData information from ext_emconf.php, used for
881 * resolving dependencies as well.
882 *
883 * @param string $packagePath
884 * @return array
885 * @throws Exception\InvalidPackageManifestException
886 */
887 protected function getExtensionEmConf($packagePath)
888 {
889 $packageKey = basename($packagePath);
890 $_EXTKEY = $packageKey;
891 $path = $packagePath . 'ext_emconf.php';
892 $EM_CONF = null;
893 if (@file_exists($path)) {
894 include $path;
895 if (is_array($EM_CONF[$_EXTKEY])) {
896 return $EM_CONF[$_EXTKEY];
897 }
898 }
899 throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
900 }
901
902 /**
903 * Fetches information from ext_emconf.php and maps it so it is treated as it would come from composer.json
904 *
905 * @param string $packageKey
906 * @param array $extensionManagerConfiguration
907 * @param \stdClass $composerManifest
908 * @return \stdClass
909 * @throws Exception\InvalidPackageManifestException
910 */
911 protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
912 {
913 $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
914 $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
915 $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title']);
916 $this->setComposerManifestValueIfEmpty($composerManifest, 'authors', [['name' => $extensionManagerConfiguration['author'], 'email' => $extensionManagerConfiguration['author_email']]]);
917 $composerManifest->version = $extensionManagerConfiguration['version'];
918 if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
919 $composerManifest->require = new \stdClass();
920 foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
921 if (!empty($requiredPackageKey)) {
922 if ($requiredPackageKey === 'typo3') {
923 // Add implicit dependency to 'core'
924 $composerManifest->require->core = $requiredPackageVersion;
925 } elseif ($requiredPackageKey !== 'php') {
926 // Skip php dependency
927 $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
928 }
929 } else {
930 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
931 }
932 }
933 }
934 if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
935 $composerManifest->conflict = new \stdClass();
936 foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
937 if (!empty($conflictingPackageKey)) {
938 $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
939 } else {
940 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
941 }
942 }
943 }
944 if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
945 $composerManifest->suggest = new \stdClass();
946 foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
947 if (!empty($suggestedPackageKey)) {
948 $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
949 } else {
950 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
951 }
952 }
953 }
954 if (isset($extensionManagerConfiguration['autoload'])) {
955 $composerManifest->autoload = json_decode(json_encode($extensionManagerConfiguration['autoload']));
956 }
957 // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
958 unset($composerManifest->{'autoload-dev'});
959 if (isset($extensionManagerConfiguration['autoload-dev'])) {
960 $composerManifest->{'autoload-dev'} = json_decode(json_encode($extensionManagerConfiguration['autoload-dev']));
961 }
962
963 return $composerManifest;
964 }
965
966 /**
967 * @param \stdClass $manifest
968 * @param string $property
969 * @param mixed $value
970 * @return \stdClass
971 */
972 protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
973 {
974 if (empty($manifest->{$property})) {
975 $manifest->{$property} = $value;
976 }
977
978 return $manifest;
979 }
980
981 /**
982 * Returns an array of dependent package keys for the given package. It will
983 * do this recursively, so dependencies of dependent packages will also be
984 * in the result.
985 *
986 * @param string $packageKey The package key to fetch the dependencies for
987 * @param array $dependentPackageKeys
988 * @param array $trace An array of already visited package keys, to detect circular dependencies
989 * @return array|NULL An array of direct or indirect dependent packages
990 * @throws Exception\InvalidPackageKeyException
991 */
992 protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = [], array $trace = [])
993 {
994 if (!isset($this->packages[$packageKey])) {
995 return null;
996 }
997 if (in_array($packageKey, $trace, true) !== false) {
998 return $dependentPackageKeys;
999 }
1000 $trace[] = $packageKey;
1001 $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1002 foreach ($dependentPackageConstraints as $constraint) {
1003 if ($constraint instanceof MetaData\PackageConstraint) {
1004 $dependentPackageKey = $constraint->getValue();
1005 if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1006 $dependentPackageKeys[] = $dependentPackageKey;
1007 }
1008 $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1009 }
1010 }
1011 return array_reverse($dependentPackageKeys);
1012 }
1013
1014 /**
1015 * Resolves package key from Composer manifest
1016 *
1017 * If it is a TYPO3 package the name of the containing directory will be used.
1018 *
1019 * Else if the composer name of the package matches the first part of the lowercased namespace of the package, the mixed
1020 * case version of the composer name / namespace will be used, with backslashes replaced by dots.
1021 *
1022 * Else the composer name will be used with the slash replaced by a dot
1023 *
1024 * @param object $manifest
1025 * @param string $packagePath
1026 * @throws Exception\InvalidPackageManifestException
1027 * @return string
1028 */
1029 protected function getPackageKeyFromManifest($manifest, $packagePath)
1030 {
1031 if (!is_object($manifest)) {
1032 throw new Exception\InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1033 }
1034 if (isset($manifest->type) && substr($manifest->type, 0, 10) === 'typo3-cms-') {
1035 $packageKey = basename($packagePath);
1036 return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
1037 } else {
1038 $packageKey = str_replace('/', '.', $manifest->name);
1039 return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
1040 }
1041 }
1042
1043 /**
1044 * The order of paths is crucial for allowing overriding of system extension by local extensions.
1045 * Pay attention if you change order of the paths here.
1046 *
1047 * @return array
1048 */
1049 protected function getPackageBasePaths()
1050 {
1051 if (empty($this->packagesBasePaths)) {
1052 // Check if the directory even exists and if it is not empty
1053 if (is_dir(PATH_typo3conf . 'ext') && count(scandir(PATH_typo3conf . 'ext')) > 2) {
1054 $this->packagesBasePaths['local'] = PATH_typo3conf . 'ext/*/';
1055 }
1056 if (is_dir(PATH_typo3 . 'ext') && count(scandir(PATH_typo3 . 'ext')) > 2) {
1057 $this->packagesBasePaths['global'] = PATH_typo3 . 'ext/*/';
1058 }
1059 $this->packagesBasePaths['system'] = PATH_typo3 . 'sysext/*/';
1060 }
1061 return $this->packagesBasePaths;
1062 }
1063 }