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