‪TYPO3CMS  ‪main
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 
51 class PackageManager implements SingletonInterface
52 {
56  protected $dependencyOrderingService;
57 
61  protected $packageCache;
62 
66  protected $packagesBasePaths = [];
67 
71  protected $packageAliasMap = [];
72 
77  protected $packagesBasePath;
78 
83  protected $packages = [];
84 
88  protected $availablePackagesScanned = false;
89 
95  protected $composerNameToPackageKeyMap = [];
96 
101  protected $activePackages = [];
102 
106  protected $packageStatesPathAndFilename;
107 
112  protected $packageStatesConfiguration = [];
113 
117  protected ?string $packagePathMatchRegex;
118 
119  public function __construct(DependencyOrderingService $dependencyOrderingService, string $packageStatesPathAndFilename = null, string $packagesBasePath = null)
120  {
121  $this->packagesBasePath = $packagesBasePath ?? ‪Environment::getPublicPath() . '/';
122  $this->packageStatesPathAndFilename = $packageStatesPathAndFilename ?? ‪Environment::getLegacyConfigPath() . '/PackageStates.php';
123  $this->dependencyOrderingService = $dependencyOrderingService;
124  }
125 
129  public function setPackageCache(PackageCacheInterface $packageCache)
130  {
131  $this->packageCache = $packageCache;
132  }
133 
138  public function initialize()
139  {
140  try {
141  $this->loadPackageManagerStatesFromCache();
142  } catch (PackageManagerCacheUnavailableException $exception) {
143  $this->loadPackageStates();
144  $this->initializePackageObjects();
145  $this->saveToPackageCache();
146  }
147  }
148 
153  public function getCacheIdentifier()
154  {
155  try {
156  return $this->packageCache->getIdentifier();
157  } catch (PackageManagerCacheUnavailableException $e) {
158  return null;
159  }
160  }
161 
165  protected function saveToPackageCache(): void
166  {
167  // Build cache entry
169  $this->packageStatesConfiguration,
170  $this->packageAliasMap,
171  $this->composerNameToPackageKeyMap,
172  $this->packages,
173  );
174  $this->packageCache->store($cacheEntry);
175  }
176 
182  protected function loadPackageManagerStatesFromCache()
183  {
184  $cacheEntry = $this->packageCache->fetch();
185  $this->packageStatesConfiguration = $cacheEntry->getConfiguration();
186  $this->packageAliasMap = $cacheEntry->getAliasMap();
187  $this->composerNameToPackageKeyMap = $cacheEntry->getComposerNameMap();
188  $this->packages = $cacheEntry->getPackages();
189  }
190 
197  protected function loadPackageStates()
198  {
199  $this->packageStatesConfiguration = (@include $this->packageStatesPathAndFilename) ?: [];
200  ‪PackageCacheEntry::ensureValidPackageConfiguration($this->packageStatesConfiguration);
201  $this->registerPackagesFromConfiguration($this->packageStatesConfiguration['packages'], false);
202  }
203 
209  protected function initializePackageObjects()
210  {
211  $requiredPackages = [];
212  $activePackages = [];
213  foreach ($this->packages as $packageKey => $package) {
214  if ($package->isProtected()) {
215  $requiredPackages[$packageKey] = $package;
216  }
217  if (isset($this->packageStatesConfiguration['packages'][$packageKey])) {
218  $activePackages[$packageKey] = $package;
219  }
220  }
221  $previousActivePackages = $activePackages;
222  $activePackages = array_merge($requiredPackages, $activePackages);
223 
224  if ($activePackages != $previousActivePackages) {
225  foreach ($requiredPackages as $requiredPackageKey => $package) {
226  $this->registerActivePackage($package);
227  }
228  $this->sortAndSavePackageStates();
229  }
230  }
231 
232  protected function registerActivePackage(PackageInterface $package)
233  {
234  // reset the active packages so they are rebuilt.
235  $this->activePackages = [];
236  $this->packagePathMatchRegex = null;
237  $this->packageStatesConfiguration['packages'][$package->getPackageKey()] = ['packagePath' => str_replace($this->packagesBasePath, '', $package->getPackagePath())];
238  }
239 
245  public function scanAvailablePackages()
246  {
248  return;
249  }
250  $packagePaths = $this->scanPackagePathsForExtensions();
251  $packages = [];
252  foreach ($packagePaths as $packageKey => $packagePath) {
253  $packages[$packageKey] = ['packagePath' => str_replace($this->packagesBasePath, '', $packagePath)];
254  }
255 
256  $this->availablePackagesScanned = true;
257  $registerOnlyNewPackages = !empty($this->packages);
258  $this->registerPackagesFromConfiguration($packages, $registerOnlyNewPackages);
259  }
260 
267  public function resolvePackagePath(string $path): string
268  {
269  $packageKey = $this->extractPackageKeyFromPackagePath($path);
270  $package = $this->getPackage($packageKey);
271 
272  return str_replace('EXT:' . $packageKey . '/', $package->getPackagePath(), $path);
273  }
274 
282  public function extractPackageKeyFromPackagePath(string $path): string
283  {
284  if (!‪PathUtility::isExtensionPath($path)) {
285  throw new UnknownPackageException('Given path is not an extension path starting with "EXT:" ' . $path, 1631630764);
286  }
287  if (!isset($this->packagePathMatchRegex)) {
288  $this->packagePathMatchRegex = sprintf(
289  '/^EXT:(%s)\//',
290  implode(
291  '|',
292  array_map(
293  static function (string $packageKey): string {
294  return preg_quote($packageKey, '/');
295  },
296  array_merge(
297  array_keys($this->getActivePackages()),
298  array_keys($this->packageAliasMap),
299  array_keys($this->composerNameToPackageKeyMap)
300  )
301  )
302  )
303  );
304  }
305  $result = preg_match($this->packagePathMatchRegex, $path, $matches);
306  if (!$result || empty($matches[1])) {
307  throw new UnknownPackagePathException('Package path "' . $path . '" is not available. Please check if the package referenced in the path exists and that the package key is correct (package keys are case sensitive).', 1631630087);
308  }
309 
310  return $matches[1];
311  }
312 
316  public function packagesMayHaveChanged(PackagesMayHaveChangedEvent $event): void
317  {
318  $this->scanAvailablePackages();
319  }
320 
326  protected function scanPackagePathsForExtensions()
327  {
328  $collectedExtensionPaths = [];
329  foreach ($this->getPackageBasePaths() as $packageBasePath) {
330  // Only add the extension if we have an EMCONF and the extension is not yet registered.
331  // This is crucial in order to allow overriding of system extension by local extensions
332  // and strongly depends on the order of paths defined in $this->packagesBasePaths.
333  ‪$finder = new Finder();
334  ‪$finder
335  ->name('ext_emconf.php')
336  ->followLinks()
337  ->depth(0)
338  ->ignoreUnreadableDirs()
339  ->in($packageBasePath);
340 
342  foreach (‪$finder as $fileInfo) {
343  $path = ‪PathUtility::dirname($fileInfo->getPathname());
344  $extensionName = ‪PathUtility::basename($path);
345  // Fix Windows backslashes
346  // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
347  $currentPath = str_replace('\\', '/', $path) . '/';
348  if (!isset($collectedExtensionPaths[$extensionName])) {
349  $collectedExtensionPaths[$extensionName] = $currentPath;
350  }
351  }
352  }
353  return $collectedExtensionPaths;
354  }
355 
364  protected function registerPackagesFromConfiguration(array $packages, $registerOnlyNewPackages = false)
365  {
366  $packageStatesHasChanged = false;
367  foreach ($packages as $packageKey => $stateConfiguration) {
368  if ($registerOnlyNewPackages && $this->isPackageRegistered($packageKey)) {
369  continue;
370  }
371 
372  if (!isset($stateConfiguration['packagePath'])) {
373  $this->unregisterPackageByPackageKey($packageKey);
374  $packageStatesHasChanged = true;
375  continue;
376  }
377 
378  try {
379  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
380  $package = new Package($this, $packageKey, $packagePath);
381  } catch (InvalidPackagePathException|InvalidPackageKeyException|InvalidPackageManifestException $exception) {
382  $this->unregisterPackageByPackageKey($packageKey);
383  $packageStatesHasChanged = true;
384  continue;
385  }
386 
387  $this->registerPackage($package);
388  }
389  if ($packageStatesHasChanged) {
390  $this->sortAndSavePackageStates();
391  }
392  }
393 
402  public function registerPackage(PackageInterface $package)
403  {
404  $packageKey = $package->getPackageKey();
405  if ($this->isPackageRegistered($packageKey)) {
406  throw new InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
407  }
408 
409  $this->composerNameToPackageKeyMap[$package->getValueFromComposerManifest('name')] = $packageKey;
410  $this->packages[$packageKey] = $package;
411 
412  if ($package instanceof PackageInterface) {
413  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
414  $this->packageAliasMap[$packageToReplace] = $package->getPackageKey();
415  }
416  }
417  return $package;
418  }
419 
425  protected function unregisterPackageByPackageKey($packageKey)
426  {
427  try {
428  $package = $this->getPackage($packageKey);
429  if ($package instanceof PackageInterface) {
430  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
431  unset($this->packageAliasMap[$packageToReplace]);
432  }
433  }
434  } catch (UnknownPackageException $e) {
435  }
436  $this->composerNameToPackageKeyMap = array_filter(
437  $this->composerNameToPackageKeyMap,
438  static function (string $aliasedKey) use ($packageKey): bool {
439  return $aliasedKey !== $packageKey;
440  }
441  );
442  unset($this->packages[$packageKey]);
443  unset($this->packageStatesConfiguration['packages'][$packageKey]);
444  }
445 
453  public function getPackageKeyFromComposerName($composerName)
454  {
455  if (isset($this->packageAliasMap[$composerName])) {
456  return $this->packageAliasMap[$composerName];
457  }
458  if (isset($this->composerNameToPackageKeyMap[$composerName])) {
459  return $this->composerNameToPackageKeyMap[$composerName];
460  }
461  return $composerName;
462  }
463 
472  public function getPackage($packageKey)
473  {
474  if (!$this->isPackageRegistered($packageKey) && !$this->isPackageAvailable($packageKey)) {
475  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);
476  }
477  return $this->packages[$this->getPackageKeyFromComposerName($packageKey)];
478  }
479 
487  public function isPackageAvailable($packageKey)
488  {
489  if ($this->isPackageRegistered($packageKey)) {
490  return true;
491  }
492 
493  // If activePackages is empty, the PackageManager is currently initializing
494  // thus packages should not be scanned
495  if (!$this->availablePackagesScanned && !empty($this->activePackages)) {
496  $this->scanAvailablePackages();
497  }
498 
499  return $this->isPackageRegistered($packageKey);
500  }
501 
508  public function isPackageActive($packageKey)
509  {
510  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
511 
512  return isset($this->packageStatesConfiguration['packages'][$packageKey]);
513  }
514 
524  public function deactivatePackage($packageKey)
525  {
526  $packagesWithDependencies = $this->sortActivePackagesByDependencies();
527 
528  foreach ($packagesWithDependencies as $packageStateKey => $packageStateConfiguration) {
529  if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies'])) {
530  continue;
531  }
532  if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
533  $this->deactivatePackage($packageStateKey);
534  }
535  }
536 
537  if (!$this->isPackageActive($packageKey)) {
538  return;
539  }
540 
541  $package = $this->getPackage($packageKey);
542  if ($package->isProtected()) {
543  throw new ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
544  }
545 
546  $this->activePackages = [];
547  $this->packagePathMatchRegex = null;
548  unset($this->packageStatesConfiguration['packages'][$packageKey]);
549  $this->sortAndSavePackageStates();
550  }
551 
556  public function activatePackage($packageKey)
557  {
558  $package = $this->getPackage($packageKey);
559  $this->registerTransientClassLoadingInformationForPackage($package);
560 
561  if ($this->isPackageActive($packageKey)) {
562  return;
563  }
564 
565  $this->registerActivePackage($package);
566  $this->sortAndSavePackageStates();
567  }
568 
572  protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
573  {
575  return;
576  }
578  }
579 
589  public function deletePackage($packageKey)
590  {
591  if (!$this->isPackageAvailable($packageKey)) {
592  throw new UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
593  }
594 
595  $package = $this->getPackage($packageKey);
596  if ($package->isProtected()) {
597  throw new ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
598  }
599 
600  if ($this->isPackageActive($packageKey)) {
601  $this->deactivatePackage($packageKey);
602  }
603 
604  $this->unregisterPackage($package);
605  $this->sortAndSavePackageStates();
606 
607  $packagePath = $package->getPackagePath();
608  $deletion = ‪GeneralUtility::rmdir($packagePath, true);
609  if ($deletion === false) {
610  throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
611  }
612  }
613 
621  public function getActivePackages()
622  {
623  if (empty($this->activePackages)) {
624  if (!empty($this->packageStatesConfiguration['packages'])) {
625  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfig) {
626  $this->activePackages[$packageKey] = $this->getPackage($packageKey);
627  }
628  }
629  }
630  return $this->activePackages;
631  }
632 
639  protected function isPackageRegistered($packageKey)
640  {
641  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
642 
643  return isset($this->packages[$packageKey]);
644  }
645 
653  protected function sortActivePackagesByDependencies()
654  {
655  $packagesWithDependencies = $this->resolvePackageDependencies($this->packageStatesConfiguration['packages']);
656 
657  // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
658  ksort($packagesWithDependencies);
659  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
660 
661  // Reorder the packages according to the loading order
662  $this->packageStatesConfiguration['packages'] = [];
663  foreach ($sortedPackageKeys as $packageKey) {
664  $this->registerActivePackage($this->packages[$packageKey]);
665  }
666  return $packagesWithDependencies;
667  }
668 
677  protected function resolvePackageDependencies($packageConfig)
678  {
679  $packagesWithDependencies = [];
680  foreach ($packageConfig as $packageKey => $_) {
681  $packagesWithDependencies[$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
682  $packagesWithDependencies[$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
683  }
684  return $packagesWithDependencies;
685  }
686 
693  protected function getSuggestionArrayForPackage($packageKey)
694  {
695  if (!isset($this->packages[$packageKey])) {
696  return null;
697  }
698  $suggestedPackageKeys = [];
699  $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(‪MetaData::CONSTRAINT_TYPE_SUGGESTS);
700  foreach ($suggestedPackageConstraints as $constraint) {
701  if ($constraint instanceof PackageConstraint) {
702  $suggestedPackageKey = $constraint->getValue();
703  if (isset($this->packages[$suggestedPackageKey])) {
704  $suggestedPackageKeys[] = $suggestedPackageKey;
705  }
706  }
707  }
708  return array_reverse($suggestedPackageKeys);
709  }
710 
717  protected function savePackageStates()
718  {
719  $this->packageStatesConfiguration['version'] = 5;
720 
721  $fileDescription = "# PackageStates.php\n\n";
722  $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
723  $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
724  $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
725  $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
726 
727  if (!@is_writable($this->packageStatesPathAndFilename)) {
728  // If file does not exist, try to create it
729  $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
730  if (!$fileHandle) {
731  throw new PackageStatesFileNotWritableException(
732  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),
733  1382449759
734  );
735  }
736  fclose($fileHandle);
737  }
738  $packageStatesCode = "<?php\n$fileDescription\nreturn " . ‪ArrayUtility::arrayExport($this->packageStatesConfiguration) . ";\n";
739  ‪GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
740  // Cache depends on package states file, therefore we invalidate it
741  $this->packageCache->invalidate();
742 
743  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
744  }
745 
752  protected function sortAndSavePackageStates()
753  {
754  $this->sortActivePackagesByDependencies();
755  $this->savePackageStates();
756  }
757 
764  public function isPackageKeyValid($packageKey)
765  {
766  return preg_match(‪PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1 || preg_match(‪PackageInterface::PATTERN_MATCH_COMPOSER_NAME, $packageKey) === 1 || preg_match(‪PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1;
767  }
768 
775  public function getAvailablePackages()
776  {
777  if ($this->availablePackagesScanned === false) {
778  $this->scanAvailablePackages();
779  }
780 
781  return $this->packages;
782  }
783 
791  public function unregisterPackage(PackageInterface $package)
792  {
793  $packageKey = $package->getPackageKey();
794  if (!$this->isPackageRegistered($packageKey)) {
795  throw new InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
796  }
797  $this->unregisterPackageByPackageKey($packageKey);
798  }
799 
807  public function reloadPackageInformation($packageKey)
808  {
809  if (!$this->isPackageRegistered($packageKey)) {
810  throw new InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
811  }
812 
813  $package = $this->packages[$packageKey];
814  $packagePath = $package->getPackagePath();
815  $newPackage = new Package($this, $packageKey, $packagePath);
816  $this->packages[$packageKey] = $newPackage;
817  unset($package);
818  }
819 
827  public function getComposerManifest(string $manifestPath, bool $ignoreExtEmConf = false)
828  {
829  $composerManifest = new \stdClass();
830  if (file_exists($manifestPath . 'composer.json')) {
831  $json = file_get_contents($manifestPath . 'composer.json');
832  if ($json !== false) {
833  $composerManifest = json_decode($json);
834  }
835  if (!$composerManifest instanceof \stdClass) {
836  throw new InvalidPackageManifestException('The composer.json found for extension "' . ‪PathUtility::basename($manifestPath) . '" is invalid!', 1439555561);
837  }
838  }
839 
840  if ($ignoreExtEmConf) {
841  return $composerManifest;
842  }
843 
844  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $manifestPath);
845  $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath, $packageKey);
846  if ($extensionManagerConfiguration !== null) {
847  $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
848  $packageKey,
849  $extensionManagerConfiguration,
850  $composerManifest
851  );
852  }
853 
854  return $composerManifest;
855  }
856 
864  protected function getExtensionEmConf(string $packagePath, string $packageKey): ?array
865  {
866  $_EXTKEY = $packageKey;
867  $path = $packagePath . 'ext_emconf.php';
868  ‪$EM_CONF = null;
869  if (@file_exists($path)) {
870  include $path;
871  if (is_array(‪$EM_CONF[$_EXTKEY])) {
872  return ‪$EM_CONF[$_EXTKEY];
873  }
874  throw new InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
875  }
876  return null;
877  }
878 
886  protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
887  {
888  $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
889  $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
890  $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title'] ?? '');
891  $this->setComposerManifestValueIfEmpty($composerManifest, 'authors', [['name' => $extensionManagerConfiguration['author'] ?? '', 'email' => $extensionManagerConfiguration['author_email'] ?? '']]);
892  $composerManifest->version = $extensionManagerConfiguration['version'] ?? '';
893  // "Invent" a new title attribute here for internal use in non Composer mode
894  $composerManifest->title = $extensionManagerConfiguration['title'] ?? null;
895  $composerManifest->require = new \stdClass();
896  $composerManifest->conflict = new \stdClass();
897  $composerManifest->suggest = new \stdClass();
898  if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
899  foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
900  if (!empty($requiredPackageKey)) {
901  if ($requiredPackageKey === 'typo3') {
902  // Add implicit dependency to 'core'
903  $composerManifest->require->core = $requiredPackageVersion;
904  } elseif ($requiredPackageKey !== 'php') {
905  // Skip php dependency
906  $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
907  }
908  } else {
909  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
910  }
911  }
912  }
913  if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
914  foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
915  if (!empty($conflictingPackageKey)) {
916  $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
917  } else {
918  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
919  }
920  }
921  }
922  if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
923  foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
924  if (!empty($suggestedPackageKey)) {
925  $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
926  } else {
927  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
928  }
929  }
930  }
931  if (isset($extensionManagerConfiguration['autoload'])) {
932  $autoload = json_encode($extensionManagerConfiguration['autoload']);
933  if ($autoload !== false) {
934  $composerManifest->autoload = json_decode($autoload);
935  }
936  }
937  // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
938  unset($composerManifest->{'autoload-dev'});
939  if (isset($extensionManagerConfiguration['autoload-dev'])) {
940  $autoloadDev = json_encode($extensionManagerConfiguration['autoload-dev']);
941  if ($autoloadDev !== false) {
942  $composerManifest->{'autoload-dev'} = json_decode($autoloadDev);
943  }
944  }
945 
946  return $composerManifest;
947  }
948 
954  protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
955  {
956  if (empty($manifest->{$property})) {
957  $manifest->{$property} = $value;
958  }
959 
960  return $manifest;
961  }
962 
973  protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = [], array $trace = [])
974  {
975  if (!isset($this->packages[$packageKey])) {
976  return null;
977  }
978  if (in_array($packageKey, $trace, true) !== false) {
979  return $dependentPackageKeys;
980  }
981  $trace[] = $packageKey;
982  $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
983  foreach ($dependentPackageConstraints as $constraint) {
984  if ($constraint instanceof PackageConstraint) {
985  $dependentPackageKey = $constraint->getValue();
986  if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
987  $dependentPackageKeys[] = $dependentPackageKey;
988  }
989  $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
990  }
991  }
992  return array_reverse($dependentPackageKeys);
993  }
994 
1010  protected function getPackageKeyFromManifest($manifest, $packagePath)
1011  {
1012  if (!is_object($manifest)) {
1013  throw new InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1014  }
1015  if (!empty($manifest->extra->{'typo3/cms'}->{'extension-key'})) {
1016  return $manifest->extra->{'typo3/cms'}->{'extension-key'};
1017  }
1018  if (empty($manifest->name) || (isset($manifest->type) && str_starts_with($manifest->type, 'typo3-cms-'))) {
1019  return PathUtility::basename($packagePath);
1020  }
1021 
1022  return $manifest->name;
1023  }
1024 
1031  protected function getPackageBasePaths()
1032  {
1033  if ($this->packagesBasePaths === []) {
1034  // Check if the directory even exists and if it is not empty
1035  if (is_dir(Environment::getExtensionsPath()) && $this->hasSubDirectories(Environment::getExtensionsPath())) {
1036  $this->packagesBasePaths['local'] = Environment::getExtensionsPath() . '/*/';
1037  }
1038  $this->packagesBasePaths['system'] = Environment::getFrameworkBasePath() . '/*/';
1039  }
1040  return $this->packagesBasePaths;
1041  }
1042 
1046  protected function hasSubDirectories(string $path): bool
1047  {
1048  return !empty(glob(rtrim($path, '/\\') . '/*', GLOB_ONLYDIR));
1049  }
1050 
1056  protected function sortPackageStatesConfigurationByDependency(array $packageStatesConfiguration)
1057  {
1058  return $this->dependencyOrderingService->calculateOrder($this->buildDependencyGraph($packageStatesConfiguration));
1059  }
1060 
1071  protected function convertConfigurationForGraph(array $packageStatesConfiguration, array $packageKeys)
1072  {
1073  $dependencies = [];
1074  foreach ($packageKeys as $packageKey) {
1075  if (!isset($packageStatesConfiguration[$packageKey]['dependencies']) && !isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1076  continue;
1077  }
1078  $dependencies[$packageKey] = [
1079  'after' => [],
1080  ];
1081  if (isset($packageStatesConfiguration[$packageKey]['dependencies'])) {
1082  foreach ($packageStatesConfiguration[$packageKey]['dependencies'] as $dependentPackageKey) {
1083  if (!in_array($dependentPackageKey, $packageKeys, true)) {
1084  if ($this->isComposerDependency($dependentPackageKey)) {
1085  // The given package has a dependency to a Composer package that has no relation to TYPO3
1086  // We can ignore those, when calculating the extension order
1087  continue;
1088  }
1089  throw new \UnexpectedValueException(
1090  'The package "' . $packageKey . '" depends on "'
1091  . $dependentPackageKey . '" which is not present in the system.',
1092  1519931815
1093  );
1094  }
1095  $dependencies[$packageKey]['after'][] = $dependentPackageKey;
1096  }
1097  }
1098  if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1099  foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
1100  // skip suggestions on not existing packages
1101  if (in_array($suggestedPackageKey, $packageKeys, true)) {
1102  // Suggestions actually have never been meant to influence loading order.
1103  // We misuse this currently, as there is no other way to influence the loading order
1104  // for not-required packages (soft-dependency).
1105  // When considering suggestions for the loading order, we might create a cyclic dependency
1106  // if the suggested package already has a real dependency on this package, so the suggestion
1107  // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
1108  $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
1109  }
1110  }
1111  }
1112  }
1113  return $dependencies;
1114  }
1115 
1120  protected function isComposerDependency(string $packageName): bool
1121  {
1122  return false;
1123  }
1124 
1135  protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys)
1136  {
1137  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1138  $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
1139  foreach ($extensionPackageKeys as $packageKey) {
1140  // Remove framework packages from list
1141  $packageKeysWithoutFramework = array_diff(
1142  $packageStateConfiguration[$packageKey]['dependencies'],
1143  $frameworkPackageKeys
1144  );
1145  // The order of the array_merge is crucial here,
1146  // we want the framework first
1147  $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
1148  $rootPackageKeys,
1149  $packageKeysWithoutFramework
1150  );
1151  }
1152  return $packageStateConfiguration;
1153  }
1154 
1164  protected function buildDependencyGraph(array $packageStateConfiguration)
1165  {
1166  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1167  $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
1168  $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
1169 
1170  $packageKeys = array_keys($packageStateConfiguration);
1171  return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
1172  }
1173 
1178  protected function findFrameworkPackages(array $packageStateConfiguration)
1179  {
1180  $frameworkPackageKeys = [];
1181  foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
1182  $package = $this->getPackage($packageKey);
1183  if ($package->getPackageMetaData()->isFrameworkType()) {
1184  $frameworkPackageKeys[] = $packageKey;
1185  }
1186  }
1187 
1188  return $frameworkPackageKeys;
1189  }
1190 
1194  public function warmupCaches(CacheWarmupEvent $event): void
1195  {
1196  if (Environment::isComposerMode()) {
1197  return;
1198  }
1199  if ($event->hasGroup('system')) {
1200  if (count($this->packageStatesConfiguration) === 0) {
1201  $this->loadPackageStates();
1202  $this->initializePackageObjects();
1203  }
1204  $this->saveToPackageCache();
1205  }
1206  }
1207 }
‪TYPO3\CMS\Core\Package\Exception\UnknownPackageException
Definition: UnknownPackageException.php:23
‪TYPO3\CMS\Core\Utility\PathUtility\isExtensionPath
‪static isExtensionPath(string $path)
Definition: PathUtility.php:117
‪TYPO3\CMS\Core\Package\Exception\PackageStatesFileNotWritableException
Definition: PackageStatesFileNotWritableException.php:23
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪$finder
‪if(PHP_SAPI !=='cli') $finder
Definition: header-comment.php:22
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException
Definition: InvalidPackageManifestException.php:23
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:3
‪TYPO3\CMS\Core\Utility\PathUtility\sanitizeTrailingSeparator
‪static sanitizeTrailingSeparator(string $path, string $separator='/')
Definition: PathUtility.php:203
‪TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException
Definition: InvalidPackagePathException.php:23
‪TYPO3\CMS\Core\Core\Environment\isComposerMode
‪static isComposerMode()
Definition: Environment.php:137
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Core\Package\Cache\PackageCacheEntry\ensureValidPackageConfiguration
‪static ensureValidPackageConfiguration(array $configuration)
Definition: PackageCacheEntry.php:83
‪TYPO3\CMS\Core\Core\ClassLoadingInformation
Definition: ClassLoadingInformation.php:35
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException
Definition: InvalidPackageKeyException.php:23
‪TYPO3\CMS\Core\Utility\ArrayUtility\arrayExport
‪static string arrayExport(array $array=[], int $level=0)
Definition: ArrayUtility.php:386
‪TYPO3\CMS\Core\Package\Cache\PackageCacheEntry\fromPackageData
‪static fromPackageData(array $packageStatesConfiguration, array $packageAliasMap, array $composerNameToPackageKeyMap, array $packageObjects)
Definition: PackageCacheEntry.php:90
‪TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent
Definition: CacheWarmupEvent.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static basename(string $path)
Definition: PathUtility.php:219
‪TYPO3\CMS\Core\Package\MetaData\CONSTRAINT_TYPE_SUGGESTS
‪const CONSTRAINT_TYPE_SUGGESTS
Definition: MetaData.php:27
‪TYPO3\CMS\Core\Core\Environment\getLegacyConfigPath
‪static getLegacyConfigPath()
Definition: Environment.php:268
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_EXTENSIONKEY
‪const PATTERN_MATCH_EXTENSIONKEY
Definition: PackageInterface.php:32
‪TYPO3\CMS\Core\Package\Cache\PackageCacheEntry
Definition: PackageCacheEntry.php:35
‪TYPO3\CMS\Core\Package\Exception\UnknownPackagePathException
Definition: UnknownPackagePathException.php:23
‪TYPO3\CMS\Core\Package\Exception\PackageManagerCacheUnavailableException
Definition: PackageManagerCacheUnavailableException.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static dirname(string $path)
Definition: PathUtility.php:243
‪TYPO3\CMS\Core\Package\Cache\PackageCacheInterface
Definition: PackageCacheInterface.php:33
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFile
‪static bool writeFile(string $file, string $content, bool $changePermissions=false)
Definition: GeneralUtility.php:1469
‪TYPO3\CMS\Core\Package\MetaData\PackageConstraint
Definition: PackageConstraint.php:22
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir(string $path, bool $removeNonEmpty=false)
Definition: GeneralUtility.php:1702
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_PACKAGEKEY
‪const PATTERN_MATCH_PACKAGEKEY
Definition: PackageInterface.php:30
‪TYPO3\CMS\Core\Service\OpcodeCacheService
Definition: OpcodeCacheService.php:27
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_COMPOSER_NAME
‪const PATTERN_MATCH_COMPOSER_NAME
Definition: PackageInterface.php:28
‪TYPO3\CMS\Core\Core\ClassLoadingInformation\registerTransientClassLoadingInformationForPackage
‪static registerTransientClassLoadingInformationForPackage(PackageInterface $package)
Definition: ClassLoadingInformation.php:144
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException
Definition: InvalidPackageStateException.php:23
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent
Definition: PackagesMayHaveChangedEvent.php:23
‪TYPO3\CMS\Core\Package
Definition: AbstractServiceProvider.php:18
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Package\Exception\ProtectedPackageKeyException
Definition: ProtectedPackageKeyException.php:23