e8a0e4a9774f5093bd71f8bbb5f503a4293c345e
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Package / PackageManager.php
1 <?php
2 namespace TYPO3\CMS\Core\Package;
3
4 /* *
5 * This script belongs to the TYPO3 Flow framework. *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13
14 use TYPO3\Flow\Annotations as Flow;
15
16 /**
17 * The default TYPO3 Package Manager
18 * Adapted from FLOW for TYPO3 CMS
19 *
20 * @api
21 * @Flow\Scope("singleton")
22 */
23 class PackageManager extends \TYPO3\Flow\Package\PackageManager implements \TYPO3\CMS\Core\SingletonInterface {
24
25 /**
26 * @var \TYPO3\CMS\Core\Core\ClassLoader
27 */
28 protected $classLoader;
29
30 /**
31 * @var \TYPO3\CMS\Core\Package\DependencyResolver
32 */
33 protected $dependencyResolver;
34
35 /**
36 * @var \TYPO3\CMS\Core\Core\Bootstrap
37 */
38 protected $bootstrap;
39
40 /**
41 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
42 */
43 protected $coreCache;
44
45 /**
46 * @var string
47 */
48 protected $cacheIdentifier;
49
50 /**
51 * @var array
52 */
53 protected $extAutoloadClassFiles;
54
55 /**
56 * @var array
57 */
58 protected $packagesBasePaths = array();
59
60 /**
61 * @var array
62 */
63 protected $packageAliasMap = array();
64
65 /**
66 * @var array
67 */
68 protected $runtimeActivatedPackages = array();
69
70 /**
71 * Absolute path leading to the various package directories
72 * @var string
73 */
74 protected $packagesBasePath = PATH_site;
75
76 /**
77 * Constructor
78 */
79 public function __construct() {
80 $this->packagesBasePaths = array(
81 'local' => PATH_typo3conf . 'ext',
82 'global' => PATH_typo3 . 'ext',
83 'sysext' => PATH_typo3 . 'sysext',
84 'composer' => PATH_site . 'Packages',
85 );
86 }
87
88 /**
89 * @param \TYPO3\CMS\Core\Core\ClassLoader $classLoader
90 */
91 public function injectClassLoader(\TYPO3\CMS\Core\Core\ClassLoader $classLoader) {
92 $this->classLoader = $classLoader;
93 }
94
95 /**
96 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
97 */
98 public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache) {
99 $this->coreCache = $coreCache;
100 }
101
102 /**
103 * @param DependencyResolver
104 */
105 public function injectDependencyResolver(DependencyResolver $dependencyResolver) {
106 $this->dependencyResolver = $dependencyResolver;
107 }
108
109 /**
110 * Initializes the package manager
111 *
112 * @param \TYPO3\CMS\Core\Core\Bootstrap|\TYPO3\Flow\Core\Bootstrap $bootstrap The current bootstrap; Flow Bootstrap is here by intention to keep the PackageManager valid to the interface
113 * @return void
114 */
115 public function initialize(\TYPO3\Flow\Core\Bootstrap $bootstrap) {
116 $this->bootstrap = $bootstrap;
117 $this->packageStatesPathAndFilename = PATH_typo3conf . 'PackageStates.php';
118 $this->packageFactory = new PackageFactory($this);
119
120 $this->loadPackageStates();
121
122 $requiredList = array();
123 foreach ($this->packages as $packageKey => $package) {
124 /** @var $package Package */
125 $protected = $package->isProtected();
126 if ($protected) {
127 $requiredList[$packageKey] = $package;
128 }
129 if (isset($this->packageStatesConfiguration['packages'][$packageKey]['state']) && $this->packageStatesConfiguration['packages'][$packageKey]['state'] === 'active') {
130 $this->activePackages[$packageKey] = $package;
131 }
132 }
133 $previousActivePackage = $this->activePackages;
134 $this->activePackages = array_merge($requiredList, $this->activePackages);
135
136 if ($this->activePackages != $previousActivePackage) {
137 foreach ($requiredList as $requiredPackageKey => $package) {
138 $this->packageStatesConfiguration['packages'][$requiredPackageKey]['state'] = 'active';
139 }
140 $this->sortAndSavePackageStates();
141 }
142
143 //@deprecated since 6.2, don't use
144 if (!defined('REQUIRED_EXTENSIONS')) {
145 // List of extensions required to run the core
146 define('REQUIRED_EXTENSIONS', implode(',', array_keys($requiredList)));
147 }
148
149 $cacheIdentifier = $this->getCacheIdentifier();
150 if ($cacheIdentifier === NULL) {
151 // Create an artificial cache identifier if the package states file is not available yet
152 // in order that the class loader and class alias map can cache anyways.
153 $cacheIdentifier = md5(implode('###', array_keys($this->activePackages)));
154 }
155 $this->classLoader->setCacheIdentifier($cacheIdentifier)->setPackages($this->activePackages);
156
157 foreach ($this->activePackages as $package) {
158 /** @var $package Package */
159 $package->boot($bootstrap);
160 }
161
162 $this->saveToPackageCache();
163 }
164
165 /**
166 * @return string
167 */
168 protected function getCacheIdentifier() {
169 if ($this->cacheIdentifier === NULL) {
170 if (@file_exists($this->packageStatesPathAndFilename)) {
171 $this->cacheIdentifier = md5_file($this->packageStatesPathAndFilename);
172 } else {
173 $this->cacheIdentifier = NULL;
174 }
175 }
176 return $this->cacheIdentifier;
177 }
178
179 /**
180 * @return string
181 */
182 protected function getCacheEntryIdentifier() {
183 $cacheIdentifier = $this->getCacheIdentifier();
184 return $cacheIdentifier !== NULL ? 'PackageManager_' . $cacheIdentifier : NULL;
185 }
186
187 /**
188 *
189 */
190 protected function saveToPackageCache() {
191 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
192 if ($cacheEntryIdentifier !== NULL && !$this->coreCache->has($cacheEntryIdentifier)) {
193 // Package objects get their own cache entry, so PHP does not have to parse the serialized string
194 $packageObjectsCacheEntryIdentifier = uniqid('PackageObjects_');
195 // Build cache file
196 $packageCache = array(
197 'packageStatesConfiguration' => $this->packageStatesConfiguration,
198 'packageAliasMap' => $this->packageAliasMap,
199 'packageKeys' => $this->packageKeys,
200 'declaringPackageClassPathsAndFilenames' => array(),
201 'packageObjectsCacheEntryIdentifier' => $packageObjectsCacheEntryIdentifier
202 );
203 foreach ($this->packages as $package) {
204 if (!isset($packageCache['declaringPackageClassPathsAndFilenames'][$packageClassName = get_class($package)])) {
205 $reflectionPackageClass = new \ReflectionClass($packageClassName);
206 $packageCache['declaringPackageClassPathsAndFilenames'][$packageClassName] = $reflectionPackageClass->getFileName();
207 }
208 }
209 $this->coreCache->set($packageObjectsCacheEntryIdentifier, serialize($this->packages));
210 $this->coreCache->set(
211 $cacheEntryIdentifier,
212 'return ' . PHP_EOL .
213 var_export($packageCache, TRUE) . ';'
214 );
215 }
216 }
217
218 /**
219 * Loads the states of available packages from the PackageStates.php file.
220 * The result is stored in $this->packageStatesConfiguration.
221 *
222 * @throws Exception\PackageStatesUnavailableException
223 * @return void
224 */
225 protected function loadPackageStates() {
226 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
227 if ($cacheEntryIdentifier !== NULL && $this->coreCache->has($cacheEntryIdentifier) && $packageCache = $this->coreCache->requireOnce($cacheEntryIdentifier)) {
228 foreach ($packageCache['declaringPackageClassPathsAndFilenames'] as $packageClassPathAndFilename) {
229 require_once $packageClassPathAndFilename;
230 }
231 $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
232 $this->packageAliasMap = $packageCache['packageAliasMap'];
233 $this->packageKeys = $packageCache['packageKeys'];
234 $GLOBALS['TYPO3_currentPackageManager'] = $this;
235 // Strip off PHP Tags from Php Cache Frontend
236 $packageObjects = substr(substr($this->coreCache->get($packageCache['packageObjectsCacheEntryIdentifier']), 6), 0, -2);
237 $this->packages = unserialize($packageObjects);
238 unset($GLOBALS['TYPO3_currentPackageManager']);
239 } else {
240 $this->packageStatesConfiguration = @include($this->packageStatesPathAndFilename) ?: array();
241 if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
242 $this->packageStatesConfiguration = array();
243 }
244 if ($this->packageStatesConfiguration !== array()) {
245 $this->registerPackagesFromConfiguration();
246 } else {
247 throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
248 }
249 }
250 }
251
252
253 /**
254 * Scans all directories in the packages directories for available packages.
255 * For each package a Package object is created and stored in $this->packages.
256 *
257 * @return void
258 * @throws \TYPO3\Flow\Package\Exception\DuplicatePackageException
259 */
260 public function scanAvailablePackages() {
261 $previousPackageStatesConfiguration = $this->packageStatesConfiguration;
262
263 if (isset($this->packageStatesConfiguration['packages'])) {
264 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $configuration) {
265 if (!@file_exists($this->packagesBasePath . $configuration['packagePath'])) {
266 unset($this->packageStatesConfiguration['packages'][$packageKey]);
267 }
268 }
269 } else {
270 $this->packageStatesConfiguration['packages'] = array();
271 }
272
273 foreach ($this->packagesBasePaths as $key => $packagesBasePath) {
274 if (!is_dir($packagesBasePath)) {
275 unset($this->packagesBasePaths[$key]);
276 }
277 }
278
279 $packagePaths = $this->scanLegacyExtensions();
280 foreach ($this->packagesBasePaths as $packagesBasePath) {
281 $this->scanPackagesInPath($packagesBasePath, $packagePaths);
282 }
283
284 foreach ($packagePaths as $packagePath => $composerManifestPath) {
285 $packagesBasePath = PATH_site;
286 foreach ($this->packagesBasePaths as $basePath) {
287 if (strpos($packagePath, $basePath) === 0) {
288 $packagesBasePath = $basePath;
289 break;
290 }
291 }
292 try {
293 $composerManifest = self::getComposerManifest($composerManifestPath);
294 $packageKey = PackageFactory::getPackageKeyFromManifest($composerManifest, $packagePath, $packagesBasePath);
295 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
296 $this->packageStatesConfiguration['packages'][$packageKey]['manifestPath'] = substr($composerManifestPath, strlen($packagePath)) ? : '';
297 $this->packageStatesConfiguration['packages'][$packageKey]['composerName'] = $composerManifest->name;
298 } catch (\TYPO3\Flow\Package\Exception\MissingPackageManifestException $exception) {
299 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
300 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
301 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageKeyException $exception) {
302 continue;
303 }
304 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
305 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
306 }
307
308 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
309
310 // Change this to read the target from Composer or any other source
311 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = Package::DIRECTORY_CLASSES;
312 }
313
314 $registerOnlyNewPackages = !empty($this->packages);
315 $this->registerPackagesFromConfiguration($registerOnlyNewPackages);
316 if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
317 $this->sortAndsavePackageStates();
318 }
319 }
320
321 /**
322 * @param array $collectedExtensionPaths
323 * @return array
324 */
325 protected function scanLegacyExtensions(&$collectedExtensionPaths = array()) {
326 $legacyCmsPackageBasePathTypes = array('sysext', 'global', 'local');
327 foreach ($this->packagesBasePaths as $type => $packageBasePath) {
328 if (!in_array($type, $legacyCmsPackageBasePathTypes)) {
329 continue;
330 }
331 /** @var $fileInfo \SplFileInfo */
332 foreach (new \DirectoryIterator($packageBasePath) as $fileInfo) {
333 if (!$fileInfo->isDir()) {
334 continue;
335 }
336 $filename = $fileInfo->getFilename();
337 if ($filename[0] !== '.') {
338 $currentPath = \TYPO3\Flow\Utility\Files::getUnixStylePath($fileInfo->getPathName()) . '/';
339 if (file_exists($currentPath . 'ext_emconf.php')) {
340 $collectedExtensionPaths[$currentPath] = $currentPath;
341 }
342 }
343 }
344 }
345 return $collectedExtensionPaths;
346 }
347
348 /**
349 * Looks for composer.json in the given path and returns a path or NULL.
350 *
351 * @param string $packagePath
352 * @return array
353 */
354 protected function findComposerManifestPaths($packagePath) {
355 // If an ext_emconf.php file is found, we don't need to look deeper
356 if (file_exists($packagePath . '/ext_emconf.php')) {
357 return array();
358 }
359 return parent::findComposerManifestPaths($packagePath);
360 }
361
362 /**
363 * Requires and registers all packages which were defined in packageStatesConfiguration
364 *
365 * @param boolean $registerOnlyNewPackages
366 * @return void
367 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
368 */
369 protected function registerPackagesFromConfiguration($registerOnlyNewPackages = FALSE) {
370 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
371
372 if ($registerOnlyNewPackages && $this->isPackageAvailable($packageKey)) {
373 continue;
374 }
375
376 $packagePath = isset($stateConfiguration['packagePath']) ? $stateConfiguration['packagePath'] : NULL;
377 $classesPath = isset($stateConfiguration['classesPath']) ? $stateConfiguration['classesPath'] : NULL;
378 $manifestPath = isset($stateConfiguration['manifestPath']) ? $stateConfiguration['manifestPath'] : NULL;
379
380 try {
381 $package = $this->packageFactory->create($this->packagesBasePath, $packagePath, $packageKey, $classesPath, $manifestPath);
382 } catch (\TYPO3\Flow\Package\Exception\InvalidPackagePathException $exception) {
383 $this->unregisterPackageByPackageKey($packageKey);
384 continue;
385 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageKeyException $exception) {
386 $this->unregisterPackageByPackageKey($packageKey);
387 continue;
388 }
389
390 $this->registerPackage($package, FALSE);
391
392 if (!$this->packages[$packageKey] instanceof \TYPO3\Flow\Package\PackageInterface) {
393 throw new \TYPO3\Flow\Package\Exception\CorruptPackageException(sprintf('The package class in package "%s" does not implement PackageInterface.', $packageKey), 1300782488);
394 }
395
396 $this->packageKeys[strtolower($packageKey)] = $packageKey;
397 if ($stateConfiguration['state'] === 'active') {
398 $this->activePackages[$packageKey] = $this->packages[$packageKey];
399 }
400 }
401 }
402
403 /**
404 * Register a native Flow package
405 *
406 * @param \TYPO3\Flow\Package\PackageInterface $package The Package to be registered
407 * @param boolean $sortAndSave allows for not saving packagestates when used in loops etc.
408 * @return \TYPO3\Flow\Package\PackageInterface
409 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
410 */
411 public function registerPackage(\TYPO3\Flow\Package\PackageInterface $package, $sortAndSave = TRUE) {
412 $package = parent::registerPackage($package, $sortAndSave);
413 if ($package instanceof PackageInterface) {
414 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
415 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
416 }
417 }
418 return $package;
419 }
420
421 /**
422 * Unregisters a package from the list of available packages
423 *
424 * @param string $packageKey Package Key of the package to be unregistered
425 * @return void
426 */
427 protected function unregisterPackageByPackageKey($packageKey) {
428 try {
429 $package = $this->getPackage($packageKey);
430 if ($package instanceof PackageInterface) {
431 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
432 unset($this->packageAliasMap[strtolower($packageToReplace)]);
433 }
434 $packageKey = $package->getPackageKey();
435 }
436 } catch (\TYPO3\Flow\Package\Exception\UnknownPackageException $e) {
437 }
438 parent::unregisterPackageByPackageKey($packageKey);
439 }
440
441 /**
442 * Resolves a Flow package key from a composer package name.
443 *
444 * @param string $composerName
445 * @return string
446 * @throws \TYPO3\Flow\Package\Exception\InvalidPackageStateException
447 */
448 public function getPackageKeyFromComposerName($composerName) {
449 if (isset($this->packageAliasMap[$composerName])) {
450 return $this->packageAliasMap[$composerName];
451 }
452 try {
453 return parent::getPackageKeyFromComposerName($composerName);
454 } catch (\TYPO3\Flow\Package\Exception\InvalidPackageStateException $exception) {
455 return $composerName;
456 }
457 }
458
459 /**
460 * @return array
461 */
462 public function getExtAutoloadRegistry() {
463 if (!isset($this->extAutoloadClassFiles)) {
464 $classRegistry = array();
465 foreach ($this->activePackages as $packageKey => $packageData) {
466 try {
467 $extensionAutoloadFile = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($packageKey, 'ext_autoload.php');
468 if (@file_exists($extensionAutoloadFile)) {
469 $classRegistry = array_merge($classRegistry, require $extensionAutoloadFile);
470 }
471 } catch (\BadFunctionCallException $e) {
472 }
473 }
474 $this->extAutoloadClassFiles = $classRegistry;
475 }
476 return $this->extAutoloadClassFiles;
477 }
478
479 /**
480 * Returns a PackageInterface object for the specified package.
481 * A package is available, if the package directory contains valid MetaData information.
482 *
483 * @param string $packageKey
484 * @return \TYPO3\Flow\Package\PackageInterface The requested package object
485 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException if the specified package is not known
486 * @api
487 */
488 public function getPackage($packageKey) {
489 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
490 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
491 }
492 return parent::getPackage($packageKey);
493 }
494
495 /**
496 * Returns TRUE if a package is available (the package's files exist in the packages directory)
497 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
498 *
499 * @param string $packageKey The key of the package to check
500 * @return boolean TRUE if the package is available, otherwise FALSE
501 * @api
502 */
503 public function isPackageAvailable($packageKey) {
504 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
505 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
506 }
507 return parent::isPackageAvailable($packageKey);
508 }
509
510 /**
511 * Returns TRUE if a package is activated or FALSE if it's not.
512 *
513 * @param string $packageKey The key of the package to check
514 * @return boolean TRUE if package is active, otherwise FALSE
515 * @api
516 */
517 public function isPackageActive($packageKey) {
518 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
519 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
520 }
521 return isset($this->runtimeActivatedPackages[$packageKey]) || parent::isPackageActive($packageKey);
522 }
523
524 /**
525 * @param string $packageKey
526 */
527 public function deactivatePackage($packageKey) {
528 $package = $this->getPackage($packageKey);
529 parent::deactivatePackage($package->getPackageKey());
530 }
531
532 /**
533 * @param string $packageKey
534 */
535 public function activatePackage($packageKey) {
536 $package = $this->getPackage($packageKey);
537 parent::activatePackage($package->getPackageKey());
538 $this->classLoader->addActivePackage($package);
539 }
540
541 /**
542 * Enables packages during runtime, but no class aliases will be available
543 *
544 * @param string $packageKey
545 * @api
546 */
547 public function activatePackageDuringRuntime($packageKey) {
548 $package = $this->getPackage($packageKey);
549 $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
550 $this->classLoader->addActivePackage($package);
551 }
552
553
554 /**
555 * @param string $packageKey
556 */
557 public function deletePackage($packageKey) {
558 $package = $this->getPackage($packageKey);
559 parent::deletePackage($package->getPackageKey());
560 }
561
562
563 /**
564 * @param string $packageKey
565 */
566 public function freezePackage($packageKey) {
567 $package = $this->getPackage($packageKey);
568 parent::freezePackage($package->getPackageKey());
569 }
570
571 /**
572 * @param string $packageKey
573 * @return bool
574 */
575 public function isPackageFrozen($packageKey) {
576 $package = $this->getPackage($packageKey);
577 return parent::isPackageFrozen($package->getPackageKey());
578 }
579
580 /**
581 * @param string $packageKey
582 */
583 public function unfreezePackage($packageKey) {
584 $package = $this->getPackage($packageKey);
585 parent::unfreezePackage($package->getPackageKey());
586 }
587
588 /**
589 * @param string $packageKey
590 */
591 public function refreezePackage($packageKey) {
592 $package = $this->getPackage($packageKey);
593 parent::refreezePackage($package->getPackageKey());
594 }
595
596 /**
597 * Returns an array of \TYPO3\Flow\Package objects of all active packages.
598 * A package is active, if it is available and has been activated in the package
599 * manager settings. This method returns runtime activated packages too
600 *
601 * @return array Array of \TYPO3\Flow\Package\PackageInterface
602 * @api
603 */
604 public function getActivePackages() {
605 return array_merge(parent::getActivePackages(), $this->runtimeActivatedPackages);
606 }
607
608 /**
609 * Orders all packages by comparing their dependencies. By this, the packages
610 * and package configurations arrays holds all packages in the correct
611 * initialization order.
612 *
613 * @return void
614 */
615 protected function sortAvailablePackagesByDependencies() {
616 $this->resolvePackageDependencies();
617
618 $this->packageStatesConfiguration['packages'] = $this->dependencyResolver->sortPackageStatesConfigurationByDependency($this->packageStatesConfiguration['packages']);
619
620 // Reorder the packages according to the loading order
621 $newPackages = array();
622 foreach (array_keys($this->packageStatesConfiguration['packages']) as $packageKey) {
623 $newPackages[$packageKey] = $this->packages[$packageKey];
624 }
625 $this->packages = $newPackages;
626 }
627
628 /**
629 * Saves the current content of $this->packageStatesConfiguration to the
630 * PackageStates.php file.
631 *
632 * @return void
633 */
634 protected function sortAndSavePackageStates() {
635 parent::sortAndSavePackageStates();
636
637 \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($this->packageStatesPathAndFilename);
638 }
639 }