‪TYPO3CMS  9.5
PackageManager.php
Go to the documentation of this file.
1 <?php
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 Symfony\Component\Finder\Finder;
18 use Symfony\Component\Finder\SplFileInfo;
29 
33 class PackageManager implements SingletonInterface
34 {
38  protected $dependencyOrderingService;
39 
43  protected $coreCache;
44 
48  protected $cacheIdentifier;
49 
53  protected $packagesBasePaths = [];
54 
58  protected $packageAliasMap = [];
59 
63  protected $runtimeActivatedPackages = [];
64 
69  protected $packagesBasePath;
70 
75  protected $packages = [];
76 
80  protected $availablePackagesScanned = false;
81 
86  protected $composerNameToPackageKeyMap = [];
87 
92  protected $activePackages = [];
93 
97  protected $packageStatesPathAndFilename;
98 
103  protected $packageStatesConfiguration = [];
104 
108  public function __construct(DependencyOrderingService $dependencyOrderingService = null)
109  {
110  $this->packagesBasePath = ‪Environment::getPublicPath() . '/';
111  $this->packageStatesPathAndFilename = ‪Environment::getLegacyConfigPath() . '/PackageStates.php';
112  if ($dependencyOrderingService === null) {
113  trigger_error(self::class . ' without constructor based dependency injection will stop working in TYPO3 v10.0.', E_USER_DEPRECATED);
114  $dependencyOrderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);
115  }
116  $this->dependencyOrderingService = $dependencyOrderingService;
117  }
118 
123  public function injectCoreCache(FrontendInterface $coreCache)
124  {
125  $this->coreCache = $coreCache;
126  }
127 
133  public function injectDependencyResolver(DependencyResolver $dependencyResolver)
134  {
135  trigger_error(self::class . '::injectDependencyResolver() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
136  }
137 
142  public function initialize()
143  {
144  try {
145  $this->loadPackageManagerStatesFromCache();
146  } catch (Exception\PackageManagerCacheUnavailableException $exception) {
147  $this->loadPackageStates();
148  $this->initializePackageObjects();
149  // @deprecated will be removed in TYPO3 v10.0
150  $this->initializeCompatibilityLoadedExtArray();
151  $this->saveToPackageCache();
152  }
153  }
154 
158  protected function getCacheIdentifier()
159  {
160  if ($this->cacheIdentifier === null) {
161  $mTime = @filemtime($this->packageStatesPathAndFilename);
162  if ($mTime !== false) {
163  $this->cacheIdentifier = md5(TYPO3_version . $this->packageStatesPathAndFilename . $mTime);
164  } else {
165  $this->cacheIdentifier = null;
166  }
167  }
168  return $this->cacheIdentifier;
169  }
170 
174  protected function getCacheEntryIdentifier()
175  {
176  $cacheIdentifier = $this->getCacheIdentifier();
177  return $cacheIdentifier !== null ? 'PackageManager_' . $cacheIdentifier : null;
178  }
179 
183  protected function saveToPackageCache()
184  {
185  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
186  if ($cacheEntryIdentifier !== null && !$this->coreCache->has($cacheEntryIdentifier)) {
187  // Build cache file
188  $packageCache = [
189  'packageStatesConfiguration' => $this->packageStatesConfiguration,
190  'packageAliasMap' => $this->packageAliasMap,
191  // @deprecated will be removed in TYPO3 v10.0
192  'loadedExtArray' => ‪$GLOBALS['TYPO3_LOADED_EXT'],
193  'composerNameToPackageKeyMap' => $this->composerNameToPackageKeyMap,
194  'packageObjects' => serialize($this->packages),
195  ];
196  $this->coreCache->set(
197  $cacheEntryIdentifier,
198  'return ' . PHP_EOL . var_export($packageCache, true) . ';'
199  );
200  }
201  }
202 
208  protected function loadPackageManagerStatesFromCache()
209  {
210  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
211  if ($cacheEntryIdentifier === null || !$this->coreCache->has($cacheEntryIdentifier) || !($packageCache = $this->coreCache->require($cacheEntryIdentifier))) {
212  throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883342);
213  }
214  $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
215  if ($this->packageStatesConfiguration['version'] < 5) {
216  throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883341);
217  }
218  $this->packageAliasMap = $packageCache['packageAliasMap'];
219  $this->composerNameToPackageKeyMap = $packageCache['composerNameToPackageKeyMap'];
220  $this->packages = unserialize($packageCache['packageObjects'], [
221  'allowed_classes' => [
222  Package::class,
223  MetaData::class,
224  MetaData\PackageConstraint::class,
225  \stdClass::class,
226  ]
227  ]);
228  // @deprecated will be removed in TYPO3 v10.0
229  ‪$GLOBALS['TYPO3_LOADED_EXT'] = $packageCache['loadedExtArray'];
230  }
231 
238  protected function loadPackageStates()
239  {
240  $forcePackageStatesRewrite = false;
241  $this->packageStatesConfiguration = @include $this->packageStatesPathAndFilename ?: [];
242  if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
243  $this->packageStatesConfiguration = [];
244  } elseif ($this->packageStatesConfiguration['version'] === 4) {
245  // Convert to v5 format which only includes a list of active packages.
246  // Deprecated since version 8, will be removed in version 10.
247  $activePackages = [];
248  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfiguration) {
249  if ($packageConfiguration['state'] !== 'active') {
250  continue;
251  }
252  $activePackages[$packageKey] = ['packagePath' => $packageConfiguration['packagePath']];
253  }
254  $this->packageStatesConfiguration['packages'] = $activePackages;
255  $this->packageStatesConfiguration['version'] = 5;
256  $forcePackageStatesRewrite = true;
257  }
258  if ($this->packageStatesConfiguration !== []) {
259  $this->registerPackagesFromConfiguration($this->packageStatesConfiguration['packages'], false, $forcePackageStatesRewrite);
260  } else {
261  throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
262  }
263  }
264 
270  protected function initializePackageObjects()
271  {
272  $requiredPackages = [];
273  $activePackages = [];
274  foreach ($this->packages as $packageKey => $package) {
275  if ($package->isProtected()) {
276  $requiredPackages[$packageKey] = $package;
277  }
278  if (isset($this->packageStatesConfiguration['packages'][$packageKey])) {
279  $activePackages[$packageKey] = $package;
280  }
281  }
282  $previousActivePackages = $activePackages;
283  $activePackages = array_merge($requiredPackages, $activePackages);
284 
285  if ($activePackages != $previousActivePackages) {
286  foreach ($requiredPackages as $requiredPackageKey => $package) {
287  $this->registerActivePackage($package);
288  }
289  $this->sortAndSavePackageStates();
290  }
291  }
292 
296  protected function registerActivePackage(PackageInterface $package)
297  {
298  // reset the active packages so they are rebuilt.
299  $this->activePackages = [];
300  $this->packageStatesConfiguration['packages'][$package->getPackageKey()] = ['packagePath' => str_replace($this->packagesBasePath, '', $package->getPackagePath())];
301  }
302 
306  protected function initializeCompatibilityLoadedExtArray()
307  {
308  // @deprecated will be removed in TYPO3 v10.0
309  $loadedExtObj = new \TYPO3\CMS\Core\Compatibility\LoadedExtensionsArray($this);
310  ‪$GLOBALS['TYPO3_LOADED_EXT'] = $loadedExtObj->toArray();
311  }
312 
318  public function scanAvailablePackages()
319  {
320  $packagePaths = $this->scanPackagePathsForExtensions();
321  $packages = [];
322  foreach ($packagePaths as $packageKey => $packagePath) {
323  try {
324  $composerManifest = $this->getComposerManifest($packagePath);
325  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath);
326  $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
327  $packages[$packageKey] = ['packagePath' => str_replace($this->packagesBasePath, '', $packagePath)];
328  } catch (Exception\MissingPackageManifestException $exception) {
329  if (!$this->isPackageKeyValid($packageKey)) {
330  continue;
331  }
332  } catch (Exception\InvalidPackageKeyException $exception) {
333  continue;
334  }
335  }
336 
337  $this->availablePackagesScanned = true;
338  $registerOnlyNewPackages = !empty($this->packages);
339  $this->registerPackagesFromConfiguration($packages, $registerOnlyNewPackages);
340  }
341 
348  protected function registerPackageDuringRuntime($packageKey)
349  {
350  $packagePaths = $this->scanPackagePathsForExtensions();
351  $packagePath = $packagePaths[$packageKey];
352  $composerManifest = $this->getComposerManifest($packagePath);
353  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath);
354  $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
355  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($packagePath);
356  $package = new Package($this, $packageKey, $packagePath);
357  $this->registerPackage($package);
358  return $package;
359  }
360 
366  protected function scanPackagePathsForExtensions()
367  {
368  $collectedExtensionPaths = [];
369  foreach ($this->getPackageBasePaths() as $packageBasePath) {
370  // Only add the extension if we have an EMCONF and the extension is not yet registered.
371  // This is crucial in order to allow overriding of system extension by local extensions
372  // and strongly depends on the order of paths defined in $this->packagesBasePaths.
373  ‪$finder = new Finder();
374  ‪$finder
375  ->name('ext_emconf.php')
376  ->followLinks()
377  ->depth(0)
378  ->ignoreUnreadableDirs()
379  ->in($packageBasePath);
380 
382  foreach (‪$finder as $fileInfo) {
383  $path = ‪PathUtility::dirname($fileInfo->getPathname());
384  $extensionName = ‪PathUtility::basename($path);
385  // Fix Windows backslashes
386  // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
387  $currentPath = str_replace('\\', '/', $path) . '/';
388  if (!isset($collectedExtensionPaths[$extensionName])) {
389  $collectedExtensionPaths[$extensionName] = $currentPath;
390  }
391  }
392  }
393  return $collectedExtensionPaths;
394  }
395 
405  protected function registerPackagesFromConfiguration(array $packages, $registerOnlyNewPackages = false, $packageStatesHasChanged = false)
406  {
407  foreach ($packages as $packageKey => $stateConfiguration) {
408  if ($registerOnlyNewPackages && $this->isPackageRegistered($packageKey)) {
409  continue;
410  }
411 
412  if (!isset($stateConfiguration['packagePath'])) {
413  $this->unregisterPackageByPackageKey($packageKey);
414  $packageStatesHasChanged = true;
415  continue;
416  }
417 
418  try {
419  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
420  $package = new Package($this, $packageKey, $packagePath);
421  } catch (Exception\InvalidPackagePathException $exception) {
422  $this->unregisterPackageByPackageKey($packageKey);
423  $packageStatesHasChanged = true;
424  continue;
425  } catch (Exception\InvalidPackageKeyException $exception) {
426  $this->unregisterPackageByPackageKey($packageKey);
427  $packageStatesHasChanged = true;
428  continue;
429  } catch (Exception\InvalidPackageManifestException $exception) {
430  $this->unregisterPackageByPackageKey($packageKey);
431  $packageStatesHasChanged = true;
432  continue;
433  }
434 
435  $this->registerPackage($package);
436  }
437  if ($packageStatesHasChanged) {
438  $this->sortAndSavePackageStates();
439  }
440  }
441 
450  public function registerPackage(PackageInterface $package)
451  {
452  $packageKey = $package->getPackageKey();
453  if ($this->isPackageRegistered($packageKey)) {
454  throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
455  }
456 
457  $this->packages[$packageKey] = $package;
458 
459  if ($package instanceof PackageInterface) {
460  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
461  $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
462  }
463  }
464  return $package;
465  }
466 
472  protected function unregisterPackageByPackageKey($packageKey)
473  {
474  try {
475  $package = $this->getPackage($packageKey);
476  if ($package instanceof PackageInterface) {
477  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
478  unset($this->packageAliasMap[strtolower($packageToReplace)]);
479  }
480  }
481  } catch (Exception\UnknownPackageException $e) {
482  }
483  unset($this->packages[$packageKey]);
484  unset($this->packageStatesConfiguration['packages'][$packageKey]);
485  }
486 
494  public function getPackageKeyFromComposerName($composerName)
495  {
496  $lowercasedComposerName = strtolower($composerName);
497  if (isset($this->packageAliasMap[$lowercasedComposerName])) {
498  return $this->packageAliasMap[$lowercasedComposerName];
499  }
500  if (isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
501  return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
502  }
503  return $composerName;
504  }
505 
514  public function getPackage($packageKey)
515  {
516  if (!$this->isPackageRegistered($packageKey) && !$this->isPackageAvailable($packageKey)) {
517  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);
518  }
519  return $this->packages[$packageKey];
520  }
521 
529  public function isPackageAvailable($packageKey)
530  {
531  if ($this->isPackageRegistered($packageKey)) {
532  return true;
533  }
534 
535  // If activePackages is empty, the PackageManager is currently initializing
536  // thus packages should not be scanned
537  if (!$this->availablePackagesScanned && !empty($this->activePackages)) {
538  $this->scanAvailablePackages();
539  }
540 
541  return $this->isPackageRegistered($packageKey);
542  }
543 
550  public function isPackageActive($packageKey)
551  {
552  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
553 
554  return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->packageStatesConfiguration['packages'][$packageKey]);
555  }
556 
566  public function deactivatePackage($packageKey)
567  {
568  $packagesWithDependencies = $this->sortActivePackagesByDependencies();
569 
570  foreach ($packagesWithDependencies as $packageStateKey => $packageStateConfiguration) {
571  if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies'])) {
572  continue;
573  }
574  if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
575  $this->deactivatePackage($packageStateKey);
576  }
577  }
578 
579  if (!$this->isPackageActive($packageKey)) {
580  return;
581  }
582 
583  $package = $this->getPackage($packageKey);
584  if ($package->isProtected()) {
585  throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
586  }
587 
588  $this->activePackages = [];
589  unset($this->packageStatesConfiguration['packages'][$packageKey]);
590  $this->sortAndSavePackageStates();
591  }
592 
597  public function activatePackage($packageKey)
598  {
599  $package = $this->getPackage($packageKey);
600  $this->registerTransientClassLoadingInformationForPackage($package);
601 
602  if ($this->isPackageActive($packageKey)) {
603  return;
604  }
605 
606  $this->registerActivePackage($package);
607  $this->sortAndSavePackageStates();
608  }
609 
615  public function activatePackageDuringRuntime($packageKey)
616  {
617  $package = $this->registerPackageDuringRuntime($packageKey);
618  $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
619  // @deprecated will be removed in TYPO3 v10.0
620  if (!isset(‪$GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()])) {
621  $loadedExtArrayElement = new LoadedExtensionArrayElement($package);
622  ‪$GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()] = $loadedExtArrayElement->toArray();
623  }
624  $this->registerTransientClassLoadingInformationForPackage($package);
625  }
626 
631  protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
632  {
634  return;
635  }
637  }
638 
648  public function deletePackage($packageKey)
649  {
650  if (!$this->isPackageAvailable($packageKey)) {
651  throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
652  }
653 
654  $package = $this->getPackage($packageKey);
655  if ($package->isProtected()) {
656  throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
657  }
658 
659  if ($this->isPackageActive($packageKey)) {
660  $this->deactivatePackage($packageKey);
661  }
662 
663  $this->unregisterPackage($package);
664  $this->sortAndSavePackageStates();
665 
666  $packagePath = $package->getPackagePath();
667  $deletion = GeneralUtility::rmdir($packagePath, true);
668  if ($deletion === false) {
669  throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
670  }
671  }
672 
680  public function getActivePackages()
681  {
682  if (empty($this->activePackages)) {
683  if (!empty($this->packageStatesConfiguration['packages'])) {
684  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfig) {
685  $this->activePackages[$packageKey] = $this->getPackage($packageKey);
686  }
687  }
688  }
689  return array_merge($this->activePackages, $this->runtimeActivatedPackages);
690  }
691 
698  protected function isPackageRegistered($packageKey)
699  {
700  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
701 
702  return isset($this->packages[$packageKey]);
703  }
704 
712  protected function sortActivePackagesByDependencies()
713  {
714  $packagesWithDependencies = $this->resolvePackageDependencies($this->packageStatesConfiguration['packages']);
715 
716  // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
717  ksort($packagesWithDependencies);
718  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
719 
720  // Reorder the packages according to the loading order
721  $this->packageStatesConfiguration['packages'] = [];
722  foreach ($sortedPackageKeys as $packageKey) {
723  $this->registerActivePackage($this->packages[$packageKey]);
724  }
725  return $packagesWithDependencies;
726  }
727 
736  protected function resolvePackageDependencies($packageConfig)
737  {
738  $packagesWithDependencies = [];
739  foreach ($packageConfig as $packageKey => $_) {
740  $packagesWithDependencies[$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
741  $packagesWithDependencies[$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
742  }
743  return $packagesWithDependencies;
744  }
745 
752  protected function getSuggestionArrayForPackage($packageKey)
753  {
754  if (!isset($this->packages[$packageKey])) {
755  return null;
756  }
757  $suggestedPackageKeys = [];
758  $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(‪MetaData::CONSTRAINT_TYPE_SUGGESTS);
759  foreach ($suggestedPackageConstraints as $constraint) {
760  if ($constraint instanceof MetaData\PackageConstraint) {
761  $suggestedPackageKey = $constraint->getValue();
762  if (isset($this->packages[$suggestedPackageKey])) {
763  $suggestedPackageKeys[] = $suggestedPackageKey;
764  }
765  }
766  }
767  return array_reverse($suggestedPackageKeys);
768  }
769 
776  protected function savePackageStates()
777  {
778  $this->packageStatesConfiguration['version'] = 5;
779 
780  $fileDescription = "# PackageStates.php\n\n";
781  $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
782  $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
783  $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
784  $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
785 
786  if (!@is_writable($this->packageStatesPathAndFilename)) {
787  // If file does not exists try to create it
788  $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
789  if (!$fileHandle) {
790  throw new Exception\PackageStatesFileNotWritableException(
791  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),
792  1382449759
793  );
794  }
795  fclose($fileHandle);
796  }
797  $packageStatesCode = "<?php\n$fileDescription\nreturn " . ‪ArrayUtility::arrayExport($this->packageStatesConfiguration) . ";\n";
798  GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
799 
800  // @deprecated will be removed in TYPO3 v10.0
801  $this->initializeCompatibilityLoadedExtArray();
802 
803  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
804  }
805 
812  protected function sortAndSavePackageStates()
813  {
814  $this->sortActivePackagesByDependencies();
815  $this->savePackageStates();
816  }
817 
824  public function isPackageKeyValid($packageKey)
825  {
826  return preg_match(‪PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(‪PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
827  }
828 
835  public function getAvailablePackages()
836  {
837  if ($this->availablePackagesScanned === false) {
838  $this->scanAvailablePackages();
839  }
840 
841  return $this->packages;
842  }
843 
851  public function unregisterPackage(PackageInterface $package)
852  {
853  $packageKey = $package->getPackageKey();
854  if (!$this->isPackageRegistered($packageKey)) {
855  throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
856  }
857  $this->unregisterPackageByPackageKey($packageKey);
858  }
859 
867  public function reloadPackageInformation($packageKey)
868  {
869  if (!$this->isPackageRegistered($packageKey)) {
870  throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
871  }
872 
874  $package = $this->packages[$packageKey];
875  $packagePath = $package->getPackagePath();
876  $newPackage = new Package($this, $packageKey, $packagePath);
877  $this->packages[$packageKey] = $newPackage;
878  unset($package);
879  }
880 
889  public function getComposerManifest($manifestPath)
890  {
891  $composerManifest = null;
892  if (file_exists($manifestPath . 'composer.json')) {
893  $json = file_get_contents($manifestPath . 'composer.json');
894  $composerManifest = json_decode($json);
895  if (!$composerManifest instanceof \stdClass) {
896  throw new Exception\InvalidPackageManifestException('The composer.json found for extension "' . ‪PathUtility::basename($manifestPath) . '" is invalid!', 1439555561);
897  }
898  }
899 
900  $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath);
901  $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
902  ‪PathUtility::basename($manifestPath),
903  $extensionManagerConfiguration,
904  $composerManifest ?: new \stdClass()
905  );
906 
907  return $composerManifest;
908  }
909 
918  protected function getExtensionEmConf($packagePath)
919  {
920  $packageKey = ‪PathUtility::basename($packagePath);
921  $_EXTKEY = $packageKey;
922  $path = $packagePath . 'ext_emconf.php';
923  ‪$EM_CONF = null;
924  if (@file_exists($path)) {
925  include $path;
926  if (is_array(‪$EM_CONF[$_EXTKEY])) {
927  return ‪$EM_CONF[$_EXTKEY];
928  }
929  }
930  throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
931  }
932 
942  protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
943  {
944  $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
945  $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
946  $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title'] ?? '');
947  $this->setComposerManifestValueIfEmpty($composerManifest, 'authors', [['name' => $extensionManagerConfiguration['author'] ?? '', 'email' => $extensionManagerConfiguration['author_email'] ?? '']]);
948  $composerManifest->version = $extensionManagerConfiguration['version'] ?? '';
949  if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
950  $composerManifest->require = new \stdClass();
951  foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
952  if (!empty($requiredPackageKey)) {
953  if ($requiredPackageKey === 'typo3') {
954  // Add implicit dependency to 'core'
955  $composerManifest->require->core = $requiredPackageVersion;
956  } elseif ($requiredPackageKey !== 'php') {
957  // Skip php dependency
958  $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
959  }
960  } else {
961  throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
962  }
963  }
964  }
965  if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
966  $composerManifest->conflict = new \stdClass();
967  foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
968  if (!empty($conflictingPackageKey)) {
969  $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
970  } else {
971  throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
972  }
973  }
974  }
975  if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
976  $composerManifest->suggest = new \stdClass();
977  foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
978  if (!empty($suggestedPackageKey)) {
979  $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
980  } else {
981  throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
982  }
983  }
984  }
985  if (isset($extensionManagerConfiguration['autoload'])) {
986  $composerManifest->autoload = json_decode(json_encode($extensionManagerConfiguration['autoload']));
987  }
988  // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
989  unset($composerManifest->{'autoload-dev'});
990  if (isset($extensionManagerConfiguration['autoload-dev'])) {
991  $composerManifest->{'autoload-dev'} = json_decode(json_encode($extensionManagerConfiguration['autoload-dev']));
992  }
993 
994  return $composerManifest;
995  }
996 
1003  protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
1004  {
1005  if (empty($manifest->{$property})) {
1006  $manifest->{$property} = $value;
1007  }
1008 
1009  return $manifest;
1010  }
1011 
1023  protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = [], array $trace = [])
1024  {
1025  if (!isset($this->packages[$packageKey])) {
1026  return null;
1027  }
1028  if (in_array($packageKey, $trace, true) !== false) {
1029  return $dependentPackageKeys;
1030  }
1031  $trace[] = $packageKey;
1032  $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1033  foreach ($dependentPackageConstraints as $constraint) {
1034  if ($constraint instanceof MetaData\PackageConstraint) {
1035  $dependentPackageKey = $constraint->getValue();
1036  if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1037  $dependentPackageKeys[] = $dependentPackageKey;
1038  }
1039  $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1040  }
1041  }
1042  return array_reverse($dependentPackageKeys);
1043  }
1044 
1060  protected function getPackageKeyFromManifest($manifest, $packagePath)
1061  {
1062  if (!is_object($manifest)) {
1063  throw new Exception\InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1064  }
1065  if (isset($manifest->type) && strpos($manifest->type, 'typo3-cms-') === 0) {
1066  $packageKey = PathUtility::basename($packagePath);
1067  return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
1068  }
1069  $packageKey = str_replace('/', '.', $manifest->name);
1070  return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
1071  }
1072 
1079  protected function getPackageBasePaths()
1080  {
1081  if (count($this->packagesBasePaths) < 3) {
1082  // Check if the directory even exists and if it is not empty
1083  if (is_dir(Environment::getExtensionsPath()) && $this->hasSubDirectories(Environment::getExtensionsPath())) {
1084  $this->packagesBasePaths['local'] = Environment::getExtensionsPath() . '/*/';
1085  }
1086  if (is_dir(Environment::getBackendPath() . '/ext') && $this->hasSubDirectories(Environment::getBackendPath() . '/ext')) {
1087  $this->packagesBasePaths['global'] = Environment::getBackendPath() . '/ext/*/';
1088  }
1089  $this->packagesBasePaths['system'] = Environment::getFrameworkBasePath() . '/*/';
1090  }
1091  return $this->packagesBasePaths;
1092  }
1093 
1100  protected function hasSubDirectories(string $path): bool
1101  {
1102  return !empty(glob(rtrim($path, '/\\') . '/*', GLOB_ONLYDIR));
1103  }
1104 
1110  protected function sortPackageStatesConfigurationByDependency(array $packageStatesConfiguration)
1111  {
1112  return $this->dependencyOrderingService->calculateOrder($this->buildDependencyGraph($packageStatesConfiguration));
1113  }
1114 
1125  protected function convertConfigurationForGraph(array $packageStatesConfiguration, array $packageKeys)
1126  {
1127  $dependencies = [];
1128  foreach ($packageKeys as $packageKey) {
1129  if (!isset($packageStatesConfiguration[$packageKey]['dependencies']) && !isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1130  continue;
1131  }
1132  $dependencies[$packageKey] = [
1133  'after' => []
1134  ];
1135  if (isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
1136  foreach ($packageStatesConfiguration[$packageKey]['dependencies'] as $dependentPackageKey) {
1137  if (!in_array($dependentPackageKey, $packageKeys, true)) {
1138  throw new \UnexpectedValueException(
1139  'The package "' . $packageKey . '" depends on "'
1140  . $dependentPackageKey . '" which is not present in the system.',
1141  1519931815
1142  );
1143  }
1144  $dependencies[$packageKey]['after'][] = $dependentPackageKey;
1145  }
1146  }
1147  if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1148  foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
1149  // skip suggestions on not existing packages
1150  if (in_array($suggestedPackageKey, $packageKeys, true)) {
1151  // Suggestions actually have never been meant to influence loading order.
1152  // We misuse this currently, as there is no other way to influence the loading order
1153  // for not-required packages (soft-dependency).
1154  // When considering suggestions for the loading order, we might create a cyclic dependency
1155  // if the suggested package already has a real dependency on this package, so the suggestion
1156  // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
1157  $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
1158  }
1159  }
1160  }
1161  }
1162  return $dependencies;
1163  }
1164 
1175  protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys)
1176  {
1177  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1178  $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
1179  foreach ($extensionPackageKeys as $packageKey) {
1180  // Remove framework packages from list
1181  $packageKeysWithoutFramework = array_diff(
1182  $packageStateConfiguration[$packageKey]['dependencies'],
1183  $frameworkPackageKeys
1184  );
1185  // The order of the array_merge is crucial here,
1186  // we want the framework first
1187  $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
1188  $rootPackageKeys,
1189  $packageKeysWithoutFramework
1190  );
1191  }
1192  return $packageStateConfiguration;
1193  }
1194 
1204  protected function buildDependencyGraph(array $packageStateConfiguration)
1205  {
1206  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1207  $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
1208  $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
1209 
1210  $packageKeys = array_keys($packageStateConfiguration);
1211  return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
1212  }
1213 
1218  protected function findFrameworkPackages(array $packageStateConfiguration)
1219  {
1220  $frameworkPackageKeys = [];
1221  foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
1222  $package = $this->getPackage($packageKey);
1223  if ($package->getValueFromComposerManifest('type') === 'typo3-cms-framework') {
1224  $frameworkPackageKeys[] = $packageKey;
1225  }
1226  }
1227 
1228  return $frameworkPackageKeys;
1229  }
1230 }
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:23
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static string getPublicPath()
Definition: Environment.php:153
‪TYPO3\CMS\Core\Compatibility\LoadedExtensionArrayElement
Definition: LoadedExtensionArrayElement.php:26
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static string dirname($path)
Definition: PathUtility.php:185
‪$finder
‪$finder
Definition: annotationChecker.php:102
‪TYPO3\CMS\Core\Core\ClassLoadingInformation
Definition: ClassLoadingInformation.php:32
‪TYPO3\CMS\Core\Utility\ArrayUtility\arrayExport
‪static string arrayExport(array $array=[], $level=0)
Definition: ArrayUtility.php:397
‪TYPO3\CMS\Core\Package\MetaData\CONSTRAINT_TYPE_SUGGESTS
‪const CONSTRAINT_TYPE_SUGGESTS
Definition: MetaData.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static string basename($path)
Definition: PathUtility.php:164
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_EXTENSIONKEY
‪const PATTERN_MATCH_EXTENSIONKEY
Definition: PackageInterface.php:24
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:31
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_PACKAGEKEY
‪const PATTERN_MATCH_PACKAGEKEY
Definition: PackageInterface.php:22
‪TYPO3\CMS\Core\Service\OpcodeCacheService
Definition: OpcodeCacheService.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\sanitizeTrailingSeparator
‪static string sanitizeTrailingSeparator($path, $separator='/')
Definition: PathUtility.php:147
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:21
‪TYPO3\CMS\Core\Core\Environment\isComposerMode
‪static bool isComposerMode()
Definition: Environment.php:117
‪TYPO3\CMS\Core\Core\ClassLoadingInformation\registerTransientClassLoadingInformationForPackage
‪static registerTransientClassLoadingInformationForPackage(PackageInterface $package)
Definition: ClassLoadingInformation.php:144
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:23
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:39
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:2
‪TYPO3\CMS\Core\Package
Definition: DependencyResolver.php:2
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Core\Core\Environment\getLegacyConfigPath
‪static string getLegacyConfigPath()
Definition: Environment.php:256