‪TYPO3CMS  ‪main
PackageArtifactBuilder.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Composer\Package\PackageInterface;
21 use Composer\Repository\PlatformRepository;
22 use Composer\Script\Event;
23 use Composer\Util\Filesystem;
24 use Composer\Util\Platform;
25 use TYPO3\CMS\Composer\Plugin\Config;
26 use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
27 use TYPO3\CMS\Composer\Plugin\Util\ExtensionKeyResolver;
28 use TYPO3\CMS\Core\Package\Cache\ComposerPackageArtifact;
34 use TYPO3\CMS\Core\Package\PackageManager;
38 
48 class ‪PackageArtifactBuilder extends PackageManager implements InstallerScript
49 {
50  private const ‪LEGACY_EXTENSION_INSTALL_PATH = '/typo3conf/ext';
51 
55  private ‪$event;
56 
60  private ‪$config;
61 
65  private ‪$fileSystem;
66 
72 
73  public function ‪__construct()
74  {
75  // Disable path determination with Environment class, which is not initialized here
76  parent::__construct(new ‪DependencyOrderingService(), '', '');
77  }
78 
79  protected function ‪isComposerDependency(string $packageName): bool
80  {
81  return PlatformRepository::isPlatformPackage($packageName) || ($this->availableComposerPackageKeys[$packageName] ?? false);
82  }
83 
92  public function ‪run(Event ‪$event): bool
93  {
94  $this->event = ‪$event;
95  $this->config = Config::load($this->event->getComposer(), $this->event->getIO());
96  $this->fileSystem = new Filesystem();
97  $composer = $this->event->getComposer();
98  $basePath = $this->config->get('base-dir');
99  $this->packagesBasePath = $basePath . '/';
100  foreach ($this->‪extractPackageMapFromComposer() as [$composerPackage, $path, $extensionKey]) {
101  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($path);
102  $package = new Package($this, $extensionKey, $packagePath, true);
103  $this->‪setTitleFromExtEmConf($package);
104  $package->makePathRelative($this->fileSystem, $basePath);
105  $package->getPackageMetaData()->setVersion($composerPackage->getPrettyVersion());
106  $this->registerPackage($package);
107  }
109  $cacheIdentifier = md5(serialize($composer->getLocker()->getLockData()) . $this->event->isDevMode());
110  $this->setPackageCache(new ComposerPackageArtifact($composer->getConfig()->get('vendor-dir') . '/typo3', $this->fileSystem, $cacheIdentifier));
111  $this->saveToPackageCache();
112 
113  return true;
114  }
115 
120  private function ‪setTitleFromExtEmConf(Package $package): void
121  {
122  $emConfPath = $package->getPackagePath() . '/ext_emconf.php';
123  if (file_exists($emConfPath)) {
124  $_EXTKEY = $package->getPackageKey();
125  ‪$EM_CONF = null;
126  include $emConfPath;
127  if (!empty(‪$EM_CONF[$_EXTKEY]['title'])) {
128  $package->getPackageMetaData()->setTitle(‪$EM_CONF[$_EXTKEY]['title']);
129  }
130  }
131  }
132 
136  private function ‪sortPackagesAndConfiguration(): void
137  {
138  $packagesWithDependencies = $this->resolvePackageDependencies($this->packages);
139  // Sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
140  ksort($packagesWithDependencies);
141  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
142  $this->packageStatesConfiguration = [];
143  $sortedPackages = [];
144  foreach ($sortedPackageKeys as $packageKey) {
145  $sortedPackages[$packageKey] = $this->packages[$packageKey];
146  // The artifact does not need path information, so it is kept empty
147  // The keys must be present, though because the PackageManager implies than a
148  // package is active by this configuration array
149  $this->packageStatesConfiguration['packages'][$packageKey] = [];
150  }
151  $this->packages = $sortedPackages;
152  $this->packageStatesConfiguration['version'] = 5;
153  }
154 
159  private function ‪extractPackageMapFromComposer(): array
160  {
161  $composer = $this->event->getComposer();
162  $rootPackage = $composer->getPackage();
163  $autoLoadGenerator = $composer->getAutoloadGenerator();
164  $localRepo = $composer->getRepositoryManager()->getLocalRepository();
165  $usedExtensionKeys = [];
166 
167  $installedTypo3Packages = array_map(
168  function (array $packageAndPath) use ($rootPackage, &$usedExtensionKeys): array {
169  [$composerPackage, $packagePath] = $packageAndPath;
170  $packageName = $composerPackage->getName();
171  $packagePath = GeneralUtility::fixWindowsFilePath($packagePath);
172  try {
173  $extensionKey = ExtensionKeyResolver::resolve($composerPackage);
174  } catch (\Throwable $e) {
175  if (str_starts_with($composerPackage->getType(), 'typo3-cms-')) {
176  // This means we have a package of type extension, and it does not have the extension key set
177  // This only happens since version > 4.0 of the installer and must be propagated to become user facing
178  throw $e;
179  }
180  // In case we can not otherwise determine the extension key, we take the composer name
181  $extensionKey = $packageName;
182  }
183  if (isset($usedExtensionKeys[$extensionKey])) {
184  throw new \UnexpectedValueException(
185  sprintf(
186  'Package with the name "%s" registered extension key "%s", but this key was already set by package with the name "%s"',
187  $packageName,
188  $extensionKey,
189  $usedExtensionKeys[$extensionKey]
190  ),
191  1638880941
192  );
193  }
194  $usedExtensionKeys[$extensionKey] = $packageName;
195  unset($this->availableComposerPackageKeys[$packageName]);
196  $this->composerNameToPackageKeyMap[$packageName] = $extensionKey;
197  if ($composerPackage === $rootPackage) {
198  return $this->‪handleRootPackage($rootPackage, $extensionKey);
199  }
200  // Add extension key to the package map for later reference
201  return [$composerPackage, $packagePath, $extensionKey];
202  },
203  array_filter(
204  $autoLoadGenerator->buildPackageMap($composer->getInstallationManager(), $rootPackage, $localRepo->getCanonicalPackages()),
205  ‪function (array $packageAndPath): bool {
207  [$composerPackage] = $packageAndPath;
208  // Filter all Composer packages without typo3/cms definition, but keep all
209  // package names, to be able to ignore Composer only dependencies when ordering the packages
210  $this->availableComposerPackageKeys[$composerPackage->getName()] = true;
211  foreach ($composerPackage->getReplaces() as $link) {
212  $this->availableComposerPackageKeys[$link->getTarget()] = true;
213  }
214  return isset($composerPackage->getExtra()['typo3/cms']);
215  }
216  )
217  );
218 
219  $this->‪publishResources($installedTypo3Packages);
220 
221  return $installedTypo3Packages;
222  }
223 
242  private function ‪handleRootPackage(PackageInterface $rootPackage, string $extensionKey): array
243  {
244  $baseDir = $this->config->get('base-dir');
245  $composer = $this->event->getComposer();
246  if ($rootPackage->getType() !== 'typo3-cms-extension'
247  || !file_exists($baseDir . '/Resources/Public/')
248  ) {
249  return [$rootPackage, $baseDir, $extensionKey];
250  }
251  $typo3ExtensionInstallPath = $composer->getInstallationManager()->getInstaller('typo3-cms-extension')->getInstallPath($rootPackage);
252  if (!str_contains($typo3ExtensionInstallPath, self::LEGACY_EXTENSION_INSTALL_PATH)) {
253  return [$rootPackage, $baseDir, $extensionKey];
254  }
255  if (!file_exists($typo3ExtensionInstallPath) && !$this->fileSystem->isSymlinkedDirectory($typo3ExtensionInstallPath)) {
256  $this->fileSystem->ensureDirectoryExists(dirname($typo3ExtensionInstallPath));
257  $this->fileSystem->relativeSymlink($baseDir, $typo3ExtensionInstallPath);
258  }
259  if (realpath($baseDir) !== realpath($typo3ExtensionInstallPath)) {
260  $this->event->getIO()->warning('The root package is of type "typo3-cms-extension" and has public resources, but could not be linked to "' . self::LEGACY_EXTENSION_INSTALL_PATH . '" directory, because target directory already exits.');
261  }
262 
263  return [$rootPackage, $typo3ExtensionInstallPath, $extensionKey];
264  }
265 
266  private function ‪publishResources(array $installedTypo3Packages): void
267  {
268  $baseDir = $this->config->get('base-dir');
269  foreach ($installedTypo3Packages as [$composerPackage, $path, $extensionKey]) {
270  $fileSystemResourcesPath = $path . '/Resources/Public';
271  // skip non-composer installation extension paths, or if resource paths does not exist.
272  if (str_ends_with($path, self::LEGACY_EXTENSION_INSTALL_PATH . '/' . $extensionKey) || !file_exists($fileSystemResourcesPath)) {
273  continue;
274  }
275  $relativePath = substr($fileSystemResourcesPath, strlen($baseDir));
276  [$relativePrefix] = explode('Resources/Public', $relativePath);
277  $publicResourcesPath = $this->fileSystem->normalizePath($this->config->get('web-dir') . '/_assets/' . md5($relativePrefix));
278  $this->fileSystem->ensureDirectoryExists(dirname($publicResourcesPath));
279  if (Platform::isWindows()) {
280  $this->‪ensureJunctionExists($fileSystemResourcesPath, $publicResourcesPath);
281  } else {
282  $this->‪ensureSymlinkExists($fileSystemResourcesPath, $publicResourcesPath);
283  }
284  }
285  }
286 
287  private function ‪ensureJunctionExists(string $target, string $junction): void
288  {
289  if (!$this->fileSystem->isJunction($junction)) {
290  // Cleanup a possible symlink that might have been installed by ourselves prior to #98434
291  // Note: Unprivileged deletion of symlinks is allowed, even if they were created by a
292  // privileged user
293  if (is_link($junction)) {
294  $this->fileSystem->unlink($junction);
295  }
296  $this->fileSystem->junction($target, $junction);
297  }
298  }
299 
300  private function ‪ensureSymlinkExists(string $target, string $link): void
301  {
302  if (!$this->fileSystem->isSymlinkedDirectory($link)) {
303  $this->fileSystem->relativeSymlink($target, $link);
304  }
305  }
306 }
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\Core\Package\Package\getPackageMetaData
‪getPackageMetaData()
Definition: Package.php:230
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException
Definition: InvalidPackageManifestException.php:23
‪TYPO3\CMS\Core\Package\Package\getPackagePath
‪string getPackagePath()
Definition: Package.php:204
‪$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\Composer\PackageArtifactBuilder\sortPackagesAndConfiguration
‪sortPackagesAndConfiguration()
Definition: PackageArtifactBuilder.php:132
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\ensureSymlinkExists
‪ensureSymlinkExists(string $target, string $link)
Definition: PackageArtifactBuilder.php:296
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$config
‪Config $config
Definition: PackageArtifactBuilder.php:58
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$availableComposerPackageKeys
‪array $availableComposerPackageKeys
Definition: PackageArtifactBuilder.php:67
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException
Definition: InvalidPackageKeyException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\__construct
‪__construct()
Definition: PackageArtifactBuilder.php:69
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\publishResources
‪publishResources(array $installedTypo3Packages)
Definition: PackageArtifactBuilder.php:262
‪TYPO3\CMS\Core\Package\Package
Definition: Package.php:30
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\LEGACY_EXTENSION_INSTALL_PATH
‪const LEGACY_EXTENSION_INSTALL_PATH
Definition: PackageArtifactBuilder.php:50
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\handleRootPackage
‪handleRootPackage(PackageInterface $rootPackage, string $extensionKey)
Definition: PackageArtifactBuilder.php:238
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$event
‪Event $event
Definition: PackageArtifactBuilder.php:54
‪TYPO3\CMS\Core\function
‪static return function(ContainerConfigurator $container, ContainerBuilder $containerBuilder)
Definition: Services.php:17
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$fileSystem
‪Filesystem $fileSystem
Definition: PackageArtifactBuilder.php:62
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException
Definition: InvalidPackageStateException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\setTitleFromExtEmConf
‪setTitleFromExtEmConf(Package $package)
Definition: PackageArtifactBuilder.php:116
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\isComposerDependency
‪isComposerDependency(string $packageName)
Definition: PackageArtifactBuilder.php:75
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\run
‪run(Event $event)
Definition: PackageArtifactBuilder.php:88
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\ensureJunctionExists
‪ensureJunctionExists(string $target, string $junction)
Definition: PackageArtifactBuilder.php:283
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder
Definition: PackageArtifactBuilder.php:49
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\extractPackageMapFromComposer
‪extractPackageMapFromComposer()
Definition: PackageArtifactBuilder.php:155
‪TYPO3\CMS\Core\Package\Package\getPackageKey
‪getPackageKey()
Definition: Package.php:176
‪TYPO3\CMS\Core\Composer
Definition: CliEntryPoint.php:18