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; } }