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