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