‪TYPO3CMS  10.4
PackageManager.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Symfony\Component\Finder\Finder;
19 use Symfony\Component\Finder\SplFileInfo;
42 
46 class PackageManager implements SingletonInterface
47 {
51  protected $dependencyOrderingService;
52 
56  protected $coreCache;
57 
61  protected $cacheIdentifier;
62 
66  protected $packagesBasePaths = [];
67 
71  protected $packageAliasMap = [];
72 
76  protected $runtimeActivatedPackages = [];
77 
82  protected $packagesBasePath;
83 
88  protected $packages = [];
89 
93  protected $availablePackagesScanned = false;
94 
99  protected $composerNameToPackageKeyMap = [];
100 
105  protected $activePackages = [];
106 
110  protected $packageStatesPathAndFilename;
111 
116  protected $packageStatesConfiguration = [];
117 
121  public function __construct(DependencyOrderingService $dependencyOrderingService)
122  {
123  $this->packagesBasePath = ‪Environment::getPublicPath() . '/';
124  $this->packageStatesPathAndFilename = ‪Environment::getLegacyConfigPath() . '/PackageStates.php';
125  $this->dependencyOrderingService = $dependencyOrderingService;
126  }
127 
132  public function injectCoreCache(FrontendInterface $coreCache)
133  {
134  $this->coreCache = $coreCache;
135  }
136 
141  public function initialize()
142  {
143  try {
144  $this->loadPackageManagerStatesFromCache();
145  } catch (PackageManagerCacheUnavailableException $exception) {
146  $this->loadPackageStates();
147  $this->initializePackageObjects();
148  $this->saveToPackageCache();
149  }
150  }
151 
156  public function getCacheIdentifier()
157  {
158  if ($this->cacheIdentifier === null) {
159  $mTime = @filemtime($this->packageStatesPathAndFilename);
160  if ($mTime !== false) {
161  $this->cacheIdentifier = md5((string)(new Typo3Version()) . $this->packageStatesPathAndFilename . $mTime);
162  } else {
163  $this->cacheIdentifier = null;
164  }
165  }
166  return $this->cacheIdentifier;
167  }
168 
172  protected function getCacheEntryIdentifier()
173  {
174  $cacheIdentifier = $this->getCacheIdentifier();
175  return $cacheIdentifier !== null ? 'PackageManager_' . $cacheIdentifier : null;
176  }
177 
181  protected function saveToPackageCache()
182  {
183  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
184  if ($cacheEntryIdentifier !== null) {
185  // Build cache file
186  $packageCache = [
187  'packageStatesConfiguration' => $this->packageStatesConfiguration,
188  'packageAliasMap' => $this->packageAliasMap,
189  'composerNameToPackageKeyMap' => $this->composerNameToPackageKeyMap,
190  'packageObjects' => serialize($this->packages),
191  ];
192  $this->coreCache->set(
193  $cacheEntryIdentifier,
194  'return ' . PHP_EOL . var_export($packageCache, true) . ';'
195  );
196  }
197  }
198 
204  protected function loadPackageManagerStatesFromCache()
205  {
206  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
207  if ($cacheEntryIdentifier === null || ($packageCache = $this->coreCache->require($cacheEntryIdentifier)) === false) {
208  throw new PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883342);
209  }
210  $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
211  if ($this->packageStatesConfiguration['version'] < 5) {
212  throw new PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883341);
213  }
214  $this->packageAliasMap = $packageCache['packageAliasMap'];
215  $this->composerNameToPackageKeyMap = $packageCache['composerNameToPackageKeyMap'];
216  $this->packages = unserialize($packageCache['packageObjects'], [
217  'allowed_classes' => [
218  Package::class,
219  MetaData::class,
220  PackageConstraint::class,
221  \stdClass::class,
222  ]
223  ]);
224  }
225 
232  protected function loadPackageStates()
233  {
234  $this->packageStatesConfiguration = @include $this->packageStatesPathAndFilename ?: [];
235  if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 5) {
236  throw new PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
237  }
238  $this->registerPackagesFromConfiguration($this->packageStatesConfiguration['packages'], false);
239  }
240 
246  protected function initializePackageObjects()
247  {
248  $requiredPackages = [];
249  $activePackages = [];
250  foreach ($this->packages as $packageKey => $package) {
251  if ($package->isProtected()) {
252  $requiredPackages[$packageKey] = $package;
253  }
254  if (isset($this->packageStatesConfiguration['packages'][$packageKey])) {
255  $activePackages[$packageKey] = $package;
256  }
257  }
258  $previousActivePackages = $activePackages;
259  $activePackages = array_merge($requiredPackages, $activePackages);
260 
261  if ($activePackages != $previousActivePackages) {
262  foreach ($requiredPackages as $requiredPackageKey => $package) {
263  $this->registerActivePackage($package);
264  }
265  $this->sortAndSavePackageStates();
266  }
267  }
268 
272  protected function registerActivePackage(PackageInterface $package)
273  {
274  // reset the active packages so they are rebuilt.
275  $this->activePackages = [];
276  $this->packageStatesConfiguration['packages'][$package->getPackageKey()] = ['packagePath' => str_replace($this->packagesBasePath, '', $package->getPackagePath())];
277  }
278 
284  public function scanAvailablePackages()
285  {
286  $packagePaths = $this->scanPackagePathsForExtensions();
287  $packages = [];
288  foreach ($packagePaths as $packageKey => $packagePath) {
289  try {
290  $composerManifest = $this->getComposerManifest($packagePath);
291  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath);
292  $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
293  $packages[$packageKey] = ['packagePath' => str_replace($this->packagesBasePath, '', $packagePath)];
294  } catch (MissingPackageManifestException $exception) {
295  if (!$this->isPackageKeyValid($packageKey)) {
296  continue;
297  }
298  } catch (InvalidPackageKeyException $exception) {
299  continue;
300  }
301  }
302 
303  $this->availablePackagesScanned = true;
304  $registerOnlyNewPackages = !empty($this->packages);
305  $this->registerPackagesFromConfiguration($packages, $registerOnlyNewPackages);
306  }
307 
313  public function packagesMayHaveChanged(PackagesMayHaveChangedEvent $event): void
314  {
315  $this->scanAvailablePackages();
316  }
317 
325  protected function registerPackageDuringRuntime($packageKey)
326  {
327  $packagePaths = $this->scanPackagePathsForExtensions();
328  $packagePath = $packagePaths[$packageKey];
329  $composerManifest = $this->getComposerManifest($packagePath);
330  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath);
331  $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
332  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($packagePath);
333  $package = new Package($this, $packageKey, $packagePath);
334  $this->registerPackage($package);
335  return $package;
336  }
337 
343  protected function scanPackagePathsForExtensions()
344  {
345  $collectedExtensionPaths = [];
346  foreach ($this->getPackageBasePaths() as $packageBasePath) {
347  // Only add the extension if we have an EMCONF and the extension is not yet registered.
348  // This is crucial in order to allow overriding of system extension by local extensions
349  // and strongly depends on the order of paths defined in $this->packagesBasePaths.
350  ‪$finder = new Finder();
351  ‪$finder
352  ->name('ext_emconf.php')
353  ->followLinks()
354  ->depth(0)
355  ->ignoreUnreadableDirs()
356  ->in($packageBasePath);
357 
359  foreach (‪$finder as $fileInfo) {
360  $path = ‪PathUtility::dirname($fileInfo->getPathname());
361  $extensionName = ‪PathUtility::basename($path);
362  // Fix Windows backslashes
363  // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
364  $currentPath = str_replace('\\', '/', $path) . '/';
365  if (!isset($collectedExtensionPaths[$extensionName])) {
366  $collectedExtensionPaths[$extensionName] = $currentPath;
367  }
368  }
369  }
370  return $collectedExtensionPaths;
371  }
372 
381  protected function registerPackagesFromConfiguration(array $packages, $registerOnlyNewPackages = false)
382  {
383  $packageStatesHasChanged = false;
384  foreach ($packages as $packageKey => $stateConfiguration) {
385  if ($registerOnlyNewPackages && $this->isPackageRegistered($packageKey)) {
386  continue;
387  }
388 
389  if (!isset($stateConfiguration['packagePath'])) {
390  $this->unregisterPackageByPackageKey($packageKey);
391  $packageStatesHasChanged = true;
392  continue;
393  }
394 
395  try {
396  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
397  $package = new Package($this, $packageKey, $packagePath);
398  } catch (InvalidPackagePathException|InvalidPackageKeyException|InvalidPackageManifestException $exception) {
399  $this->unregisterPackageByPackageKey($packageKey);
400  $packageStatesHasChanged = true;
401  continue;
402  }
403 
404  $this->registerPackage($package);
405  }
406  if ($packageStatesHasChanged) {
407  $this->sortAndSavePackageStates();
408  }
409  }
410 
419  public function registerPackage(PackageInterface $package)
420  {
421  $packageKey = $package->getPackageKey();
422  if ($this->isPackageRegistered($packageKey)) {
423  throw new InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
424  }
425 
426  $this->packages[$packageKey] = $package;
427 
428  if ($package instanceof PackageInterface) {
429  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
430  $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
431  }
432  }
433  return $package;
434  }
435 
441  protected function unregisterPackageByPackageKey($packageKey)
442  {
443  try {
444  $package = $this->getPackage($packageKey);
445  if ($package instanceof PackageInterface) {
446  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
447  unset($this->packageAliasMap[strtolower($packageToReplace)]);
448  }
449  }
450  } catch (UnknownPackageException $e) {
451  }
452  unset($this->packages[$packageKey]);
453  unset($this->packageStatesConfiguration['packages'][$packageKey]);
454  }
455 
463  public function getPackageKeyFromComposerName($composerName)
464  {
465  $lowercasedComposerName = strtolower($composerName);
466  if (isset($this->packageAliasMap[$lowercasedComposerName])) {
467  return $this->packageAliasMap[$lowercasedComposerName];
468  }
469  if (isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
470  return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
471  }
472  return $composerName;
473  }
474 
483  public function getPackage($packageKey)
484  {
485  if (!$this->isPackageRegistered($packageKey) && !$this->isPackageAvailable($packageKey)) {
486  throw new 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);
487  }
488  return $this->packages[$packageKey];
489  }
490 
498  public function isPackageAvailable($packageKey)
499  {
500  if ($this->isPackageRegistered($packageKey)) {
501  return true;
502  }
503 
504  // If activePackages is empty, the PackageManager is currently initializing
505  // thus packages should not be scanned
506  if (!$this->availablePackagesScanned && !empty($this->activePackages)) {
507  $this->scanAvailablePackages();
508  }
509 
510  return $this->isPackageRegistered($packageKey);
511  }
512 
519  public function isPackageActive($packageKey)
520  {
521  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
522 
523  return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->packageStatesConfiguration['packages'][$packageKey]);
524  }
525 
535  public function deactivatePackage($packageKey)
536  {
537  $packagesWithDependencies = $this->sortActivePackagesByDependencies();
538 
539  foreach ($packagesWithDependencies as $packageStateKey => $packageStateConfiguration) {
540  if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies'])) {
541  continue;
542  }
543  if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
544  $this->deactivatePackage($packageStateKey);
545  }
546  }
547 
548  if (!$this->isPackageActive($packageKey)) {
549  return;
550  }
551 
552  $package = $this->getPackage($packageKey);
553  if ($package->isProtected()) {
554  throw new ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
555  }
556 
557  $this->activePackages = [];
558  unset($this->packageStatesConfiguration['packages'][$packageKey]);
559  $this->sortAndSavePackageStates();
560  }
561 
566  public function activatePackage($packageKey)
567  {
568  $package = $this->getPackage($packageKey);
569  $this->registerTransientClassLoadingInformationForPackage($package);
570 
571  if ($this->isPackageActive($packageKey)) {
572  return;
573  }
574 
575  $this->registerActivePackage($package);
576  $this->sortAndSavePackageStates();
577  }
578 
585  public function activatePackageDuringRuntime($packageKey)
586  {
587  $package = $this->registerPackageDuringRuntime($packageKey);
588  $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
589  $this->registerTransientClassLoadingInformationForPackage($package);
590  }
591 
596  protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
597  {
599  return;
600  }
602  }
603 
613  public function deletePackage($packageKey)
614  {
615  if (!$this->isPackageAvailable($packageKey)) {
616  throw new UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
617  }
618 
619  $package = $this->getPackage($packageKey);
620  if ($package->isProtected()) {
621  throw new ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
622  }
623 
624  if ($this->isPackageActive($packageKey)) {
625  $this->deactivatePackage($packageKey);
626  }
627 
628  $this->unregisterPackage($package);
629  $this->sortAndSavePackageStates();
630 
631  $packagePath = $package->getPackagePath();
632  $deletion = ‪GeneralUtility::rmdir($packagePath, true);
633  if ($deletion === false) {
634  throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
635  }
636  }
637 
645  public function getActivePackages()
646  {
647  if (empty($this->activePackages)) {
648  if (!empty($this->packageStatesConfiguration['packages'])) {
649  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfig) {
650  $this->activePackages[$packageKey] = $this->getPackage($packageKey);
651  }
652  }
653  }
654  return array_merge($this->activePackages, $this->runtimeActivatedPackages);
655  }
656 
663  protected function isPackageRegistered($packageKey)
664  {
665  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
666 
667  return isset($this->packages[$packageKey]);
668  }
669 
677  protected function sortActivePackagesByDependencies()
678  {
679  $packagesWithDependencies = $this->resolvePackageDependencies($this->packageStatesConfiguration['packages']);
680 
681  // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
682  ksort($packagesWithDependencies);
683  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
684 
685  // Reorder the packages according to the loading order
686  $this->packageStatesConfiguration['packages'] = [];
687  foreach ($sortedPackageKeys as $packageKey) {
688  $this->registerActivePackage($this->packages[$packageKey]);
689  }
690  return $packagesWithDependencies;
691  }
692 
701  protected function resolvePackageDependencies($packageConfig)
702  {
703  $packagesWithDependencies = [];
704  foreach ($packageConfig as $packageKey => $_) {
705  $packagesWithDependencies[$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
706  $packagesWithDependencies[$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
707  }
708  return $packagesWithDependencies;
709  }
710 
717  protected function getSuggestionArrayForPackage($packageKey)
718  {
719  if (!isset($this->packages[$packageKey])) {
720  return null;
721  }
722  $suggestedPackageKeys = [];
723  $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(‪MetaData::CONSTRAINT_TYPE_SUGGESTS);
724  foreach ($suggestedPackageConstraints as $constraint) {
725  if ($constraint instanceof PackageConstraint) {
726  $suggestedPackageKey = $constraint->getValue();
727  if (isset($this->packages[$suggestedPackageKey])) {
728  $suggestedPackageKeys[] = $suggestedPackageKey;
729  }
730  }
731  }
732  return array_reverse($suggestedPackageKeys);
733  }
734 
741  protected function savePackageStates()
742  {
743  $this->packageStatesConfiguration['version'] = 5;
744 
745  $fileDescription = "# PackageStates.php\n\n";
746  $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
747  $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
748  $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
749  $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
750 
751  if (!@is_writable($this->packageStatesPathAndFilename)) {
752  // If file does not exist, try to create it
753  $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
754  if (!$fileHandle) {
755  throw new PackageStatesFileNotWritableException(
756  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),
757  1382449759
758  );
759  }
760  fclose($fileHandle);
761  }
762  $packageStatesCode = "<?php\n$fileDescription\nreturn " . ‪ArrayUtility::arrayExport($this->packageStatesConfiguration) . ";\n";
763  ‪GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
764  // Cache identifier depends on package states file, therefore we invalidate the identifier
765  $this->cacheIdentifier = null;
766 
767  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
768  }
769 
776  protected function sortAndSavePackageStates()
777  {
778  $this->sortActivePackagesByDependencies();
779  $this->savePackageStates();
780  }
781 
788  public function isPackageKeyValid($packageKey)
789  {
790  return preg_match(‪PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(‪PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
791  }
792 
799  public function getAvailablePackages()
800  {
801  if ($this->availablePackagesScanned === false) {
802  $this->scanAvailablePackages();
803  }
804 
805  return $this->packages;
806  }
807 
815  public function unregisterPackage(PackageInterface $package)
816  {
817  $packageKey = $package->getPackageKey();
818  if (!$this->isPackageRegistered($packageKey)) {
819  throw new InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
820  }
821  $this->unregisterPackageByPackageKey($packageKey);
822  }
823 
831  public function reloadPackageInformation($packageKey)
832  {
833  if (!$this->isPackageRegistered($packageKey)) {
834  throw new InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
835  }
836 
838  $package = $this->packages[$packageKey];
839  $packagePath = $package->getPackagePath();
840  $newPackage = new Package($this, $packageKey, $packagePath);
841  $this->packages[$packageKey] = $newPackage;
842  unset($package);
843  }
844 
853  public function getComposerManifest($manifestPath)
854  {
855  $composerManifest = null;
856  if (file_exists($manifestPath . 'composer.json')) {
857  $json = file_get_contents($manifestPath . 'composer.json');
858  if ($json !== false) {
859  $composerManifest = json_decode($json);
860  }
861  if (!$composerManifest instanceof \stdClass) {
862  throw new InvalidPackageManifestException('The composer.json found for extension "' . ‪PathUtility::basename($manifestPath) . '" is invalid!', 1439555561);
863  }
864  }
865 
866  $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath);
867  $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
868  ‪PathUtility::basename($manifestPath),
869  $extensionManagerConfiguration,
870  $composerManifest ?: new \stdClass()
871  );
872 
873  return $composerManifest;
874  }
875 
884  protected function getExtensionEmConf($packagePath)
885  {
886  $packageKey = ‪PathUtility::basename($packagePath);
887  $_EXTKEY = $packageKey;
888  $path = $packagePath . 'ext_emconf.php';
889  ‪$EM_CONF = null;
890  if (@file_exists($path)) {
891  include $path;
892  if (is_array(‪$EM_CONF[$_EXTKEY])) {
893  return ‪$EM_CONF[$_EXTKEY];
894  }
895  }
896  throw new InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
897  }
898 
908  protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
909  {
910  $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
911  $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
912  $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title'] ?? '');
913  $this->setComposerManifestValueIfEmpty($composerManifest, 'authors', [['name' => $extensionManagerConfiguration['author'] ?? '', 'email' => $extensionManagerConfiguration['author_email'] ?? '']]);
914  $composerManifest->version = $extensionManagerConfiguration['version'] ?? '';
915  if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
916  $composerManifest->require = new \stdClass();
917  foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
918  if (!empty($requiredPackageKey)) {
919  if ($requiredPackageKey === 'typo3') {
920  // Add implicit dependency to 'core'
921  $composerManifest->require->core = $requiredPackageVersion;
922  } elseif ($requiredPackageKey !== 'php') {
923  // Skip php dependency
924  $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
925  }
926  } else {
927  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
928  }
929  }
930  }
931  if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
932  $composerManifest->conflict = new \stdClass();
933  foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
934  if (!empty($conflictingPackageKey)) {
935  $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
936  } else {
937  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
938  }
939  }
940  }
941  if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
942  $composerManifest->suggest = new \stdClass();
943  foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
944  if (!empty($suggestedPackageKey)) {
945  $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
946  } else {
947  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
948  }
949  }
950  }
951  if (isset($extensionManagerConfiguration['autoload'])) {
952  $autoload = json_encode($extensionManagerConfiguration['autoload']);
953  if ($autoload !== false) {
954  $composerManifest->autoload = json_decode($autoload);
955  }
956  }
957  // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
958  unset($composerManifest->{'autoload-dev'});
959  if (isset($extensionManagerConfiguration['autoload-dev'])) {
960  $autoloadDev = json_encode($extensionManagerConfiguration['autoload-dev']);
961  if ($autoloadDev !== false) {
962  $composerManifest->{'autoload-dev'} = json_decode($autoloadDev);
963  }
964  }
965 
966  return $composerManifest;
967  }
968 
975  protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
976  {
977  if (empty($manifest->{$property})) {
978  $manifest->{$property} = $value;
979  }
980 
981  return $manifest;
982  }
983 
995  protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = [], array $trace = [])
996  {
997  if (!isset($this->packages[$packageKey])) {
998  return null;
999  }
1000  if (in_array($packageKey, $trace, true) !== false) {
1001  return $dependentPackageKeys;
1002  }
1003  $trace[] = $packageKey;
1004  $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1005  foreach ($dependentPackageConstraints as $constraint) {
1006  if ($constraint instanceof PackageConstraint) {
1007  $dependentPackageKey = $constraint->getValue();
1008  if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1009  $dependentPackageKeys[] = $dependentPackageKey;
1010  }
1011  $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1012  }
1013  }
1014  return array_reverse($dependentPackageKeys);
1015  }
1016 
1032  protected function getPackageKeyFromManifest($manifest, $packagePath)
1033  {
1034  if (!is_object($manifest)) {
1035  throw new InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1036  }
1037  if (isset($manifest->type) && strpos($manifest->type, 'typo3-cms-') === 0) {
1038  $packageKey = PathUtility::basename($packagePath);
1039  return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
1040  }
1041  $packageKey = str_replace('/', '.', $manifest->name);
1042  return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
1043  }
1044 
1051  protected function getPackageBasePaths()
1052  {
1053  if (count($this->packagesBasePaths) < 3) {
1054  // Check if the directory even exists and if it is not empty
1055  if (is_dir(Environment::getExtensionsPath()) && $this->hasSubDirectories(Environment::getExtensionsPath())) {
1056  $this->packagesBasePaths['local'] = Environment::getExtensionsPath() . '/*/';
1057  }
1058  if (is_dir(Environment::getBackendPath() . '/ext') && $this->hasSubDirectories(Environment::getBackendPath() . '/ext')) {
1059  $this->packagesBasePaths['global'] = Environment::getBackendPath() . '/ext/*/';
1060  }
1061  $this->packagesBasePaths['system'] = Environment::getFrameworkBasePath() . '/*/';
1062  }
1063  return $this->packagesBasePaths;
1064  }
1065 
1072  protected function hasSubDirectories(string $path): bool
1073  {
1074  return !empty(glob(rtrim($path, '/\\') . '/*', GLOB_ONLYDIR));
1075  }
1076 
1082  protected function sortPackageStatesConfigurationByDependency(array $packageStatesConfiguration)
1083  {
1084  return $this->dependencyOrderingService->calculateOrder($this->buildDependencyGraph($packageStatesConfiguration));
1085  }
1086 
1097  protected function convertConfigurationForGraph(array $packageStatesConfiguration, array $packageKeys)
1098  {
1099  $dependencies = [];
1100  foreach ($packageKeys as $packageKey) {
1101  if (!isset($packageStatesConfiguration[$packageKey]['dependencies']) && !isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1102  continue;
1103  }
1104  $dependencies[$packageKey] = [
1105  'after' => []
1106  ];
1107  if (isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
1108  foreach ($packageStatesConfiguration[$packageKey]['dependencies'] as $dependentPackageKey) {
1109  if (!in_array($dependentPackageKey, $packageKeys, true)) {
1110  throw new \UnexpectedValueException(
1111  'The package "' . $packageKey . '" depends on "'
1112  . $dependentPackageKey . '" which is not present in the system.',
1113  1519931815
1114  );
1115  }
1116  $dependencies[$packageKey]['after'][] = $dependentPackageKey;
1117  }
1118  }
1119  if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1120  foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
1121  // skip suggestions on not existing packages
1122  if (in_array($suggestedPackageKey, $packageKeys, true)) {
1123  // Suggestions actually have never been meant to influence loading order.
1124  // We misuse this currently, as there is no other way to influence the loading order
1125  // for not-required packages (soft-dependency).
1126  // When considering suggestions for the loading order, we might create a cyclic dependency
1127  // if the suggested package already has a real dependency on this package, so the suggestion
1128  // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
1129  $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
1130  }
1131  }
1132  }
1133  }
1134  return $dependencies;
1135  }
1136 
1147  protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys)
1148  {
1149  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1150  $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
1151  foreach ($extensionPackageKeys as $packageKey) {
1152  // Remove framework packages from list
1153  $packageKeysWithoutFramework = array_diff(
1154  $packageStateConfiguration[$packageKey]['dependencies'],
1155  $frameworkPackageKeys
1156  );
1157  // The order of the array_merge is crucial here,
1158  // we want the framework first
1159  $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
1160  $rootPackageKeys,
1161  $packageKeysWithoutFramework
1162  );
1163  }
1164  return $packageStateConfiguration;
1165  }
1166 
1176  protected function buildDependencyGraph(array $packageStateConfiguration)
1177  {
1178  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1179  $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
1180  $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
1181 
1182  $packageKeys = array_keys($packageStateConfiguration);
1183  return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
1184  }
1185 
1190  protected function findFrameworkPackages(array $packageStateConfiguration)
1191  {
1192  $frameworkPackageKeys = [];
1193  foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
1194  $package = $this->getPackage($packageKey);
1195  if ($package->getValueFromComposerManifest('type') === 'typo3-cms-framework') {
1196  $frameworkPackageKeys[] = $packageKey;
1197  }
1198  }
1199 
1200  return $frameworkPackageKeys;
1201  }
1202 }
‪TYPO3\CMS\Core\Package\Exception\UnknownPackageException
Definition: UnknownPackageException.php:24
‪TYPO3\CMS\Core\Package\Exception\PackageStatesFileNotWritableException
Definition: PackageStatesFileNotWritableException.php:24
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:24
‪$finder
‪if(PHP_SAPI !=='cli') $finder
Definition: header-comment.php:22
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException
Definition: InvalidPackageManifestException.php:24
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static string getPublicPath()
Definition: Environment.php:180
‪TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException
Definition: InvalidPackagePathException.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static string dirname($path)
Definition: PathUtility.php:186
‪TYPO3\CMS\Core\Information\Typo3Version
Definition: Typo3Version.php:21
‪TYPO3\CMS\Core\Core\ClassLoadingInformation
Definition: ClassLoadingInformation.php:35
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException
Definition: InvalidPackageKeyException.php:24
‪TYPO3\CMS\Core\Utility\ArrayUtility\arrayExport
‪static string arrayExport(array $array=[], $level=0)
Definition: ArrayUtility.php:402
‪TYPO3\CMS\Core\Package\MetaData\CONSTRAINT_TYPE_SUGGESTS
‪const CONSTRAINT_TYPE_SUGGESTS
Definition: MetaData.php:27
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static string basename($path)
Definition: PathUtility.php:165
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_EXTENSIONKEY
‪const PATTERN_MATCH_EXTENSIONKEY
Definition: PackageInterface.php:25
‪TYPO3\CMS\Core\Package\Exception\PackageStatesUnavailableException
Definition: PackageStatesUnavailableException.php:24
‪TYPO3\CMS\Core\Package\Exception\PackageManagerCacheUnavailableException
Definition: PackageManagerCacheUnavailableException.php:24
‪TYPO3\CMS\Core\Package\MetaData\PackageConstraint
Definition: PackageConstraint.php:22
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_PACKAGEKEY
‪const PATTERN_MATCH_PACKAGEKEY
Definition: PackageInterface.php:23
‪TYPO3\CMS\Core\Service\OpcodeCacheService
Definition: OpcodeCacheService.php:25
‪TYPO3\CMS\Core\Utility\PathUtility\sanitizeTrailingSeparator
‪static string sanitizeTrailingSeparator($path, $separator='/')
Definition: PathUtility.php:148
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Core\Environment\isComposerMode
‪static bool isComposerMode()
Definition: Environment.php:144
‪TYPO3\CMS\Core\Core\ClassLoadingInformation\registerTransientClassLoadingInformationForPackage
‪static registerTransientClassLoadingInformationForPackage(PackageInterface $package)
Definition: ClassLoadingInformation.php:171
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:24
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:23
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException
Definition: InvalidPackageStateException.php:24
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:40
‪TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent
Definition: PackagesMayHaveChangedEvent.php:24
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir($path, $removeNonEmpty=false)
Definition: GeneralUtility.php:2075
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:3
‪TYPO3\CMS\Core\Package
Definition: AbstractServiceProvider.php:18
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFile
‪static bool writeFile($file, $content, $changePermissions=false)
Definition: GeneralUtility.php:1836
‪TYPO3\CMS\Core\Package\Exception\MissingPackageManifestException
Definition: MissingPackageManifestException.php:24
‪TYPO3\CMS\Core\Package\Exception\ProtectedPackageKeyException
Definition: ProtectedPackageKeyException.php:24
‪TYPO3\CMS\Core\Core\Environment\getLegacyConfigPath
‪static string getLegacyConfigPath()
Definition: Environment.php:282