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