1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Core\Package; 17 18use Composer\Util\Filesystem; 19use TYPO3\CMS\Core\Core\Environment; 20use TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException; 21use TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException; 22use TYPO3\CMS\Core\Package\MetaData\PackageConstraint; 23 24/** 25 * A Package representing the details of an extension and/or a composer package 26 */ 27class Package implements PackageInterface 28{ 29 /** 30 * If this package is part of factory default, it will be activated 31 * during first installation. 32 * 33 * @var bool 34 */ 35 protected $partOfFactoryDefault = false; 36 37 /** 38 * If this package is part of minimal usable system, it will be 39 * activated if PackageStates is created from scratch. 40 * 41 * @var bool 42 */ 43 protected $partOfMinimalUsableSystem = false; 44 45 /** 46 * ServiceProvider class name. This property and the corresponding 47 * composer.json setting is internal and therefore no api (yet). 48 * 49 * @var string 50 * @internal 51 */ 52 protected $serviceProvider; 53 54 /** 55 * Unique key of this package. 56 * @var string 57 */ 58 protected $packageKey; 59 60 /** 61 * Full path to this package's main directory 62 * @var string 63 */ 64 protected $packagePath; 65 66 /** 67 * @var bool 68 */ 69 protected $isRelativePackagePath = false; 70 71 /** 72 * If this package is protected and therefore cannot be deactivated or deleted 73 * @var bool 74 */ 75 protected $protected = false; 76 77 /** 78 * @var \stdClass 79 */ 80 protected $composerManifest; 81 82 /** 83 * Meta information about this package 84 * @var MetaData 85 */ 86 protected $packageMetaData; 87 88 /** 89 * Constructor 90 * 91 * @param PackageManager $packageManager the package manager which knows this package 92 * @param string $packageKey Key of this package 93 * @param string $packagePath Absolute path to the location of the package's composer manifest 94 * @param bool $ignoreExtEmConf When set ext_emconf.php is ignored when building composer manifest 95 * @throws Exception\InvalidPackageManifestException if no composer manifest file could be found 96 * @throws InvalidPackageKeyException if an invalid package key was passed 97 * @throws InvalidPackagePathException if an invalid package path was passed 98 */ 99 public function __construct(PackageManager $packageManager, string $packageKey, string $packagePath, bool $ignoreExtEmConf = false) 100 { 101 if (!$packageManager->isPackageKeyValid($packageKey)) { 102 throw new InvalidPackageKeyException('"' . $packageKey . '" is not a valid package key.', 1217959511); 103 } 104 if (!(@is_dir($packagePath) || (is_link($packagePath) && is_dir($packagePath)))) { 105 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); 106 } 107 if (substr($packagePath, -1, 1) !== '/') { 108 throw new InvalidPackagePathException(sprintf('The package path "%s" provided for package "%s" has no trailing forward slash.', $packagePath, $packageKey), 1166633722); 109 } 110 $this->packageKey = $packageKey; 111 $this->packagePath = $packagePath; 112 $this->composerManifest = $packageManager->getComposerManifest($this->packagePath, $ignoreExtEmConf); 113 $this->loadFlagsFromComposerManifest(); 114 $this->createPackageMetaData($packageManager); 115 } 116 117 /** 118 * Loads package management related flags from the "extra:typo3/cms:Package" section 119 * of extensions composer.json files into local properties 120 */ 121 protected function loadFlagsFromComposerManifest() 122 { 123 $extraFlags = $this->getValueFromComposerManifest('extra'); 124 if ($extraFlags !== null && isset($extraFlags->{'typo3/cms'}->{'Package'})) { 125 foreach ($extraFlags->{'typo3/cms'}->{'Package'} as $flagName => $flagValue) { 126 if (property_exists($this, $flagName)) { 127 $this->{$flagName} = $flagValue; 128 } 129 } 130 } 131 } 132 133 /** 134 * Creates the package meta data object of this package. 135 * 136 * @param PackageManager $packageManager 137 */ 138 protected function createPackageMetaData(PackageManager $packageManager) 139 { 140 $this->packageMetaData = new MetaData($this->getPackageKey()); 141 $description = (string)$this->getValueFromComposerManifest('description'); 142 $this->packageMetaData->setDescription($description); 143 $this->packageMetaData->setTitle($this->getValueFromComposerManifest('title') ?? $description); 144 $this->packageMetaData->setVersion((string)$this->getValueFromComposerManifest('version')); 145 $this->packageMetaData->setPackageType((string)$this->getValueFromComposerManifest('type')); 146 $requirements = $this->getValueFromComposerManifest('require'); 147 if ($requirements !== null) { 148 foreach ($requirements as $requirement => $version) { 149 $packageKey = $packageManager->getPackageKeyFromComposerName($requirement); 150 $constraint = new PackageConstraint(MetaData::CONSTRAINT_TYPE_DEPENDS, $packageKey); 151 $this->packageMetaData->addConstraint($constraint); 152 } 153 } 154 $suggestions = $this->getValueFromComposerManifest('suggest'); 155 if ($suggestions !== null) { 156 foreach ($suggestions as $suggestion => $version) { 157 $packageKey = $packageManager->getPackageKeyFromComposerName($suggestion); 158 $constraint = new PackageConstraint(MetaData::CONSTRAINT_TYPE_SUGGESTS, $packageKey); 159 $this->packageMetaData->addConstraint($constraint); 160 } 161 } 162 } 163 164 /** 165 * Get the Service Provider class name 166 * 167 * @return string 168 * @internal 169 */ 170 public function getServiceProvider(): string 171 { 172 return $this->serviceProvider ?? PseudoServiceProvider::class; 173 } 174 175 /** 176 * @return bool 177 * @internal 178 */ 179 public function isPartOfFactoryDefault() 180 { 181 return $this->partOfFactoryDefault; 182 } 183 184 /** 185 * @return bool 186 * @internal 187 */ 188 public function isPartOfMinimalUsableSystem() 189 { 190 return $this->partOfMinimalUsableSystem; 191 } 192 193 /** 194 * Returns the package key of this package. 195 * 196 * @return string 197 */ 198 public function getPackageKey() 199 { 200 return $this->packageKey; 201 } 202 203 /** 204 * Tells if this package is protected and therefore cannot be deactivated or deleted 205 * 206 * @return bool 207 */ 208 public function isProtected() 209 { 210 return $this->protected; 211 } 212 213 /** 214 * Sets the protection flag of the package 215 * 216 * @param bool $protected TRUE if the package should be protected, otherwise FALSE 217 */ 218 public function setProtected($protected) 219 { 220 $this->protected = (bool)$protected; 221 } 222 223 /** 224 * Returns the full path to this package's main directory 225 * 226 * @return string Path to this package's main directory 227 */ 228 public function getPackagePath() 229 { 230 if (!$this->isRelativePackagePath) { 231 return $this->packagePath; 232 } 233 $this->isRelativePackagePath = false; 234 235 return $this->packagePath = Environment::getComposerRootPath() . '/' . $this->packagePath; 236 } 237 238 /** 239 * Used by PackageArtifactBuilder to make package path relative 240 * 241 * @param Filesystem $filesystem 242 * @param string $composerRootPath 243 * @internal 244 */ 245 public function makePathRelative(Filesystem $filesystem, string $composerRootPath): void 246 { 247 $this->isRelativePackagePath = true; 248 $this->packagePath = ($composerRootPath . '/') === $this->packagePath ? '' : $filesystem->findShortestPath($composerRootPath, $this->packagePath, true) . '/'; 249 } 250 251 /** 252 * Returns the package meta data object of this package. 253 * 254 * @return MetaData 255 * @internal 256 */ 257 public function getPackageMetaData() 258 { 259 return $this->packageMetaData; 260 } 261 262 /** 263 * Returns an array of packages this package replaces 264 * 265 * @return array 266 * @internal 267 */ 268 public function getPackageReplacementKeys() 269 { 270 // The cast to array is required since the manifest returns data with type mixed 271 return (array)$this->getValueFromComposerManifest('replace') ?: []; 272 } 273 274 /** 275 * Returns contents of Composer manifest - or part there of if a key is given. 276 * 277 * @param string $key Optional. Only return the part of the manifest indexed by 'key' 278 * @return mixed|null 279 * @see json_decode for return values 280 * @internal 281 */ 282 public function getValueFromComposerManifest($key = null) 283 { 284 if ($key === null) { 285 return $this->composerManifest; 286 } 287 288 if (isset($this->composerManifest->{$key})) { 289 $value = $this->composerManifest->{$key}; 290 } else { 291 $value = null; 292 } 293 return $value; 294 } 295} 296