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