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