‪TYPO3CMS  11.5
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 $packageCache;
57 
61  protected $packagesBasePaths = [];
62 
66  protected $packageAliasMap = [];
67 
72  protected $packagesBasePath;
73 
78  protected $packages = [];
79 
83  protected $availablePackagesScanned = false;
84 
89  protected $composerNameToPackageKeyMap = [];
90 
95  protected $activePackages = [];
96 
100  protected $packageStatesPathAndFilename;
101 
106  protected $packageStatesConfiguration = [];
107 
113  protected ?string $packagePathMatchRegex;
114 
120  public function __construct(DependencyOrderingService $dependencyOrderingService, string $packageStatesPathAndFilename = null, string $packagesBasePath = null)
121  {
122  $this->packagesBasePath = $packagesBasePath ?? ‪Environment::getPublicPath() . '/';
123  $this->packageStatesPathAndFilename = $packageStatesPathAndFilename ?? ‪Environment::getLegacyConfigPath() . '/PackageStates.php';
124  $this->dependencyOrderingService = $dependencyOrderingService;
125  }
126 
131  public function setPackageCache(PackageCacheInterface $packageCache)
132  {
133  $this->packageCache = $packageCache;
134  }
135 
140  public function initialize()
141  {
142  try {
143  $this->loadPackageManagerStatesFromCache();
144  } catch (PackageManagerCacheUnavailableException $exception) {
145  $this->loadPackageStates();
146  $this->initializePackageObjects();
147  $this->saveToPackageCache();
148  }
149  }
150 
155  public function getCacheIdentifier()
156  {
157  try {
158  return $this->packageCache->getIdentifier();
159  } catch (PackageManagerCacheUnavailableException $e) {
160  return null;
161  }
162  }
163 
167  protected function saveToPackageCache(): void
168  {
169  // Build cache entry
171  $this->packageStatesConfiguration,
172  $this->packageAliasMap,
173  $this->composerNameToPackageKeyMap,
174  $this->packages,
175  );
176  $this->packageCache->store($cacheEntry);
177  }
178 
184  protected function loadPackageManagerStatesFromCache()
185  {
186  $cacheEntry = $this->packageCache->fetch();
187  $this->packageStatesConfiguration = $cacheEntry->getConfiguration();
188  $this->packageAliasMap = $cacheEntry->getAliasMap();
189  $this->composerNameToPackageKeyMap = $cacheEntry->getComposerNameMap();
190  $this->packages = $cacheEntry->getPackages();
191  }
192 
199  protected function loadPackageStates()
200  {
201  $this->packageStatesConfiguration = (@include $this->packageStatesPathAndFilename) ?: [];
202  ‪PackageCacheEntry::ensureValidPackageConfiguration($this->packageStatesConfiguration);
203  $this->registerPackagesFromConfiguration($this->packageStatesConfiguration['packages'], false);
204  }
205 
211  protected function initializePackageObjects()
212  {
213  $requiredPackages = [];
214  $activePackages = [];
215  foreach ($this->packages as $packageKey => $package) {
216  if ($package->isProtected()) {
217  $requiredPackages[$packageKey] = $package;
218  }
219  if (isset($this->packageStatesConfiguration['packages'][$packageKey])) {
220  $activePackages[$packageKey] = $package;
221  }
222  }
223  $previousActivePackages = $activePackages;
224  $activePackages = array_merge($requiredPackages, $activePackages);
225 
226  if ($activePackages != $previousActivePackages) {
227  foreach ($requiredPackages as $requiredPackageKey => $package) {
228  $this->registerActivePackage($package);
229  }
230  $this->sortAndSavePackageStates();
231  }
232  }
233 
237  protected function registerActivePackage(PackageInterface $package)
238  {
239  // reset the active packages so they are rebuilt.
240  $this->activePackages = [];
241  $this->packagePathMatchRegex = null;
242  $this->packageStatesConfiguration['packages'][$package->getPackageKey()] = ['packagePath' => str_replace($this->packagesBasePath, '', $package->getPackagePath())];
243  }
244 
250  public function scanAvailablePackages()
251  {
253  return;
254  }
255  $packagePaths = $this->scanPackagePathsForExtensions();
256  $packages = [];
257  foreach ($packagePaths as $packageKey => $packagePath) {
258  $packages[$packageKey] = ['packagePath' => str_replace($this->packagesBasePath, '', $packagePath)];
259  }
260 
261  $this->availablePackagesScanned = true;
262  $registerOnlyNewPackages = !empty($this->packages);
263  $this->registerPackagesFromConfiguration($packages, $registerOnlyNewPackages);
264  }
265 
274  public function resolvePackagePath(string $path): string
275  {
276  $packageKey = $this->extractPackageKeyFromPackagePath($path);
277  $package = $this->getPackage($packageKey);
278 
279  return str_replace('EXT:' . $packageKey . '/', $package->getPackagePath(), $path);
280  }
281 
291  public function extractPackageKeyFromPackagePath(string $path): string
292  {
293  if (!‪PathUtility::isExtensionPath($path)) {
294  throw new UnknownPackageException('Given path is not an extension path starting with "EXT:" ' . $path, 1631630764);
295  }
296  if (!isset($this->packagePathMatchRegex)) {
297  $this->packagePathMatchRegex = sprintf(
298  '/^EXT:(%s)\//',
299  implode(
300  '|',
301  array_map(
302  static function ($packageKey) {
303  return preg_quote($packageKey, '/');
304  },
305  array_merge(
306  array_keys($this->getActivePackages()),
307  array_keys($this->packageAliasMap),
308  array_keys($this->composerNameToPackageKeyMap)
309  )
310  )
311  )
312  );
313  }
314  $result = preg_match($this->packagePathMatchRegex, $path, $matches);
315  if (!$result || empty($matches[1])) {
316  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);
317  }
318 
319  return $matches[1];
320  }
321 
327  public function packagesMayHaveChanged(PackagesMayHaveChangedEvent $event): void
328  {
329  $this->scanAvailablePackages();
330  }
331 
337  protected function scanPackagePathsForExtensions()
338  {
339  $collectedExtensionPaths = [];
340  foreach ($this->getPackageBasePaths() as $packageBasePath) {
341  // Only add the extension if we have an EMCONF and the extension is not yet registered.
342  // This is crucial in order to allow overriding of system extension by local extensions
343  // and strongly depends on the order of paths defined in $this->packagesBasePaths.
344  ‪$finder = new Finder();
345  ‪$finder
346  ->name('ext_emconf.php')
347  ->followLinks()
348  ->depth(0)
349  ->ignoreUnreadableDirs()
350  ->in($packageBasePath);
351 
353  foreach (‪$finder as $fileInfo) {
354  $path = ‪PathUtility::dirname($fileInfo->getPathname());
355  $extensionName = ‪PathUtility::basename($path);
356  // Fix Windows backslashes
357  // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
358  $currentPath = str_replace('\\', '/', $path) . '/';
359  if (!isset($collectedExtensionPaths[$extensionName])) {
360  $collectedExtensionPaths[$extensionName] = $currentPath;
361  }
362  }
363  }
364  return $collectedExtensionPaths;
365  }
366 
375  protected function registerPackagesFromConfiguration(array $packages, $registerOnlyNewPackages = false)
376  {
377  $packageStatesHasChanged = false;
378  foreach ($packages as $packageKey => $stateConfiguration) {
379  if ($registerOnlyNewPackages && $this->isPackageRegistered($packageKey)) {
380  continue;
381  }
382 
383  if (!isset($stateConfiguration['packagePath'])) {
384  $this->unregisterPackageByPackageKey($packageKey);
385  $packageStatesHasChanged = true;
386  continue;
387  }
388 
389  try {
390  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
391  $package = new Package($this, $packageKey, $packagePath);
392  } catch (InvalidPackagePathException|InvalidPackageKeyException|InvalidPackageManifestException $exception) {
393  $this->unregisterPackageByPackageKey($packageKey);
394  $packageStatesHasChanged = true;
395  continue;
396  }
397 
398  $this->registerPackage($package);
399  }
400  if ($packageStatesHasChanged) {
401  $this->sortAndSavePackageStates();
402  }
403  }
404 
413  public function registerPackage(PackageInterface $package)
414  {
415  $packageKey = $package->getPackageKey();
416  if ($this->isPackageRegistered($packageKey)) {
417  throw new InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
418  }
419 
420  $this->composerNameToPackageKeyMap[$package->getValueFromComposerManifest('name')] = $packageKey;
421  $this->packages[$packageKey] = $package;
422 
423  if ($package instanceof PackageInterface) {
424  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
425  $this->packageAliasMap[$packageToReplace] = $package->getPackageKey();
426  }
427  }
428  return $package;
429  }
430 
436  protected function unregisterPackageByPackageKey($packageKey)
437  {
438  try {
439  $package = $this->getPackage($packageKey);
440  if ($package instanceof PackageInterface) {
441  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
442  unset($this->packageAliasMap[$packageToReplace]);
443  }
444  }
445  } catch (UnknownPackageException $e) {
446  }
447  $this->composerNameToPackageKeyMap = array_filter(
448  $this->composerNameToPackageKeyMap,
449  static function ($aliasedKey) use ($packageKey) {
450  return $aliasedKey !== $packageKey;
451  }
452  );
453  unset($this->packages[$packageKey]);
454  unset($this->packageStatesConfiguration['packages'][$packageKey]);
455  }
456 
464  public function getPackageKeyFromComposerName($composerName)
465  {
466  if (isset($this->packageAliasMap[$composerName])) {
467  return $this->packageAliasMap[$composerName];
468  }
469  if (isset($this->composerNameToPackageKeyMap[$composerName])) {
470  return $this->composerNameToPackageKeyMap[$composerName];
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[$this->getPackageKeyFromComposerName($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->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  $this->packagePathMatchRegex = null;
559  unset($this->packageStatesConfiguration['packages'][$packageKey]);
560  $this->sortAndSavePackageStates();
561  }
562 
567  public function activatePackage($packageKey)
568  {
569  $package = $this->getPackage($packageKey);
570  $this->registerTransientClassLoadingInformationForPackage($package);
571 
572  if ($this->isPackageActive($packageKey)) {
573  return;
574  }
575 
576  $this->registerActivePackage($package);
577  $this->sortAndSavePackageStates();
578  }
579 
584  protected function registerTransientClassLoadingInformationForPackage(PackageInterface $package)
585  {
587  return;
588  }
590  }
591 
601  public function deletePackage($packageKey)
602  {
603  if (!$this->isPackageAvailable($packageKey)) {
604  throw new UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
605  }
606 
607  $package = $this->getPackage($packageKey);
608  if ($package->isProtected()) {
609  throw new ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
610  }
611 
612  if ($this->isPackageActive($packageKey)) {
613  $this->deactivatePackage($packageKey);
614  }
615 
616  $this->unregisterPackage($package);
617  $this->sortAndSavePackageStates();
618 
619  $packagePath = $package->getPackagePath();
620  $deletion = ‪GeneralUtility::rmdir($packagePath, true);
621  if ($deletion === false) {
622  throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
623  }
624  }
625 
633  public function getActivePackages()
634  {
635  if (empty($this->activePackages)) {
636  if (!empty($this->packageStatesConfiguration['packages'])) {
637  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageConfig) {
638  $this->activePackages[$packageKey] = $this->getPackage($packageKey);
639  }
640  }
641  }
642  return $this->activePackages;
643  }
644 
651  protected function isPackageRegistered($packageKey)
652  {
653  $packageKey = $this->getPackageKeyFromComposerName($packageKey);
654 
655  return isset($this->packages[$packageKey]);
656  }
657 
665  protected function sortActivePackagesByDependencies()
666  {
667  $packagesWithDependencies = $this->resolvePackageDependencies($this->packageStatesConfiguration['packages']);
668 
669  // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
670  ksort($packagesWithDependencies);
671  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
672 
673  // Reorder the packages according to the loading order
674  $this->packageStatesConfiguration['packages'] = [];
675  foreach ($sortedPackageKeys as $packageKey) {
676  $this->registerActivePackage($this->packages[$packageKey]);
677  }
678  return $packagesWithDependencies;
679  }
680 
689  protected function resolvePackageDependencies($packageConfig)
690  {
691  $packagesWithDependencies = [];
692  foreach ($packageConfig as $packageKey => $_) {
693  $packagesWithDependencies[$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
694  $packagesWithDependencies[$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
695  }
696  return $packagesWithDependencies;
697  }
698 
705  protected function getSuggestionArrayForPackage($packageKey)
706  {
707  if (!isset($this->packages[$packageKey])) {
708  return null;
709  }
710  $suggestedPackageKeys = [];
711  $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(‪MetaData::CONSTRAINT_TYPE_SUGGESTS);
712  foreach ($suggestedPackageConstraints as $constraint) {
713  if ($constraint instanceof PackageConstraint) {
714  $suggestedPackageKey = $constraint->getValue();
715  if (isset($this->packages[$suggestedPackageKey])) {
716  $suggestedPackageKeys[] = $suggestedPackageKey;
717  }
718  }
719  }
720  return array_reverse($suggestedPackageKeys);
721  }
722 
729  protected function savePackageStates()
730  {
731  $this->packageStatesConfiguration['version'] = 5;
732 
733  $fileDescription = "# PackageStates.php\n\n";
734  $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
735  $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
736  $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
737  $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
738 
739  if (!@is_writable($this->packageStatesPathAndFilename)) {
740  // If file does not exist, try to create it
741  $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
742  if (!$fileHandle) {
743  throw new PackageStatesFileNotWritableException(
744  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),
745  1382449759
746  );
747  }
748  fclose($fileHandle);
749  }
750  $packageStatesCode = "<?php\n$fileDescription\nreturn " . ‪ArrayUtility::arrayExport($this->packageStatesConfiguration) . ";\n";
751  ‪GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
752  // Cache depends on package states file, therefore we invalidate it
753  $this->packageCache->invalidate();
754 
755  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
756  }
757 
764  protected function sortAndSavePackageStates()
765  {
766  $this->sortActivePackagesByDependencies();
767  $this->savePackageStates();
768  }
769 
776  public function isPackageKeyValid($packageKey)
777  {
778  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;
779  }
780 
787  public function getAvailablePackages()
788  {
789  if ($this->availablePackagesScanned === false) {
790  $this->scanAvailablePackages();
791  }
792 
793  return $this->packages;
794  }
795 
803  public function unregisterPackage(PackageInterface $package)
804  {
805  $packageKey = $package->getPackageKey();
806  if (!$this->isPackageRegistered($packageKey)) {
807  throw new InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
808  }
809  $this->unregisterPackageByPackageKey($packageKey);
810  }
811 
819  public function reloadPackageInformation($packageKey)
820  {
821  if (!$this->isPackageRegistered($packageKey)) {
822  throw new InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
823  }
824 
825  $package = $this->packages[$packageKey];
826  $packagePath = $package->getPackagePath();
827  $newPackage = new Package($this, $packageKey, $packagePath);
828  $this->packages[$packageKey] = $newPackage;
829  unset($package);
830  }
831 
841  public function getComposerManifest(string $manifestPath, bool $ignoreExtEmConf = false)
842  {
843  $composerManifest = new \stdClass();
844  if (file_exists($manifestPath . 'composer.json')) {
845  $json = file_get_contents($manifestPath . 'composer.json');
846  if ($json !== false) {
847  $composerManifest = json_decode($json);
848  }
849  if (!$composerManifest instanceof \stdClass) {
850  throw new InvalidPackageManifestException('The composer.json found for extension "' . ‪PathUtility::basename($manifestPath) . '" is invalid!', 1439555561);
851  }
852  }
853 
854  if ($ignoreExtEmConf) {
855  return $composerManifest;
856  }
857 
858  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $manifestPath);
859  $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath, $packageKey);
860  if ($extensionManagerConfiguration !== null) {
861  $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
862  $packageKey,
863  $extensionManagerConfiguration,
864  $composerManifest
865  );
866  }
867 
868  return $composerManifest;
869  }
870 
880  protected function getExtensionEmConf(string $packagePath, string $packageKey): ?array
881  {
882  $_EXTKEY = $packageKey;
883  $path = $packagePath . 'ext_emconf.php';
884  ‪$EM_CONF = null;
885  if (@file_exists($path)) {
886  include $path;
887  if (is_array(‪$EM_CONF[$_EXTKEY])) {
888  return ‪$EM_CONF[$_EXTKEY];
889  }
890  throw new InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
891  }
892  return null;
893  }
894 
904  protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
905  {
906  $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
907  $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
908  $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title'] ?? '');
909  $this->setComposerManifestValueIfEmpty($composerManifest, 'authors', [['name' => $extensionManagerConfiguration['author'] ?? '', 'email' => $extensionManagerConfiguration['author_email'] ?? '']]);
910  $composerManifest->version = $extensionManagerConfiguration['version'] ?? '';
911  // "Invent" a new title attribute here for internal use in non Composer mode
912  $composerManifest->title = $extensionManagerConfiguration['title'] ?? null;
913  $composerManifest->require = new \stdClass();
914  $composerManifest->conflict = new \stdClass();
915  $composerManifest->suggest = new \stdClass();
916  if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
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  foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
933  if (!empty($conflictingPackageKey)) {
934  $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
935  } else {
936  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
937  }
938  }
939  }
940  if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
941  foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
942  if (!empty($suggestedPackageKey)) {
943  $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
944  } else {
945  throw new InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
946  }
947  }
948  }
949  if (isset($extensionManagerConfiguration['autoload'])) {
950  $autoload = json_encode($extensionManagerConfiguration['autoload']);
951  if ($autoload !== false) {
952  $composerManifest->autoload = json_decode($autoload);
953  }
954  }
955  // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
956  unset($composerManifest->{'autoload-dev'});
957  if (isset($extensionManagerConfiguration['autoload-dev'])) {
958  $autoloadDev = json_encode($extensionManagerConfiguration['autoload-dev']);
959  if ($autoloadDev !== false) {
960  $composerManifest->{'autoload-dev'} = json_decode($autoloadDev);
961  }
962  }
963 
964  return $composerManifest;
965  }
966 
973  protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
974  {
975  if (empty($manifest->{$property})) {
976  $manifest->{$property} = $value;
977  }
978 
979  return $manifest;
980  }
981 
993  protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = [], array $trace = [])
994  {
995  if (!isset($this->packages[$packageKey])) {
996  return null;
997  }
998  if (in_array($packageKey, $trace, true) !== false) {
999  return $dependentPackageKeys;
1000  }
1001  $trace[] = $packageKey;
1002  $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1003  foreach ($dependentPackageConstraints as $constraint) {
1004  if ($constraint instanceof PackageConstraint) {
1005  $dependentPackageKey = $constraint->getValue();
1006  if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1007  $dependentPackageKeys[] = $dependentPackageKey;
1008  }
1009  $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1010  }
1011  }
1012  return array_reverse($dependentPackageKeys);
1013  }
1014 
1030  protected function getPackageKeyFromManifest($manifest, $packagePath)
1031  {
1032  if (!is_object($manifest)) {
1033  throw new InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1034  }
1035  if (!empty($manifest->extra->{'typo3/cms'}->{'extension-key'})) {
1036  return $manifest->extra->{'typo3/cms'}->{'extension-key'};
1037  }
1038  if (empty($manifest->name) || (isset($manifest->type) && strpos($manifest->type, 'typo3-cms-') === 0)) {
1039  return PathUtility::basename($packagePath);
1040  }
1041 
1042  return $manifest->name;
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  if ($this->isComposerDependency($dependentPackageKey)) {
1111  // The given package has a dependency to a Composer package that has no relation to TYPO3
1112  // We can ignore those, when calculating the extension order
1113  continue;
1114  }
1115  throw new \UnexpectedValueException(
1116  'The package "' . $packageKey . '" depends on "'
1117  . $dependentPackageKey . '" which is not present in the system.',
1118  1519931815
1119  );
1120  }
1121  $dependencies[$packageKey]['after'][] = $dependentPackageKey;
1122  }
1123  }
1124  if (isset($packageStatesConfiguration[$packageKey]['suggestions'])) {
1125  foreach ($packageStatesConfiguration[$packageKey]['suggestions'] as $suggestedPackageKey) {
1126  // skip suggestions on not existing packages
1127  if (in_array($suggestedPackageKey, $packageKeys, true)) {
1128  // Suggestions actually have never been meant to influence loading order.
1129  // We misuse this currently, as there is no other way to influence the loading order
1130  // for not-required packages (soft-dependency).
1131  // When considering suggestions for the loading order, we might create a cyclic dependency
1132  // if the suggested package already has a real dependency on this package, so the suggestion
1133  // has do be dropped in this case and must *not* be taken into account for loading order evaluation.
1134  $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey;
1135  }
1136  }
1137  }
1138  }
1139  return $dependencies;
1140  }
1141 
1149  protected function isComposerDependency(string $packageName): bool
1150  {
1151  return false;
1152  }
1153 
1164  protected function addDependencyToFrameworkToAllExtensions(array $packageStateConfiguration, array $rootPackageKeys)
1165  {
1166  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1167  $extensionPackageKeys = array_diff(array_keys($packageStateConfiguration), $frameworkPackageKeys);
1168  foreach ($extensionPackageKeys as $packageKey) {
1169  // Remove framework packages from list
1170  $packageKeysWithoutFramework = array_diff(
1171  $packageStateConfiguration[$packageKey]['dependencies'],
1172  $frameworkPackageKeys
1173  );
1174  // The order of the array_merge is crucial here,
1175  // we want the framework first
1176  $packageStateConfiguration[$packageKey]['dependencies'] = array_merge(
1177  $rootPackageKeys,
1178  $packageKeysWithoutFramework
1179  );
1180  }
1181  return $packageStateConfiguration;
1182  }
1183 
1193  protected function buildDependencyGraph(array $packageStateConfiguration)
1194  {
1195  $frameworkPackageKeys = $this->findFrameworkPackages($packageStateConfiguration);
1196  $frameworkPackagesDependencyGraph = $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $frameworkPackageKeys));
1197  $packageStateConfiguration = $this->addDependencyToFrameworkToAllExtensions($packageStateConfiguration, $this->dependencyOrderingService->findRootIds($frameworkPackagesDependencyGraph));
1198 
1199  $packageKeys = array_keys($packageStateConfiguration);
1200  return $this->dependencyOrderingService->buildDependencyGraph($this->convertConfigurationForGraph($packageStateConfiguration, $packageKeys));
1201  }
1202 
1207  protected function findFrameworkPackages(array $packageStateConfiguration)
1208  {
1209  $frameworkPackageKeys = [];
1210  foreach ($packageStateConfiguration as $packageKey => $packageConfiguration) {
1211  $package = $this->getPackage($packageKey);
1212  if ($package->getPackageMetaData()->isFrameworkType()) {
1213  $frameworkPackageKeys[] = $packageKey;
1214  }
1215  }
1216 
1217  return $frameworkPackageKeys;
1218  }
1219 
1223  public function warmupCaches(CacheWarmupEvent $event): void
1224  {
1225  if (Environment::isComposerMode()) {
1226  return;
1227  }
1228  if ($event->hasGroup('system')) {
1229  if (count($this->packageStatesConfiguration) === 0) {
1230  $this->loadPackageStates();
1231  $this->initializePackageObjects();
1232  }
1233  $this->saveToPackageCache();
1234  }
1235  }
1236 }
‪TYPO3\CMS\Core\Package\Exception\UnknownPackageException
Definition: UnknownPackageException.php:23
‪TYPO3\CMS\Core\Package\Exception\PackageStatesFileNotWritableException
Definition: PackageStatesFileNotWritableException.php:23
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:25
‪$finder
‪if(PHP_SAPI !=='cli') $finder
Definition: header-comment.php:22
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException
Definition: InvalidPackageManifestException.php:23
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static string getPublicPath()
Definition: Environment.php:206
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:3
‪TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException
Definition: InvalidPackagePathException.php:23
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static string dirname($path)
Definition: PathUtility.php:251
‪TYPO3\CMS\Core\Utility\PathUtility\isExtensionPath
‪static bool isExtensionPath(string $path)
Definition: PathUtility.php:121
‪TYPO3\CMS\Core\Package\Cache\PackageCacheEntry\ensureValidPackageConfiguration
‪static ensureValidPackageConfiguration(array $configuration)
Definition: PackageCacheEntry.php:84
‪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=[], $level=0)
Definition: ArrayUtility.php:402
‪TYPO3\CMS\Core\Package\Cache\PackageCacheEntry\fromPackageData
‪static fromPackageData(array $packageStatesConfiguration, array $packageAliasMap, array $composerNameToPackageKeyMap, array $packageObjects)
Definition: PackageCacheEntry.php:91
‪TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent
Definition: CacheWarmupEvent.php:24
‪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:226
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_EXTENSIONKEY
‪const PATTERN_MATCH_EXTENSIONKEY
Definition: PackageInterface.php:30
‪TYPO3\CMS\Core\Package\Cache\PackageCacheEntry
Definition: PackageCacheEntry.php:33
‪TYPO3\CMS\Core\Package\Exception\UnknownPackagePathException
Definition: UnknownPackagePathException.php:23
‪TYPO3\CMS\Core\Package\Exception\PackageManagerCacheUnavailableException
Definition: PackageManagerCacheUnavailableException.php:24
‪TYPO3\CMS\Core\Package\Cache\PackageCacheInterface
Definition: PackageCacheInterface.php:31
‪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:28
‪TYPO3\CMS\Core\Service\OpcodeCacheService
Definition: OpcodeCacheService.php:25
‪TYPO3\CMS\Core\Utility\PathUtility\sanitizeTrailingSeparator
‪static string sanitizeTrailingSeparator($path, $separator='/')
Definition: PathUtility.php:209
‪TYPO3\CMS\Core\Package\PackageInterface\PATTERN_MATCH_COMPOSER_NAME
‪const PATTERN_MATCH_COMPOSER_NAME
Definition: PackageInterface.php:26
‪TYPO3\CMS\Core\Core\Environment\isComposerMode
‪static bool isComposerMode()
Definition: Environment.php:152
‪TYPO3\CMS\Core\Core\ClassLoadingInformation\registerTransientClassLoadingInformationForPackage
‪static registerTransientClassLoadingInformationForPackage(PackageInterface $package)
Definition: ClassLoadingInformation.php:170
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:24
‪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:43
‪TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent
Definition: PackagesMayHaveChangedEvent.php:23
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir($path, $removeNonEmpty=false)
Definition: GeneralUtility.php:1961
‪TYPO3\CMS\Core\Package
Definition: AbstractServiceProvider.php:18
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFile
‪static bool writeFile($file, $content, $changePermissions=false)
Definition: GeneralUtility.php:1722
‪TYPO3\CMS\Core\Package\Exception\ProtectedPackageKeyException
Definition: ProtectedPackageKeyException.php:23
‪TYPO3\CMS\Core\Core\Environment\getLegacyConfigPath
‪static string getLegacyConfigPath()
Definition: Environment.php:308