[FEATURE] Integrate preliminary PackageManager API
[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 *
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 = $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 protected 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 $packagesBasePath) {
254 if (!is_dir($packagesBasePath)) {
255 \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir_deep($packagesBasePath);
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 }
279 catch (\TYPO3\Flow\Package\Exception\MissingPackageManifestException $exception) {
280 $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
281 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
282 }
283 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
284 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
285 }
286
287 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
288
289 // Change this to read the target from Composer or any other source
290 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = \TYPO3\Flow\Package\Package::DIRECTORY_CLASSES;
291 }
292
293 $this->registerPackagesFromConfiguration();
294 if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
295 $this->sortAndsavePackageStates();
296 }
297 }
298
299 /**
300 * @return array
301 */
302 protected function scanLegacyExtensions(&$collectedExtensionPaths = array()) {
303 $legacyCmsPackageBasePathTypes = array('sysext', 'global', 'local');
304 foreach ($this->packagesBasePaths as $type => $packageBasePath) {
305 if (!in_array($type, $legacyCmsPackageBasePathTypes)) {
306 continue;
307 }
308 /** @var $fileInfo \SplFileInfo */
309 foreach (new \DirectoryIterator($packageBasePath) as $fileInfo) {
310 if (!$fileInfo->isDir()) {
311 continue;
312 }
313 $filename = $fileInfo->getFilename();
314 if ($filename[0] !== '.') {
315 $currentPath = \TYPO3\Flow\Utility\Files::getUnixStylePath($fileInfo->getPathName()) . '/';
316 $collectedExtensionPaths[$currentPath] = $currentPath;
317 }
318 }
319 }
320 return $collectedExtensionPaths;
321 }
322
323 /**
324 * Looks for composer.json in the given path and returns a path or NULL.
325 *
326 * @param string $packagePath
327 * @return array
328 */
329 protected function findComposerManifestPaths($packagePath) {
330 // If an ext_emconf.php file is found, we don't need to look deeper
331 if (file_exists($packagePath . '/ext_emconf.php')) {
332 return array();
333 }
334 return parent::findComposerManifestPaths($packagePath);
335 }
336
337 /**
338 * Requires and registers all packages which were defined in packageStatesConfiguration
339 *
340 * @return void
341 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
342 */
343 protected function registerPackagesFromConfiguration() {
344 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
345
346 $packagePath = isset($stateConfiguration['packagePath']) ? $stateConfiguration['packagePath'] : NULL;
347 $classesPath = isset($stateConfiguration['classesPath']) ? $stateConfiguration['classesPath'] : NULL;
348 $manifestPath = isset($stateConfiguration['manifestPath']) ? $stateConfiguration['manifestPath'] : NULL;
349
350 try {
351 $package = $this->packageFactory->create($this->packagesBasePath, $packagePath, $packageKey, $classesPath, $manifestPath);
352 } catch (\TYPO3\Flow\Package\Exception\InvalidPackagePathException $exception) {
353 $this->unregisterPackageByPackageKey($packageKey);
354 continue;
355 }
356
357 $this->registerPackage($package, FALSE);
358
359 if (!$this->packages[$packageKey] instanceof \TYPO3\Flow\Package\PackageInterface) {
360 throw new \TYPO3\Flow\Package\Exception\CorruptPackageException(sprintf('The package class in package "%s" does not implement PackageInterface.', $packageKey), 1300782487);
361 }
362
363 $this->packageKeys[strtolower($packageKey)] = $packageKey;
364 if ($stateConfiguration['state'] === 'active') {
365 $this->activePackages[$packageKey] = $this->packages[$packageKey];
366 }
367 }
368 }
369
370 /**
371 * Register a native Flow package
372 *
373 * @param string $packageKey The Package to be registered
374 * @param boolean $sortAndSave allows for not saving packagestates when used in loops etc.
375 * @return \TYPO3\Flow\Package\PackageInterface
376 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
377 */
378 public function registerPackage(\TYPO3\Flow\Package\PackageInterface $package, $sortAndSave = TRUE) {
379 $package = parent::registerPackage($package, $sortAndSave);
380 if ($package instanceof PackageInterface) {
381 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
382 $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
383 }
384 }
385 return $package;
386 }
387
388 /**
389 * Unregisters a package from the list of available packages
390 *
391 * @param string $packageKey Package Key of the package to be unregistered
392 * @return void
393 */
394 protected function unregisterPackageByPackageKey($packageKey) {
395 try {
396 $package = $this->getPackage($packageKey);
397 if ($package instanceof PackageInterface) {
398 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
399 unset($this->packageAliasMap[strtolower($packageToReplace)]);
400 }
401 $packageKey = $package->getPackageKey();
402 }
403 } catch (\TYPO3\Flow\Package\Exception\UnknownPackageException $e) {
404 }
405 parent::unregisterPackageByPackageKey($packageKey);
406 }
407
408 /**
409 * Resolves a Flow package key from a composer package name.
410 *
411 * @param string $composerName
412 * @return string
413 * @throws \TYPO3\Flow\Package\Exception\InvalidPackageStateException
414 */
415 public function getPackageKeyFromComposerName($composerName) {
416 if (isset($this->packageAliasMap[$composerName])) {
417 return $this->packageAliasMap[$composerName];
418 }
419 return parent::getPackageKeyFromComposerName($composerName);
420 }
421
422 /**
423 * @return array
424 */
425 public function getExtAutoloadRegistry() {
426 if (!isset($this->extAutoloadClassFiles)) {
427 $classRegistry = array();
428 foreach ($this->activePackages as $packageKey => $packageData) {
429 try {
430 $extensionAutoloadFile = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($packageKey, 'ext_autoload.php');
431 if (@file_exists($extensionAutoloadFile)) {
432 $classRegistry = array_merge($classRegistry, require $extensionAutoloadFile);
433 }
434 } catch (\BadFunctionCallException $e) {
435 }
436 }
437 $this->extAutoloadClassFiles = $classRegistry;
438 }
439 return $this->extAutoloadClassFiles;
440 }
441
442 /**
443 * Returns a PackageInterface object for the specified package.
444 * A package is available, if the package directory contains valid MetaData information.
445 *
446 * @param string $packageKey
447 * @return \TYPO3\Flow\Package\PackageInterface The requested package object
448 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException if the specified package is not known
449 * @api
450 */
451 public function getPackage($packageKey) {
452 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
453 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
454 }
455 return parent::getPackage($packageKey);
456 }
457
458 /**
459 * Returns TRUE if a package is available (the package's files exist in the packages directory)
460 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
461 *
462 * @param string $packageKey The key of the package to check
463 * @return boolean TRUE if the package is available, otherwise FALSE
464 * @api
465 */
466 public function isPackageAvailable($packageKey) {
467 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
468 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
469 }
470 return parent::isPackageAvailable($packageKey);
471 }
472
473 /**
474 * Returns TRUE if a package is activated or FALSE if it's not.
475 *
476 * @param string $packageKey The key of the package to check
477 * @return boolean TRUE if package is active, otherwise FALSE
478 * @api
479 */
480 public function isPackageActive($packageKey) {
481 if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
482 $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
483 }
484 return parent::isPackageActive($packageKey);
485 }
486
487 /**
488 * @param string $packageKey
489 */
490 public function deactivatePackage($packageKey) {
491 $package = $this->getPackage($packageKey);
492 parent::deactivatePackage($package->getPackageKey());
493 }
494
495 /**
496 * @param string $packageKey
497 */
498 public function activatePackage($packageKey) {
499 $package = $this->getPackage($packageKey);
500 parent::activatePackage($package->getPackageKey());
501 }
502
503
504 /**
505 * @param string $packageKey
506 */
507 public function deletePackage($packageKey) {
508 $package = $this->getPackage($packageKey);
509 parent::deletePackage($package->getPackageKey());
510 }
511
512
513 /**
514 * @param string $packageKey
515 */
516 public function freezePackage($packageKey) {
517 $package = $this->getPackage($packageKey);
518 parent::freezePackage($package->getPackageKey());
519 }
520
521 /**
522 * @param string $packageKey
523 */
524 public function isPackageFrozen($packageKey) {
525 $package = $this->getPackage($packageKey);
526 parent::isPackageFrozen($package->getPackageKey());
527 }
528
529 /**
530 * @param string $packageKey
531 */
532 public function unfreezePackage($packageKey) {
533 $package = $this->getPackage($packageKey);
534 parent::unfreezePackage($package->getPackageKey());
535 }
536
537 /**
538 * @param string $packageKey
539 */
540 public function refreezePackage($packageKey) {
541 $package = $this->getPackage($packageKey);
542 parent::refreezePackage($package->getPackageKey());
543 }
544
545 }