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