[BUGFIX] Adhere "suggestions" when resolving ext loading order
[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 use TYPO3\CMS\Core\Utility\PathUtility;
18
19 /**
20 * A Package representing the details of an extension and/or a composer package
21 * Adapted from FLOW for TYPO3 CMS
22 */
23 class Package implements PackageInterface {
24
25 /**
26 * @var array
27 */
28 protected $extensionManagerConfiguration = array();
29
30 /**
31 * @var array
32 */
33 protected $classAliases;
34
35 /**
36 * @var array
37 */
38 protected $ignoredClassNames = array();
39
40 /**
41 * If this package is part of factory default, it will be activated
42 * during first installation.
43 *
44 * @var bool
45 */
46 protected $partOfFactoryDefault = FALSE;
47
48 /**
49 * If this package is part of minimal usable system, it will be
50 * activated if PackageStates is created from scratch.
51 *
52 * @var bool
53 */
54 protected $partOfMinimalUsableSystem = FALSE;
55
56 /**
57 * Unique key of this package.
58 * @var string
59 */
60 protected $packageKey;
61
62 /**
63 * @var string
64 */
65 protected $manifestPath = '';
66
67 /**
68 * Full path to this package's main directory
69 * @var string
70 */
71 protected $packagePath;
72
73 /**
74 * Full path to this package's PSR-0 class loader entry point
75 * @var string
76 */
77 protected $classesPath;
78
79 /**
80 * If this package is protected and therefore cannot be deactivated or deleted
81 * @var bool
82 * @api
83 */
84 protected $protected = FALSE;
85
86 /**
87 * @var \stdClass
88 */
89 protected $composerManifest;
90
91 /**
92 * Meta information about this package
93 * @var MetaData
94 */
95 protected $packageMetaData;
96
97 /**
98 * The namespace of the classes contained in this package
99 * @var string
100 */
101 protected $namespace;
102
103 /**
104 * @var PackageManager
105 */
106 protected $packageManager;
107
108 /**
109 * Constructor
110 *
111 * @param PackageManager $packageManager the package manager which knows this package
112 * @param string $packageKey Key of this package
113 * @param string $packagePath Absolute path to the location of the package's composer manifest
114 * @throws Exception\InvalidPackageKeyException if an invalid package key was passed
115 * @throws Exception\InvalidPackagePathException if an invalid package path was passed
116 * @throws Exception\InvalidPackageManifestException if no composer manifest file could be found
117 */
118 public function __construct(PackageManager $packageManager, $packageKey, $packagePath) {
119 if (!$packageManager->isPackageKeyValid($packageKey)) {
120 throw new Exception\InvalidPackageKeyException('"' . $packageKey . '" is not a valid package key.', 1217959511);
121 }
122 if (!(@is_dir($packagePath) || (is_link($packagePath) && is_dir($packagePath)))) {
123 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);
124 }
125 if (substr($packagePath, -1, 1) !== '/') {
126 throw new Exception\InvalidPackagePathException(sprintf('The package path "%s" provided for package "%s" has no trailing forward slash.', $packagePath, $packageKey), 1166633722);
127 }
128 $this->packageManager = $packageManager;
129 $this->packageKey = $packageKey;
130 $this->packagePath = PathUtility::sanitizeTrailingSeparator($packagePath);
131 $this->classesPath = PathUtility::sanitizeTrailingSeparator($this->packagePath . self::DIRECTORY_CLASSES);
132 try {
133 $this->getComposerManifest();
134 } catch (Exception\MissingPackageManifestException $exception) {
135 if (!$this->loadExtensionEmconf()) {
136 throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
137 }
138 }
139 $this->loadFlagsFromComposerManifest();
140 }
141
142 /**
143 * Loads package management related flags from the "extra:typo3/cms:Package" section
144 * of extensions composer.json files into local properties
145 *
146 * @return void
147 */
148 protected function loadFlagsFromComposerManifest() {
149 $extraFlags = $this->getComposerManifest('extra');
150 if ($extraFlags !== NULL && isset($extraFlags->{"typo3/cms"}->{"Package"})) {
151 foreach ($extraFlags->{"typo3/cms"}->{"Package"} as $flagName => $flagValue) {
152 if (property_exists($this, $flagName)) {
153 $this->{$flagName} = $flagValue;
154 }
155 }
156 }
157 }
158
159 /**
160 * @return bool
161 */
162 public function isPartOfFactoryDefault() {
163 return $this->partOfFactoryDefault;
164 }
165
166 /**
167 * @return bool
168 */
169 public function isPartOfMinimalUsableSystem() {
170 return $this->partOfMinimalUsableSystem;
171 }
172
173 /**
174 * Invokes custom PHP code directly after the package manager has been initialized.
175 *
176 * @param \TYPO3\CMS\Core\Core\Bootstrap $bootstrap The current bootstrap
177 * @return void
178 */
179 public function boot(\TYPO3\CMS\Core\Core\Bootstrap $bootstrap) {
180 }
181
182 /**
183 * Returns the package key of this package.
184 *
185 * @return string
186 * @api
187 */
188 public function getPackageKey() {
189 return $this->packageKey;
190 }
191
192 /**
193 * Tells if this package is protected and therefore cannot be deactivated or deleted
194 *
195 * @return bool
196 * @api
197 */
198 public function isProtected() {
199 return $this->protected;
200 }
201
202 /**
203 * Sets the protection flag of the package
204 *
205 * @param bool $protected TRUE if the package should be protected, otherwise FALSE
206 * @return void
207 * @api
208 */
209 public function setProtected($protected) {
210 $this->protected = (bool)$protected;
211 }
212
213 /**
214 * Returns the full path to this package's main directory
215 *
216 * @return string Path to this package's main directory
217 * @api
218 */
219 public function getPackagePath() {
220 return $this->packagePath;
221 }
222
223 /**
224 * Returns the full path to the packages Composer manifest
225 *
226 * @return string
227 */
228 public function getManifestPath() {
229 return $this->packagePath . $this->manifestPath;
230 }
231
232 /**
233 * Returns the full path to this package's Classes directory
234 *
235 * @return string Path to this package's Classes directory
236 * @api
237 */
238 public function getClassesPath() {
239 return $this->classesPath;
240 }
241
242 /**
243 * Returns the full path to this package's Resources directory
244 *
245 * @return string Path to this package's Resources directory
246 * @api
247 */
248 public function getResourcesPath() {
249 return $this->packagePath . self::DIRECTORY_RESOURCES;
250 }
251
252 /**
253 * Returns the full path to this package's Configuration directory
254 *
255 * @return string Path to this package's Configuration directory
256 * @api
257 */
258 public function getConfigurationPath() {
259 return $this->packagePath . self::DIRECTORY_CONFIGURATION;
260 }
261
262 /**
263 * @return bool
264 */
265 protected function loadExtensionEmconf() {
266 $_EXTKEY = $this->packageKey;
267 $path = $this->packagePath . 'ext_emconf.php';
268 $EM_CONF = NULL;
269 if (@file_exists($path)) {
270 include $path;
271 if (is_array($EM_CONF[$_EXTKEY])) {
272 $this->extensionManagerConfiguration = $EM_CONF[$_EXTKEY];
273 $this->mapExtensionManagerConfigurationToComposerManifest();
274 return TRUE;
275 }
276 }
277 return FALSE;
278 }
279
280 /**
281 *
282 */
283 protected function mapExtensionManagerConfigurationToComposerManifest() {
284 if (is_array($this->extensionManagerConfiguration)) {
285 $extensionManagerConfiguration = $this->extensionManagerConfiguration;
286 $composerManifest = $this->composerManifest = new \stdClass();
287 $composerManifest->name = $this->getPackageKey();
288 $composerManifest->type = 'typo3-cms-extension';
289 $composerManifest->description = $extensionManagerConfiguration['title'];
290 $composerManifest->version = $extensionManagerConfiguration['version'];
291 if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
292 $composerManifest->require = new \stdClass();
293 foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
294 if (!empty($requiredPackageKey)) {
295 $composerManifest->require->$requiredPackageKey = $requiredPackageVersion;
296 } else {
297 // @todo throw meaningful exception or fail silently?
298 }
299 }
300 }
301 if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
302 $composerManifest->conflict = new \stdClass();
303 foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
304 if (!empty($conflictingPackageKey)) {
305 $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
306 } else {
307 // @todo throw meaningful exception or fail silently?
308 }
309 }
310 }
311 if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
312 $composerManifest->suggest = new \stdClass();
313 foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
314 if (!empty($suggestedPackageKey)) {
315 $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
316 } else {
317 // @todo throw meaningful exception or fail silently?
318 }
319 }
320 }
321 }
322 }
323
324 /**
325 * Returns the package meta data object of this package.
326 *
327 * @return MetaData
328 */
329 public function getPackageMetaData() {
330 if ($this->packageMetaData === NULL) {
331 $this->packageMetaData = new MetaData($this->getPackageKey());
332 $this->packageMetaData->setDescription($this->getComposerManifest('description'));
333 $this->packageMetaData->setVersion($this->getComposerManifest('version'));
334 $requirements = $this->getComposerManifest('require');
335 if ($requirements !== NULL) {
336 foreach ($requirements as $requirement => $version) {
337 if ($this->packageRequirementIsComposerPackage($requirement) === FALSE) {
338 // Skip non-package requirements
339 continue;
340 }
341 $packageKey = $this->packageManager->getPackageKeyFromComposerName($requirement);
342 $constraint = new MetaData\PackageConstraint(MetaData::CONSTRAINT_TYPE_DEPENDS, $packageKey);
343 $this->packageMetaData->addConstraint($constraint);
344 }
345 }
346 $suggestions = $this->getComposerManifest('suggest');
347 if ($suggestions !== NULL) {
348 foreach ($suggestions as $suggestion => $version) {
349 if ($this->packageRequirementIsComposerPackage($suggestion) === FALSE) {
350 // Skip non-package requirements
351 continue;
352 }
353 $packageKey = $this->packageManager->getPackageKeyFromComposerName($suggestion);
354 $constraint = new MetaData\PackageConstraint(MetaData::CONSTRAINT_TYPE_SUGGESTS, $packageKey);
355 $this->packageMetaData->addConstraint($constraint);
356 }
357 }
358 }
359 return $this->packageMetaData;
360 }
361
362 /**
363 * @return array
364 */
365 public function getPackageReplacementKeys() {
366 // The cast to array is required since the manifest returns data with type mixed
367 return (array)$this->getComposerManifest('replace') ?: array();
368 }
369
370 /**
371 * Returns the PHP namespace of classes in this package.
372 *
373 * @return string
374 * @throws Exception\InvalidPackageStateException
375 */
376 public function getNamespace() {
377 if(!$this->namespace) {
378 $packageKey = $this->getPackageKey();
379 if (strpos($packageKey, '.') === FALSE) {
380 // Old school with unknown vendor name
381 $this->namespace = '*\\' . \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToUpperCamelCase($packageKey);
382 } else {
383 $this->namespace = str_replace('.', '\\', $packageKey);
384 }
385 }
386 return $this->namespace;
387 }
388
389 /**
390 * @param array $classFiles
391 * @return array
392 */
393 protected function filterClassFiles(array $classFiles) {
394 $classesNotMatchingClassRule = array_filter(array_keys($classFiles), function($className) {
395 return preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\\\x7f-\xff]*$/', $className) !== 1;
396 });
397 foreach ($classesNotMatchingClassRule as $forbiddenClassName) {
398 unset($classFiles[$forbiddenClassName]);
399 }
400 foreach ($this->ignoredClassNames as $ignoredClassName) {
401 if (isset($classFiles[$ignoredClassName])) {
402 unset($classFiles[$ignoredClassName]);
403 }
404 }
405 return $classFiles;
406 }
407
408 /**
409 * @return array
410 */
411 public function getClassFilesFromAutoloadRegistry() {
412 $autoloadRegistryPath = $this->packagePath . 'ext_autoload.php';
413 if (@file_exists($autoloadRegistryPath)) {
414 return require $autoloadRegistryPath;
415 }
416 return array();
417 }
418
419 /**
420 *
421 */
422 public function getClassAliases() {
423 if (!is_array($this->classAliases)) {
424 try {
425 $extensionClassAliasMapPathAndFilename = $this->getPackagePath() . 'Migrations/Code/ClassAliasMap.php';
426 if (@file_exists($extensionClassAliasMapPathAndFilename)) {
427 $this->classAliases = require $extensionClassAliasMapPathAndFilename;
428 }
429 } catch (\BadFunctionCallException $e) {
430 }
431 if (!is_array($this->classAliases)) {
432 $this->classAliases = array();
433 }
434 }
435 return $this->classAliases;
436 }
437
438 /**
439 * Check whether the given package requirement (like "typo3/flow" or "php") is a composer package or not
440 *
441 * @param string $requirement the composer requirement string
442 * @return bool TRUE if $requirement is a composer package (contains a slash), FALSE otherwise
443 */
444 protected function packageRequirementIsComposerPackage($requirement) {
445 // According to http://getcomposer.org/doc/02-libraries.md#platform-packages
446 // the following regex should capture all non composer requirements.
447 // typo3 is included in the list because it's a meta package and not supported for now.
448 // composer/installers is included until extensionmanager can handle composer packages natively
449 return preg_match('/^(php(-64bit)?|ext-[^\/]+|lib-(curl|iconv|libxml|openssl|pcre|uuid|xsl)|typo3|composer\/installers)$/', $requirement) !== 1;
450 }
451
452 /**
453 * Returns contents of Composer manifest - or part there of.
454 *
455 * @param string $key Optional. Only return the part of the manifest indexed by 'key'
456 * @return mixed|NULL
457 * @see json_decode for return values
458 */
459 public function getComposerManifest($key = NULL) {
460 if (!isset($this->composerManifest)) {
461 $this->composerManifest = PackageManager::getComposerManifest($this->getManifestPath());
462 }
463
464 return PackageManager::getComposerManifest($this->getManifestPath(), $key, $this->composerManifest);
465 }
466
467 /**
468 * Builds and returns an array of class names => file names of all
469 * *.php files in the package's Classes directory and its sub-
470 * directories.
471 *
472 * @param string $classesPath Base path acting as the parent directory for potential class files
473 * @param string $extraNamespaceSegment A PHP class namespace segment which should be inserted like so: \TYPO3\PackageKey\{namespacePrefix\}PathSegment\PathSegment\Filename
474 * @param string $subDirectory Used internally
475 * @param int $recursionLevel Used internally
476 * @return array
477 * @throws Exception if recursion into directories was too deep or another error occurred
478 */
479 protected function buildArrayOfClassFiles($classesPath, $extraNamespaceSegment = '', $subDirectory = '', $recursionLevel = 0) {
480 $classFiles = array();
481 $currentPath = $classesPath . $subDirectory;
482 $currentRelativePath = substr($currentPath, strlen($this->packagePath));
483
484 if (!is_dir($currentPath)) {
485 return array();
486 }
487 if ($recursionLevel > 100) {
488 throw new Exception('Recursion too deep.', 1166635495);
489 }
490
491 try {
492 $classesDirectoryIterator = new \DirectoryIterator($currentPath);
493 while ($classesDirectoryIterator->valid()) {
494 $filename = $classesDirectoryIterator->getFilename();
495 if ($filename[0] !== '.') {
496 if (is_dir($currentPath . $filename)) {
497 $classFiles = array_merge($classFiles, $this->buildArrayOfClassFiles($classesPath, $extraNamespaceSegment, $subDirectory . $filename . '/', ($recursionLevel + 1)));
498 } else {
499 if (substr($filename, -4, 4) === '.php') {
500 $className = (str_replace('/', '\\', ($extraNamespaceSegment . substr($currentPath, strlen($classesPath)) . substr($filename, 0, -4))));
501 $classFiles[$className] = $currentRelativePath . $filename;
502 }
503 }
504 }
505 $classesDirectoryIterator->next();
506 }
507
508 } catch (\Exception $exception) {
509 throw new Exception($exception->getMessage(), 1166633721);
510 }
511 return $classFiles;
512 }
513
514 /**
515 * Added by TYPO3 CMS
516 *
517 * The package caching serializes package objects.
518 * The package manager instance may not be serialized
519 * as a fresh instance is created upon every request.
520 *
521 * This method will be removed once the package is
522 * released of the package manager dependency.
523 *
524 * @return array
525 */
526 public function __sleep() {
527 $properties = get_class_vars(get_class($this));
528 unset($properties['packageManager']);
529 return array_keys($properties);
530 }
531
532 /**
533 * Added by TYPO3 CMS
534 *
535 * The package caching deserializes package objects.
536 * A fresh package manager instance has to be set
537 * during bootstrapping.
538 *
539 * This method will be removed once the package is
540 * released of the package manager dependency.
541 */
542 public function __wakeup() {
543 if (isset($GLOBALS['TYPO3_currentPackageManager'])) {
544 $this->packageManager = $GLOBALS['TYPO3_currentPackageManager'];
545 }
546 }
547 }