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