[FOLLOWUP][BUGFIX] Ignore transient class loading info in composer mode
[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 /**
310 * Scans all directories in the packages directories for available packages.
311 * For each package a Package object is created and stored in $this->packages.
312 *
313 * @return void
314 */
315 public function scanAvailablePackages()
316 {
317 $previousPackageStatesConfiguration = $this->packageStatesConfiguration;
318
319 if (isset($this->packageStatesConfiguration['packages'])) {
320 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $configuration) {
321 if (!@file_exists($this->packagesBasePath . $configuration['packagePath'])) {
322 unset($this->packageStatesConfiguration['packages'][$packageKey]);
323 }
324 }
325 } else {
326 $this->packageStatesConfiguration['packages'] = array();
327 }
328
329 foreach ($this->packagesBasePaths as $key => $packagesBasePath) {
330 if (!is_dir($packagesBasePath)) {
331 unset($this->packagesBasePaths[$key]);
332 }
333 }
334
335 $packagePaths = $this->scanLegacyExtensions();
336 foreach ($this->packagesBasePaths as $packagesBasePath) {
337 $packagePaths = $this->scanPackagesInPath($packagesBasePath, $packagePaths);
338 }
339
340 foreach ($packagePaths as $packagePath) {
341 $packagesBasePath = $this->packagesBasePath;
342 foreach ($this->packagesBasePaths as $basePath) {
343 if (strpos($packagePath, $basePath) === 0) {
344 $packagesBasePath = $basePath;
345 break;
346 }
347 }
348 try {
349 $composerManifest = $this->getComposerManifest($packagePath);
350 $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath, $packagesBasePath);
351 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
352 $this->packageStatesConfiguration['packages'][$packageKey]['composerName'] = $composerManifest->name;
353 } catch (Exception\MissingPackageManifestException $exception) {
354 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
355 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
356 if (!$this->isPackageKeyValid($packageKey)) {
357 continue;
358 }
359 } catch (Exception\InvalidPackageKeyException $exception) {
360 continue;
361 }
362 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
363 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
364 }
365
366 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
367 }
368
369 $registerOnlyNewPackages = !empty($this->packages);
370 $this->registerPackagesFromConfiguration($registerOnlyNewPackages);
371 if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
372 $this->sortAndsavePackageStates();
373 }
374 }
375
376 /**
377 * Fetches all directories from sysext/global/local locations and checks if the extension contains an ext_emconf.php
378 *
379 * @param array $collectedExtensionPaths
380 * @return array
381 */
382 protected function scanLegacyExtensions(&$collectedExtensionPaths = array())
383 {
384 $legacyCmsPackageBasePathTypes = array('sysext', 'global', 'local');
385 foreach ($this->packagesBasePaths as $type => $packageBasePath) {
386 if (!in_array($type, $legacyCmsPackageBasePathTypes, true)) {
387 continue;
388 }
389 /** @var $fileInfo \SplFileInfo */
390 foreach (new \DirectoryIterator($packageBasePath) as $fileInfo) {
391 if (!$fileInfo->isDir()) {
392 continue;
393 }
394 $filename = $fileInfo->getFilename();
395 if ($filename[0] !== '.') {
396 // Fix Windows backslashes
397 // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
398 $currentPath = str_replace('\\', '/', $fileInfo->getPathName()) . '/';
399 // Only add the extension if we have an EMCONF and the extension is not yet registered.
400 // This is crucial in order to allow overriding of system extension by local extensions
401 // and strongly depends on the order of paths defined in $this->packagesBasePaths.
402 if (file_exists($currentPath . 'ext_emconf.php') && !isset($collectedExtensionPaths[$filename])) {
403 $collectedExtensionPaths[$filename] = $currentPath;
404 }
405 }
406 }
407 }
408 return $collectedExtensionPaths;
409 }
410
411 /**
412 * Looks for composer.json in the given path and returns TRUE or FALSE if an ext_emconf.php exists
413 * or no composer.json is found.
414 *
415 * @param string $packagePath
416 * @return bool TRUE if a composer.json exists or FALSE if none exists
417 */
418 protected function hasComposerManifestFile($packagePath)
419 {
420 // If an ext_emconf.php file is found, we don't need to look further
421 if (file_exists($packagePath . 'ext_emconf.php')) {
422 return false;
423 }
424 if (file_exists($packagePath . 'composer.json')) {
425 return true;
426 }
427 return false;
428 }
429
430 /**
431 * Requires and registers all packages which were defined in packageStatesConfiguration
432 *
433 * @param bool $registerOnlyNewPackages
434 * @return void
435 */
436 protected function registerPackagesFromConfiguration($registerOnlyNewPackages = false)
437 {
438 $packageStatesHasChanged = false;
439 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
440 if ($registerOnlyNewPackages && $this->isPackageAvailable($packageKey)) {
441 continue;
442 }
443
444 if (!isset($stateConfiguration['packagePath'])) {
445 $this->unregisterPackageByPackageKey($packageKey);
446 $packageStatesHasChanged = true;
447 continue;
448 }
449
450 try {
451 $packagePath = PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
452 $package = new Package($this, $packageKey, $packagePath);
453 } catch (Exception\InvalidPackagePathException $exception) {
454 $this->unregisterPackageByPackageKey($packageKey);
455 $packageStatesHasChanged = true;
456 continue;
457 } catch (Exception\InvalidPackageKeyException $exception) {
458 $this->unregisterPackageByPackageKey($packageKey);
459 $packageStatesHasChanged = true;
460 continue;
461 } catch (Exception\InvalidPackageManifestException $exception) {
462 $this->unregisterPackageByPackageKey($packageKey);
463 $packageStatesHasChanged = true;
464 continue;
465 }
466
467 $this->registerPackage($package, false);
468
469 $this->packageKeys[strtolower($packageKey)] = $packageKey;
470 if ($stateConfiguration['state'] === 'active') {
471 $this->activePackages[$packageKey] = $this->packages[$packageKey];
472 }
473 }
474 if ($packageStatesHasChanged) {
475 $this->sortAndSavePackageStates();
476 }
477 }
478
479 /**
480 * Register a native TYPO3 package
481 *
482 * @param PackageInterface $package The Package to be registered
483 * @param bool $sortAndSave allows for not saving packagestates when used in loops etc.
484 * @return PackageInterface
485 * @throws Exception\InvalidPackageStateException
486 * @throws Exception\PackageStatesFileNotWritableException
487 */
488 public function registerPackage(PackageInterface $package, $sortAndSave = true)
489 {
490 $packageKey = $package->getPackageKey();
491 if ($this->isPackageAvailable($packageKey)) {
492 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
493 }
494
495 $this->packages[$packageKey] = $package;
496 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
497
498 if ($sortAndSave === true) {
499 $this->sortAndSavePackageStates();
500 }
501
502 if ($package instanceof PackageInterface) {
503 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
504 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
505 }
506 }
507 return $package;
508 }
509
510 /**
511 * Unregisters a package from the list of available packages
512 *
513 * @param string $packageKey Package Key of the package to be unregistered
514 * @return void
515 */
516 protected function unregisterPackageByPackageKey($packageKey)
517 {
518 try {
519 $package = $this->getPackage($packageKey);
520 if ($package instanceof PackageInterface) {
521 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
522 unset($this->packageAliasMap[strtolower($packageToReplace)]);
523 }
524 }
525 } catch (Exception\UnknownPackageException $e) {
526 }
527 unset($this->packages[$packageKey]);
528 unset($this->packageKeys[strtolower($packageKey)]);
529 unset($this->packageStatesConfiguration['packages'][$packageKey]);
530 }
531
532 /**
533 * Resolves a TYPO3 package key from a composer package name.
534 *
535 * @param string $composerName
536 * @return string
537 */
538 public function getPackageKeyFromComposerName($composerName)
539 {
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 // Hard coded compatibility layer for old cms extension
548 // @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
549 $this->composerNameToPackageKeyMap['typo3/cms-cms'] = 'cms';
550 }
551 $lowercasedComposerName = strtolower($composerName);
552 if (!isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
553 return $composerName;
554 }
555 return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
556 }
557
558 /**
559 * Returns a PackageInterface object for the specified package.
560 * A package is available, if the package directory contains valid MetaData information.
561 *
562 * @param string $packageKey
563 * @return PackageInterface The requested package object
564 * @throws Exception\UnknownPackageException if the specified package is not known
565 * @api
566 */
567 public function getPackage($packageKey)
568 {
569 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
570 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
571 }
572 if (!$this->isPackageAvailable($packageKey)) {
573 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);
574 }
575 return $this->packages[$packageKey];
576 }
577
578 /**
579 * Returns TRUE if a package is available (the package's files exist in the packages directory)
580 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
581 *
582 * @param string $packageKey The key of the package to check
583 * @return bool TRUE if the package is available, otherwise FALSE
584 * @api
585 */
586 public function isPackageAvailable($packageKey)
587 {
588 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
589 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
590 }
591 return isset($this->packages[$packageKey]);
592 }
593
594 /**
595 * Returns TRUE if a package is activated or FALSE if it's not.
596 *
597 * @param string $packageKey The key of the package to check
598 * @return bool TRUE if package is active, otherwise FALSE
599 * @api
600 */
601 public function isPackageActive($packageKey)
602 {
603 return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->activePackages[$packageKey]);
604 }
605
606 /**
607 * Deactivates a package and updates the packagestates configuration
608 *
609 * @param string $packageKey
610 * @throws Exception\PackageStatesFileNotWritableException
611 * @throws Exception\ProtectedPackageKeyException
612 * @throws Exception\UnknownPackageException
613 */
614 public function deactivatePackage($packageKey)
615 {
616 $this->sortAvailablePackagesByDependencies();
617
618 foreach ($this->packageStatesConfiguration['packages'] as $packageStateKey => $packageStateConfiguration) {
619 if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies']) || $packageStateConfiguration['state'] !== 'active') {
620 continue;
621 }
622 if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
623 $this->deactivatePackage($packageStateKey);
624 }
625 }
626
627 if (!$this->isPackageActive($packageKey)) {
628 return;
629 }
630
631 $package = $this->getPackage($packageKey);
632 if ($package->isProtected()) {
633 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
634 }
635
636 unset($this->activePackages[$packageKey]);
637 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
638 $this->sortAndSavePackageStates();
639 }
640
641 /**
642 * @param string $packageKey
643 */
644 public function activatePackage($packageKey)
645 {
646 $package = $this->getPackage($packageKey);
647 $this->registerTransientClassLoadingInformationForPackage($package);
648
649 if ($this->isPackageActive($packageKey)) {
650 return;
651 }
652
653 $this->activePackages[$packageKey] = $package;
654 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'active';
655 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['packagePath'])) {
656 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
657 }
658 $this->sortAndSavePackageStates();
659 }
660
661 /**
662 * Enables packages during runtime, but no class aliases will be available
663 *
664 * @param string $packageKey
665 * @api
666 */
667 public function activatePackageDuringRuntime($packageKey)
668 {
669 $package = $this->getPackage($packageKey);
670 $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
671 if (!isset($GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()])) {
672 $loadedExtArrayElement = new LoadedExtensionArrayElement($package);
673 $GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()] = $loadedExtArrayElement->toArray();
674 }
675 $this->registerTransientClassLoadingInformationForPackage($package);
676 }
677
678 /**
679 * @param PackageInterface $package
680 * @throws \TYPO3\CMS\Core\Exception
681 */
682 protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
683 {
684 if (Bootstrap::usesComposerClassLoading()) {
685 return;
686 }
687 ClassLoadingInformation::registerTransientClassLoadingInformationForPackage($package);
688 }
689
690 /**
691 * Removes a package from the file system.
692 *
693 * @param string $packageKey
694 * @throws Exception
695 * @throws Exception\InvalidPackageStateException
696 * @throws Exception\ProtectedPackageKeyException
697 * @throws Exception\UnknownPackageException
698 */
699 public function deletePackage($packageKey)
700 {
701 if (!$this->isPackageAvailable($packageKey)) {
702 throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
703 }
704
705 $package = $this->getPackage($packageKey);
706 if ($package->isProtected()) {
707 throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
708 }
709
710 if ($this->isPackageActive($packageKey)) {
711 $this->deactivatePackage($packageKey);
712 }
713
714 $this->unregisterPackage($package);
715 $this->sortAndSavePackageStates();
716
717 $packagePath = $package->getPackagePath();
718 $deletion = GeneralUtility::rmdir($packagePath, true);
719 if ($deletion === false) {
720 throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
721 }
722 }
723
724
725 /**
726 * Returns an array of \TYPO3\CMS\Core\Package objects of all active packages.
727 * A package is active, if it is available and has been activated in the package
728 * manager settings. This method returns runtime activated packages too
729 *
730 * @return PackageInterface[]
731 * @api
732 */
733 public function getActivePackages()
734 {
735 return array_merge($this->activePackages, $this->runtimeActivatedPackages);
736 }
737
738 /**
739 * Orders all packages by comparing their dependencies. By this, the packages
740 * and package configurations arrays holds all packages in the correct
741 * initialization order.
742 *
743 * @return void
744 */
745 protected function sortAvailablePackagesByDependencies()
746 {
747 $this->resolvePackageDependencies();
748
749 // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
750 ksort($this->packageStatesConfiguration['packages']);
751 $this->packageStatesConfiguration['packages'] = $this->dependencyResolver->sortPackageStatesConfigurationByDependency($this->packageStatesConfiguration['packages']);
752
753 // Reorder the packages according to the loading order
754 $newPackages = array();
755 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $_) {
756 $newPackages[$packageKey] = $this->packages[$packageKey];
757 }
758 $this->packages = $newPackages;
759 }
760
761 /**
762 * Resolves the dependent packages from the meta data of all packages recursively. The
763 * resolved direct or indirect dependencies of each package will put into the package
764 * states configuration array.
765 *
766 * @return void
767 */
768 protected function resolvePackageDependencies()
769 {
770 foreach ($this->packages as $packageKey => $package) {
771 $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
772 }
773 foreach ($this->packages as $packageKey => $package) {
774 $this->packageStatesConfiguration['packages'][$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
775 }
776 }
777
778 /**
779 * Returns an array of suggested package keys for the given package.
780 *
781 * @param string $packageKey The package key to fetch the suggestions for
782 * @return array|NULL An array of directly suggested packages
783 */
784 protected function getSuggestionArrayForPackage($packageKey)
785 {
786 if (!isset($this->packages[$packageKey])) {
787 return null;
788 }
789 $suggestedPackageKeys = array();
790 $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_SUGGESTS);
791 foreach ($suggestedPackageConstraints as $constraint) {
792 if ($constraint instanceof MetaData\PackageConstraint) {
793 $suggestedPackageKey = $constraint->getValue();
794 if (isset($this->packages[$suggestedPackageKey])) {
795 $suggestedPackageKeys[] = $suggestedPackageKey;
796 }
797 }
798 }
799 return array_reverse($suggestedPackageKeys);
800 }
801
802 /**
803 * Saves the current content of $this->packageStatesConfiguration to the
804 * PackageStates.php file.
805 *
806 * @throws Exception\PackageStatesFileNotWritableException
807 */
808 protected function sortAndSavePackageStates()
809 {
810 $this->sortAvailablePackagesByDependencies();
811
812 $this->packageStatesConfiguration['version'] = 4;
813
814 $fileDescription = "# PackageStates.php\n\n";
815 $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
816 $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
817 $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
818 $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
819
820 // We do not need the dependencies on disk...
821 foreach ($this->packageStatesConfiguration['packages'] as &$packageConfiguration) {
822 if (isset($packageConfiguration['dependencies'])) {
823 unset($packageConfiguration['dependencies']);
824 }
825 }
826 if (!@is_writable($this->packageStatesPathAndFilename)) {
827 // If file does not exists try to create it
828 $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
829 if (!$fileHandle) {
830 throw new Exception\PackageStatesFileNotWritableException(
831 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),
832 1382449759
833 );
834 }
835 fclose($fileHandle);
836 }
837 $packageStatesCode = "<?php\n$fileDescription\nreturn " . var_export($this->packageStatesConfiguration, true) . "\n ?>";
838 GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
839
840 $this->initializeCompatibilityLoadedExtArray();
841
842 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
843 }
844
845 /**
846 * Check the conformance of the given package key
847 *
848 * @param string $packageKey The package key to validate
849 * @return bool If the package key is valid, returns TRUE otherwise FALSE
850 * @api
851 */
852 public function isPackageKeyValid($packageKey)
853 {
854 return preg_match(PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
855 }
856
857 /**
858 * Returns an array of \TYPO3\CMS\Core\Package objects of all available packages.
859 * A package is available, if the package directory contains valid meta information.
860 *
861 * @return PackageInterface[] Array of PackageInterface
862 * @api
863 */
864 public function getAvailablePackages()
865 {
866 return $this->packages;
867 }
868
869 /**
870 * Unregisters a package from the list of available packages
871 *
872 * @param PackageInterface $package The package to be unregistered
873 * @return void
874 * @throws Exception\InvalidPackageStateException
875 */
876 public function unregisterPackage(PackageInterface $package)
877 {
878 $packageKey = $package->getPackageKey();
879 if (!$this->isPackageAvailable($packageKey)) {
880 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
881 }
882 $this->unregisterPackageByPackageKey($packageKey);
883 }
884
885 /**
886 * Reloads a package and its information
887 *
888 * @param string $packageKey
889 * @throws Exception\InvalidPackageStateException if the package isn't available
890 * @throws Exception\InvalidPackageKeyException if an invalid package key was passed
891 * @throws Exception\InvalidPackagePathException if an invalid package path was passed
892 * @throws Exception\InvalidPackageManifestException if no extension configuration file could be found
893 */
894 public function reloadPackageInformation($packageKey)
895 {
896 if (!$this->isPackageAvailable($packageKey)) {
897 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
898 }
899
900 /** @var PackageInterface $package */
901 $package = $this->packages[$packageKey];
902 $packagePath = $package->getPackagePath();
903 $newPackage = new Package($this, $packageKey, $packagePath);
904 $this->packages[$packageKey] = $newPackage;
905 unset($package);
906 }
907
908 /**
909 * Scans all sub directories of the specified directory and collects the package keys of packages it finds.
910 *
911 * The return of the array is to make this method usable in array_merge.
912 *
913 * @param string $startPath
914 * @param array $collectedPackagePaths
915 * @return array
916 */
917 protected function scanPackagesInPath($startPath, array $collectedPackagePaths)
918 {
919 foreach (new \DirectoryIterator($startPath) as $fileInfo) {
920 if (!$fileInfo->isDir()) {
921 continue;
922 }
923 $filename = $fileInfo->getFilename();
924 if ($filename[0] !== '.') {
925 $currentPath = $fileInfo->getPathName();
926 $currentPath = PathUtility::sanitizeTrailingSeparator($currentPath);
927 if ($this->hasComposerManifestFile($currentPath)) {
928 $collectedPackagePaths[$currentPath] = $currentPath;
929 }
930 }
931 }
932 return $collectedPackagePaths;
933 }
934
935 /**
936 * Returns contents of Composer manifest as a stdObject
937 *
938 * @param string $manifestPath
939 * @return \stdClass
940 * @throws Exception\MissingPackageManifestException
941 * @throws Exception\InvalidPackageManifestException
942 */
943 public function getComposerManifest($manifestPath)
944 {
945 $composerManifest = null;
946 if (file_exists($manifestPath . 'composer.json')) {
947 $json = file_get_contents($manifestPath . 'composer.json');
948 $composerManifest = json_decode($json);
949 if (!$composerManifest instanceof \stdClass) {
950 throw new Exception\InvalidPackageManifestException('The composer.json found for extension "' . basename($manifestPath) . '" is invalid!', 1439555561);
951 }
952 }
953
954 try {
955 $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath);
956 $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
957 basename($manifestPath),
958 $extensionManagerConfiguration,
959 $composerManifest ?: new \stdClass()
960 );
961 } catch (Exception\InvalidPackageManifestException $e) {
962 if ($composerManifest === null) {
963 throw $e;
964 }
965 }
966
967 return $composerManifest;
968 }
969
970 /**
971 * Fetches MetaData information from ext_emconf.php, used for
972 * resolving dependencies as well.
973 *
974 * @param string $packagePath
975 * @return array
976 * @throws Exception\InvalidPackageManifestException
977 */
978 protected function getExtensionEmConf($packagePath)
979 {
980 $packageKey = basename($packagePath);
981 $_EXTKEY = $packageKey;
982 $path = $packagePath . 'ext_emconf.php';
983 $EM_CONF = null;
984 if (@file_exists($path)) {
985 include $path;
986 if (is_array($EM_CONF[$_EXTKEY])) {
987 return $EM_CONF[$_EXTKEY];
988 }
989 }
990 throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
991 }
992
993 /**
994 * Fetches information from ext_emconf.php and maps it so it is treated as it would come from composer.json
995 *
996 * @param string $packageKey
997 * @param array $extensionManagerConfiguration
998 * @param \stdClass $composerManifest
999 * @return \stdClass
1000 * @throws Exception\InvalidPackageManifestException
1001 */
1002 protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
1003 {
1004 $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
1005 $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
1006 $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title']);
1007 $composerManifest->version = $extensionManagerConfiguration['version'];
1008 if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
1009 $composerManifest->require = new \stdClass();
1010 foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
1011 if (!empty($requiredPackageKey)) {
1012 if ($requiredPackageKey === 'typo3') {
1013 // Add implicit dependency to 'core'
1014 $composerManifest->require->core = $requiredPackageVersion;
1015 } elseif ($requiredPackageKey !== 'php') {
1016 // Skip php dependency
1017 $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
1018 }
1019 } else {
1020 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
1021 }
1022 }
1023 }
1024 if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
1025 $composerManifest->conflict = new \stdClass();
1026 foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
1027 if (!empty($conflictingPackageKey)) {
1028 $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
1029 } else {
1030 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
1031 }
1032 }
1033 }
1034 if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
1035 $composerManifest->suggest = new \stdClass();
1036 foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
1037 if (!empty($suggestedPackageKey)) {
1038 $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
1039 } else {
1040 throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
1041 }
1042 }
1043 }
1044 if (isset($extensionManagerConfiguration['autoload'])) {
1045 $composerManifest->autoload = json_decode(json_encode($extensionManagerConfiguration['autoload']));
1046 }
1047 // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
1048 unset($composerManifest->{'autoload-dev'});
1049 if (isset($extensionManagerConfiguration['autoload-dev'])) {
1050 $composerManifest->{'autoload-dev'} = json_decode(json_encode($extensionManagerConfiguration['autoload-dev']));
1051 }
1052
1053 return $composerManifest;
1054 }
1055
1056 /**
1057 * @param \stdClass $manifest
1058 * @param string $property
1059 * @param mixed $value
1060 * @return \stdClass
1061 */
1062 protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
1063 {
1064 if (empty($manifest->{$property})) {
1065 $manifest->{$property} = $value;
1066 }
1067
1068 return $manifest;
1069 }
1070
1071 /**
1072 * Returns an array of dependent package keys for the given package. It will
1073 * do this recursively, so dependencies of dependant packages will also be
1074 * in the result.
1075 *
1076 * @param string $packageKey The package key to fetch the dependencies for
1077 * @param array $dependentPackageKeys
1078 * @param array $trace An array of already visited package keys, to detect circular dependencies
1079 * @return array|NULL An array of direct or indirect dependant packages
1080 * @throws Exception\InvalidPackageKeyException
1081 */
1082 protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = array(), array $trace = array())
1083 {
1084 if (!isset($this->packages[$packageKey])) {
1085 return null;
1086 }
1087 if (in_array($packageKey, $trace, true) !== false) {
1088 return $dependentPackageKeys;
1089 }
1090 $trace[] = $packageKey;
1091 $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1092 foreach ($dependentPackageConstraints as $constraint) {
1093 if ($constraint instanceof MetaData\PackageConstraint) {
1094 $dependentPackageKey = $constraint->getValue();
1095 if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1096 $dependentPackageKeys[] = $dependentPackageKey;
1097 }
1098 $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1099 }
1100 }
1101 return array_reverse($dependentPackageKeys);
1102 }
1103
1104
1105 /**
1106 * Resolves package key from Composer manifest
1107 *
1108 * If it is a TYPO3 package the name of the containing directory will be used.
1109 *
1110 * Else if the composer name of the package matches the first part of the lowercased namespace of the package, the mixed
1111 * case version of the composer name / namespace will be used, with backslashes replaced by dots.
1112 *
1113 * Else the composer name will be used with the slash replaced by a dot
1114 *
1115 * @param object $manifest
1116 * @param string $packagePath
1117 * @param string $packagesBasePath
1118 * @throws Exception\InvalidPackageManifestException
1119 * @return string
1120 */
1121 protected function getPackageKeyFromManifest($manifest, $packagePath, $packagesBasePath)
1122 {
1123 if (!is_object($manifest)) {
1124 throw new Exception\InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1125 }
1126 if (isset($manifest->type) && substr($manifest->type, 0, 10) === 'typo3-cms-') {
1127 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
1128 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
1129 return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
1130 } else {
1131 $packageKey = str_replace('/', '.', $manifest->name);
1132 return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
1133 }
1134 }
1135 }