693befef660d415e2fdd13d8e9ec8aff362476e5
[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 if (!isset($stateConfiguration['packagePath'])) {
467 $this->unregisterPackageByPackageKey($packageKey);
468 $packageStatesHasChanged = TRUE;
469 continue;
470 }
471
472 try {
473 $packagePath = PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
474 $package = new Package($this, $packageKey, $packagePath);
475 } catch (Exception\InvalidPackagePathException $exception) {
476 $this->unregisterPackageByPackageKey($packageKey);
477 $packageStatesHasChanged = TRUE;
478 continue;
479 } catch (Exception\InvalidPackageKeyException $exception) {
480 $this->unregisterPackageByPackageKey($packageKey);
481 $packageStatesHasChanged = TRUE;
482 continue;
483 }
484
485 $this->registerPackage($package, FALSE);
486
487 if (!$this->packages[$packageKey] instanceof PackageInterface) {
488 throw new Exception\CorruptPackageException(sprintf('The package class in package "%s" does not implement PackageInterface.', $packageKey), 1300782488);
489 }
490
491 $this->packageKeys[strtolower($packageKey)] = $packageKey;
492 if ($stateConfiguration['state'] === 'active') {
493 $this->activePackages[$packageKey] = $this->packages[$packageKey];
494 }
495 }
496 if ($packageStatesHasChanged) {
497 $this->sortAndSavePackageStates();
498 }
499 }
500
501 /**
502 * Register a native TYPO3 package
503 *
504 * @param PackageInterface $package The Package to be registered
505 * @param bool $sortAndSave allows for not saving packagestates when used in loops etc.
506 * @return PackageInterface
507 * @throws Exception\InvalidPackageStateException
508 * @throws Exception\PackageStatesFileNotWritableException
509 */
510 public function registerPackage(PackageInterface $package, $sortAndSave = TRUE) {
511 $packageKey = $package->getPackageKey();
512 if ($this->isPackageAvailable($packageKey)) {
513 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
514 }
515
516 $this->packages[$packageKey] = $package;
517 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
518
519 if ($sortAndSave === TRUE) {
520 $this->sortAndSavePackageStates();
521 }
522
523 if ($package instanceof PackageInterface) {
524 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
525 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
526 }
527 }
528 return $package;
529 }
530
531 /**
532 * Unregisters a package from the list of available packages
533 *
534 * @param string $packageKey Package Key of the package to be unregistered
535 * @return void
536 */
537 protected function unregisterPackageByPackageKey($packageKey) {
538 try {
539 $package = $this->getPackage($packageKey);
540 if ($package instanceof PackageInterface) {
541 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
542 unset($this->packageAliasMap[strtolower($packageToReplace)]);
543 }
544 }
545 } catch (Exception\UnknownPackageException $e) {
546 }
547 unset($this->packages[$packageKey]);
548 unset($this->packageKeys[strtolower($packageKey)]);
549 unset($this->packageStatesConfiguration['packages'][$packageKey]);
550 }
551
552 /**
553 * Resolves a TYPO3 package key from a composer package name.
554 *
555 * @param string $composerName
556 * @return string
557 */
558 public function getPackageKeyFromComposerName($composerName) {
559 if (isset($this->packageAliasMap[$composerName])) {
560 return $this->packageAliasMap[$composerName];
561 }
562 if (empty($this->composerNameToPackageKeyMap)) {
563 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageStateConfiguration) {
564 $this->composerNameToPackageKeyMap[strtolower($packageStateConfiguration['composerName'])] = $packageKey;
565 }
566 }
567 $lowercasedComposerName = strtolower($composerName);
568 if (!isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
569 return $composerName;
570 }
571 return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
572 }
573
574 /**
575 * @return array
576 */
577 public function getExtAutoloadRegistry() {
578 if (!isset($this->extAutoloadClassFiles)) {
579 $classRegistry = array();
580 foreach ($this->activePackages as $packageKey => $packageData) {
581 try {
582 $extensionAutoloadFile = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($packageKey, 'ext_autoload.php');
583 if (@file_exists($extensionAutoloadFile)) {
584 $classRegistry = array_merge($classRegistry, require $extensionAutoloadFile);
585 }
586 } catch (\BadFunctionCallException $e) {
587 }
588 }
589 $this->extAutoloadClassFiles = $classRegistry;
590 }
591 return $this->extAutoloadClassFiles;
592 }
593
594 /**
595 * Returns a PackageInterface object for the specified package.
596 * A package is available, if the package directory contains valid MetaData information.
597 *
598 * @param string $packageKey
599 * @return PackageInterface The requested package object
600 * @throws Exception\UnknownPackageException if the specified package is not known
601 * @api
602 */
603 public function getPackage($packageKey) {
604 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
605 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
606 }
607 if (!$this->isPackageAvailable($packageKey)) {
608 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);
609 }
610 return $this->packages[$packageKey];
611 }
612
613 /**
614 * Returns TRUE if a package is available (the package's files exist in the packages directory)
615 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
616 *
617 * @param string $packageKey The key of the package to check
618 * @return bool TRUE if the package is available, otherwise FALSE
619 * @api
620 */
621 public function isPackageAvailable($packageKey) {
622 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
623 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
624 }
625 return isset($this->packages[$packageKey]);
626 }
627
628 /**
629 * Returns TRUE if a package is activated or FALSE if it's not.
630 *
631 * @param string $packageKey The key of the package to check
632 * @return bool TRUE if package is active, otherwise FALSE
633 * @api
634 */
635 public function isPackageActive($packageKey) {
636 return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->activePackages[$packageKey]);
637 }
638
639 /**
640 * Deactivates a package and updates the packagestates configuration
641 *
642 * @param string $packageKey
643 * @throws Exception\PackageStatesFileNotWritableException
644 * @throws Exception\ProtectedPackageKeyException
645 * @throws Exception\UnknownPackageException
646 */
647 public function deactivatePackage($packageKey) {
648 $this->sortAvailablePackagesByDependencies();
649
650 foreach ($this->packageStatesConfiguration['packages'] as $packageStateKey => $packageStateConfiguration) {
651 if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies']) || $packageStateConfiguration['state'] !== 'active') {
652 continue;
653 }
654 if (in_array($packageKey, $packageStateConfiguration['dependencies'], TRUE)) {
655 $this->deactivatePackage($packageStateKey);
656 }
657 }
658
659 if (!$this->isPackageActive($packageKey)) {
660 return;
661 }
662
663 $package = $this->getPackage($packageKey);
664 if ($package->isProtected()) {
665 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
666 }
667
668 unset($this->activePackages[$packageKey]);
669 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
670 $this->sortAndSavePackageStates();
671 $this->classLoader->setPackages($this->activePackages);
672 }
673
674 /**
675 * @param string $packageKey
676 */
677 public function activatePackage($packageKey) {
678 if ($this->isPackageActive($packageKey)) {
679 return;
680 }
681
682 $package = $this->getPackage($packageKey);
683 $this->activePackages[$packageKey] = $package;
684 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'active';
685 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['packagePath'])) {
686 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
687 }
688 $this->sortAndSavePackageStates();
689 $this->classLoader->addActivePackage($package);
690 }
691
692 /**
693 * Enables packages during runtime, but no class aliases will be available
694 *
695 * @param string $packageKey
696 * @api
697 */
698 public function activatePackageDuringRuntime($packageKey) {
699 $package = $this->getPackage($packageKey);
700 $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
701 $this->classLoader->addActivePackage($package);
702 if (!isset($GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()])) {
703 $loadedExtArrayElement = new LoadedExtensionArrayElement($package);
704 $GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()] = $loadedExtArrayElement->toArray();
705 }
706 }
707
708 /**
709 * Removes a package from the file system.
710 *
711 * @param string $packageKey
712 * @throws Exception
713 * @throws Exception\InvalidPackageStateException
714 * @throws Exception\ProtectedPackageKeyException
715 * @throws Exception\UnknownPackageException
716 */
717 public function deletePackage($packageKey) {
718 if (!$this->isPackageAvailable($packageKey)) {
719 throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
720 }
721
722 $package = $this->getPackage($packageKey);
723 if ($package->isProtected()) {
724 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
725 }
726
727 if ($this->isPackageActive($packageKey)) {
728 $this->deactivatePackage($packageKey);
729 }
730
731 $this->unregisterPackage($package);
732 $this->sortAndSavePackageStates();
733
734 $packagePath = $package->getPackagePath();
735 $deletion = GeneralUtility::rmdir($packagePath, TRUE);
736 if ($deletion === FALSE) {
737 throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
738 }
739 }
740
741
742 /**
743 * Returns an array of \TYPO3\CMS\Core\Package objects of all active packages.
744 * A package is active, if it is available and has been activated in the package
745 * manager settings. This method returns runtime activated packages too
746 *
747 * @return PackageInterface[]
748 * @api
749 */
750 public function getActivePackages() {
751 return array_merge($this->activePackages, $this->runtimeActivatedPackages);
752 }
753
754 /**
755 * Orders all packages by comparing their dependencies. By this, the packages
756 * and package configurations arrays holds all packages in the correct
757 * initialization order.
758 *
759 * @return void
760 */
761 protected function sortAvailablePackagesByDependencies() {
762 $this->resolvePackageDependencies();
763
764 // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
765 ksort($this->packageStatesConfiguration['packages']);
766 $this->packageStatesConfiguration['packages'] = $this->dependencyResolver->sortPackageStatesConfigurationByDependency($this->packageStatesConfiguration['packages']);
767
768 // Reorder the packages according to the loading order
769 $newPackages = array();
770 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $_) {
771 $newPackages[$packageKey] = $this->packages[$packageKey];
772 }
773 $this->packages = $newPackages;
774 }
775
776 /**
777 * Resolves the dependent packages from the meta data of all packages recursively. The
778 * resolved direct or indirect dependencies of each package will put into the package
779 * states configuration array.
780 *
781 * @return void
782 */
783 protected function resolvePackageDependencies() {
784 foreach ($this->packages as $packageKey => $package) {
785 $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
786 }
787 foreach ($this->packages as $packageKey => $package) {
788 $this->packageStatesConfiguration['packages'][$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
789 }
790 }
791
792 /**
793 * Returns an array of suggested package keys for the given package.
794 *
795 * @param string $packageKey The package key to fetch the suggestions for
796 * @return array|NULL An array of directly suggested packages
797 */
798 protected function getSuggestionArrayForPackage($packageKey) {
799 if (!isset($this->packages[$packageKey])) {
800 return NULL;
801 }
802 $suggestedPackageKeys = array();
803 $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_SUGGESTS);
804 foreach ($suggestedPackageConstraints as $constraint) {
805 if ($constraint instanceof MetaData\PackageConstraint) {
806 $suggestedPackageKey = $constraint->getValue();
807 if (isset($this->packages[$suggestedPackageKey])) {
808 $suggestedPackageKeys[] = $suggestedPackageKey;
809 }
810 }
811 }
812 return array_reverse($suggestedPackageKeys);
813 }
814
815 /**
816 * Saves the current content of $this->packageStatesConfiguration to the
817 * PackageStates.php file.
818 *
819 * @throws Exception\PackageStatesFileNotWritableException
820 */
821 protected function sortAndSavePackageStates() {
822 $this->sortAvailablePackagesByDependencies();
823
824 $this->packageStatesConfiguration['version'] = 4;
825
826 $fileDescription = "# PackageStates.php\n\n";
827 $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
828 $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
829 $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
830 $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
831
832 // we do not need the dependencies on disk...
833 foreach ($this->packageStatesConfiguration['packages'] as &$packageConfiguration) {
834 if (isset($packageConfiguration['dependencies'])) {
835 unset($packageConfiguration['dependencies']);
836 }
837 }
838 if (!@is_writable($this->packageStatesPathAndFilename)) {
839 // If file does not exists try to create it
840 $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
841 if (!$fileHandle) {
842 throw new Exception\PackageStatesFileNotWritableException(
843 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),
844 1382449759
845 );
846 }
847 fclose($fileHandle);
848 }
849 $packageStatesCode = "<?php\n$fileDescription\nreturn " . var_export($this->packageStatesConfiguration, TRUE) . "\n ?>";
850 GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, TRUE);
851
852 $this->initializeCompatibilityLoadedExtArray();
853 \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($this->packageStatesPathAndFilename);
854 }
855
856 /**
857 * Check the conformance of the given package key
858 *
859 * @param string $packageKey The package key to validate
860 * @return bool If the package key is valid, returns TRUE otherwise FALSE
861 * @api
862 */
863 public function isPackageKeyValid($packageKey) {
864 return preg_match(PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
865 }
866
867 /**
868 * Returns an array of \TYPO3\CMS\Core\Package objects of all available packages.
869 * A package is available, if the package directory contains valid meta information.
870 *
871 * @return array Array of PackageInterface
872 * @api
873 */
874 public function getAvailablePackages() {
875 return $this->packages;
876 }
877
878 /**
879 * Unregisters a package from the list of available packages
880 *
881 * @param PackageInterface $package The package to be unregistered
882 * @return void
883 * @throws Exception\InvalidPackageStateException
884 */
885 public function unregisterPackage(PackageInterface $package) {
886 $packageKey = $package->getPackageKey();
887 if (!$this->isPackageAvailable($packageKey)) {
888 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
889 }
890 $this->unregisterPackageByPackageKey($packageKey);
891 }
892
893 /**
894 * Scans all sub directories of the specified directory and collects the package keys of packages it finds.
895 *
896 * The return of the array is to make this method usable in array_merge.
897 *
898 * @param string $startPath
899 * @param array $collectedPackagePaths
900 * @return array
901 */
902 protected function scanPackagesInPath($startPath, array $collectedPackagePaths) {
903 foreach (new \DirectoryIterator($startPath) as $fileInfo) {
904 if (!$fileInfo->isDir()) {
905 continue;
906 }
907 $filename = $fileInfo->getFilename();
908 if ($filename[0] !== '.') {
909 $currentPath = $fileInfo->getPathName();
910 $currentPath = PathUtility::sanitizeTrailingSeparator($currentPath);
911 if ($this->hasComposerManifestFile($currentPath)) {
912 $collectedPackagePaths[$currentPath] = $currentPath;
913 }
914 }
915 }
916 return $collectedPackagePaths;
917 }
918
919 /**
920 * Returns contents of Composer manifest as a stdObject
921 *
922 * @param string $manifestPath
923 * @return mixed
924 * @throws Exception\MissingPackageManifestException
925 * @see json_decode for return values
926 */
927 public function getComposerManifest($manifestPath) {
928 if (!file_exists($manifestPath . 'composer.json')) {
929 throw new Exception\MissingPackageManifestException('No composer manifest file found at "' . $manifestPath . '/composer.json".', 1349868540);
930 }
931 $json = file_get_contents($manifestPath . 'composer.json');
932 return json_decode($json);
933 }
934
935 /**
936 * Returns an array of dependent package keys for the given package. It will
937 * do this recursively, so dependencies of dependant packages will also be
938 * in the result.
939 *
940 * @param string $packageKey The package key to fetch the dependencies for
941 * @param array $dependentPackageKeys
942 * @param array $trace An array of already visited package keys, to detect circular dependencies
943 * @return array|NULL An array of direct or indirect dependant packages
944 * @throws Exception\InvalidPackageKeyException
945 */
946 protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = array(), array $trace = array()) {
947 if (!isset($this->packages[$packageKey])) {
948 return NULL;
949 }
950 if (in_array($packageKey, $trace, TRUE) !== FALSE) {
951 return $dependentPackageKeys;
952 }
953 $trace[] = $packageKey;
954 $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
955 foreach ($dependentPackageConstraints as $constraint) {
956 if ($constraint instanceof MetaData\PackageConstraint) {
957 $dependentPackageKey = $constraint->getValue();
958 if (in_array($dependentPackageKey, $dependentPackageKeys, TRUE) === FALSE && in_array($dependentPackageKey, $trace, TRUE) === FALSE) {
959 $dependentPackageKeys[] = $dependentPackageKey;
960 }
961 $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
962 }
963 }
964 return array_reverse($dependentPackageKeys);
965 }
966
967
968 /**
969 * Resolves package key from Composer manifest
970 *
971 * If it is a TYPO3 package the name of the containing directory will be used.
972 *
973 * Else if the composer name of the package matches the first part of the lowercased namespace of the package, the mixed
974 * case version of the composer name / namespace will be used, with backslashes replaced by dots.
975 *
976 * Else the composer name will be used with the slash replaced by a dot
977 *
978 * @param object $manifest
979 * @param string $packagePath
980 * @param string $packagesBasePath
981 * @throws Exception\InvalidPackageManifestException
982 * @return string
983 */
984 protected function getPackageKeyFromManifest($manifest, $packagePath, $packagesBasePath) {
985 if (!is_object($manifest)) {
986 throw new Exception\InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
987 }
988 if (isset($manifest->type) && substr($manifest->type, 0, 10) === 'typo3-cms-') {
989 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
990 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
991 return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
992 } else {
993 $packageKey = str_replace('/', '.', $manifest->name);
994 return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
995 }
996 }
997 }