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