TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Composer;
use Composer\Script\Event;
use Composer\Util\Filesystem as FilesystemUtility;
use Symfony\Component\Filesystem\Filesystem;
use TYPO3\CMS\Composer\Plugin\Config;
use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
class CliEntryPoint implements InstallerScript
{
/**
* Absolute path to entry script source
*
* @var string
*/
private $source;
/**
* The target file relative to the web directory
*
* @var string
*/
private $target;
public function __construct(string $source, string $target)
{
$this->source = $source;
$this->target = $target;
}
public function run(Event $event): bool
{
$composer = $event->getComposer();
$filesystemUtility = new FilesystemUtility();
$filesystem = new Filesystem();
$pluginConfig = Config::load($composer);
$entryPointContent = file_get_contents($this->source);
if ($entryPointContent === false) {
return false;
}
$targetFile = $pluginConfig->get('root-dir') . '/' . $this->target;
$autoloadFile = $composer->getConfig()->get('vendor-dir') . '/autoload.php';
$entryPointContent = preg_replace(
'/__DIR__ . \'[^\']*\'/',
$filesystemUtility->findShortestPathCode($targetFile, $autoloadFile),
$entryPointContent
);
$filesystemUtility->ensureDirectoryExists(dirname($targetFile));
$filesystem->dumpFile($targetFile, $entryPointContent);
$filesystem->chmod($targetFile, 0755);
return $filesystem->exists($targetFile);
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Composer;
class CommandExecutionFailedException extends \Exception
{
public function __construct(
public array $typo3Command,
public string $errorOutput = '',
int $code = 1765277390,
) {
$message = sprintf('Failed to run command %s', implode(' ', $this->typo3Command));
$message .= chr(10) . $this->errorOutput;
parent::__construct($message, $code);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Composer;
use Composer\Pcre\Preg;
use Composer\Script\Event;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\PhpExecutableFinder;
use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
final readonly class ConsoleCommand implements InstallerScript
{
public function __construct(
private array $command,
private string $message = '',
) {}
public function run(Event $event): bool
{
$io = $event->getIO();
if ($this->message) {
$io->writeError(sprintf('<info>%s</info>', $this->message));
}
try {
$this->executeProcess($event);
} catch (CommandExecutionFailedException $e) {
$io->writeError(sprintf('<error>%s</error>', $e->getMessage()));
return false;
}
return true;
}
private function executeProcess(Event $event): void
{
$io = $event->getIO();
$typo3Command = $this->getTypo3Command($event);
array_unshift($typo3Command, ...$this->getPhpExecCommand());
$process = new ProcessExecutor($io);
$exitCode = $process->execute($typo3Command, $commandOutput);
if ($exitCode !== 0) {
$errorOutput = trim($commandOutput);
if ($process->getErrorOutput() !== '') {
$errorOutput .= chr(10) . $process->getErrorOutput();
}
throw new CommandExecutionFailedException(
$this->command,
$errorOutput,
1765283208,
);
}
$io->writeError($commandOutput, false);
}
private function getTypo3Command(Event $event): array
{
$composer = $event->getComposer();
$binDir = $composer->getConfig()->get('bin-dir');
$finder = new ExecutableFinder();
$pathToTypo3Binary = $finder->find('typo3', null, [$binDir]);
if ($pathToTypo3Binary === null) {
throw new \RuntimeException('Could not determine path to typo3 binary', 1765273845);
}
if (Platform::isWindows()) {
$pathToTypo3BinaryWithoutExt = Preg::replace('{\.(exe|bat|cmd|com)$}i', '', $pathToTypo3Binary);
// prefer non-extension file if it exists when executing with PHP
if (file_exists($pathToTypo3BinaryWithoutExt)) {
$pathToTypo3Binary = $pathToTypo3BinaryWithoutExt;
}
unset($pathToTypo3BinaryWithoutExt);
}
$typo3Command = $this->command;
array_unshift($typo3Command, $pathToTypo3Binary);
return $typo3Command;
}
private function getPhpExecCommand(): array
{
$finder = new PhpExecutableFinder();
$phpPath = $finder->find(false);
if (!$phpPath) {
throw new \RuntimeException('Failed to locate PHP binary to execute ' . $phpPath, 1765274260);
}
$phpArgs = $finder->findArguments();
array_unshift($phpArgs, $phpPath);
$phpArgs[] = '-d';
$phpArgs[] = 'allow_url_fopen=' . ini_get('allow_url_fopen');
$phpArgs[] = '-d';
$phpArgs[] = 'disable_functions=' . ini_get('disable_functions');
$phpArgs[] = '-d';
$phpArgs[] = 'memory_limit=' . ini_get('memory_limit');
return $phpArgs;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Composer;
use Composer\Script\Event;
use TYPO3\CMS\Composer\Plugin\Config;
use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
use TYPO3\CMS\Core\Utility\ArrayUtility;
final class FrameworkPackageWriter implements InstallerScript
{
private const string CORE_RESOURCE_PATH = '/typo3/sysext/core/Resources/Private/Php/framework-packages.php';
public function run(Event $event): bool
{
$config = Config::load($event->getComposer(), $event->getIO());
$basePath = $config->get('base-dir');
$frameworkPackageNames = $this->getFrameworkPackageNames($config);
$io = $event->getIO();
$io->writeError('TYPO3: Dumping framework package names', true, $io::VERBOSE);
file_put_contents(
$basePath . self::CORE_RESOURCE_PATH,
'<?php'
. chr(10)
. chr(10)
. 'return '
. ArrayUtility::arrayExport($frameworkPackageNames)
. ';'
. chr(10)
);
return true;
}
/**
* @return string[]
*/
private function getFrameworkPackageNames(Config $config): array
{
$typo3Json = file_get_contents($config->get('base-dir') . '/composer.json');
if ($typo3Json === false) {
throw new \RuntimeException('The main TYPO3 composer.json file was not found.', 1774091461);
}
return array_keys(
array_filter(
json_decode($typo3Json, true, 512, JSON_THROW_ON_ERROR)['replace'] ?? [],
static fn($value) => $value === 'self.version'
)
);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Composer;
use Composer\Script\Event;
use TYPO3\CMS\Composer\Plugin\Core\InstallerScripts\EntryPoint;
use TYPO3\CMS\Composer\Plugin\Core\InstallerScriptsRegistration;
use TYPO3\CMS\Composer\Plugin\Core\ScriptDispatcher;
/**
* Hook into Composer build to generate TYPO3 cli tool entry script
* @internal only used for TYPO3 internally for setting up the installation.
*/
readonly class InstallerScripts implements InstallerScriptsRegistration
{
public static function register(Event $event, ScriptDispatcher $scriptDispatcher)
{
$scriptDispatcher->addInstallerScript(
new EntryPoint(
dirname(__DIR__, 2) . '/Resources/Private/Php/index.php',
'index.php'
)
);
if ($event->getComposer()->getPackage()->getName() === 'typo3/cms') {
// We only need to provide the binary in monorepo classic mode (regular Composer mode receives it via typo3/cms-cli)
$source = dirname(__DIR__, 2) . '/Resources/Private/Php/cli.php';
$target = 'typo3/sysext/core/bin/typo3';
$scriptDispatcher->addInstallerScript(new CliEntryPoint($source, $target));
$scriptDispatcher->addInstallerScript(new FrameworkPackageWriter());
} else {
// Provide package artifact in regular composer mode (not needed for monorepo classic mode)
$scriptDispatcher->addInstallerScript(
new PackageArtifactBuilder()
);
if (!getenv('TYPO3_SKIP_ASSET_PUBLISH')) {
$command = ['asset:publish'];
if ($event->getIO()->isVerbose()) {
$command[] = '-v';
}
$scriptDispatcher->addInstallerScript(
new ConsoleCommand(
$command
)
);
}
}
}
}
+214
View File
@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Composer;
use Composer\Package\PackageInterface;
use Composer\Script\Event;
use Composer\Util\Filesystem;
use TYPO3\CMS\Composer\Plugin\Config;
use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
use TYPO3\CMS\Composer\Plugin\Util\ExtensionKeyResolver;
use TYPO3\CMS\Core\Package\Cache\ComposerPackageArtifact;
use TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException;
use TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException;
use TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException;
use TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException;
use TYPO3\CMS\Core\Package\Package;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Package\VirtualAppPackage;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* The builder is a subclass of PackageManager as it shares much of its functionality.
* It evaluates the installed Composer packages for applicable TYPO3 extensions.
* All Composer packages will be discovered, that have an extra.typo3/cms definition in their composer.json.
*
* @template packageMap of array<int, array{PackageInterface, string, non-empty-string}>
* @template IOMessage of array{severity: 'title'|'info'|'warning', verbosity: int, message: string}
*
* @internal This class is an implementation detail and does not represent public API
*/
class PackageArtifactBuilder extends PackageManager implements InstallerScript
{
/**
* @var Event $event
*/
private $event;
/**
* @var Config $config
*/
private $config;
/**
* @var Filesystem $fileSystem
*/
private $fileSystem;
private array $installedTypo3Extensions = [];
public function __construct()
{
// Disable path determination with Environment class, which is not initialized here
parent::__construct(new DependencyOrderingService(), '', '');
}
public function isComposerDependency(string $packageName): bool
{
return !in_array($packageName, $this->installedTypo3Extensions, true);
}
/**
* Entry method called in Composer post-dump-autoload hook
*
* @throws InvalidPackageKeyException
* @throws InvalidPackageManifestException
* @throws InvalidPackagePathException
* @throws InvalidPackageStateException
*/
public function run(Event $event): bool
{
$io = $event->getIO();
$this->event = $event;
$this->config = Config::load($this->event->getComposer(), $io);
$this->fileSystem = new Filesystem();
$composer = $this->event->getComposer();
$basePath = $this->config->get('base-dir');
$this->packagesBasePath = $basePath . '/';
foreach ($this->extractPackageMapFromComposer() as [$composerPackage, $path, $extensionKey]) {
$packagePath = PathUtility::sanitizeTrailingSeparator($path);
$package = new Package($this, $extensionKey, $packagePath, true);
$package->getPackageMetaData()->setVersion($composerPackage->getPrettyVersion());
$this->registerPackage($package);
}
$this->sortPackagesAndConfiguration();
$appPackage = new VirtualAppPackage(
$this,
$this->packagesBasePath,
rtrim($this->config->get('web-dir', $this->config::RELATIVE_PATHS), '/') . '/',
);
$this->registerPackage($appPackage);
$this->packageStatesConfiguration['packages'][$appPackage->getPackageKey()] = [];
$cacheIdentifier = md5(serialize($composer->getLocker()->getLockData()) . $this->event->isDevMode());
$this->setPackageCache(new ComposerPackageArtifact($composer->getConfig()->get('vendor-dir') . '/typo3', $this->fileSystem, $cacheIdentifier));
$this->validateResources();
$this->saveToPackageCache();
return true;
}
/**
* Make package paths of all packages relative
* so that it does not matter in which environment
* the "composer install" operation is performed
*/
protected function saveToPackageCache(): void
{
$basePath = $this->config->get('base-dir');
foreach ($this->packages as $package) {
if ($package instanceof Package) {
$package->makePathRelative($this->fileSystem, $basePath);
}
}
parent::saveToPackageCache();
}
/**
* Sorts all TYPO3 extension packages by dependency defined in composer.json file
*/
private function sortPackagesAndConfiguration(): void
{
$packagesWithDependencies = $this->resolvePackageDependencies($this->packages);
// Sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
ksort($packagesWithDependencies);
$sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
$this->packageStatesConfiguration = [];
$sortedPackages = [];
foreach ($sortedPackageKeys as $packageKey) {
$sortedPackages[$packageKey] = $this->packages[$packageKey];
// The artifact does not need path information, so it is kept empty
// The keys must be present, though because the PackageManager implies than a
// package is active by this configuration array
$this->packageStatesConfiguration['packages'][$packageKey] = [];
}
$this->packages = $sortedPackages;
$this->packageStatesConfiguration['version'] = 5;
}
/**
* Fetch a map of all installed packages and filter them, when they apply
* for TYPO3.
*
* @return packageMap
*/
private function extractPackageMapFromComposer(): array
{
$composer = $this->event->getComposer();
$rootPackage = $composer->getPackage();
$autoLoadGenerator = $composer->getAutoloadGenerator();
$localRepo = $composer->getRepositoryManager()->getLocalRepository();
return array_map(
function (array $packageAndPath) use ($rootPackage): array {
[$composerPackage, $packagePath] = $packageAndPath;
$packageName = $composerPackage->getName();
$packagePath = GeneralUtility::fixWindowsFilePath($packagePath);
try {
$extensionKey = ExtensionKeyResolver::resolve($composerPackage);
} catch (\Throwable $e) {
if (str_starts_with($composerPackage->getType(), 'typo3-cms-')) {
// This means we have a package of type extension, and it does not have the extension key set
// This only happens since version > 4.0 of the installer and must be propagated to become user facing
throw $e;
}
// In case we can not otherwise determine the extension key, we take the composer name
$extensionKey = $packageName;
}
if (isset($this->installedTypo3Extensions[$extensionKey])) {
throw new \UnexpectedValueException(
sprintf(
'Package with the name "%s" registered extension key "%s", but this key was already set by package with the name "%s"',
$packageName,
$extensionKey,
$this->installedTypo3Extensions[$extensionKey]
),
1638880941
);
}
$this->installedTypo3Extensions[$extensionKey] = $packageName;
$this->composerNameToPackageKeyMap[$packageName] = $extensionKey;
if ($composerPackage === $rootPackage) {
// The root package's path is the Composer base dir
$packagePath = $this->config->get('base-dir');
}
// Add extension key to the package map for later reference
return [$composerPackage, $packagePath, $extensionKey];
},
array_filter(
$autoLoadGenerator->buildPackageMap($composer->getInstallationManager(), $rootPackage, $localRepo->getCanonicalPackages()),
static function (array $packageAndPath): bool {
/** @var PackageInterface $composerPackage */
[$composerPackage] = $packageAndPath;
return isset($composerPackage->getExtra()['typo3/cms']);
}
)
);
}
}