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