[BUGFIX] File PackageStates.php isn't created at all
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Resources / PHP / TYPO3.Flow / Classes / TYPO3 / Flow / Package / PackageManager.php
1 <?php
2 namespace TYPO3\Flow\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\Package\Package;
15 use TYPO3\Flow\Package\PackageFactory;
16 use TYPO3\Flow\Package\PackageInterface;
17 use TYPO3\Flow\Utility\Files;
18 use TYPO3\Flow\Annotations as Flow;
19
20 /**
21 * The default TYPO3 Package Manager
22 *
23 * @api
24 * @Flow\Scope("singleton")
25 */
26 class PackageManager implements \TYPO3\Flow\Package\PackageManagerInterface {
27
28 /**
29 * @var \TYPO3\Flow\Core\ClassLoader
30 */
31 protected $classLoader;
32
33 /**
34 * @var \TYPO3\Flow\Core\Bootstrap
35 */
36 protected $bootstrap;
37
38 /**
39 * @var PackageFactory
40 */
41 protected $packageFactory;
42
43 /**
44 * Array of available packages, indexed by package key
45 * @var array
46 */
47 protected $packages = array();
48
49 /**
50 * A translation table between lower cased and upper camel cased package keys
51 * @var array
52 */
53 protected $packageKeys = array();
54
55 /**
56 * A map between ComposerName and PackageKey, only available when scanAvailablePackages is run
57 * @var array
58 */
59 protected $composerNameToPackageKeyMap = array();
60
61 /**
62 * List of active packages as package key => package object
63 * @var array
64 */
65 protected $activePackages = array();
66
67 /**
68 * Absolute path leading to the various package directories
69 * @var string
70 */
71 protected $packagesBasePath;
72
73 /**
74 * @var string
75 */
76 protected $packageStatesPathAndFilename;
77
78 /**
79 * Package states configuration as stored in the PackageStates.php file
80 * @var array
81 */
82 protected $packageStatesConfiguration = array();
83
84 /**
85 * @var array
86 */
87 protected $settings;
88
89 /**
90 * @var \TYPO3\Flow\Log\SystemLoggerInterface
91 */
92 protected $systemLogger;
93
94 /**
95 * @param \TYPO3\Flow\Core\ClassLoader $classLoader
96 * @return void
97 */
98 public function injectClassLoader(\TYPO3\Flow\Core\ClassLoader $classLoader) {
99 $this->classLoader = $classLoader;
100 }
101
102 /**
103 * @param array $settings
104 * @return void
105 */
106 public function injectSettings(array $settings) {
107 $this->settings = $settings;
108 }
109
110 /**
111 * @param \TYPO3\Flow\Log\SystemLoggerInterface $systemLogger
112 * @return void
113 */
114 public function injectSystemLogger(\TYPO3\Flow\Log\SystemLoggerInterface $systemLogger) {
115 if ($this->systemLogger instanceof \TYPO3\Flow\Log\EarlyLogger) {
116 $this->systemLogger->replayLogsOn($systemLogger);
117 unset($this->systemLogger);
118 }
119 $this->systemLogger = $systemLogger;
120 }
121
122 /**
123 * Initializes the package manager
124 *
125 * @param \TYPO3\Flow\Core\Bootstrap $bootstrap The current bootstrap
126 * @param string $packagesBasePath Absolute path of the Packages directory
127 * @param string $packageStatesPathAndFilename
128 * @return void
129 */
130 public function initialize(\TYPO3\Flow\Core\Bootstrap $bootstrap, $packagesBasePath = FLOW_PATH_PACKAGES, $packageStatesPathAndFilename = '') {
131 $this->systemLogger = new \TYPO3\Flow\Log\EarlyLogger();
132
133 $this->bootstrap = $bootstrap;
134 $this->packagesBasePath = $packagesBasePath;
135 $this->packageStatesPathAndFilename = ($packageStatesPathAndFilename === '') ? FLOW_PATH_CONFIGURATION . 'PackageStates.php' : $packageStatesPathAndFilename;
136 $this->packageFactory = new PackageFactory($this);
137
138 $this->loadPackageStates();
139
140 foreach ($this->packages as $packageKey => $package) {
141 if ($package->isProtected() || (isset($this->packageStatesConfiguration['packages'][$packageKey]['state']) && $this->packageStatesConfiguration['packages'][$packageKey]['state'] === 'active')) {
142 $this->activePackages[$packageKey] = $package;
143 }
144 }
145
146 $this->classLoader->setPackages($this->activePackages);
147
148 foreach ($this->activePackages as $package) {
149 $package->boot($bootstrap);
150 }
151
152 }
153
154 /**
155 * Returns TRUE if a package is available (the package's files exist in the packages directory)
156 * or FALSE if it's not. If a package is available it doesn't mean necessarily that it's active!
157 *
158 * @param string $packageKey The key of the package to check
159 * @return boolean TRUE if the package is available, otherwise FALSE
160 * @api
161 */
162 public function isPackageAvailable($packageKey) {
163 return (isset($this->packages[$packageKey]));
164 }
165
166 /**
167 * Returns TRUE if a package is activated or FALSE if it's not.
168 *
169 * @param string $packageKey The key of the package to check
170 * @return boolean TRUE if package is active, otherwise FALSE
171 * @api
172 */
173 public function isPackageActive($packageKey) {
174 return (isset($this->activePackages[$packageKey]));
175 }
176
177 /**
178 * Returns the base path for packages
179 *
180 * @return string
181 */
182 public function getPackagesBasePath() {
183 return $this->packagesBasePath;
184 }
185
186 /**
187 * Returns a PackageInterface object for the specified package.
188 * A package is available, if the package directory contains valid MetaData information.
189 *
190 * @param string $packageKey
191 * @return \TYPO3\Flow\Package\PackageInterface The requested package object
192 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException if the specified package is not known
193 * @api
194 */
195 public function getPackage($packageKey) {
196 if (!$this->isPackageAvailable($packageKey)) {
197 throw new \TYPO3\Flow\Package\Exception\UnknownPackageException('Package "' . $packageKey . '" is not available. Please check if the package exists and that the package key is correct (package keys are case sensitive).', 1166546734);
198 }
199 return $this->packages[$packageKey];
200 }
201
202 /**
203 * Finds a package by a given object of that package; if no such package
204 * could be found, NULL is returned. This basically works with comparing the package class' location
205 * against the given class' location. In order to not being satisfied with a shorter package's root path,
206 * the packages to check are sorted by the length of their root path descending.
207 *
208 * Please note that the class itself must be existing anyways, else PHP's generic "class not found"
209 * exception will be thrown.
210 *
211 * @param object $object The object to find the possessing package of
212 * @return \TYPO3\Flow\Package\PackageInterface The package the given object belongs to or NULL if it could not be found
213 */
214 public function getPackageOfObject($object) {
215 $sortedAvailablePackages = $this->getAvailablePackages();
216 usort($sortedAvailablePackages, function (PackageInterface $packageOne, PackageInterface $packageTwo) {
217 return strlen($packageTwo->getPackagePath()) - strlen($packageOne->getPackagePath());
218 });
219
220 $className = $this->bootstrap->getObjectManager()->get('TYPO3\Flow\Reflection\ReflectionService')->getClassNameByObject($object);
221 $reflectedClass = new \ReflectionClass($className);
222 $fileName = Files::getUnixStylePath($reflectedClass->getFileName());
223
224 foreach ($sortedAvailablePackages as $package) {
225 $packagePath = Files::getUnixStylePath($package->getPackagePath());
226 if (strpos($fileName, $packagePath) === 0) {
227 return $package;
228 }
229 }
230 return NULL;
231 }
232
233 /**
234 * Returns an array of \TYPO3\Flow\Package objects of all available packages.
235 * A package is available, if the package directory contains valid meta information.
236 *
237 * @return array Array of \TYPO3\Flow\Package\PackageInterface
238 * @api
239 */
240 public function getAvailablePackages() {
241 return $this->packages;
242 }
243
244 /**
245 * Returns an array of \TYPO3\Flow\Package objects of all active packages.
246 * A package is active, if it is available and has been activated in the package
247 * manager settings.
248 *
249 * @return array Array of \TYPO3\Flow\Package\PackageInterface
250 * @api
251 */
252 public function getActivePackages() {
253 return $this->activePackages;
254 }
255
256 /**
257 * Returns an array of \TYPO3\Flow\Package objects of all frozen packages.
258 * A frozen package is not considered by file monitoring and provides some
259 * precompiled reflection data in order to improve performance.
260 *
261 * @return array Array of \TYPO3\Flow\Package\PackageInterface
262 */
263 public function getFrozenPackages() {
264 $frozenPackages = array();
265 if ($this->bootstrap->getContext()->isDevelopment()) {
266 foreach ($this->packages as $packageKey => $package) {
267 if (isset($this->packageStatesConfiguration['packages'][$packageKey]['frozen']) &&
268 $this->packageStatesConfiguration['packages'][$packageKey]['frozen'] === TRUE) {
269 $frozenPackages[$packageKey] = $package;
270 }
271 }
272 }
273 return $frozenPackages;
274 }
275
276 /**
277 * Returns an array of \TYPO3\Flow\PackageInterface objects of all packages that match
278 * the given package state, path, and type filters. All three filters must match, if given.
279 *
280 * @param string $packageState defaults to available
281 * @param string $packagePath
282 * @param string $packageType
283 *
284 * @return array Array of \TYPO3\Flow\Package\PackageInterface
285 * @throws Exception\InvalidPackageStateException
286 * @api
287 */
288 public function getFilteredPackages($packageState = 'available', $packagePath = NULL, $packageType = NULL) {
289 $packages = array();
290 switch (strtolower($packageState)) {
291 case 'available':
292 $packages = $this->getAvailablePackages();
293 break;
294 case 'active':
295 $packages = $this->getActivePackages();
296 break;
297 case 'frozen':
298 $packages = $this->getFrozenPackages();
299 break;
300 default:
301 throw new \TYPO3\Flow\Package\Exception\InvalidPackageStateException('The package state "' . $packageState . '" is invalid', 1372458274);
302 }
303
304 if($packagePath !== NULL) {
305 $packages = $this->filterPackagesByPath($packages, $packagePath);
306 }
307 if($packageType !== NULL) {
308 $packages = $this->filterPackagesByType($packages, $packageType);
309 }
310
311 return $packages;
312 }
313
314 /**
315 * Returns an array of \TYPO3\Flow\Package objects in the given array of packages
316 * that are in the specified Package Path
317 *
318 * @param array $packages Array of \TYPO3\Flow\Package\PackageInterface to be filtered
319 * @param string $filterPath Filter out anything that's not in this path
320 * @return array Array of \TYPO3\Flow\Package\PackageInterface
321 */
322 protected function filterPackagesByPath(&$packages, $filterPath) {
323 $filteredPackages = array();
324 /** @var $package Package */
325 foreach ($packages as $package) {
326 $packagePath = substr($package->getPackagePath(), strlen(FLOW_PATH_PACKAGES));
327 $packageGroup = substr($packagePath, 0, strpos($packagePath, '/'));
328 if ($packageGroup === $filterPath) {
329 $filteredPackages[$package->getPackageKey()] = $package;
330 }
331 }
332 return $filteredPackages;
333 }
334
335 /**
336 * Returns an array of \TYPO3\Flow\Package objects in the given array of packages
337 * that are of the specified package type.
338 *
339 * @param array $packages Array of \TYPO3\Flow\Package\PackageInterface to be filtered
340 * @param string $packageType Filter out anything that's not of this packageType
341 * @return array Array of \TYPO3\Flow\Package\PackageInterface
342 */
343 protected function filterPackagesByType(&$packages, $packageType) {
344 $filteredPackages = array();
345 /** @var $package Package */
346 foreach ($packages as $package) {
347 if ($package->getComposerManifest('type') === $packageType) {
348 $filteredPackages[$package->getPackageKey()] = $package;
349 }
350 }
351 return $filteredPackages;
352 }
353
354 /**
355 * Returns the upper camel cased version of the given package key or FALSE
356 * if no such package is available.
357 *
358 * @param string $unknownCasedPackageKey The package key to convert
359 * @return mixed The upper camel cased package key or FALSE if no such package exists
360 * @api
361 */
362 public function getCaseSensitivePackageKey($unknownCasedPackageKey) {
363 $lowerCasedPackageKey = strtolower($unknownCasedPackageKey);
364 return (isset($this->packageKeys[$lowerCasedPackageKey])) ? $this->packageKeys[$lowerCasedPackageKey] : FALSE;
365 }
366
367 /**
368 * Resolves a Flow package key from a composer package name.
369 *
370 * @param string $composerName
371 * @return string
372 * @throws Exception\InvalidPackageStateException
373 */
374 public function getPackageKeyFromComposerName($composerName) {
375 if (count($this->composerNameToPackageKeyMap) === 0) {
376 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageStateConfiguration) {
377 $this->composerNameToPackageKeyMap[strtolower($packageStateConfiguration['composerName'])] = $packageKey;
378 }
379 }
380 $lowercasedComposerName = strtolower($composerName);
381 if (!isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
382 throw new \TYPO3\Flow\Package\Exception\InvalidPackageStateException('Could not find package with composer name "' . $composerName . '" in PackageStates configuration.', 1352320649);
383 }
384 return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
385 }
386
387 /**
388 * Check the conformance of the given package key
389 *
390 * @param string $packageKey The package key to validate
391 * @return boolean If the package key is valid, returns TRUE otherwise FALSE
392 * @api
393 */
394 public function isPackageKeyValid($packageKey) {
395 return preg_match(PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1;
396 }
397
398 /**
399 * Create a package, given the package key
400 *
401 * @param string $packageKey The package key of the new package
402 * @param \TYPO3\Flow\Package\MetaData $packageMetaData If specified, this package meta object is used for writing the Package.xml file, otherwise a rudimentary Package.xml file is created
403 * @param string $packagesPath If specified, the package will be created in this path, otherwise the default "Application" directory is used
404 * @param string $packageType If specified, the package type will be set, otherwise it will default to "typo3-flow-package"
405 * @return \TYPO3\Flow\Package\PackageInterface The newly created package
406 * @throws \TYPO3\Flow\Package\Exception
407 * @throws \TYPO3\Flow\Package\Exception\PackageKeyAlreadyExistsException
408 * @throws \TYPO3\Flow\Package\Exception\InvalidPackageKeyException
409 * @api
410 */
411 public function createPackage($packageKey, \TYPO3\Flow\Package\MetaData $packageMetaData = NULL, $packagesPath = NULL, $packageType = 'typo3-flow-package') {
412 if (!$this->isPackageKeyValid($packageKey)) {
413 throw new \TYPO3\Flow\Package\Exception\InvalidPackageKeyException('The package key "' . $packageKey . '" is invalid', 1220722210);
414 }
415 if ($this->isPackageAvailable($packageKey)) {
416 throw new \TYPO3\Flow\Package\Exception\PackageKeyAlreadyExistsException('The package key "' . $packageKey . '" already exists', 1220722873);
417 }
418
419 if ($packagesPath === NULL) {
420 if (is_array($this->settings['package']['packagesPathByType']) && isset($this->settings['package']['packagesPathByType'][$packageType])) {
421 $packagesPath = $this->settings['package']['packagesPathByType'][$packageType];
422 } else {
423 $packagesPath = 'Application';
424 }
425 $packagesPath = Files::getUnixStylePath(Files::concatenatePaths(array($this->packagesBasePath, $packagesPath)));
426 }
427
428 if ($packageMetaData === NULL) {
429 $packageMetaData = new MetaData($packageKey);
430 }
431 if ($packageMetaData->getPackageType() === NULL) {
432 $packageMetaData->setPackageType($packageType);
433 }
434
435 $packagePath = Files::concatenatePaths(array($packagesPath, $packageKey)) . '/';
436 Files::createDirectoryRecursively($packagePath);
437
438 foreach (
439 array(
440 PackageInterface::DIRECTORY_METADATA,
441 PackageInterface::DIRECTORY_CLASSES,
442 PackageInterface::DIRECTORY_CONFIGURATION,
443 PackageInterface::DIRECTORY_DOCUMENTATION,
444 PackageInterface::DIRECTORY_RESOURCES,
445 PackageInterface::DIRECTORY_TESTS_UNIT,
446 PackageInterface::DIRECTORY_TESTS_FUNCTIONAL,
447 ) as $path) {
448 Files::createDirectoryRecursively(Files::concatenatePaths(array($packagePath, $path)));
449 }
450
451 $this->writeComposerManifest($packagePath, $packageKey, $packageMetaData);
452
453 $packagePath = str_replace($this->packagesBasePath, '', $packagePath);
454 $package = $this->packageFactory->create($this->packagesBasePath, $packagePath, $packageKey, PackageInterface::DIRECTORY_CLASSES);
455
456 $this->packages[$packageKey] = $package;
457 foreach (array_keys($this->packages) as $upperCamelCasedPackageKey) {
458 $this->packageKeys[strtolower($upperCamelCasedPackageKey)] = $upperCamelCasedPackageKey;
459 }
460
461 $this->activatePackage($packageKey);
462
463 return $package;
464 }
465
466 /**
467 * Write a composer manifest for the package.
468 *
469 * @param string $manifestPath
470 * @param string $packageKey
471 * @param MetaData $packageMetaData
472 * @return void
473 */
474 protected function writeComposerManifest($manifestPath, $packageKey, \TYPO3\Flow\Package\MetaData $packageMetaData = NULL) {
475 $manifest = array();
476
477 $nameParts = explode('.', $packageKey);
478 $vendor = array_shift($nameParts);
479 $manifest['name'] = strtolower($vendor . '/' . implode('-', $nameParts));
480 if ($packageMetaData !== NULL) {
481 $manifest['type'] = $packageMetaData->getPackageType();
482 $manifest['description'] = $packageMetaData->getDescription();
483 $manifest['version'] = $packageMetaData->getVersion();
484 } else {
485 $manifest['type'] = 'typo3-flow-package';
486 $manifest['description'] = '';
487 }
488 $manifest['require'] = array('typo3/flow' => '*');
489 $manifest['autoload'] = array('psr-0' => array(str_replace('.', '\\', $packageKey) => 'Classes'));
490
491 if (defined('JSON_PRETTY_PRINT')) {
492 file_put_contents(Files::concatenatePaths(array($manifestPath, 'composer.json')), json_encode($manifest, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
493 } else {
494 file_put_contents(Files::concatenatePaths(array($manifestPath, 'composer.json')), json_encode($manifest));
495 }
496 }
497
498 /**
499 * Deactivates a package
500 *
501 * @param string $packageKey The package to deactivate
502 * @return void
503 * @throws \TYPO3\Flow\Package\Exception\ProtectedPackageKeyException if a package is protected and cannot be deactivated
504 * @api
505 */
506 public function deactivatePackage($packageKey) {
507 if (!$this->isPackageActive($packageKey)) {
508 return;
509 }
510
511 $package = $this->getPackage($packageKey);
512 if ($package->isProtected()) {
513 throw new \TYPO3\Flow\Package\Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
514 }
515
516 unset($this->activePackages[$packageKey]);
517 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
518 $this->sortAndSavePackageStates();
519 }
520
521 /**
522 * Activates a package
523 *
524 * @param string $packageKey The package to activate
525 * @return void
526 * @api
527 */
528 public function activatePackage($packageKey) {
529 if ($this->isPackageActive($packageKey)) {
530 return;
531 }
532
533 $package = $this->getPackage($packageKey);
534 $this->activePackages[$packageKey] = $package;
535 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'active';
536 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['packagePath'])) {
537 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
538 }
539 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['classesPath'])) {
540 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = Package::DIRECTORY_CLASSES;
541 }
542 $this->sortAndSavePackageStates();
543 }
544
545 /**
546 * Freezes a package
547 *
548 * @param string $packageKey The package to freeze
549 * @return void
550 * @throws \LogicException
551 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException
552 */
553 public function freezePackage($packageKey) {
554 if (!$this->bootstrap->getContext()->isDevelopment()) {
555 throw new \LogicException('Package freezing is only supported in Development context.', 1338810870);
556 }
557
558 if (!$this->isPackageActive($packageKey)) {
559 throw new \TYPO3\Flow\Package\Exception\UnknownPackageException('Package "' . $packageKey . '" is not available or active.', 1331715956);
560 }
561 if ($this->isPackageFrozen($packageKey)) {
562 return;
563 }
564
565 $this->bootstrap->getObjectManager()->get('TYPO3\Flow\Reflection\ReflectionService')->freezePackageReflection($packageKey);
566
567 $this->packageStatesConfiguration['packages'][$packageKey]['frozen'] = TRUE;
568 $this->sortAndSavePackageStates();
569 }
570
571 /**
572 * Tells if a package is frozen
573 *
574 * @param string $packageKey The package to check
575 * @return boolean
576 */
577 public function isPackageFrozen($packageKey) {
578 return (
579 $this->bootstrap->getContext()->isDevelopment()
580 && isset($this->packageStatesConfiguration['packages'][$packageKey]['frozen'])
581 && $this->packageStatesConfiguration['packages'][$packageKey]['frozen'] === TRUE
582 );
583 }
584
585 /**
586 * Unfreezes a package
587 *
588 * @param string $packageKey The package to unfreeze
589 * @return void
590 */
591 public function unfreezePackage($packageKey) {
592 if (!$this->isPackageFrozen($packageKey)) {
593 return;
594 }
595
596 $this->bootstrap->getObjectManager()->get('TYPO3\Flow\Reflection\ReflectionService')->unfreezePackageReflection($packageKey);
597
598 unset($this->packageStatesConfiguration['packages'][$packageKey]['frozen']);
599 $this->sortAndSavePackageStates();
600 }
601
602 /**
603 * Refreezes a package
604 *
605 * @param string $packageKey The package to refreeze
606 * @return void
607 */
608 public function refreezePackage($packageKey) {
609 if (!$this->isPackageFrozen($packageKey)) {
610 return;
611 }
612
613 $this->bootstrap->getObjectManager()->get('TYPO3\Flow\Reflection\ReflectionService')->unfreezePackageReflection($packageKey);
614 }
615
616 /**
617 * Register a native Flow package
618 *
619 * @param string $packageKey The Package to be registered
620 * @param boolean $sortAndSave allows for not saving packagestates when used in loops etc.
621 * @return PackageInterface
622 * @throws Exception\CorruptPackageException
623 */
624 public function registerPackage(PackageInterface $package, $sortAndSave = TRUE) {
625 $packageKey = $package->getPackageKey();
626 if ($this->isPackageAvailable($packageKey)) {
627 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
628 }
629
630 $this->packages[$packageKey] = $package;
631 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
632 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = str_replace($package->getPackagePath(), '', $package->getClassesPath());
633
634 if ($sortAndSave === TRUE) {
635 $this->sortAndSavePackageStates();
636 }
637
638 return $package;
639 }
640
641 /**
642 * Unregisters a package from the list of available packages
643 *
644 * @param PackageInterface $package The package to be unregistered
645 * @return void
646 * @throws Exception\InvalidPackageStateException
647 */
648 public function unregisterPackage(PackageInterface $package) {
649 $packageKey = $package->getPackageKey();
650 if (!$this->isPackageAvailable($packageKey)) {
651 throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
652 }
653 $this->unregisterPackageByPackageKey($packageKey);
654 }
655
656 /**
657 * Unregisters a package from the list of available packages
658 *
659 * @param string $packageKey Package Key of the package to be unregistered
660 * @return void
661 */
662 protected function unregisterPackageByPackageKey($packageKey) {
663 unset($this->packages[$packageKey]);
664 unset($this->packageKeys[strtolower($packageKey)]);
665 unset($this->packageStatesConfiguration['packages'][$packageKey]);
666 $this->sortAndSavePackageStates();
667 }
668
669 /**
670 * Removes a package from registry and deletes it from filesystem
671 *
672 * @param string $packageKey package to remove
673 * @return void
674 * @throws \TYPO3\Flow\Package\Exception\UnknownPackageException if the specified package is not known
675 * @throws \TYPO3\Flow\Package\Exception\ProtectedPackageKeyException if a package is protected and cannot be deleted
676 * @throws \TYPO3\Flow\Package\Exception
677 * @api
678 */
679 public function deletePackage($packageKey) {
680 if (!$this->isPackageAvailable($packageKey)) {
681 throw new \TYPO3\Flow\Package\Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
682 }
683
684 $package = $this->getPackage($packageKey);
685 if ($package->isProtected()) {
686 throw new \TYPO3\Flow\Package\Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
687 }
688
689 if ($this->isPackageActive($packageKey)) {
690 $this->deactivatePackage($packageKey);
691 }
692
693 $packagePath = $package->getPackagePath();
694 try {
695 Files::removeDirectoryRecursively($packagePath);
696 } catch (\TYPO3\Flow\Utility\Exception $exception) {
697 throw new \TYPO3\Flow\Package\Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089, $exception);
698 }
699
700 $this->unregisterPackage($package);
701 }
702
703 /**
704 * Loads the states of available packages from the PackageStates.php file.
705 * The result is stored in $this->packageStatesConfiguration.
706 *
707 * @return void
708 */
709 protected function loadPackageStates() {
710 $this->packageStatesConfiguration = file_exists($this->packageStatesPathAndFilename) ? include($this->packageStatesPathAndFilename) : array();
711 if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
712 $this->packageStatesConfiguration = array();
713 }
714 if ($this->packageStatesConfiguration === array() || !$this->bootstrap->getContext()->isProduction()) {
715 $this->scanAvailablePackages();
716 } else {
717 $this->registerPackagesFromConfiguration();
718 }
719 }
720
721 /**
722 * Scans all directories in the packages directories for available packages.
723 * For each package a Package object is created and stored in $this->packages.
724 *
725 * @return void
726 * @throws \TYPO3\Flow\Package\Exception\DuplicatePackageException
727 */
728 protected function scanAvailablePackages() {
729 $previousPackageStatesConfiguration = $this->packageStatesConfiguration;
730
731 if (isset($this->packageStatesConfiguration['packages'])) {
732 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $configuration) {
733 if (!file_exists($this->packagesBasePath . $configuration['packagePath'])) {
734 unset($this->packageStatesConfiguration['packages'][$packageKey]);
735 }
736 }
737 } else {
738 $this->packageStatesConfiguration['packages'] = array();
739 }
740
741 $packagePaths = array();
742 foreach (new \DirectoryIterator($this->packagesBasePath) as $parentFileInfo) {
743 $parentFilename = $parentFileInfo->getFilename();
744 if ($parentFilename[0] !== '.' && $parentFileInfo->isDir()) {
745 $packagePaths = array_merge($packagePaths, $this->scanPackagesInPath($parentFileInfo->getPathName()));
746 }
747 }
748
749 /**
750 * @todo similar functionality in registerPackage - should be refactored
751 */
752 foreach ($packagePaths as $packagePath => $composerManifestPath) {
753 try {
754 $composerManifest = self::getComposerManifest($composerManifestPath);
755 $packageKey = PackageFactory::getPackageKeyFromManifest($composerManifest, $packagePath, $this->packagesBasePath);
756 $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
757 $this->packageStatesConfiguration['packages'][$packageKey]['manifestPath'] = substr($composerManifestPath, strlen($packagePath)) ?: '';
758 $this->packageStatesConfiguration['packages'][$packageKey]['composerName'] = $composerManifest->name;
759 } catch (\TYPO3\Flow\Package\Exception\MissingPackageManifestException $exception) {
760 $relativePackagePath = substr($packagePath, strlen($this->packagesBasePath));
761 $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
762 }
763 if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
764 /**
765 * @todo doesn't work, settings not available at this time
766 */
767 if (is_array($this->settings['package']['inactiveByDefault']) && in_array($packageKey, $this->settings['package']['inactiveByDefault'], TRUE)) {
768 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
769 } else {
770 $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'active';
771 }
772 }
773
774 $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
775
776 // Change this to read the target from Composer or any other source
777 $this->packageStatesConfiguration['packages'][$packageKey]['classesPath'] = Package::DIRECTORY_CLASSES;
778 }
779
780 $this->registerPackagesFromConfiguration();
781 if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
782 $this->sortAndsavePackageStates();
783 }
784 }
785
786 /**
787 * Looks for composer.json in the given path and returns a path or NULL.
788 *
789 * @param string $packagePath
790 * @return array
791 */
792 protected function findComposerManifestPaths($packagePath) {
793 $manifestPaths = array();
794 if (file_exists($packagePath . '/composer.json')) {
795 $manifestPaths[] = $packagePath . '/';
796 } else {
797 $jsonPathsAndFilenames = Files::readDirectoryRecursively($packagePath, '.json');
798 asort($jsonPathsAndFilenames);
799 while (list($unusedKey, $jsonPathAndFilename) = each($jsonPathsAndFilenames)) {
800 if (basename($jsonPathAndFilename) === 'composer.json') {
801 $manifestPath = dirname($jsonPathAndFilename) . '/';
802 $manifestPaths[] = $manifestPath;
803 $isNotSubPathOfManifestPath = function ($otherPath) use ($manifestPath) {
804 return strpos($otherPath, $manifestPath) !== 0;
805 };
806 $jsonPathsAndFilenames = array_filter($jsonPathsAndFilenames, $isNotSubPathOfManifestPath);
807 }
808 }
809 }
810
811 return $manifestPaths;
812 }
813
814 /**
815 * Scans all sub directories of the specified directory and collects the package keys of packages it finds.
816 *
817 * The return of the array is to make this method usable in array_merge.
818 *
819 * @param string $startPath
820 * @param array $collectedPackagePaths
821 * @return array
822 */
823 protected function scanPackagesInPath($startPath, array &$collectedPackagePaths = array()) {
824 foreach (new \DirectoryIterator($startPath) as $fileInfo) {
825 if (!$fileInfo->isDir()) {
826 continue;
827 }
828 $filename = $fileInfo->getFilename();
829 if ($filename[0] !== '.') {
830 $currentPath = Files::getUnixStylePath($fileInfo->getPathName());
831 $composerManifestPaths = $this->findComposerManifestPaths($currentPath);
832 foreach ($composerManifestPaths as $composerManifestPath) {
833 $targetDirectory = rtrim(self::getComposerManifest($composerManifestPath, 'target-dir'), '/');
834 $packagePath = $targetDirectory ? substr(rtrim($composerManifestPath, '/'), 0, -strlen((string)$targetDirectory)) : $composerManifestPath;
835 $collectedPackagePaths[$packagePath] = $composerManifestPath;
836 }
837 }
838 }
839 return $collectedPackagePaths;
840 }
841
842 /**
843 * Returns contents of Composer manifest - or part there of.
844 *
845 * @param string $manifestPath
846 * @param string $key Optional. Only return the part of the manifest indexed by 'key'
847 * @param object $composerManifest Optional. Manifest to use instead of reading it from file
848 * @return mixed
849 * @throws \TYPO3\Flow\Package\Exception\MissingPackageManifestException
850 * @see json_decode for return values
851 */
852 static public function getComposerManifest($manifestPath, $key = NULL, $composerManifest = NULL) {
853 if ($composerManifest === NULL) {
854 if (!file_exists($manifestPath . 'composer.json')) {
855 throw new \TYPO3\Flow\Package\Exception\MissingPackageManifestException('No composer manifest file found at "' . $manifestPath . '/composer.json".', 1349868540);
856 }
857 $json = file_get_contents($manifestPath . 'composer.json');
858 $composerManifest = json_decode($json);
859 }
860
861 if ($key !== NULL) {
862 if (isset($composerManifest->{$key})) {
863 $value = $composerManifest->{$key};
864 } else {
865 $value = NULL;
866 }
867 } else {
868 $value = $composerManifest;
869 }
870 return $value;
871 }
872
873 /**
874 * Requires and registers all packages which were defined in packageStatesConfiguration
875 *
876 * @return void
877 * @throws \TYPO3\Flow\Package\Exception\CorruptPackageException
878 */
879 protected function registerPackagesFromConfiguration() {
880 foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
881
882 $packagePath = isset($stateConfiguration['packagePath']) ? $stateConfiguration['packagePath'] : NULL;
883 $classesPath = isset($stateConfiguration['classesPath']) ? $stateConfiguration['classesPath'] : NULL;
884 $manifestPath = isset($stateConfiguration['manifestPath']) ? $stateConfiguration['manifestPath'] : NULL;
885
886 try {
887 $package = $this->packageFactory->create($this->packagesBasePath, $packagePath, $packageKey, $classesPath, $manifestPath);
888 } catch (\TYPO3\Flow\Package\Exception\InvalidPackagePathException $exception) {
889 $this->unregisterPackageByPackageKey($packageKey);
890 $this->systemLogger->log('Package ' . $packageKey . ' could not be loaded, it has been unregistered. Error description: "' . $exception->getMessage() . '" (' . $exception->getCode() . ')', LOG_WARNING);
891 continue;
892 }
893
894 $this->registerPackage($package, FALSE);
895
896 if (!$this->packages[$packageKey] instanceof PackageInterface) {
897 throw new \TYPO3\Flow\Package\Exception\CorruptPackageException(sprintf('The package class in package "%s" does not implement PackageInterface.', $packageKey), 1300782487);
898 }
899
900 $this->packageKeys[strtolower($packageKey)] = $packageKey;
901 if ($stateConfiguration['state'] === 'active') {
902 $this->activePackages[$packageKey] = $this->packages[$packageKey];
903 }
904 }
905 }
906
907 /**
908 * Saves the current content of $this->packageStatesConfiguration to the
909 * PackageStates.php file.
910 *
911 * @return void
912 */
913 protected function sortAndSavePackageStates() {
914 $this->sortAvailablePackagesByDependencies();
915
916 $this->packageStatesConfiguration['version'] = 4;
917
918 $fileDescription = "# PackageStates.php\n\n";
919 $fileDescription .= "# This file is maintained by Flow's package management. Although you can edit it\n";
920 $fileDescription .= "# manually, you should rather use the command line commands for maintaining packages.\n";
921 $fileDescription .= "# You'll find detailed information about the typo3.flow:package:* commands in their\n";
922 $fileDescription .= "# respective help screens.\n\n";
923 $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
924 $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
925
926 // we do not need the dependencies on disk...
927 foreach ($this->packageStatesConfiguration['packages'] as &$packageConfiguration) {
928 if (isset($packageConfiguration['dependencies'])) {
929 unset($packageConfiguration['dependencies']);
930 }
931 }
932 if (!@is_writable($this->packageStatesPathAndFilename)) {
933 // If file does not exists try to create it
934 $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
935 if (!$fileHandle) {
936 throw new \TYPO3\Flow\Package\Exception\PackageStatesFileNotWritableException(
937 sprintf('We could not update the list of installed packages because the file %s is not writable. Please, check the file system permissions for this file and make sure that the web server can update it.', $this->packageStatesPathAndFilename),
938 1382449759
939 );
940 }
941 fclose($fileHandle);
942 }
943 $packageStatesCode = "<?php\n$fileDescription\nreturn " . var_export($this->packageStatesConfiguration, TRUE) . "\n ?>";
944 @file_put_contents($this->packageStatesPathAndFilename, $packageStatesCode);
945 }
946
947 /**
948 * Resolves the dependent packages from the meta data of all packages recursively. The
949 * resolved direct or indirect dependencies of each package will put into the package
950 * states configuration array.
951 *
952 * @return void
953 */
954 protected function resolvePackageDependencies() {
955 foreach ($this->packages as $packageKey => $package) {
956 $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
957 }
958 }
959
960 /**
961 * Returns an array of dependent package keys for the given package. It will
962 * do this recursively, so dependencies of dependant packages will also be
963 * in the result.
964 *
965 * @param string $packageKey The package key to fetch the dependencies for
966 * @param array $dependentPackageKeys
967 * @param array $trace An array of already visited package keys, to detect circular dependencies
968 * @return array|NULL An array of direct or indirect dependant packages
969 * @throws \TYPO3\Flow\Mvc\Exception\InvalidPackageKeyException
970 */
971 protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = array(), array $trace = array()) {
972 if (!isset($this->packages[$packageKey])) {
973 return NULL;
974 }
975 if (in_array($packageKey, $trace) !== FALSE) {
976 return $dependentPackageKeys;
977 }
978 $trace[] = $packageKey;
979 $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaDataInterface::CONSTRAINT_TYPE_DEPENDS);
980 foreach ($dependentPackageConstraints as $constraint) {
981 if ($constraint instanceof \TYPO3\Flow\Package\MetaData\PackageConstraint) {
982 $dependentPackageKey = $constraint->getValue();
983 if (in_array($dependentPackageKey, $dependentPackageKeys) === FALSE && in_array($dependentPackageKey, $trace) === FALSE) {
984 $dependentPackageKeys[] = $dependentPackageKey;
985 }
986 $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
987 }
988 }
989 return array_reverse($dependentPackageKeys);
990 }
991
992 /**
993 * Orders all packages by comparing their dependencies. By this, the packages
994 * and package configurations arrays holds all packages in the correct
995 * initialization order.
996 *
997 * @return void
998 */
999 protected function sortAvailablePackagesByDependencies() {
1000 $this->resolvePackageDependencies();
1001
1002 $packageStatesConfiguration = $this->packageStatesConfiguration['packages'];
1003
1004 $comparator = function ($firstPackageKey, $secondPackageKey) use ($packageStatesConfiguration) {
1005 if (isset($packageStatesConfiguration[$firstPackageKey]['dependencies'])
1006 && (in_array($secondPackageKey, $packageStatesConfiguration[$firstPackageKey]['dependencies'])
1007 && !in_array($firstPackageKey, $packageStatesConfiguration[$secondPackageKey]['dependencies']))) {
1008 return 1;
1009 } elseif (isset($packageStatesConfiguration[$secondPackageKey]['dependencies'])
1010 && (in_array($firstPackageKey, $packageStatesConfiguration[$secondPackageKey]['dependencies'])
1011 && !in_array($secondPackageKey, $packageStatesConfiguration[$firstPackageKey]['dependencies']))) {
1012 return -1;
1013 }
1014 return strcmp($firstPackageKey, $secondPackageKey);
1015 };
1016
1017 uasort($this->packages,
1018 function (\TYPO3\Flow\Package\PackageInterface $firstPackage, \TYPO3\Flow\Package\PackageInterface $secondPackage) use ($comparator) {
1019 return $comparator($firstPackage->getPackageKey(), $secondPackage->getPackageKey());
1020 }
1021 );
1022
1023 uksort($this->packageStatesConfiguration['packages'],
1024 function ($firstPackageKey, $secondPackageKey) use ($comparator) {
1025 return $comparator($firstPackageKey, $secondPackageKey);
1026 }
1027 );
1028 }
1029 }
1030
1031 ?>