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