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