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