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