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
+309
View File
@@ -0,0 +1,309 @@
<?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\Package;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerAwareInterface;
use Symfony\Component\Finder\Finder;
use TYPO3\CMS\Core\DependencyInjection\ServiceProviderInterface;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationOrigin;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationOriginType;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Site\Set\InvalidCategoryDefinitionsException;
use TYPO3\CMS\Core\Site\Set\InvalidSetException;
use TYPO3\CMS\Core\Site\Set\InvalidSetRouteEnhancersException;
use TYPO3\CMS\Core\Site\Set\InvalidSettingsDefinitionsException;
use TYPO3\CMS\Core\Site\Set\InvalidSettingsException;
use TYPO3\CMS\Core\Site\Set\SetCollector;
use TYPO3\CMS\Core\Site\Set\SetError;
use TYPO3\CMS\Core\Site\Set\YamlSetDefinitionProvider;
use TYPO3\CMS\Core\Type\Map;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
abstract class AbstractServiceProvider implements ServiceProviderInterface
{
/**
* Return the path to the package location, including trailing slash
* should return the value of: __DIR__ . '/../'
* for ServiceProviders located in the Classes/ directory
*/
abstract protected static function getPackagePath(): string;
/**
* Return the composer package name. This is the 'name' attribute in composer.json.
* Note composer.json existence for 'extensions' is still not mandatory
* in non-composer mode, the method returns empty string in this case.
*/
abstract protected static function getPackageName(): string;
abstract public function getFactories(): array;
public function getExtensions(): array
{
return [
'middlewares' => [ static::class, 'configureMiddlewares' ],
'backend.routes' => [ static::class, 'configureBackendRoutes' ],
'backend.modules' => [ static::class, 'configureBackendModules' ],
'content.security.policies' => [ static::class, 'configureContentSecurityPolicies' ],
'icons' => [ static::class, 'configureIcons' ],
'fluid.namespaces' => [ static::class, 'configureFluidNamespaces' ],
'fluid.component.collections' => [ static::class, 'configureFluidComponentCollections' ],
SetCollector::class => [ static::class, 'configureSetCollector' ],
];
}
public static function configureFluidNamespaces(ContainerInterface $container, \ArrayObject $namespaces, ?string $path = null): \ArrayObject
{
$packageConfiguration = ($path ?? static::getPackagePath()) . 'Configuration/Fluid/Namespaces.php';
if (file_exists($packageConfiguration)) {
$namespacesInPackage = self::requireFile($packageConfiguration);
if (is_array($namespacesInPackage)) {
$namespaces->exchangeArray(array_merge_recursive($namespaces->getArrayCopy(), $namespacesInPackage));
}
}
return $namespaces;
}
public static function configureFluidComponentCollections(ContainerInterface $container, \ArrayObject $componentCollections, ?string $path = null): \ArrayObject
{
$packageConfiguration = ($path ?? static::getPackagePath()) . 'Configuration/Fluid/ComponentCollections.php';
if (file_exists($packageConfiguration)) {
$componentCollectionsInPackage = self::requireFile($packageConfiguration);
if (is_array($componentCollectionsInPackage)) {
$componentCollections->exchangeArray(array_replace_recursive($componentCollections->getArrayCopy(), $componentCollectionsInPackage));
}
}
return $componentCollections;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
*/
public static function configureMiddlewares(ContainerInterface $container, \ArrayObject $middlewares, ?string $path = null): \ArrayObject
{
$packageConfiguration = ($path ?? static::getPackagePath()) . 'Configuration/RequestMiddlewares.php';
if (file_exists($packageConfiguration)) {
$middlewaresInPackage = self::requireFile($packageConfiguration);
if (is_array($middlewaresInPackage)) {
$middlewares->exchangeArray(array_replace_recursive($middlewares->getArrayCopy(), $middlewaresInPackage));
}
}
return $middlewares;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
* @param string|null $packageName supplied when invoked internally through PseudoServiceProvider
*/
public static function configureBackendRoutes(ContainerInterface $container, \ArrayObject $routes, ?string $path = null, ?string $packageName = null): \ArrayObject
{
$path = $path ?? static::getPackagePath();
$packageName = $packageName ?? static::getPackageName();
$routesFileNameForPackage = $path . 'Configuration/Backend/Routes.php';
if (file_exists($routesFileNameForPackage)) {
$definedRoutesInPackage = self::requireFile($routesFileNameForPackage);
if (is_array($definedRoutesInPackage)) {
array_walk($definedRoutesInPackage, static function (array &$options) use ($packageName, $path): void {
// Add packageName and absolutePackagePath to all routes
$options['packageName'] = $packageName;
$options['absolutePackagePath'] = $path;
});
$routes->exchangeArray(array_merge($routes->getArrayCopy(), $definedRoutesInPackage));
}
}
$routesFileNameForPackage = $path . 'Configuration/Backend/AjaxRoutes.php';
if (file_exists($routesFileNameForPackage)) {
$definedRoutesInPackage = self::requireFile($routesFileNameForPackage);
if (is_array($definedRoutesInPackage)) {
foreach ($definedRoutesInPackage as $routeIdentifier => $routeOptions) {
// prefix the route with "ajax_" as "namespace"
$routeOptions['path'] = '/ajax' . $routeOptions['path'];
$routeOptions['packageName'] = $packageName;
$routeOptions['absolutePackagePath'] = $path;
$routes['ajax_' . $routeIdentifier] = $routeOptions;
$routes['ajax_' . $routeIdentifier]['ajax'] = true;
}
}
}
return $routes;
}
/**
* @param string|null $path supplied when invoked internally through PseudoServiceProvider
* @param string|null $packageName supplied when invoked internally through PseudoServiceProvider
*/
public static function configureBackendModules(ContainerInterface $container, \ArrayObject $modules, ?string $path = null, ?string $packageName = null): \ArrayObject
{
$path = $path ?? static::getPackagePath();
$packageName = $packageName ?? static::getPackageName();
$modulesFileNameForPackage = $path . 'Configuration/Backend/Modules.php';
if (file_exists($modulesFileNameForPackage)) {
$definedModulesInPackage = self::requireFile($modulesFileNameForPackage);
if (is_array($definedModulesInPackage)) {
array_walk($definedModulesInPackage, static function (array &$module) use ($packageName, $path): void {
// Add packageName and absolutePackagePath to all modules
$module['packageName'] = $packageName;
$module['absolutePackagePath'] = $path;
});
$modules->exchangeArray(array_merge($modules->getArrayCopy(), $definedModulesInPackage));
}
}
return $modules;
}
/**
* @param Map<Scope, Map<MutationOrigin, MutationCollection>> $mutations
* @return Map<Scope, Map<MutationOrigin, MutationCollection>>
*/
public static function configureContentSecurityPolicies(ContainerInterface $container, Map $mutations, ?string $path = null, ?string $packageName = null): Map
{
$path = $path ?? static::getPackagePath();
$packageName = $packageName ?? static::getPackageName();
$fileName = $path . 'Configuration/ContentSecurityPolicies.php';
if (file_exists($fileName)) {
/** @var Map<Scope, MutationCollection> $mutationsInPackage */
$mutationsInPackage = self::requireFile($fileName);
foreach ($mutationsInPackage as $scope => $mutation) {
if (!isset($mutations[$scope])) {
$mutations[$scope] = new Map();
}
$origin = new MutationOrigin(MutationOriginType::package, $packageName);
$mutations[$scope][$origin] = $mutation;
}
}
return $mutations;
}
public static function configureIcons(ContainerInterface $container, \ArrayObject $icons, ?string $path = null): \ArrayObject
{
$path = $path ?? static::getPackagePath();
$iconsFileNameForPackage = $path . 'Configuration/Icons.php';
if (file_exists($iconsFileNameForPackage)) {
$definedIconsInPackage = self::requireFile($iconsFileNameForPackage);
if (is_array($definedIconsInPackage)) {
$icons->exchangeArray(array_merge($icons->getArrayCopy(), $definedIconsInPackage));
}
}
return $icons;
}
public static function configureSetCollector(
ContainerInterface $container,
SetCollector $setCollector,
?string $path = null,
?string $packageName = null,
): SetCollector {
$path ??= static::getPackagePath();
$packageName ??= static::getPackageName();
$extensionKey = $container->get(PackageManager::class)->getPackage($packageName)->getPackageKey();
$setPath = $path . 'Configuration/Sets';
try {
$finder = Finder::create()
->files()
->sortByName()
->depth(1)
->ignoreUnreadableDirs()
->name('config.yaml')
->in($setPath);
} catch (\InvalidArgumentException) {
// No such directory in this package
return $setCollector;
}
$setProvider = $container->get(YamlSetDefinitionProvider::class);
foreach ($finder as $fileInfo) {
$errorMap = [
InvalidSettingsDefinitionsException::class => [
'error' => SetError::invalidSettingsDefinitions,
'logLine' => 'Set {setName} invalidated {file} because of invalid settings.definitions.yaml: {reason}',
],
InvalidCategoryDefinitionsException::class => [
'error' => SetError::invalidCategoryDefinitions,
'logLine' => 'Set {setName} invalidated {file} because of invalid category in settings.definitions.yaml: {reason}',
],
InvalidSettingsException::class => [
'error' => SetError::invalidSettings,
'logLine' => 'Set {setName} invalidated {file} because of invalid settings.yaml: {reason}',
],
InvalidSetRouteEnhancersException::class => [
'error' => SetError::invalidRouteEnhancers,
'logLine' => 'Set {setName} invalidated {file} because of invalid route-enhancers.yaml: {reason}',
],
InvalidSetException::class => [
'error' => SetError::invalidSet,
'logLine' => 'Invalid set {setName} in {file}: {reason}',
],
];
try {
$virtualSetPath = 'EXT:' . $extensionKey . '/Configuration/Sets/' . basename(dirname($fileInfo->getPathname())) . '/';
$setCollector->add($setProvider->get($fileInfo, $virtualSetPath));
} catch (InvalidSettingsDefinitionsException|InvalidCategoryDefinitionsException|InvalidSettingsException|InvalidSetRouteEnhancersException|InvalidSetException $e) {
$errorDetails = $errorMap[get_class($e)];
$setCollector->addError(
$errorDetails['error'],
$e->getSetName(),
$e->getMessage(),
);
$logger = $container->get(LogManager::class)->getLogger(self::class);
$logger->error($errorDetails['logLine'], [
'file' => $fileInfo->getPathname(),
'setName' => $e->getSetName(),
'reason' => $e->getMessage(),
]);
}
}
return $setCollector;
}
/**
* Create an instance of a class. Supports auto injection of the logger.
*
* @param string $className name of the class to instantiate, must not be empty and not start with a backslash
* @param array $constructorArguments Arguments for the constructor
* @return mixed
*/
protected static function new(ContainerInterface $container, string $className, array $constructorArguments = [])
{
// Support $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'] (xclasses) and class alias maps
$instance = GeneralUtility::makeInstanceForDi($className, ...$constructorArguments);
if ($instance instanceof LoggerAwareInterface) {
$instance->setLogger($container->get(LogManager::class)->getLogger($className));
}
return $instance;
}
/**
* Require a file in a safe scoped environment avoiding local variable clashes.
*/
protected static function requireFile(string $filename): mixed
{
return require $filename;
}
}
@@ -0,0 +1,110 @@
<?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\Package\Cache;
use Composer\Util\Filesystem;
/**
* TYPO3 Package "cache" for Composer mode.
* This class is used in two contexts:
* During Composer build time the artifact is stored
* and during TYPO3 runtime the artifact is only read.
* The context is decided on object construction.
*
* @internal This class is an implementation detail and does not represent public API
*/
class ComposerPackageArtifact implements PackageCacheInterface
{
/**
* Location of the file inside the var folder
*/
private const string ARTIFACTS_FILE = '/PackageArtifact.php';
/**
* Full filesystem path to the file
*/
private string $packageArtifactsFile;
/**
* The cache entry generated from the artifact
*/
private PackageCacheEntry $cacheEntry;
/**
* Composer filesystem, provided during Composer build time
*/
private ?Filesystem $filesystem;
/**
* The cache identifier that is stored alongside the artifact
* and used as part of TYPO3 cache identifiers
*/
private ?string $cacheIdentifier;
public function __construct(string $packageArtifactsPath, ?Filesystem $filesystem = null, ?string $cacheIdentifier = null)
{
$this->packageArtifactsFile = $packageArtifactsPath . self::ARTIFACTS_FILE;
$this->filesystem = $filesystem;
$this->cacheIdentifier = $cacheIdentifier;
}
public function fetch(): PackageCacheEntry
{
if ($this->isComposerBuildContext()) {
throw new \RuntimeException('Can not load package states during generation', 1629820498);
}
if (isset($this->cacheEntry)) {
return $this->cacheEntry;
}
$packageData = @include $this->packageArtifactsFile;
if (!$packageData) {
throw new \RuntimeException('Package artifact not found. Run "composer install" to create it.', 1629819799);
}
return $this->cacheEntry = PackageCacheEntry::fromCache($packageData);
}
public function store(PackageCacheEntry $cacheEntry): void
{
if (!$this->isComposerBuildContext()) {
throw new \RuntimeException('Can not modify package states in Composer mode', 1629819858);
}
$this->filesystem->ensureDirectoryExists(dirname($this->packageArtifactsFile));
file_put_contents($this->packageArtifactsFile, '<?php' . PHP_EOL . 'return ' . PHP_EOL . $cacheEntry->withIdentifier($this->cacheIdentifier)->serialize() . ';');
}
public function invalidate(): void
{
throw new \RuntimeException('Can not modify package states in Composer mode', 1629824596);
}
public function getIdentifier(): string
{
if (!isset($this->cacheEntry)) {
$this->fetch();
}
return $this->cacheEntry->getIdentifier();
}
private function isComposerBuildContext(): bool
{
return isset($this->filesystem, $this->cacheIdentifier);
}
}
+229
View File
@@ -0,0 +1,229 @@
<?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\Package\Cache;
use TYPO3\CMS\Core\Package\Exception\PackageManagerCacheUnavailableException;
use TYPO3\CMS\Core\Package\Exception\PackageStatesUnavailableException;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Serializer\DeserializationService;
/**
* A TYPO3 Package cache entry.
* Represents a concrete state of TYPO3 packages.
* It interfaces between PackageManager and PackageCacheInterface.
*
* @internal
*
* @phpstan-import-type PackageKey from PackageManager
* @phpstan-import-type PackageName from PackageManager
* @phpstan-import-type StatesConfiguration from PackageManager
* @phpstan-type CacheArray array{
* identifier: string|null,
* packageStatesConfiguration: StatesConfiguration,
* packageAliasMap: array<PackageName, PackageKey>,
* composerNameToPackageKeyMap: array<PackageName, PackageKey>,
* packageObjects: string,
* packageClasses: list<class-string>,
* }
*/
class PackageCacheEntry
{
/**
* Package configuration. Used by the PackageManager to identify "active" packages.
* Every key in this array represents an active extension.
*
* @var StatesConfiguration
*/
private array $configuration;
/**
* Alternative names for packages mapping to the package key.
* Typically filled from replace section in composer.json
*
* @var array<PackageName, PackageKey>
*/
private array $aliasMap;
/**
* Map from composer name of a package (key in this array) to its package key (value)
*
* @var array<PackageName, PackageKey>
*/
private array $composerNameMap;
/**
* @var array<PackageKey, PackageInterface>
*/
private array $packages;
/**
* Identifier for the current state, which can optionally
* stored in the cache entry or artifact.
* Currently, only used in Composer mode, where the identifier
* is comprised from the composer.lock file and stored alongside the artifact.
*/
private ?string $identifier = null;
/**
* @param StatesConfiguration $configuration
* @param array<PackageName, PackageKey> $aliasMap
* @param array<PackageName, PackageKey> $composerNameMap
* @param array<PackageKey, PackageInterface> $packages
*/
private function __construct(
array $configuration,
array $aliasMap,
array $composerNameMap,
array $packages
) {
$this->configuration = $configuration;
$this->aliasMap = $aliasMap;
$this->composerNameMap = $composerNameMap;
$this->packages = $packages;
}
/**
* Validates whether the configuration has the correct version
*
* @throws PackageStatesUnavailableException
*/
public static function ensureValidPackageConfiguration(array $configuration): void
{
if (($configuration['version'] ?? 0) < 5) {
throw new PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
}
}
/**
* @param StatesConfiguration $packageStatesConfiguration
* @param array<PackageName, PackageKey> $packageAliasMap
* @param array<PackageName, PackageKey> $composerNameToPackageKeyMap
* @param array<PackageKey, PackageInterface> $packageObjects
*/
public static function fromPackageData(
array $packageStatesConfiguration,
array $packageAliasMap,
array $composerNameToPackageKeyMap,
array $packageObjects
): self {
self::ensureValidPackageConfiguration($packageStatesConfiguration);
return new self(
$packageStatesConfiguration,
$packageAliasMap,
$composerNameToPackageKeyMap,
$packageObjects
);
}
/**
* @param CacheArray $packageData
*/
public static function fromCache(array $packageData): self
{
try {
self::ensureValidPackageConfiguration($packageData['packageStatesConfiguration'] ?? []);
} catch (PackageStatesUnavailableException $e) {
// Invalidate the cache entry
throw new PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883341, $e);
}
// Package objects can now contain classes from userland.
// We nevertheless restrict the unserialize call here to classes,
// that have been identified during caching.
// Yes, this list of classes could be tainted, but then, why not taint,
// the complete cache file directly? A tainted list of class names isn't
// really less obvious than other PHP code in the cache file.
$cacheEntry = new self(
$packageData['packageStatesConfiguration'],
$packageData['packageAliasMap'],
$packageData['composerNameToPackageKeyMap'],
unserialize(
$packageData['packageObjects'],
// data is read from `PackageStates.php` already, if that file is tainted for
// deserialization, code execution would have been possible anyway already
['allowed_classes' => $packageData['packageClasses']],
),
);
$cacheEntry->identifier = $packageData['identifier'] ?? null;
return $cacheEntry;
}
public function serialize(): string
{
$serializedPackages = serialize($this->packages);
$deserializationService = new DeserializationService();
return var_export(
[
'identifier' => $this->identifier,
'packageStatesConfiguration' => $this->configuration,
'packageAliasMap' => $this->aliasMap,
'composerNameToPackageKeyMap' => $this->composerNameMap,
'packageObjects' => $serializedPackages,
'packageClasses' => $deserializationService->parseClassNames($serializedPackages),
],
true
);
}
public function getIdentifier(): ?string
{
return $this->identifier;
}
public function withIdentifier(string $identifier): self
{
$newEntry = clone $this;
$newEntry->identifier = $identifier;
return $newEntry;
}
/**
* @return StatesConfiguration
*/
public function getConfiguration(): array
{
return $this->configuration;
}
/**
* @return array<PackageName, PackageKey>
*/
public function getAliasMap(): array
{
return $this->aliasMap;
}
/**
* @return array<PackageName, PackageKey>
*/
public function getComposerNameMap(): array
{
return $this->composerNameMap;
}
/**
* @return array<PackageKey, PackageInterface>
*/
public function getPackages(): array
{
return $this->packages;
}
}
@@ -0,0 +1,56 @@
<?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\Package\Cache;
use TYPO3\CMS\Core\Package\Exception\PackageManagerCacheUnavailableException;
/**
* Interface for TYPO3 Package cache.
*
* This is an implementation detail to abstract the way the PackageManager
* stores and retrieves package information of installed TYPO3 packages (extensions).
*
* In non Composer mode, the implementation remains to be around the availability
* of a PackageStates.php file, which is used to generate a cache entry of Package objects.
* in Composer mode, the package information is put in a persistent artifact file.
*
* @internal
*/
interface PackageCacheInterface
{
/**
* Fetch the (package states) entry from a persistent or transient location
*/
public function fetch(): PackageCacheEntry;
/**
* Store the entry
*/
public function store(PackageCacheEntry $cacheEntry): void;
/**
* Invalidate the current entry (only applicable in non Composer mode)
*/
public function invalidate(): void;
/**
* Identifier that identifies the current state (typically a hash)
* @throws PackageManagerCacheUnavailableException
*/
public function getIdentifier(): string;
}
@@ -0,0 +1,62 @@
<?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\Package\Cache;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Package\PackageManager;
/**
* Represents a cache identifier to be used for caches that depend on
* the list of packages bundled with TYPO3.
* @internal
*/
#[AsAlias('package-dependent-cache-identifier', public: true)]
class PackageDependentCacheIdentifier
{
private string $baseIdentifier;
private string $prefix = '';
private string $additionalIdentifier = '';
public function __construct(PackageManager $packageManager)
{
$this->baseIdentifier = (new Typo3Version())->getVersion() . Environment::getProjectPath() . ($packageManager->getCacheIdentifier() ?? '');
}
public function toString(): string
{
return $this->prefix . hash('xxh3', $this->baseIdentifier . $this->additionalIdentifier);
}
public function withPrefix(string $prefix): self
{
$newIdentifier = clone $this;
$newIdentifier->prefix = $prefix . '_';
return $newIdentifier;
}
public function withAdditionalHashedIdentifier(string $additionalIdentifier): self
{
$newIdentifier = clone $this;
$newIdentifier->additionalIdentifier = $additionalIdentifier;
return $newIdentifier;
}
}
@@ -0,0 +1,93 @@
<?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\Package\Cache;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Package\Exception\PackageManagerCacheUnavailableException;
/**
* TYPO3 Package cache for package states file.
* This replicates previous behaviour around the availability
* of a PackageStates.php file and has been extracted from PackageManager
*
* @internal
*/
class PackageStatesPackageCache implements PackageCacheInterface
{
private const string CACHE_IDENTIFIER_PREFIX = 'PackageManager_';
private ?string $cacheIdentifier;
private string $packageStatesFile;
private FrontendInterface $coreCache;
public function __construct(string $packageStatesFile, FrontendInterface $coreCache)
{
$this->packageStatesFile = $packageStatesFile;
$this->coreCache = $coreCache;
}
public function fetch(): PackageCacheEntry
{
$packageData = $this->coreCache->require(self::CACHE_IDENTIFIER_PREFIX . $this->getIdentifier());
if ($packageData === false || !is_array($packageData)) {
throw new PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1714031925);
}
return PackageCacheEntry::fromCache($packageData ?: []);
}
public function store(PackageCacheEntry $cacheEntry): void
{
$cacheIdentifier = $this->getIdentifier();
$this->coreCache->set(
self::CACHE_IDENTIFIER_PREFIX . $cacheIdentifier,
'return ' . PHP_EOL . $cacheEntry->withIdentifier($cacheIdentifier)->serialize() . ';'
);
}
public function invalidate(): void
{
if (!isset($this->cacheIdentifier)) {
return;
}
$this->coreCache->remove($this->cacheIdentifier);
$this->cacheIdentifier = null;
clearstatcache();
}
/**
* Combines mtime and filesize to detect PackageStates.php changes.
* mtime alone has 1-second resolution: a write within the same second
* produces an identical identifier, causing PackageActivationService
* to load a stale DI container missing the just-activated extension.
*
* @throws PackageManagerCacheUnavailableException
*/
public function getIdentifier(): string
{
if (!isset($this->cacheIdentifier)) {
$stat = @stat($this->packageStatesFile);
if ($stat === false) {
throw new PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1629817141);
}
$this->cacheIdentifier = md5((string)(new Typo3Version()) . $this->packageStatesFile . $stat['mtime'] . $stat['size']);
}
return $this->cacheIdentifier;
}
}
@@ -0,0 +1,45 @@
<?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\Package\Event;
/**
* Event that is triggered after a package has been activated
*/
final readonly class AfterPackageActivationEvent
{
public function __construct(
private string $packageKey,
private string $type,
private ?object $emitter = null
) {}
public function getPackageKey(): string
{
return $this->packageKey;
}
public function getType(): string
{
return $this->type;
}
public function getEmitter(): ?object
{
return $this->emitter;
}
}
@@ -0,0 +1,45 @@
<?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\Package\Event;
/**
* Event that is triggered after a package has been de-activated
*/
final readonly class AfterPackageDeactivationEvent
{
public function __construct(
private string $packageKey,
private string $type,
private ?object $emitter = null
) {}
public function getPackageKey(): string
{
return $this->packageKey;
}
public function getType(): string
{
return $this->type;
}
public function getEmitter(): ?object
{
return $this->emitter;
}
}
@@ -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\Package\Event;
/**
* Event that is triggered before a number of packages should become active
*/
final readonly class BeforePackageActivationEvent
{
public function __construct(private array $packageKeys) {}
public function getPackageKeys(): array
{
return $this->packageKeys;
}
}
@@ -0,0 +1,97 @@
<?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\Package\Event;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Package\Exception\InvalidPackageInitializationResultIdentifierException;
use TYPO3\CMS\Core\Package\PackageInitializationResult;
use TYPO3\CMS\Core\Package\PackageInterface;
/**
* Event that is triggered after a package has been activated (or required in composer
* mode), allowing listeners to execute initialization tasks, such as importing static data.
*/
final class PackageInitializationEvent
{
/**
* @param PackageInitializationResult[] $storage
*/
public function __construct(
private readonly string $extensionKey,
private readonly PackageInterface $package,
private readonly bool $packageActivated = false,
private readonly ?ContainerInterface $container = null,
private readonly ?object $emitter = null,
private array $storage = [],
) {}
public function getExtensionKey(): string
{
return $this->extensionKey;
}
public function getPackage(): PackageInterface
{
return $this->package;
}
public function isPackageActivated(): bool
{
return $this->packageActivated;
}
/**
* @todo deprecate properly with TYPO3 v15
*/
public function getContainer(): ?ContainerInterface
{
return $this->container;
}
/**
* @todo deprecate properly with TYPO3 v15
*/
public function getEmitter(): ?object
{
return $this->emitter;
}
public function hasStorageEntry(string $identifier): bool
{
return isset($this->storage[$identifier]);
}
public function getStorageEntry(string $identifier): PackageInitializationResult
{
if (!$this->hasStorageEntry($identifier)) {
throw new InvalidPackageInitializationResultIdentifierException('No package initialization result entry exists for ' . $identifier, 1706203511);
}
return $this->storage[$identifier];
}
public function addStorageEntry(string $identifier, mixed $data): void
{
$this->storage[$identifier] = new PackageInitializationResult($identifier, $data);
}
public function removeStorageEntry(string $identifier): void
{
unset($this->storage[$identifier]);
}
}
@@ -0,0 +1,23 @@
<?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\Package\Event;
/**
* Marker event to ensure that Core is re-triggering the package ordering and package listings
*/
final class PackagesMayHaveChangedEvent {}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* 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\Package;
/**
* A package exception
*/
class Exception extends \TYPO3\CMS\Core\Exception {}
@@ -0,0 +1,25 @@
<?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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* Thrown in case a package tries to import data while necessary component (EXT:impexp) is not installed
*/
class ImportRequirementsException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* Thrown in case an invalid package initialization result entry is requested
*/
class InvalidPackageInitializationResultIdentifierException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* An "Invalid Package Key" exception
*/
class InvalidPackageKeyException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* An "Invalid Package Manifest" exception
*/
class InvalidPackageManifestException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* "Invalid Package Path" Exception
*/
class InvalidPackagePathException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* An "Invalid Package State" exception
*/
class InvalidPackageStateException extends Exception {}
@@ -0,0 +1,29 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
class PackageAssetsPublishingFailedException extends Exception
{
public function __construct(
public readonly string $publishingStrategy,
int $code = 0,
?\Throwable $previous = null,
) {
parent::__construct(sprintf('Asset publishing by "%s" failed', $publishingStrategy), $code, $previous);
}
}
@@ -0,0 +1,29 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* A package cache unavailable exception
*/
class PackageManagerCacheUnavailableException extends Exception
{
// @TODO remove this comment once it is committed
// This comment is here to prevent false positive git rename detection
// It has to have a certain length to get around the 90% similarity limit.
// It can be removed at any time and all text is just here for that case.
}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* "Package states file not writable" Exception
*/
class PackageStatesFileNotWritableException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* A package unavailable exception
*/
class PackageStatesUnavailableException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* A "Protected Package Key" exception
*/
class ProtectedPackageKeyException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* "Unknown Package" Exception
*/
class UnknownPackageException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Package\Exception;
use TYPO3\CMS\Core\Package\Exception;
/**
* "Unknown Package Path" Exception
*/
class UnknownPackagePathException extends Exception {}
@@ -0,0 +1,91 @@
<?php
/*
* 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\Package;
use TYPO3\CMS\Core\Package\Exception\PackageStatesUnavailableException;
/**
* This is an intermediate package manager that loads just
* the required extensions for the install in case the package
* states are unavailable.
*/
class FailsafePackageManager extends PackageManager
{
/**
* @var bool TRUE if package manager is in failsafe mode
*/
protected $inFailsafeMode = false;
/**
* Loads the states of available packages from the PackageStates.php file.
* The result is stored in $this->packageStatesConfiguration.
*/
protected function loadPackageStates()
{
try {
parent::loadPackageStates();
} catch (PackageStatesUnavailableException $exception) {
$this->inFailsafeMode = true;
$this->scanAvailablePackages();
}
}
/**
* Never try to access the cache in failsafe mode
*/
protected function saveToPackageCache(): void
{
// Do not save cache if in rescue mode
if (!$this->inFailsafeMode) {
parent::saveToPackageCache();
}
}
/**
* Save states
*/
protected function savePackageStates()
{
// Do not save if in rescue mode
if (!$this->inFailsafeMode) {
parent::savePackageStates();
}
}
/**
* To enable writing of the package states file the package states
* migration needs to override eventual failsafe blocks.
*/
public function forceSortAndSavePackageStates()
{
$this->sortActivePackagesByDependencies();
parent::savePackageStates();
}
/**
* This method is a workaround for SetupService to make sure that this PackageManager
* can be used to mutate PackageStates to apply changes for distribution package
* activation.
*
* @todo Consider creating a non-failsafe PackageManager in BootService, to avoid
* the need to disable failsafe mode, where it should not be enabled
* @internal
*/
public function disableFailsafeMode(): void
{
$this->inFailsafeMode = false;
}
}
@@ -0,0 +1,90 @@
<?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\Package\Initialization;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Package\Exception\ImportRequirementsException;
use TYPO3\CMS\Impexp\Initialization\ImportContentOnPackageInitialization;
use TYPO3\CMS\Impexp\Initialization\ImportSiteConfigurationsOnPackageInitialization;
/**
* Listener to check import requirements in case an extension contains data to be imported
*/
final readonly class CheckForImportRequirements
{
public function __construct(
private ListenerProvider $listenerProvider,
private LoggerInterface $logger,
) {}
#[AsEventListener]
public function __invoke(PackageInitializationEvent $event): void
{
$packagePath = $event->getPackage()->getPackagePath();
$importFiles = [];
foreach (['t3d', 'xml'] as $importFileExtension) {
if (file_exists(($importFile = $packagePath . 'Initialisation/data.' . $importFileExtension))) {
$importFiles[] = $importFile;
}
}
$siteInitialisationDirectoryExists = is_dir($packagePath . 'Initialisation/Site');
if ($importFiles === [] && !$siteInitialisationDirectoryExists) {
return;
}
foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
if ($listener instanceof \Closure || !isset($listener[0])) {
continue;
}
if (($importFiles !== [] && $listener[0] instanceof ImportContentOnPackageInitialization)
|| ($siteInitialisationDirectoryExists && $listener[0] instanceof ImportSiteConfigurationsOnPackageInitialization)
) {
return;
}
}
// Add a exception which might be thrown by other listeners
$missingImportComponentException = new ImportRequirementsException(
$event->getExtensionKey() . ' contains data to be imported, but the required component is not installed. Make sure to define corresponding requirements.',
1706287389
);
$this->logger->warning(
$missingImportComponentException->getMessage(),
[
'exception' => $missingImportComponentException,
'extensionKey' => $event->getExtensionKey(),
'packageKey' => $event->getPackage()->getPackageKey(),
'importFiles' => $importFiles,
'siteInitialisationDirectoryExists' => $siteInitialisationDirectoryExists,
]
);
$event->addStorageEntry(
__CLASS__,
[
'exception' => $missingImportComponentException,
'importFiles' => $importFiles,
'siteInitialisationDirectoryExists' => $siteInitialisationDirectoryExists,
]
);
}
}
@@ -0,0 +1,52 @@
<?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\Package\Initialization;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Listener to import extension data after package activation
*/
final readonly class ImportExtensionDataOnPackageInitialization
{
public function __construct(
private Registry $registry,
) {}
#[AsEventListener]
public function __invoke(PackageInitializationEvent $event): void
{
$package = $event->getPackage();
$extensionKey = $event->getExtensionKey();
$importFolder = $package->getPackagePath() . 'Initialisation/Files';
$registryKey = $extensionKey . ':Initialisation/Files';
if ($this->registry->get('extensionDataImport', $registryKey) || !file_exists($importFolder)) {
return;
}
$destinationAbsolutePath = GeneralUtility::getFileAbsFileName($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . $extensionKey);
if (!file_exists($destinationAbsolutePath) && GeneralUtility::isAllowedAbsPath($destinationAbsolutePath)) {
GeneralUtility::mkdir($destinationAbsolutePath);
}
GeneralUtility::copyDirectory($importFolder, $destinationAbsolutePath);
$this->registry->set('extensionDataImport', $registryKey, 1);
$event->addStorageEntry(__CLASS__, $destinationAbsolutePath);
}
}
@@ -0,0 +1,60 @@
<?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\Package\Initialization;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Registry;
/**
* Listener to import static sql data ("ext_tables_static+adt.sql") after package activation
*/
final readonly class ImportStaticSqlDataOnPackageInitialization
{
public function __construct(
private Registry $registry,
private SqlReader $sqlReader,
private SchemaMigrator $schemaMigrator,
) {}
#[AsEventListener(after: ImportExtensionDataOnPackageInitialization::class)]
public function __invoke(PackageInitializationEvent $event): void
{
$extTablesStaticSqlFile = $event->getPackage()->getPackagePath() . 'ext_tables_static+adt.sql';
$registryKey = $event->getExtensionKey() . ':ext_tables_static+adt.sql';
$oldFileHash = $this->registry->get('extensionDataImport', $registryKey);
$currentFileHash = '';
// We used to only store "1" in the database when data was imported
$needsUpdate = !$oldFileHash || $oldFileHash === 1;
if (file_exists($extTablesStaticSqlFile)) {
$currentFileHash = hash_file('xxh3', $extTablesStaticSqlFile);
$needsUpdate = $oldFileHash !== $currentFileHash;
if ($needsUpdate) {
$extTablesStaticSqlContent = (string)file_get_contents($extTablesStaticSqlFile);
$statements = $this->sqlReader->getStatementArray($extTablesStaticSqlContent);
$this->schemaMigrator->importStaticData($statements, true);
}
}
if ($needsUpdate) {
$this->registry->set('extensionDataImport', $registryKey, $currentFileHash);
$event->addStorageEntry(__CLASS__, $extTablesStaticSqlFile);
}
}
}
@@ -0,0 +1,38 @@
<?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\Package\Initialization;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
/**
* Listener to publish assets after package activation
*/
final readonly class PublishAssetsOnPackageInitialization
{
public function __construct(
private SystemResourcePublisherInterface $publisher,
) {}
#[AsEventListener]
public function __invoke(PackageInitializationEvent $event): void
{
$this->publisher->publishResources($event->getPackage());
}
}
+317
View File
@@ -0,0 +1,317 @@
<?php
/*
* 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\Package;
use Composer\Semver\VersionParser;
use TYPO3\CMS\Core\Package\MetaData\PackageConstraint;
/**
* The default TYPO3 Package MetaData implementation
*/
class MetaData
{
public const CONSTRAINT_TYPE_DEPENDS = 'depends';
public const CONSTRAINT_TYPE_CONFLICTS = 'conflicts';
public const CONSTRAINT_TYPE_SUGGESTS = 'suggests';
private const string FRAMEWORK_TYPE = 'typo3-cms-framework';
/**
* @var array
*/
protected static $CONSTRAINT_TYPES = [self::CONSTRAINT_TYPE_DEPENDS, self::CONSTRAINT_TYPE_CONFLICTS, self::CONSTRAINT_TYPE_SUGGESTS];
/**
* @var string
*/
protected $packageKey;
/**
* Package type
*
* @var string|null
*/
protected $packageType;
/**
* The normalized pretty version number
*/
protected string $version;
protected Stability $stability;
protected ?string $build = null;
protected bool $excludeFromUpdates = false;
/**
* Package title
* @var string|null
*/
protected $title;
/**
* Package description
* @var string|null
*/
protected $description;
/**
* constraints by constraint type (depends, conflicts, suggests)
* @var array
*/
protected $constraints = [];
/**
* Get all available constraint types
*
* @return array All constraint types
*/
public function getConstraintTypes()
{
return self::$CONSTRAINT_TYPES;
}
/**
* Package metadata constructor
*
* @param string $packageKey The package key
*/
public function __construct($packageKey)
{
$this->packageKey = $packageKey;
}
/**
* @return string The package key
*/
public function getPackageKey()
{
return $this->packageKey;
}
public function isExtensionType(): bool
{
return is_string($this->packageType) && str_starts_with($this->packageType, 'typo3-cms-');
}
public function isFrameworkType(): bool
{
return $this->packageType === self::FRAMEWORK_TYPE;
}
/**
* Get package type
*
* @return string|null
*/
public function getPackageType()
{
return $this->packageType;
}
/**
* Set package type
*
* @param string|null $packageType
*/
public function setPackageType($packageType)
{
$this->packageType = $packageType;
}
/**
* @return string The package version
*/
public function getVersion()
{
return $this->version;
}
public function getStability(): Stability
{
return $this->stability;
}
/**
* @param string $version The package version to set
*/
public function setVersion($version)
{
$this->stability = Stability::from(VersionParser::parseStability($version));
[$version, $build] = $this->splitBuildMetadata($version);
$this->build = $build;
$normalizedVersion = (new VersionParser())->normalize($version);
$this->version = $this->normalizedToPrettyVersion($normalizedVersion);
}
/**
* Converts a Composer-normalized version string into a human-friendly TYPO3 version string.
*
* Composer normalization typically produces versions in one of these forms:
*
* - Numeric versions with four segments:
* `1.2.3.0`, `3.0.0.0`, `5.2.32.0-RC2`
* - Special dev branch form:
* `9999999-dev`
* - Explicit dev branch names:
* `dev-main`
*
* This method transforms those normalized values into a prettier representation:
*
* - drops the fourth numeric segment
* - removes trailing `.0` parts, while keeping at least `major.minor`
* - preserves any stability suffix such as `-dev`, `-alpha1`, `-beta2`, `-RC3`
* - maps Composer's special `9999999-dev` value to plain `dev`
* - leaves non-matching or branch-like versions unchanged
*
* Examples:
*
* - `1.2.3.0` -> `1.2.3`
* - `3.0.0.0` -> `3.0`
* - `1.2.0.0` -> `1.2`
* - `5.2.32.0-RC2` -> `5.2.32-RC2`
* - `1.2.3.0-beta2` -> `1.2.3-beta2`
* - `1.2.3.0-dev` -> `1.2.3-dev`
* - `9999999-dev` -> `dev`
* - `dev-main` -> `dev-main`
*/
private function normalizedToPrettyVersion(string $normalized): string
{
// Common Composer special-case for branches.
if ($normalized === '9999999-dev') {
return 'dev';
}
// Leave obvious non-numeric/dev branch versions untouched.
if (str_starts_with($normalized, 'dev-')) {
return $normalized;
}
// Match actual numbers but return if we don't get a match
if (!preg_match('/^(\d+)\.(\d+)\.(\d+)\.(\d+)(-.+)?$/', $normalized, $matches)) {
return $normalized;
}
$parts = [$matches[1], $matches[2], $matches[3]];
$suffix = $matches[5] ?? '';
return implode('.', $parts) . $suffix;
}
/**
* Splits semver build metadata off the version string.
*
* Returns:
* [0] => version without "+..."
* [1] => metadata after "+" or null if none exists
*
* Only the first "+" is treated as the separator.
*
* @return array{0: string, 1: string|null}
*/
private function splitBuildMetadata(string $version): array
{
$pos = strpos($version, '+');
if ($pos === false) {
return [$version, null];
}
$baseVersion = substr($version, 0, $pos);
$buildMetadata = substr($version, $pos + 1);
if ($baseVersion === '') {
throw new \InvalidArgumentException('Version base must not be empty.', 1775558333);
}
return [$baseVersion, $buildMetadata];
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): void
{
$this->title = $title;
}
/**
* @return string|null The package description
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string|null $description The package description to set
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* Get all constraints
*
* @return array Package constraints
*/
public function getConstraints()
{
return $this->constraints;
}
/**
* Get the constraints by type
*
* @param string $constraintType Type of the constraints to get: CONSTRAINT_TYPE_*
* @return array Package constraints
*/
public function getConstraintsByType($constraintType)
{
if (!isset($this->constraints[$constraintType])) {
return [];
}
return $this->constraints[$constraintType];
}
/**
* Add a constraint
*
* @param MetaData\PackageConstraint $constraint The constraint to add
*/
public function addConstraint(PackageConstraint $constraint)
{
$this->constraints[$constraint->getConstraintType()][] = $constraint;
}
public function isExcludedFromUpdates(): bool
{
return $this->excludeFromUpdates;
}
public function setExcludeFromUpdates(bool $excludeFromUpdates): void
{
$this->excludeFromUpdates = $excludeFromUpdates;
}
public function getBuild(): ?string
{
return $this->build;
}
}
@@ -0,0 +1,107 @@
<?php
/*
* 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\Package\MetaData;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\VersionParser;
/**
* Package constraint meta model
*/
class PackageConstraint
{
private ?ConstraintInterface $constraint = null;
public function __construct(
protected readonly string $constraintType,
protected readonly string $value,
protected ?string $minVersion = null,
protected ?string $maxVersion = null,
protected ?string $versionConstraints = null,
) {}
private function initConstraint(): void
{
if ($this->constraint !== null) {
return;
}
if ($this->versionConstraints === null) {
if ($this->minVersion !== null || $this->maxVersion !== null) {
$this->versionConstraints = sprintf('%s - %s', $this->minVersion, $this->maxVersion);
} else {
$constraint = new MatchAllConstraint();
}
}
try {
$versionParser = new VersionParser();
$constraint ??= $versionParser->parseConstraints($this->versionConstraints);
} catch (\UnexpectedValueException) {
$constraint = new MatchAllConstraint();
}
$this->constraint = $constraint;
$this->calculateMinMaxVersion();
}
private function calculateMinMaxVersion(): void
{
$this->minVersion = $this->prettyVersion($this->constraint->getLowerBound()->getVersion());
$upperBound = $this->constraint->getUpperBound();
$maxVersion = $upperBound->getVersion();
if (!$this->constraint instanceof MatchAllConstraint && !$upperBound->isInclusive()) {
[$major, $minor, $patch] = explode('.', $upperBound->getVersion());
if ($minor === '0' && $patch === '0') {
$minor = $patch = '999';
$major = (int)$major - 1;
}
if ($patch === '0') {
$patch = '999';
$minor = (int)$minor - 1;
}
$maxVersion = sprintf('%s.%s.%s', $major, $minor, $patch);
}
$this->maxVersion = $maxVersion;
}
private function prettyVersion(string $normalizedVersion): string
{
[$major, $minor, $patch] = explode('.', $normalizedVersion);
return sprintf('%s.%s.%s', $major, $minor, $patch);
}
/**
* @return string The constraint name or value
*/
public function getValue(): string
{
return $this->value;
}
public function getVersionRange(): string
{
$this->initConstraint();
return sprintf('%s - %s', $this->minVersion, $this->maxVersion);
}
/**
* @return string The constraint type (depends, conflicts, suggests)
*/
public function getConstraintType(): string
{
return $this->constraintType;
}
}
+400
View File
@@ -0,0 +1,400 @@
<?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\Package;
use Composer\Util\Filesystem;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException;
use TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException;
use TYPO3\CMS\Core\Package\MetaData\PackageConstraint;
use TYPO3\CMS\Core\Package\Resource\ResourceCollection;
use TYPO3\CMS\Core\Package\Resource\ResourceCollectionInterface;
/**
* A Package representing the details of an extension and/or a composer package
*/
class Package implements PackageInterface
{
private const string NO_VERSION_SET = '1.0.0+no-version-set';
/**
* If this package is part of factory default, it will be activated
* during first installation.
*/
protected bool $partOfFactoryDefault = false;
/**
* If this package is part of minimal usable system, it will be
* activated if PackageStates is created from scratch.
*/
protected bool $partOfMinimalUsableSystem = false;
/**
* ServiceProvider class name. This property and the corresponding
* composer.json setting is internal and therefore no api (yet).
*
* @internal
*/
protected ?string $serviceProvider;
/**
* Composer Packages this package provides in classic mode
* The composer.json property is public, the implementation
* here is private
*
* @internal
*/
protected array $providesPackages = [];
/**
* Unique key of this package.
*/
protected string $packageKey;
/**
* Full path to this package's main directory
*/
protected string $packagePath;
protected bool $isRelativePackagePath = false;
/**
* If this package is protected and therefore cannot be deactivated or deleted
*/
protected bool $protected = false;
protected ?\stdClass $composerManifest;
/**
* Meta information about this package
*/
protected MetaData $packageMetaData;
protected ResourceCollectionInterface $resources;
/**
* @param PackageManager $packageManager the package manager which knows this package
* @param string $packageKey Key of this package
* @param string $packagePath Absolute path to the location of the package's composer manifest
* @param bool $isBuildingPackageArtifact When set we are in Composer mode and building the package artifact
* @throws Exception\InvalidPackageManifestException if no composer manifest file could be found
* @throws InvalidPackageKeyException if an invalid package key was passed
* @throws InvalidPackagePathException if an invalid package path was passed
*/
public function __construct(PackageManager $packageManager, string $packageKey, string $packagePath, bool $isBuildingPackageArtifact = false)
{
if (!$packageManager->isPackageKeyValid($packageKey)) {
throw new InvalidPackageKeyException('"' . $packageKey . '" is not a valid package key.', 1217959511);
}
if (!(@is_dir($packagePath) || (is_link($packagePath) && is_dir($packagePath)))) {
throw new InvalidPackagePathException(sprintf('Tried to instantiate a package object for package "%s" with a non-existing package path "%s". Either the package does not exist anymore, or the code creating this object contains an error.', $packageKey, $packagePath), 1166631890);
}
if (!str_ends_with($packagePath, '/')) {
throw new InvalidPackagePathException(sprintf('The package path "%s" provided for package "%s" has no trailing forward slash.', $packagePath, $packageKey), 1166633722);
}
$this->packageKey = $packageKey;
$this->packagePath = $packagePath;
$this->composerManifest = $packageManager->getComposerManifest($this->packagePath, $isBuildingPackageArtifact);
$this->loadFlagsFromComposerManifest($isBuildingPackageArtifact);
$this->createPackageMetaData($packageManager, $isBuildingPackageArtifact);
$this->createResources();
}
/**
* Loads package management related flags from the "extra:typo3/cms:Package" section
* of extensions composer.json files into local properties
*/
protected function loadFlagsFromComposerManifest(bool $ignoreProvidesPackages = false): void
{
$extraFlags = $this->getValueFromComposerManifest('extra');
if ($extraFlags !== null && isset($extraFlags->{'typo3/cms'}->{'Package'})) {
foreach ($extraFlags->{'typo3/cms'}->{'Package'} as $flagName => $flagValue) {
if ($flagName === 'providesPackages') {
if ($ignoreProvidesPackages) {
continue;
}
$flagValue = (array)$flagValue;
}
if (property_exists($this, $flagName)) {
$this->{$flagName} = $flagValue;
}
}
}
}
/**
* Creates the package meta data object of this package.
*/
protected function createPackageMetaData(PackageManager $packageManager, bool $isBuildingPackageArtifact = false): void
{
$this->packageMetaData = new MetaData($this->getPackageKey());
$manifest = $this->getValueFromComposerManifest();
$title = $description = $manifest->description ?? null;
$descriptionParts = explode(' - ', $description ?? '', 2);
if (count($descriptionParts) === 2) {
[$title, $description] = $descriptionParts;
}
$this->packageMetaData->setTitle($title);
$this->packageMetaData->setDescription($title !== $description ? $description : null);
$this->packageMetaData->setPackageType($manifest->type ?? null);
$isFrameworkPackage = $packageManager->isFrameworkPackage($this->getValueFromComposerManifest('name') ?? $this->packageKey);
$version = $manifest->extra->{'typo3/cms'}->{'version'} ?? $manifest->version ?? self::NO_VERSION_SET;
if ($isFrameworkPackage) {
$version = (new Typo3Version())->getVersion();
}
$this->packageMetaData->setVersion($version);
$this->packageMetaData->setExcludeFromUpdates($manifest->extra->{'typo3/cms'}->{'exclude-from-updates'} ?? false);
$requirements = $manifest->require ?? null;
if ($requirements !== null) {
foreach ($requirements as $packageName => $versionConstraints) {
if ($this->ignoreDependencyInPackageConstraint($packageName, $packageManager, $isBuildingPackageArtifact)) {
continue;
}
$this->packageMetaData->addConstraint(
new PackageConstraint(
constraintType: MetaData::CONSTRAINT_TYPE_DEPENDS,
value: $packageName,
versionConstraints: $versionConstraints,
)
);
}
}
$suggestions = $manifest->suggest ?? null;
if ($suggestions !== null) {
foreach ($suggestions as $packageName => $description) {
if ($this->ignoreDependencyInPackageConstraint($packageName, $packageManager, $isBuildingPackageArtifact)) {
continue;
}
$constraint = new PackageConstraint(MetaData::CONSTRAINT_TYPE_SUGGESTS, $packageName);
$this->packageMetaData->addConstraint($constraint);
}
}
$conflicts = $manifest->conflict ?? null;
if ($conflicts !== null) {
foreach ($conflicts as $packageName => $versionConstraints) {
if ($this->ignoreDependencyInPackageConstraint($packageName, $packageManager, $isBuildingPackageArtifact)) {
continue;
}
$this->packageMetaData->addConstraint(
new PackageConstraint(
constraintType: MetaData::CONSTRAINT_TYPE_CONFLICTS,
value: $packageName,
versionConstraints: $versionConstraints,
)
);
}
}
}
/**
* In Composer mode, $packageManager->isComposerDependency() will always be true already for composer dependencies,
* since all packages are known.
* In Classic mode providesPackages is evaluated for third party extensions
* while for framework packages only dependencies to other framework packages are tracked
*/
private function ignoreDependencyInPackageConstraint(string $packageName, PackageManager $packageManager, bool $isBuildingPackageArtifact): bool
{
$isKnownComposerDependency = $packageManager->isComposerDependency($packageName);
if ($isBuildingPackageArtifact) {
return $isKnownComposerDependency;
}
// The "php" package name is kept, so that the extension manager in classic mode can check PHP version constraints
// It will be ignored in extension dependency ordering in PackageManager though
return ($packageName !== 'php' && $isKnownComposerDependency)
// provided Composer packages as specified by third party extensions (loaded on demand in classic mode)
|| isset($this->providesPackages[$packageName])
|| ($this->packageMetaData->isFrameworkType() && !$packageManager->isFrameworkPackage($packageName))
;
}
protected function createResources(): void
{
$relativeIconPath = $this->getPackageIcon();
$iconIdentifier = $relativeIconPath !== null ? sprintf(
'PKG:%s:%s',
$this->getValueFromComposerManifest('name') ?? $this->getPackageKey(),
$relativeIconPath,
) : null;
$resourceDefinitionClosure = $this->getResourceDefinitions(
__DIR__ . '/../../Configuration/DefaultPackageResources.php'
);
$customResourceDefinitionClosure = $this->getResourceDefinitions(
$this->getPackagePath() . 'Configuration/Resources.php'
);
$resourceDefinitions = array_merge(
$resourceDefinitionClosure($this),
$customResourceDefinitionClosure === null ? [] : $customResourceDefinitionClosure($this),
);
$this->resources = new ResourceCollection(
$resourceDefinitions,
$iconIdentifier,
);
}
protected function getResourceDefinitions(string $configPath): ?\Closure
{
if (!file_exists($configPath)) {
return null;
}
return (static function ($configPath) {
return require $configPath;
})($configPath);
}
public function getResources(): ResourceCollectionInterface
{
return $this->resources;
}
/**
* Get the Service Provider class name
*
* @internal
*/
public function getServiceProvider(): string
{
return $this->serviceProvider ?? PseudoServiceProvider::class;
}
/**
* @internal
*/
public function isPartOfFactoryDefault(): bool
{
return $this->partOfFactoryDefault;
}
/**
* @internal
*/
public function isPartOfMinimalUsableSystem(): bool
{
return $this->partOfMinimalUsableSystem;
}
/**
* Returns the package key of this package.
*/
public function getPackageKey(): string
{
return $this->packageKey;
}
/**
* Tells if this package is protected and therefore cannot be deactivated or deleted
*/
public function isProtected(): bool
{
return $this->protected;
}
/**
* Sets the protection flag of the package
*
* @param bool $protected TRUE if the package should be protected, otherwise FALSE
*/
public function setProtected(bool $protected): void
{
$this->protected = (bool)$protected;
}
/**
* @internal Only for use in ClassLoadingInformationGenerator
*/
public function getProvidesPackages(): array
{
return $this->providesPackages;
}
/**
* Returns the full path to this package's main directory
*
* @return string Path to this package's main directory
*/
public function getPackagePath(): string
{
if (!$this->isRelativePackagePath) {
return $this->packagePath;
}
$this->isRelativePackagePath = false;
return $this->packagePath = Environment::getProjectPath() . '/' . $this->packagePath;
}
/**
* Used by PackageArtifactBuilder to make package path relative
*
* @internal
*/
public function makePathRelative(Filesystem $filesystem, string $composerRootPath): void
{
$this->isRelativePackagePath = true;
$this->packagePath = ($composerRootPath . '/') === $this->packagePath ? '' : $filesystem->findShortestPath($composerRootPath, $this->packagePath, true) . '/';
}
/**
* Returns the package meta data object of this package.
*
* @internal
*/
public function getPackageMetaData(): MetaData
{
return $this->packageMetaData;
}
/**
* Returns an array of packages this package replaces
* @internal
*/
public function getPackageReplacementKeys(): array
{
// The cast to array is required since the manifest returns data with type mixed
return (array)$this->getValueFromComposerManifest('replace') ?: [];
}
/**
* Returns contents of Composer manifest - or part there of if a key is given.
*
* @param string $key Optional. Only return the part of the manifest indexed by 'key'
* @see json_decode for return values
* @internal
*/
public function getValueFromComposerManifest($key = null): mixed
{
if ($key === null) {
return $this->composerManifest;
}
return $this->composerManifest->{$key} ?? null;
}
/**
* Find package icon location relative to the package path
*/
public function getPackageIcon(): ?string
{
$resourcePath = 'Resources/Public/Icons/Extension.';
foreach (['svg', 'png', 'gif'] as $fileExtension) {
if (file_exists($this->getPackagePath() . $resourcePath . $fileExtension)) {
return $resourcePath . $fileExtension;
}
}
return null;
}
}
@@ -0,0 +1,72 @@
<?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\Package;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Configuration\Extension\ExtLocalconfFactory;
use TYPO3\CMS\Core\Configuration\Tca\TcaFactory;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
/**
* Service for activating packages in classic mode.
* Takes care of DI container juggling and flushing caches,
* while also setting up the activated package
*
* @internal Only for use in InstallUtility
*/
#[Autoconfigure(public: true)]
class PackageActivationService
{
public function __construct(
private PackageManager $packageManager,
private BootService $bootService,
private OpcodeCacheService $opcodeCacheService,
) {}
public function activate(array $packageKeys, ?object $emitter = null): void
{
$packages = [];
foreach ($packageKeys as $packageKey) {
$this->packageManager->activatePackage($packageKey);
$packages[$packageKey] = $this->packageManager->getPackage($packageKey);
}
// Load a new container as we are reloading ext_localconf.php files
$container = $this->bootService->getContainer();
$backupContainer = $this->bootService->makeCurrent($container);
// Reload cache files and Typo3LoadedExtensions
$backupTca = $GLOBALS['TCA'];
$container->get(CacheManager::class)->flushCaches();
$this->opcodeCacheService->clearAllActive();
$container->get(ExtLocalconfFactory::class)->loadUncached();
$tcaFactory = $container->get(TcaFactory::class);
$GLOBALS['TCA'] = $tcaFactory->create();
$container->get(TcaSchemaFactory::class)->rebuild($GLOBALS['TCA']);
// Set up packages
$packageSetup = $container->get(PackageSetup::class);
$packageSetup->setup($packages, true, $emitter, $container);
// Reset to the original container instance and original TCA
$GLOBALS['TCA'] = $backupTca;
$this->bootService->makeCurrent(null, $backupContainer);
}
}
@@ -0,0 +1,39 @@
<?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\Package;
/**
* Dto for package initialization result data. Used by listeners to the PackageInitializationEvent
*/
final readonly class PackageInitializationResult
{
public function __construct(
private string $identifier,
private mixed $result
) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getResult(): mixed
{
return $this->result;
}
}
+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\Package;
use TYPO3\CMS\Core\Package\Resource\ResourceCollectionInterface;
/**
* Interface for a TYPO3 Package class
*/
interface PackageInterface
{
/**
* See https://github.com/composer/composer/blob/2.1.6/src/Composer/Command/InitCommand.php#L100
*/
public const PATTERN_MATCH_COMPOSER_NAME = '{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D';
public const PATTERN_MATCH_PACKAGEKEY = '/^[a-z0-9]+\.(?:[a-z0-9][\.a-z0-9]*)+$/i';
public const PATTERN_MATCH_EXTENSIONKEY = '/^[0-9a-z_-]+$/i';
/**
* @internal
*/
public function getResources(): ResourceCollectionInterface;
/**
* @internal
*/
public function getPackageReplacementKeys(): array;
/**
* Tells if the package is part of the default factory configuration
* and therefor activated at first installation.
*
* @internal
*/
public function isPartOfFactoryDefault(): bool;
/**
* Tells if the package is required for a minimal usable (backend) system
* and therefor activated if PackageStates is created from scratch for
* whatever reason.
*
* @internal
*/
public function isPartOfMinimalUsableSystem(): bool;
/**
* Returns contents of Composer manifest - or part there of if a key is given.
*
* @param string|null $key Optional. Only return the part of the manifest indexed by 'key'
* @see json_decode for return values
* @internal
*/
public function getValueFromComposerManifest(?string $key = null): mixed;
/**
* Returns the package meta object of this package.
*
* @internal
*/
public function getPackageMetaData(): MetaData;
/**
* Returns the package key of this package.
*
* @return string
*/
public function getPackageKey(): string;
/**
* Tells if this package is protected and therefore cannot be deactivated or deleted
*
* @return bool
*/
public function isProtected(): bool;
/**
* Sets the protection flag of the package
*
* @param bool $protected TRUE if the package should be protected, otherwise FALSE
*/
public function setProtected(bool $protected): void;
/**
* Returns the full path to this package's main directory
*
* @return string Path to this package's main directory
*/
public function getPackagePath(): string;
/**
* Find package icon location relative to the package path or null if nothing was found.
*/
public function getPackageIcon(): ?string;
}
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
<?php
/*
* 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\Package;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Package\Initialization\CheckForImportRequirements;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
/**
* Only for use in extension:setup and TYPO3 installation
* The class can not be final, because it is mocked in tests
* @internal
*/
#[Autoconfigure(public: true)]
readonly class PackageSetup
{
public function __construct(
private SqlReader $sqlReader,
private SchemaMigrator $schemaMigrator,
private ExtensionConfiguration $extensionConfiguration,
private EventDispatcherInterface $eventDispatcher,
) {}
/**
* @return FlashMessage[]
*/
public function setup(array $packagesToSetUp, bool $packageActivated = false, ?object $emitter = null, ?ContainerInterface $container = null): array
{
$messages = [];
$this->updateDatabaseSchemaForAllPackages();
foreach ($packagesToSetUp as $packageKey => $package) {
$this->extensionConfiguration->synchronizeExtConfTemplateWithLocalConfiguration($packageKey);
$event = $this->eventDispatcher->dispatch(
new PackageInitializationEvent(
extensionKey: $packageKey,
package: $package,
packageActivated: $packageActivated,
container: $container,
emitter: $emitter,
),
);
if ($event->hasStorageEntry(CheckForImportRequirements::class)) {
$messages[] = new FlashMessage(
$event->getStorageEntry(CheckForImportRequirements::class)->getResult()['exception']?->getMessage() ?? '',
'',
ContextualFeedbackSeverity::WARNING
);
}
}
return $messages;
}
private function updateDatabaseSchemaForAllPackages(): void
{
$sqlStatements = [];
$sqlStatements[] = $this->sqlReader->getTablesDefinitionString();
$sqlStatements = $this->sqlReader->getCreateTableStatementArray(implode(LF . LF, array_filter($sqlStatements)));
$updateStatements = $this->schemaMigrator->getUpdateSuggestions($sqlStatements);
$updateStatements = array_merge_recursive(...array_values($updateStatements));
$selectedStatements = [];
foreach (['add', 'change', 'create_table', 'change_table'] as $action) {
if (empty($updateStatements[$action])) {
continue;
}
$statements = array_combine(array_keys($updateStatements[$action]), array_fill(0, count($updateStatements[$action]), true));
$selectedStatements = array_merge(
$selectedStatements,
$statements
);
}
$this->schemaMigrator->migrate($sqlStatements, $selectedStatements);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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\Package;
use Psr\Container\ContainerInterface;
/**
* @internal
*/
final class PseudoServiceProvider extends AbstractServiceProvider
{
/**
* @var PackageInterface
*/
private $package;
public function __construct(PackageInterface $package)
{
$this->package = $package;
}
protected static function getPackagePath(): string
{
throw new \BadMethodCallException('PseudoServiceProvider does not support the getPackagePath() method.', 1562354465);
}
protected static function getPackageName(): string
{
throw new \BadMethodCallException('PseudoServiceProvider does not support the getPackageName() method.', 1643372902);
}
public function getFactories(): array
{
return [];
}
public function getExtensions(): array
{
$packagePath = $this->package->getPackagePath();
// Fallback to empty string if dealing with an extension in non-composer mode
// that still does not provide composer.json.
$packageName = $this->package->getValueFromComposerManifest('name') ?? '';
$extensions = parent::getExtensions();
// The static configure*() methods in AbstractServiceProvider use the
// static getPackagePath() method to retrieve the package path.
// We can not provide a static package path for pseudo service providers,
// therefore we dynamically inject the package path to the static service
// configure methods by wrapping these in a Closure.
// AbstractServiceProvider configure methods are aware of this and
// provide an optional third parameter which is forwarded as
// dynamic path to getPackagePath().
// Same logic for $packageName.
foreach ($extensions as $serviceName => $previousCallable) {
$extensions[$serviceName] = static function (ContainerInterface $container, $value) use ($previousCallable, $packagePath, $packageName) {
return ($previousCallable)($container, $value, $packagePath, $packageName);
};
}
return $extensions;
}
}
@@ -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\Package\Resource\Definition;
use TYPO3\CMS\Core\Package\PackageInterface;
/**
* @internal This is subject to change during v14 development. Do not use.
*/
final class DefaultPublicFilePrefix implements DynamicPublicPrefixInterface
{
public function calculatePrefix(PackageInterface $package, PublicResourceDefinition $definition): string
{
return (new DefaultPublicFolderPrefix())->calculatePrefix($package, $definition) . '/' . basename($definition->getRelativePath());
}
}
@@ -0,0 +1,32 @@
<?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\Package\Resource\Definition;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Package\PackageInterface;
/**
* @internal This is subject to change during v14 development. Do not use.
*/
final class DefaultPublicFolderPrefix implements DynamicPublicPrefixInterface
{
public function calculatePrefix(PackageInterface $package, PublicResourceDefinition $definition): string
{
return md5(substr($package->getPackagePath() . $definition->getRelativePath(), strlen(Environment::getProjectPath())));
}
}
@@ -0,0 +1,32 @@
<?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\Package\Resource\Definition;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Package\PackageInterface;
/**
* @internal This is subject to change during v14 development. Do not use.
*/
final class DefaultPublicPrefix implements DynamicPublicPrefixInterface
{
public function calculatePrefix(PackageInterface $package, PublicResourceDefinition $definition): string
{
return md5(substr($package->getPackagePath(), strlen(Environment::getProjectPath())));
}
}
@@ -0,0 +1,28 @@
<?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\Package\Resource\Definition;
use TYPO3\CMS\Core\Package\PackageInterface;
/**
* @internal This is subject to change during v14 development. Do not use.
*/
interface DynamicPublicPrefixInterface
{
public function calculatePrefix(PackageInterface $package, PublicResourceDefinition $definition): string;
}
@@ -0,0 +1,37 @@
<?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\Package\Resource\Definition;
/**
* This class is meant to be used for resource definition in Configuration/Resources.php
*/
final readonly class PublicFileDefinition extends PublicResourceDefinition
{
public function __construct(
string $relativePath,
string|DynamicPublicPrefixInterface $publicPrefix = new DefaultPublicFilePrefix(),
?string $identifier = null,
) {
parent::__construct($relativePath, $publicPrefix, $identifier);
}
public function matches(string $relativePath): bool
{
return str_starts_with($relativePath, $this->relativePath);
}
}
@@ -0,0 +1,62 @@
<?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\Package\Resource\Definition;
/**
* This class is meant to be used for resource definition in Configuration/Resources.php
*/
readonly class PublicResourceDefinition implements ResourceDefinitionInterface
{
protected string $identifier;
protected string|DynamicPublicPrefixInterface $publicPrefix;
public function __construct(
protected string $relativePath,
string|DynamicPublicPrefixInterface|null $publicPrefix = null,
?string $identifier = null,
) {
$this->identifier = $identifier ?? $relativePath;
if (is_string($publicPrefix)) {
$this->publicPrefix = $publicPrefix;
} elseif ($relativePath === 'Resources/Public') {
$this->publicPrefix = $publicPrefix ?? new DefaultPublicPrefix();
} else {
$this->publicPrefix = $publicPrefix ?? new DefaultPublicFolderPrefix();
}
}
public function matches(string $relativePath): bool
{
return str_starts_with($relativePath, $this->relativePath . '/');
}
public function getPublicPrefix(): DynamicPublicPrefixInterface|string
{
return $this->publicPrefix;
}
public function getRelativePath(): string
{
return $this->relativePath;
}
public function getIdentifier(): string
{
return $this->identifier;
}
}
@@ -0,0 +1,43 @@
<?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\Package\Resource\Definition;
/**
* This class is meant to be used for resource definition in Configuration/Resources.php
*/
final readonly class ResourceDefinition implements ResourceDefinitionInterface
{
public function __construct(
private string $relativePath,
) {}
public function matches(string $relativePath): bool
{
return str_starts_with($relativePath, $this->relativePath);
}
public function getRelativePath(): string
{
return $this->relativePath;
}
public function getIdentifier(): string
{
return $this->relativePath;
}
}
@@ -0,0 +1,28 @@
<?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\Package\Resource\Definition;
/**
* @internal This interface is only meant to be used in TYPO3\CMS\Core\Package\Resource\Definition namespace
*/
interface ResourceDefinitionInterface
{
public function matches(string $relativePath): bool;
public function getRelativePath(): string;
public function getIdentifier(): string;
}
@@ -0,0 +1,83 @@
<?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\Package\Resource;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;
use TYPO3\CMS\Core\Package\Resource\Definition\ResourceDefinition;
use TYPO3\CMS\Core\Package\Resource\Definition\ResourceDefinitionInterface;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceDefinitionNotFoundException;
/**
* @internal This is subject to change during v14 development. Do not use.
*/
final readonly class ResourceCollection implements ResourceCollectionInterface
{
/**
* @param list<ResourceDefinitionInterface> $resourceDefinitions
*/
public function __construct(
private array $resourceDefinitions = [],
private ?string $iconIdentifier = null,
private bool $createResourcesOnTheFly = true,
) {}
/**
* @internal Only to be used in VirtualAppPackage. Will be removed when deprecated asset config is removed
*/
public function withAdditionalResources(self $resources): ResourceCollectionInterface
{
return new self(
array_merge($this->resourceDefinitions, $resources->resourceDefinitions),
$this->iconIdentifier,
);
}
public function definitionForPath(string $relativePath): ResourceDefinitionInterface
{
foreach ($this->resourceDefinitions as $config) {
if ($config->matches($relativePath)) {
return $config;
}
}
if ($this->createResourcesOnTheFly) {
trigger_error('Resource identifiers outside ouf Resources/Private, Resources/Public or Configuration folder of extensions are deprecated. Define custom resources if required. Used resource: ' . $relativePath, E_USER_DEPRECATED);
return new ResourceDefinition($relativePath);
}
throw new SystemResourceDefinitionNotFoundException(sprintf('Project path "%s" is not allowed. Define custom resources if required.', $relativePath), 1763381519);
}
public function isPublicPath(string $relativePath): bool
{
return $this->definitionForPath($relativePath) instanceof PublicResourceDefinition;
}
public function getPublicResourceDefinitions(): array
{
return array_filter(
array_map(
static fn(ResourceDefinitionInterface $definition) => $definition instanceof PublicResourceDefinition ? $definition : null,
$this->resourceDefinitions,
)
);
}
public function getPackageIcon(): ?string
{
return $this->iconIdentifier;
}
}
@@ -0,0 +1,38 @@
<?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\Package\Resource;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;
use TYPO3\CMS\Core\Package\Resource\Definition\ResourceDefinitionInterface;
/**
* @internal This is subject to change during v14 development. Do not use.
*/
interface ResourceCollectionInterface
{
public function isPublicPath(string $relativePath): bool;
public function definitionForPath(string $relativePath): ResourceDefinitionInterface;
public function getPackageIcon(): ?string;
/**
* @return PublicResourceDefinition[]
*/
public function getPublicResourceDefinitions(): array;
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/*
* 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\Package;
/**
* Stability flags for a package
*/
enum Stability: string
{
case dev = 'dev';
case stable = 'stable';
case alpha = 'alpha';
case beta = 'beta';
case RC = 'RC';
case unknown = 'unknown';
}
@@ -0,0 +1,45 @@
<?php
/*
* 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\Package;
/**
* This is an intermediate package manager that loads
* all extensions that are present in one of the package base paths,
* so that the class loader can find the classes of all tests,
* whether the according extension is active in the installation itself or not.
*/
class UnitTestPackageManager extends PackageManager
{
/**
* Initializes the package manager
*/
public function initialize()
{
$this->scanAvailablePackages();
foreach ($this->packages as $package) {
$this->registerActivePackage($package);
}
}
/**
* Overwrite the original method to avoid resolving dependencies (which we do not need)
* and saving the PackageStates.php file (which we do not want), when calling scanAvailablePackages()
*/
protected function sortAndSavePackageStates()
{
// Deliberately empty!
}
}
+70
View File
@@ -0,0 +1,70 @@
<?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\Package;
use TYPO3\CMS\Core\Package\Resource\ResourceCollection;
use TYPO3\CMS\Core\Package\Resource\ResourceCollectionInterface;
/**
* This represents the app package (root package in Composer terms)
*
* @internal Only to be used in TYPO3\CMS\Core\Package and TYPO3\CMS\Core\SystemResource namespace
*/
final class VirtualAppPackage extends Package
{
public const string APP_PACKAGE_KEY = 'typo3/app';
public function __construct(
PackageManager $packageManager,
string $packagePath,
private readonly string $relativePublicPath,
) {
parent::__construct($packageManager, self::APP_PACKAGE_KEY, $packagePath, true);
$this->packageMetaData = new MetaData(self::APP_PACKAGE_KEY);
$this->composerManifest = new \stdClass();
$this->composerManifest->name = self::APP_PACKAGE_KEY;
}
protected function createResources(): void
{
$resourceDefinitionClosure = $this->getResourceDefinitions(
__DIR__ . '/../../Configuration/DefaultAppResources.php'
);
$customResourceDefinitionClosure = $this->getResourceDefinitions(
$this->getPackagePath() . 'config/system/resources.php'
);
$resourceDefinitions = array_merge(
$resourceDefinitionClosure($this, $this->relativePublicPath),
$customResourceDefinitionClosure === null ? [] : $customResourceDefinitionClosure($this, $this->relativePublicPath),
);
$this->resources = new ResourceCollection(
$resourceDefinitions,
null,
false,
);
}
public function getResources(): ResourceCollectionInterface
{
$resources = parent::getResources();
if (!$resources instanceof ResourceCollection) {
throw new \RuntimeException('Resource object must not be overridden', 1774537784);
}
return $resources;
}
}