‪TYPO3CMS  11.5
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 Symfony\Component\Finder\Finder;
26 use TYPO3\CMS\Composer\Plugin\Config;
27 use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
28 use TYPO3\CMS\Composer\Plugin\Util\ExtensionKeyResolver;
29 use TYPO3\CMS\Core\Package\Cache\ComposerPackageArtifact;
35 use TYPO3\CMS\Core\Package\PackageManager;
39 
49 class ‪PackageArtifactBuilder extends PackageManager implements InstallerScript
50 {
51  private const ‪LEGACY_EXTENSION_INSTALL_PATH = '/typo3conf/ext';
52 
56  private ‪$event;
57 
61  private ‪$config;
62 
66  private ‪$fileSystem;
67 
73 
74  public function ‪__construct()
75  {
76  // Disable path determination with Environment class, which is not initialized here
77  parent::__construct(new ‪DependencyOrderingService(), '', '');
78  }
79 
80  protected function ‪isComposerDependency(string $packageName): bool
81  {
82  return PlatformRepository::isPlatformPackage($packageName) || ($this->availableComposerPackageKeys[$packageName] ?? false);
83  }
84 
95  public function ‪run(Event ‪$event): bool
96  {
97  $this->event = ‪$event;
98  $this->config = Config::load($this->event->getComposer(), $this->event->getIO());
99  $this->fileSystem = new Filesystem();
100  $composer = $this->event->getComposer();
101  $basePath = $this->config->get('base-dir');
102  $this->packagesBasePath = $basePath . '/';
103  foreach ($this->‪extractPackageMapFromComposer() as [$composerPackage, $path, $extensionKey]) {
104  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($path);
105  $package = new Package($this, $extensionKey, $packagePath, true);
106  $this->‪setTitleFromExtEmConf($package);
107  $package->makePathRelative($this->fileSystem, $basePath);
108  $package->getPackageMetaData()->setVersion($composerPackage->getPrettyVersion());
109  $this->registerPackage($package);
110  }
112  $cacheIdentifier = md5(serialize($composer->getLocker()->getLockData()) . $this->event->isDevMode());
113  $this->setPackageCache(new ComposerPackageArtifact($composer->getConfig()->get('vendor-dir') . '/typo3', $this->fileSystem, $cacheIdentifier));
114  $this->saveToPackageCache();
115 
116  return true;
117  }
118 
125  private function ‪setTitleFromExtEmConf(Package $package): void
126  {
127  $emConfPath = $package->getPackagePath() . '/ext_emconf.php';
128  if (file_exists($emConfPath)) {
129  $_EXTKEY = $package->getPackageKey();
130  ‪$EM_CONF = null;
131  include $emConfPath;
132  if (!empty(‪$EM_CONF[$_EXTKEY]['title'])) {
133  $package->getPackageMetaData()->setTitle(‪$EM_CONF[$_EXTKEY]['title']);
134  }
135  }
136  }
137 
141  private function ‪sortPackagesAndConfiguration(): void
142  {
143  $packagesWithDependencies = $this->resolvePackageDependencies($this->packages);
144  // Sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
145  ksort($packagesWithDependencies);
146  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
147  $this->packageStatesConfiguration = [];
148  $sortedPackages = [];
149  foreach ($sortedPackageKeys as $packageKey) {
150  $sortedPackages[$packageKey] = $this->packages[$packageKey];
151  // The artifact does not need path information, so it is kept empty
152  // The keys must be present, though because the PackageManager implies than a
153  // package is active by this configuration array
154  $this->packageStatesConfiguration['packages'][$packageKey] = [];
155  }
156  $this->packages = $sortedPackages;
157  $this->packageStatesConfiguration['version'] = 5;
158  }
159 
166  private function ‪extractPackageMapFromComposer(): array
167  {
168  $composer = $this->event->getComposer();
169  $rootPackage = $composer->getPackage();
170  $autoLoadGenerator = $composer->getAutoloadGenerator();
171  $localRepo = $composer->getRepositoryManager()->getLocalRepository();
172  $usedExtensionKeys = [];
173 
174  $installedTypo3Packages = array_map(
175  function (array $packageAndPath) use ($rootPackage, &$usedExtensionKeys) {
176  [$composerPackage, $packagePath] = $packageAndPath;
177  $packageName = $composerPackage->getName();
178  $packagePath = GeneralUtility::fixWindowsFilePath($packagePath);
179  try {
180  $extensionKey = ExtensionKeyResolver::resolve($composerPackage);
181  } catch (\Throwable $e) {
182  if (strpos($composerPackage->getType(), 'typo3-cms-') === 0) {
183  // This means we have a package of type extension, and it does not have the extension key set
184  // This only happens since version > 4.0 of the installer and must be propagated to become user facing
185  throw $e;
186  }
187  // In case we can not otherwise determine the extension key, we take the composer name
188  $extensionKey = $packageName;
189  }
190  if (isset($usedExtensionKeys[$extensionKey])) {
191  throw new \UnexpectedValueException(
192  sprintf(
193  'Package with the name "%s" registered extension key "%s", but this key was already set by package with the name "%s"',
194  $packageName,
195  $extensionKey,
196  $usedExtensionKeys[$extensionKey]
197  ),
198  1638880941
199  );
200  }
201  $usedExtensionKeys[$extensionKey] = $packageName;
202  unset($this->availableComposerPackageKeys[$packageName]);
203  $this->composerNameToPackageKeyMap[$packageName] = $extensionKey;
204  if ($composerPackage === $rootPackage) {
205  return $this->‪handleRootPackage($rootPackage, $extensionKey);
206  }
207  // Add extension key to the package map for later reference
208  return [$composerPackage, $packagePath, $extensionKey];
209  },
210  array_filter(
211  $autoLoadGenerator->buildPackageMap($composer->getInstallationManager(), $rootPackage, $localRepo->getCanonicalPackages()),
212  ‪function (array $packageAndPath) {
214  [$composerPackage] = $packageAndPath;
215  // Filter all Composer packages without typo3/cms definition, but keep all
216  // package names, to be able to ignore Composer only dependencies when ordering the packages
217  $this->availableComposerPackageKeys[$composerPackage->getName()] = true;
218  foreach ($composerPackage->getReplaces() as $link) {
219  $this->availableComposerPackageKeys[$link->getTarget()] = true;
220  }
221  return isset($composerPackage->getExtra()['typo3/cms']);
222  }
223  )
224  );
225 
226  $installedTypo3Packages = $this->‪amendWithLocallyAvailableExtensions($installedTypo3Packages);
227  $this->‪publishResources($installedTypo3Packages);
228 
229  return $installedTypo3Packages;
230  }
231 
251  private function ‪handleRootPackage(PackageInterface $rootPackage, string $extensionKey): array
252  {
253  $baseDir = $this->config->get('base-dir');
254  $composer = $this->event->getComposer();
255  if ($rootPackage->getType() !== 'typo3-cms-extension'
256  || !file_exists($baseDir . '/Resources/Public/')
257  ) {
258  return [$rootPackage, $baseDir, $extensionKey];
259  }
260  $typo3ExtensionInstallPath = $composer->getInstallationManager()->getInstaller('typo3-cms-extension')->getInstallPath($rootPackage);
261  if (strpos($typo3ExtensionInstallPath, self::LEGACY_EXTENSION_INSTALL_PATH) === false) {
262  return [$rootPackage, $baseDir, $extensionKey];
263  }
264  if (!file_exists($typo3ExtensionInstallPath) && !$this->fileSystem->isSymlinkedDirectory($typo3ExtensionInstallPath)) {
265  $this->fileSystem->ensureDirectoryExists(dirname($typo3ExtensionInstallPath));
266  $this->fileSystem->relativeSymlink($baseDir, $typo3ExtensionInstallPath);
267  }
268  if (realpath($baseDir) !== realpath($typo3ExtensionInstallPath)) {
269  $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.');
270  }
271 
272  return [$rootPackage, $typo3ExtensionInstallPath, $extensionKey];
273  }
274 
275  private function ‪publishResources(array $installedTypo3Packages): void
276  {
277  $baseDir = $this->config->get('base-dir');
278  foreach ($installedTypo3Packages as [$composerPackage, $path, $extensionKey]) {
279  $fileSystemResourcesPath = $path . '/Resources/Public';
280  if (strpos($path, 'ext/' . $extensionKey) !== false || !file_exists($fileSystemResourcesPath)) {
281  continue;
282  }
283  $relativePath = substr($fileSystemResourcesPath, strlen($baseDir));
284  [$relativePrefix] = explode('Resources/Public', $relativePath);
285  $publicResourcesPath = $this->fileSystem->normalizePath($this->config->get('web-dir') . '/_assets/' . md5($relativePrefix));
286  $this->fileSystem->ensureDirectoryExists(dirname($publicResourcesPath));
287  if (Platform::isWindows()) {
288  $this->‪ensureJunctionExists($fileSystemResourcesPath, $publicResourcesPath);
289  } else {
290  $this->‪ensureSymlinkExists($fileSystemResourcesPath, $publicResourcesPath);
291  }
292  }
293  }
294 
295  private function ‪ensureJunctionExists(string $target, string $junction): void
296  {
297  if (!$this->fileSystem->isJunction($junction)) {
298  // Cleanup a possible symlink that might have been installed by ourselves prior to #98434
299  // Note: Unprivileged deletion of symlinks is allowed, even if they were created by a
300  // privileged user
301  if (is_link($junction)) {
302  $this->fileSystem->unlink($junction);
303  }
304  $this->fileSystem->junction($target, $junction);
305  }
306  }
307 
308  private function ‪ensureSymlinkExists(string $target, string $link): void
309  {
310  if (!$this->fileSystem->isSymlinkedDirectory($link)) {
311  $this->fileSystem->relativeSymlink($target, $link);
312  }
313  }
314 
324  private function ‪amendWithLocallyAvailableExtensions(array $installedTypo3Packages): array
325  {
326  $installedExtensionKeys = array_map(
327  static function (array $packageAndPathAndKey) {
328  [, , $extensionKey] = $packageAndPathAndKey;
329  return $extensionKey;
330  },
331  $installedTypo3Packages
332  );
333 
334  foreach ($this->‪scanForRootExtensions($installedExtensionKeys) as [$composerPackage, $path, $extensionKey]) {
335  $this->event->getIO()->warning(sprintf('Extension "%s" not installed with Composer. This is deprecated and will not work any more with TYPO3 12.', $extensionKey));
336  $installedTypo3Packages[] = [$composerPackage, $path, $extensionKey];
337  }
338 
339  return $installedTypo3Packages;
340  }
341 
348  private function ‪scanForRootExtensions(array $installedExtensionKeys): array
349  {
350  $thirdPartyExtensionDir = $this->config->get('root-dir') . ‪self::LEGACY_EXTENSION_INSTALL_PATH;
351  if (!is_dir($thirdPartyExtensionDir) || !$this->hasSubDirectories($thirdPartyExtensionDir)) {
352  return [];
353  }
354  $rootExtensionPackages = [];
355  ‪$finder = new Finder();
356  ‪$finder
357  ->name('composer.json')
358  ->followLinks()
359  ->depth(0)
360  ->ignoreUnreadableDirs()
361  ->in($thirdPartyExtensionDir . '/*/');
362 
363  foreach (‪$finder as $splFileInfo) {
364  $foundExtensionKey = basename($splFileInfo->getPath());
365  if (in_array($foundExtensionKey, $installedExtensionKeys, true)) {
366  // Found the extension to be installed with Composer, so no need to register it again
367  continue;
368  }
369  $composerJson = json_decode($splFileInfo->getContents(), true);
370  $extPackage = new \Composer\Package\Package($composerJson['name'], '1.0.0', '1.0.0.0');
371  $extPackage->setExtra($composerJson['extra'] ?? []);
372  $extPackage->setType($composerJson['type'] ?? 'typo3-cms-extension');
373  $rootExtensionPackages[] = [$extPackage, $splFileInfo->getPath(), $foundExtensionKey];
374  }
375 
376  return $rootExtensionPackages;
377  }
378 }
‪TYPO3\CMS\Core\Package\Package\getPackageMetaData
‪MetaData getPackageMetaData()
Definition: Package.php:248
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\run
‪bool run(Event $event)
Definition: PackageArtifactBuilder.php:91
‪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\Package\Package\getPackagePath
‪string getPackagePath()
Definition: Package.php:219
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:3
‪TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException
Definition: InvalidPackagePathException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\sortPackagesAndConfiguration
‪sortPackagesAndConfiguration()
Definition: PackageArtifactBuilder.php:137
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\extractPackageMapFromComposer
‪array extractPackageMapFromComposer()
Definition: PackageArtifactBuilder.php:162
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\ensureSymlinkExists
‪ensureSymlinkExists(string $target, string $link)
Definition: PackageArtifactBuilder.php:304
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$config
‪Config $config
Definition: PackageArtifactBuilder.php:59
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$availableComposerPackageKeys
‪array $availableComposerPackageKeys
Definition: PackageArtifactBuilder.php:68
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException
Definition: InvalidPackageKeyException.php:23
‪TYPO3\CMS\Core\Package\Package\getPackageKey
‪string getPackageKey()
Definition: Package.php:189
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\__construct
‪__construct()
Definition: PackageArtifactBuilder.php:70
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\amendWithLocallyAvailableExtensions
‪array amendWithLocallyAvailableExtensions(array $installedTypo3Packages)
Definition: PackageArtifactBuilder.php:320
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\scanForRootExtensions
‪array scanForRootExtensions(array $installedExtensionKeys)
Definition: PackageArtifactBuilder.php:344
‪TYPO3\CMS\Core\Package\MetaData\setTitle
‪setTitle(?string $title)
Definition: MetaData.php:143
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\publishResources
‪publishResources(array $installedTypo3Packages)
Definition: PackageArtifactBuilder.php:271
‪TYPO3\CMS\Core\Package\Package
Definition: Package.php:28
‪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:51
‪TYPO3\CMS\Core\Utility\PathUtility\sanitizeTrailingSeparator
‪static string sanitizeTrailingSeparator($path, $separator='/')
Definition: PathUtility.php:209
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$event
‪Event $event
Definition: PackageArtifactBuilder.php:55
‪TYPO3\CMS\Core\function
‪static return function(ContainerConfigurator $container, ContainerBuilder $containerBuilder)
Definition: Services.php:13
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$fileSystem
‪Filesystem $fileSystem
Definition: PackageArtifactBuilder.php:63
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException
Definition: InvalidPackageStateException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\handleRootPackage
‪array handleRootPackage(PackageInterface $rootPackage, string $extensionKey)
Definition: PackageArtifactBuilder.php:247
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\setTitleFromExtEmConf
‪setTitleFromExtEmConf(Package $package)
Definition: PackageArtifactBuilder.php:121
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\isComposerDependency
‪isComposerDependency(string $packageName)
Definition: PackageArtifactBuilder.php:76
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\ensureJunctionExists
‪ensureJunctionExists(string $target, string $junction)
Definition: PackageArtifactBuilder.php:291
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder
Definition: PackageArtifactBuilder.php:50
‪TYPO3\CMS\Core\Composer
Definition: CliEntryPoint.php:18