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