[!!!][FOLLOWUP][TASK] Simplify PackageManagement
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Package / Package.php
1 <?php
2 namespace TYPO3\CMS\Core\Package;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * A Package representing the details of an extension and/or a composer package
19 * Adapted from FLOW for TYPO3 CMS
20 */
21 class Package implements PackageInterface {
22
23 /**
24 * @var array
25 */
26 protected $extensionManagerConfiguration = array();
27
28 /**
29 * @var array
30 */
31 protected $classAliases;
32
33 /**
34 * If this package is part of factory default, it will be activated
35 * during first installation.
36 *
37 * @var bool
38 */
39 protected $partOfFactoryDefault = FALSE;
40
41 /**
42 * If this package is part of minimal usable system, it will be
43 * activated if PackageStates is created from scratch.
44 *
45 * @var bool
46 */
47 protected $partOfMinimalUsableSystem = FALSE;
48
49 /**
50 * Unique key of this package.
51 * @var string
52 */
53 protected $packageKey;
54
55 /**
56 * Full path to this package's main directory
57 * @var string
58 */
59 protected $packagePath;
60
61 /**
62 * If this package is protected and therefore cannot be deactivated or deleted
63 * @var bool
64 */
65 protected $protected = FALSE;
66
67 /**
68 * @var \stdClass
69 */
70 protected $composerManifest;
71
72 /**
73 * Meta information about this package
74 * @var MetaData
75 */
76 protected $packageMetaData;
77
78 /**
79 * The namespace of the classes contained in this package
80 * @var string
81 */
82 protected $namespace;
83
84 /**
85 * @var PackageManager
86 */
87 protected $packageManager;
88
89 /**
90 * Constructor
91 *
92 * @param PackageManager $packageManager the package manager which knows this package
93 * @param string $packageKey Key of this package
94 * @param string $packagePath Absolute path to the location of the package's composer manifest
95 * @throws Exception\InvalidPackageKeyException if an invalid package key was passed
96 * @throws Exception\InvalidPackagePathException if an invalid package path was passed
97 * @throws Exception\InvalidPackageManifestException if no composer manifest file could be found
98 */
99 public function __construct(PackageManager $packageManager, $packageKey, $packagePath) {
100 if (!$packageManager->isPackageKeyValid($packageKey)) {
101 throw new Exception\InvalidPackageKeyException('"' . $packageKey . '" is not a valid package key.', 1217959511);
102 }
103 if (!(@is_dir($packagePath) || (is_link($packagePath) && is_dir($packagePath)))) {
104 throw new Exception\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);
105 }
106 if (substr($packagePath, -1, 1) !== '/') {
107 throw new Exception\InvalidPackagePathException(sprintf('The package path "%s" provided for package "%s" has no trailing forward slash.', $packagePath, $packageKey), 1166633722);
108 }
109 $this->packageManager = $packageManager;
110 $this->packageKey = $packageKey;
111 $this->packagePath = $packagePath;
112 try {
113 $this->composerManifest = $this->packageManager->getComposerManifest($this->packagePath);
114 } catch (Exception\MissingPackageManifestException $exception) {
115 if (!$this->loadExtensionEmconf()) {
116 throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
117 }
118 }
119 $this->loadFlagsFromComposerManifest();
120 }
121
122 /**
123 * Loads package management related flags from the "extra:typo3/cms:Package" section
124 * of extensions composer.json files into local properties
125 *
126 * @return void
127 */
128 protected function loadFlagsFromComposerManifest() {
129 $extraFlags = $this->getValueFromComposerManifest('extra');
130 if ($extraFlags !== NULL && isset($extraFlags->{"typo3/cms"}->{"Package"})) {
131 foreach ($extraFlags->{"typo3/cms"}->{"Package"} as $flagName => $flagValue) {
132 if (property_exists($this, $flagName)) {
133 $this->{$flagName} = $flagValue;
134 }
135 }
136 }
137 }
138
139 /**
140 * @return bool
141 */
142 public function isPartOfFactoryDefault() {
143 return $this->partOfFactoryDefault;
144 }
145
146 /**
147 * @return bool
148 */
149 public function isPartOfMinimalUsableSystem() {
150 return $this->partOfMinimalUsableSystem;
151 }
152
153 /**
154 * Invokes custom PHP code directly after the package manager has been initialized.
155 *
156 * @param \TYPO3\CMS\Core\Core\Bootstrap $bootstrap The current bootstrap
157 * @return void
158 */
159 public function boot(\TYPO3\CMS\Core\Core\Bootstrap $bootstrap) {
160 }
161
162 /**
163 * Returns the package key of this package.
164 *
165 * @return string
166 * @api
167 */
168 public function getPackageKey() {
169 return $this->packageKey;
170 }
171
172 /**
173 * Tells if this package is protected and therefore cannot be deactivated or deleted
174 *
175 * @return bool
176 * @api
177 */
178 public function isProtected() {
179 return $this->protected;
180 }
181
182 /**
183 * Sets the protection flag of the package
184 *
185 * @param bool $protected TRUE if the package should be protected, otherwise FALSE
186 * @return void
187 * @api
188 */
189 public function setProtected($protected) {
190 $this->protected = (bool)$protected;
191 }
192
193 /**
194 * Returns the full path to this package's main directory
195 *
196 * @return string Path to this package's main directory
197 * @api
198 */
199 public function getPackagePath() {
200 return $this->packagePath;
201 }
202
203 /**
204 * Returns the full path to this package's Classes directory
205 *
206 * @return string Path to this package's Classes directory
207 * @api
208 */
209 public function getClassesPath() {
210 return $this->packagePath . self::DIRECTORY_CLASSES;
211 }
212
213 /**
214 * Returns the full path to this package's Resources directory
215 *
216 * @return string Path to this package's Resources directory
217 * @api
218 */
219 public function getResourcesPath() {
220 return $this->packagePath . self::DIRECTORY_RESOURCES;
221 }
222
223 /**
224 * Returns the full path to this package's Configuration directory
225 *
226 * @return string Path to this package's Configuration directory
227 * @api
228 */
229 public function getConfigurationPath() {
230 return $this->packagePath . self::DIRECTORY_CONFIGURATION;
231 }
232
233 /**
234 * Fetches MetaData information from ext_emconf.php, used for resolving dependencies as well
235 * @return bool
236 */
237 protected function loadExtensionEmconf() {
238 $_EXTKEY = $this->packageKey;
239 $path = $this->packagePath . 'ext_emconf.php';
240 $EM_CONF = NULL;
241 if (@file_exists($path)) {
242 include $path;
243 if (is_array($EM_CONF[$_EXTKEY])) {
244 $this->extensionManagerConfiguration = $EM_CONF[$_EXTKEY];
245 $this->mapExtensionManagerConfigurationToComposerManifest();
246 return TRUE;
247 }
248 }
249 return FALSE;
250 }
251
252 /**
253 * Fetches information from ext_emconf.php and maps it so it is treated as it would come from composer.json
254 *
255 * @return void
256 */
257 protected function mapExtensionManagerConfigurationToComposerManifest() {
258 if (is_array($this->extensionManagerConfiguration)) {
259 $extensionManagerConfiguration = $this->extensionManagerConfiguration;
260 $composerManifest = $this->composerManifest = new \stdClass();
261 $composerManifest->name = $this->getPackageKey();
262 $composerManifest->type = 'typo3-cms-extension';
263 $composerManifest->description = $extensionManagerConfiguration['title'];
264 $composerManifest->version = $extensionManagerConfiguration['version'];
265 if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
266 $composerManifest->require = new \stdClass();
267 foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
268 if (!empty($requiredPackageKey)) {
269 $composerManifest->require->$requiredPackageKey = $requiredPackageVersion;
270 } else {
271 // @todo throw meaningful exception or fail silently?
272 }
273 }
274 }
275 if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
276 $composerManifest->conflict = new \stdClass();
277 foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
278 if (!empty($conflictingPackageKey)) {
279 $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
280 } else {
281 // @todo throw meaningful exception or fail silently?
282 }
283 }
284 }
285 if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
286 $composerManifest->suggest = new \stdClass();
287 foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
288 if (!empty($suggestedPackageKey)) {
289 $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
290 } else {
291 // @todo throw meaningful exception or fail silently?
292 }
293 }
294 }
295 }
296 }
297
298 /**
299 * Returns the package meta data object of this package.
300 *
301 * @return MetaData
302 */
303 public function getPackageMetaData() {
304 if ($this->packageMetaData === NULL) {
305 $this->packageMetaData = new MetaData($this->getPackageKey());
306 $this->packageMetaData->setDescription($this->getValueFromComposerManifest('description'));
307 $this->packageMetaData->setVersion($this->getValueFromComposerManifest('version'));
308 $requirements = $this->getValueFromComposerManifest('require');
309 if ($requirements !== NULL) {
310 foreach ($requirements as $requirement => $version) {
311 if ($this->packageRequirementIsComposerPackage($requirement) === FALSE) {
312 // Skip non-package requirements
313 continue;
314 }
315 $packageKey = $this->packageManager->getPackageKeyFromComposerName($requirement);
316 $constraint = new MetaData\PackageConstraint(MetaData::CONSTRAINT_TYPE_DEPENDS, $packageKey);
317 $this->packageMetaData->addConstraint($constraint);
318 }
319 }
320 $suggestions = $this->getValueFromComposerManifest('suggest');
321 if ($suggestions !== NULL) {
322 foreach ($suggestions as $suggestion => $version) {
323 if ($this->packageRequirementIsComposerPackage($suggestion) === FALSE) {
324 // Skip non-package requirements
325 continue;
326 }
327 $packageKey = $this->packageManager->getPackageKeyFromComposerName($suggestion);
328 $constraint = new MetaData\PackageConstraint(MetaData::CONSTRAINT_TYPE_SUGGESTS, $packageKey);
329 $this->packageMetaData->addConstraint($constraint);
330 }
331 }
332 }
333 return $this->packageMetaData;
334 }
335
336 /**
337 * Returns an array of packages this package replaces
338 *
339 * @return array
340 */
341 public function getPackageReplacementKeys() {
342 // The cast to array is required since the manifest returns data with type mixed
343 return (array)$this->getValueFromComposerManifest('replace') ?: array();
344 }
345
346 /**
347 * Returns the PHP namespace of classes in this package, also uses a fallback for extensions without having a "."
348 * in the package key.
349 *
350 * @return string
351 */
352 public function getNamespace() {
353 if (!$this->namespace) {
354 $packageKey = $this->getPackageKey();
355 if (strpos($packageKey, '.') === FALSE) {
356 // Old school with unknown vendor name
357 $this->namespace = '*\\' . \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToUpperCamelCase($packageKey);
358 } else {
359 $this->namespace = str_replace('.', '\\', $packageKey);
360 }
361 }
362 return $this->namespace;
363 }
364
365 /**
366 * @return array
367 */
368 public function getClassFilesFromAutoloadRegistry() {
369 $autoloadRegistryPath = $this->packagePath . 'ext_autoload.php';
370 if (@file_exists($autoloadRegistryPath)) {
371 return require $autoloadRegistryPath;
372 }
373 return array();
374 }
375
376 /**
377 * Fetches class aliases registered via Migrations/Code/ClassAliasMap.php
378 *
379 * @return array
380 */
381 public function getClassAliases() {
382 if (!is_array($this->classAliases)) {
383 try {
384 $extensionClassAliasMapPathAndFilename = $this->packagePath . 'Migrations/Code/ClassAliasMap.php';
385 if (@file_exists($extensionClassAliasMapPathAndFilename)) {
386 $this->classAliases = require $extensionClassAliasMapPathAndFilename;
387 }
388 } catch (\BadFunctionCallException $e) {
389 }
390 if (!is_array($this->classAliases)) {
391 $this->classAliases = array();
392 }
393 }
394 return $this->classAliases;
395 }
396
397 /**
398 * Check whether the given package requirement (like "typo3/flow" or "php") is a composer package or not
399 *
400 * @param string $requirement the composer requirement string
401 * @return bool TRUE if $requirement is a composer package (contains a slash), FALSE otherwise
402 */
403 protected function packageRequirementIsComposerPackage($requirement) {
404 // According to http://getcomposer.org/doc/02-libraries.md#platform-packages
405 // the following regex should capture all non composer requirements.
406 // typo3 is included in the list because it's a meta package and not supported for now.
407 // composer/installers is included until extensionmanager can handle composer packages natively
408 return preg_match('/^(php(-64bit)?|ext-[^\/]+|lib-(curl|iconv|libxml|openssl|pcre|uuid|xsl)|typo3|composer\/installers)$/', $requirement) !== 1;
409 }
410
411 /**
412 * Returns contents of Composer manifest - or part there of if a key is given.
413 *
414 * @param string $key Optional. Only return the part of the manifest indexed by 'key'
415 * @return mixed|NULL
416 * @see json_decode for return values
417 */
418 public function getValueFromComposerManifest($key = NULL) {
419 if ($key === NULL) {
420 return $this->composerManifest;
421 }
422
423 if (isset($this->composerManifest->{$key})) {
424 $value = $this->composerManifest->{$key};
425 } else {
426 $value = NULL;
427 }
428 return $value;
429 }
430
431 /**
432 * Added by TYPO3 CMS
433 *
434 * The package caching serializes package objects.
435 * The package manager instance may not be serialized
436 * as a fresh instance is created upon every request.
437 *
438 * This method will be removed once the package is
439 * released of the package manager dependency.
440 *
441 * @return array
442 */
443 public function __sleep() {
444 $properties = get_class_vars(get_class($this));
445 unset($properties['packageManager']);
446 return array_keys($properties);
447 }
448
449 /**
450 * Added by TYPO3 CMS
451 *
452 * The package caching deserializes package objects.
453 * A fresh package manager instance has to be set
454 * during bootstrapping.
455 *
456 * This method will be removed once the package is
457 * released of the package manager dependency.
458 */
459 public function __wakeup() {
460 if (isset($GLOBALS['TYPO3_currentPackageManager'])) {
461 $this->packageManager = $GLOBALS['TYPO3_currentPackageManager'];
462 }
463 }
464 }