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