TYPO3  7.6
PackageManager.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Package;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
24 
30 {
35 
39  protected $bootstrap;
40 
44  protected $coreCache;
45 
49  protected $cacheIdentifier;
50 
54  protected $packagesBasePaths = array();
55 
59  protected $packageAliasMap = array();
60 
64  protected $requiredPackageKeys = array();
65 
69  protected $runtimeActivatedPackages = array();
70 
75  protected $packagesBasePath = PATH_site;
76 
81  protected $packages = array();
82 
87  protected $packageKeys = array();
88 
93  protected $composerNameToPackageKeyMap = array();
94 
99  protected $activePackages = array();
100 
105 
110  protected $packageStatesConfiguration = array();
111 
115  public function __construct()
116  {
117  // The order of paths is crucial for allowing overriding of system extension by local extensions.
118  // Pay attention if you change order of the paths here.
119  $this->packagesBasePaths = array(
120  'local' => PATH_typo3conf . 'ext',
121  'global' => PATH_typo3 . 'ext',
122  'sysext' => PATH_typo3 . 'sysext',
123  );
124  $this->packageStatesPathAndFilename = PATH_typo3conf . 'PackageStates.php';
125  }
126 
130  public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache)
131  {
132  $this->coreCache = $coreCache;
133  }
134 
139  {
140  $this->dependencyResolver = $dependencyResolver;
141  }
142 
150  public function initialize(Bootstrap $bootstrap)
151  {
152  $this->bootstrap = $bootstrap;
153 
154  $loadedFromCache = false;
155  try {
157  $loadedFromCache = true;
158  } catch (Exception\PackageManagerCacheUnavailableException $exception) {
159  $this->loadPackageStates();
160  $this->initializePackageObjects();
162  }
163 
164  if (!$loadedFromCache) {
165  $this->saveToPackageCache();
166  }
167  }
168 
172  protected function getCacheIdentifier()
173  {
174  if ($this->cacheIdentifier === null) {
175  $mTime = @filemtime($this->packageStatesPathAndFilename);
176  if ($mTime !== false) {
177  $this->cacheIdentifier = md5($this->packageStatesPathAndFilename . $mTime);
178  } else {
179  $this->cacheIdentifier = null;
180  }
181  }
182  return $this->cacheIdentifier;
183  }
184 
188  protected function getCacheEntryIdentifier()
189  {
191  return $cacheIdentifier !== null ? 'PackageManager_' . $cacheIdentifier : null;
192  }
193 
197  protected function saveToPackageCache()
198  {
199  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
200  if ($cacheEntryIdentifier !== null && !$this->coreCache->has($cacheEntryIdentifier)) {
201  // Package objects get their own cache entry, so PHP does not have to parse the serialized string
202  $packageObjectsCacheEntryIdentifier = StringUtility::getUniqueId('PackageObjects_');
203  // Build cache file
204  $packageCache = array(
205  'packageStatesConfiguration' => $this->packageStatesConfiguration,
206  'packageAliasMap' => $this->packageAliasMap,
207  'packageKeys' => $this->packageKeys,
208  'activePackageKeys' => array_keys($this->activePackages),
209  'requiredPackageKeys' => $this->requiredPackageKeys,
210  'loadedExtArray' => $GLOBALS['TYPO3_LOADED_EXT'],
211  'packageObjectsCacheEntryIdentifier' => $packageObjectsCacheEntryIdentifier
212  );
213  $this->coreCache->set($packageObjectsCacheEntryIdentifier, serialize($this->packages));
214  $this->coreCache->set(
215  $cacheEntryIdentifier,
216  'return ' . PHP_EOL . var_export($packageCache, true) . ';'
217  );
218  }
219  }
220 
227  {
228  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
229  if ($cacheEntryIdentifier === null || !$this->coreCache->has($cacheEntryIdentifier) || !($packageCache = $this->coreCache->requireOnce($cacheEntryIdentifier))) {
230  throw new Exception\PackageManagerCacheUnavailableException('The package state cache could not be loaded.', 1393883342);
231  }
232  $this->packageStatesConfiguration = $packageCache['packageStatesConfiguration'];
233  $this->packageAliasMap = $packageCache['packageAliasMap'];
234  $this->packageKeys = $packageCache['packageKeys'];
235  $this->requiredPackageKeys = $packageCache['requiredPackageKeys'];
236  $GLOBALS['TYPO3_LOADED_EXT'] = $packageCache['loadedExtArray'];
237  $GLOBALS['TYPO3_currentPackageManager'] = $this;
238  // Strip off PHP Tags from Php Cache Frontend
239  $packageObjects = substr(substr($this->coreCache->get($packageCache['packageObjectsCacheEntryIdentifier']), 6), 0, -2);
240  $this->packages = unserialize($packageObjects);
241  foreach ($packageCache['activePackageKeys'] as $activePackageKey) {
242  $this->activePackages[$activePackageKey] = $this->packages[$activePackageKey];
243  }
244  unset($GLOBALS['TYPO3_currentPackageManager']);
245  }
246 
254  protected function loadPackageStates()
255  {
256  $this->packageStatesConfiguration = @include($this->packageStatesPathAndFilename) ?: array();
257  if (!isset($this->packageStatesConfiguration['version']) || $this->packageStatesConfiguration['version'] < 4) {
258  $this->packageStatesConfiguration = array();
259  }
260  if ($this->packageStatesConfiguration !== array()) {
262  } else {
263  throw new Exception\PackageStatesUnavailableException('The PackageStates.php file is either corrupt or unavailable.', 1381507733);
264  }
265  }
266 
274  protected function initializePackageObjects()
275  {
276  $requiredPackages = array();
277  foreach ($this->packages as $packageKey => $package) {
278  if ($package->isProtected()) {
279  $requiredPackages[$packageKey] = $package;
280  }
281  if (isset($this->packageStatesConfiguration['packages'][$packageKey]['state']) && $this->packageStatesConfiguration['packages'][$packageKey]['state'] === 'active') {
282  $this->activePackages[$packageKey] = $package;
283  }
284  }
285  $previousActivePackages = $this->activePackages;
286  $this->activePackages = array_merge($requiredPackages, $this->activePackages);
287  $this->requiredPackageKeys = array_keys($requiredPackages);
288 
289  if ($this->activePackages != $previousActivePackages) {
290  foreach ($this->requiredPackageKeys as $requiredPackageKey) {
291  $this->packageStatesConfiguration['packages'][$requiredPackageKey]['state'] = 'active';
292  }
293  $this->sortAndSavePackageStates();
294  }
295  }
296 
303  {
304  $loadedExtObj = new \TYPO3\CMS\Core\Compatibility\LoadedExtensionsArray($this);
305  $GLOBALS['TYPO3_LOADED_EXT'] = $loadedExtObj->toArray();
306  }
307 
308 
315  public function scanAvailablePackages()
316  {
317  $previousPackageStatesConfiguration = $this->packageStatesConfiguration;
318 
319  if (isset($this->packageStatesConfiguration['packages'])) {
320  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $configuration) {
321  if (!@file_exists($this->packagesBasePath . $configuration['packagePath'])) {
322  unset($this->packageStatesConfiguration['packages'][$packageKey]);
323  }
324  }
325  } else {
326  $this->packageStatesConfiguration['packages'] = array();
327  }
328 
329  foreach ($this->packagesBasePaths as $key => $packagesBasePath) {
330  if (!is_dir($packagesBasePath)) {
331  unset($this->packagesBasePaths[$key]);
332  }
333  }
334 
335  $packagePaths = $this->scanLegacyExtensions();
336  foreach ($this->packagesBasePaths as $packagesBasePath) {
337  $packagePaths = $this->scanPackagesInPath($packagesBasePath, $packagePaths);
338  }
339 
340  foreach ($packagePaths as $packagePath) {
341  $packagesBasePath = $this->packagesBasePath;
342  foreach ($this->packagesBasePaths as $basePath) {
343  if (strpos($packagePath, $basePath) === 0) {
344  $packagesBasePath = $basePath;
345  break;
346  }
347  }
348  try {
349  $composerManifest = $this->getComposerManifest($packagePath);
350  $packageKey = $this->getPackageKeyFromManifest($composerManifest, $packagePath, $packagesBasePath);
351  $this->composerNameToPackageKeyMap[strtolower($composerManifest->name)] = $packageKey;
352  $this->packageStatesConfiguration['packages'][$packageKey]['composerName'] = $composerManifest->name;
353  } catch (Exception\MissingPackageManifestException $exception) {
354  $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
355  $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
356  if (!$this->isPackageKeyValid($packageKey)) {
357  continue;
358  }
359  } catch (Exception\InvalidPackageKeyException $exception) {
360  continue;
361  }
362  if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['state'])) {
363  $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
364  }
365 
366  $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $packagePath);
367  }
368 
369  $registerOnlyNewPackages = !empty($this->packages);
370  $this->registerPackagesFromConfiguration($registerOnlyNewPackages);
371  if ($this->packageStatesConfiguration != $previousPackageStatesConfiguration) {
372  $this->sortAndsavePackageStates();
373  }
374  }
375 
382  protected function scanLegacyExtensions(&$collectedExtensionPaths = array())
383  {
384  $legacyCmsPackageBasePathTypes = array('sysext', 'global', 'local');
385  foreach ($this->packagesBasePaths as $type => $packageBasePath) {
386  if (!in_array($type, $legacyCmsPackageBasePathTypes, true)) {
387  continue;
388  }
390  foreach (new \DirectoryIterator($packageBasePath) as $fileInfo) {
391  if (!$fileInfo->isDir()) {
392  continue;
393  }
394  $filename = $fileInfo->getFilename();
395  if ($filename[0] !== '.') {
396  // Fix Windows backslashes
397  // we can't use GeneralUtility::fixWindowsFilePath as we have to keep double slashes for Unit Tests (vfs://)
398  $currentPath = str_replace('\\', '/', $fileInfo->getPathName()) . '/';
399  // Only add the extension if we have an EMCONF and the extension is not yet registered.
400  // This is crucial in order to allow overriding of system extension by local extensions
401  // and strongly depends on the order of paths defined in $this->packagesBasePaths.
402  if (file_exists($currentPath . 'ext_emconf.php') && !isset($collectedExtensionPaths[$filename])) {
403  $collectedExtensionPaths[$filename] = $currentPath;
404  }
405  }
406  }
407  }
408  return $collectedExtensionPaths;
409  }
410 
418  protected function hasComposerManifestFile($packagePath)
419  {
420  // If an ext_emconf.php file is found, we don't need to look further
421  if (file_exists($packagePath . 'ext_emconf.php')) {
422  return false;
423  }
424  if (file_exists($packagePath . 'composer.json')) {
425  return true;
426  }
427  return false;
428  }
429 
436  protected function registerPackagesFromConfiguration($registerOnlyNewPackages = false)
437  {
438  $packageStatesHasChanged = false;
439  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $stateConfiguration) {
440  if ($registerOnlyNewPackages && $this->isPackageAvailable($packageKey)) {
441  continue;
442  }
443 
444  if (!isset($stateConfiguration['packagePath'])) {
445  $this->unregisterPackageByPackageKey($packageKey);
446  $packageStatesHasChanged = true;
447  continue;
448  }
449 
450  try {
451  $packagePath = PathUtility::sanitizeTrailingSeparator($this->packagesBasePath . $stateConfiguration['packagePath']);
452  $package = new Package($this, $packageKey, $packagePath);
453  } catch (Exception\InvalidPackagePathException $exception) {
454  $this->unregisterPackageByPackageKey($packageKey);
455  $packageStatesHasChanged = true;
456  continue;
457  } catch (Exception\InvalidPackageKeyException $exception) {
458  $this->unregisterPackageByPackageKey($packageKey);
459  $packageStatesHasChanged = true;
460  continue;
461  } catch (Exception\InvalidPackageManifestException $exception) {
462  $this->unregisterPackageByPackageKey($packageKey);
463  $packageStatesHasChanged = true;
464  continue;
465  }
466 
467  $this->registerPackage($package, false);
468 
469  $this->packageKeys[strtolower($packageKey)] = $packageKey;
470  if ($stateConfiguration['state'] === 'active') {
471  $this->activePackages[$packageKey] = $this->packages[$packageKey];
472  }
473  }
474  if ($packageStatesHasChanged) {
475  $this->sortAndSavePackageStates();
476  }
477  }
478 
488  public function registerPackage(PackageInterface $package, $sortAndSave = true)
489  {
490  $packageKey = $package->getPackageKey();
491  if ($this->isPackageAvailable($packageKey)) {
492  throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is already registered.', 1338996122);
493  }
494 
495  $this->packages[$packageKey] = $package;
496  $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
497 
498  if ($sortAndSave === true) {
499  $this->sortAndSavePackageStates();
500  }
501 
502  if ($package instanceof PackageInterface) {
503  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
504  $this->packageAliasMap[strtolower($packageToReplace)] = $package->getPackageKey();
505  }
506  }
507  return $package;
508  }
509 
516  protected function unregisterPackageByPackageKey($packageKey)
517  {
518  try {
519  $package = $this->getPackage($packageKey);
520  if ($package instanceof PackageInterface) {
521  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
522  unset($this->packageAliasMap[strtolower($packageToReplace)]);
523  }
524  }
525  } catch (Exception\UnknownPackageException $e) {
526  }
527  unset($this->packages[$packageKey]);
528  unset($this->packageKeys[strtolower($packageKey)]);
529  unset($this->packageStatesConfiguration['packages'][$packageKey]);
530  }
531 
538  public function getPackageKeyFromComposerName($composerName)
539  {
540  if (isset($this->packageAliasMap[$composerName])) {
541  return $this->packageAliasMap[$composerName];
542  }
543  if (empty($this->composerNameToPackageKeyMap)) {
544  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $packageStateConfiguration) {
545  $this->composerNameToPackageKeyMap[strtolower($packageStateConfiguration['composerName'])] = $packageKey;
546  }
547  // Hard coded compatibility layer for old cms extension
548  // @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
549  $this->composerNameToPackageKeyMap['typo3/cms-cms'] = 'cms';
550  }
551  $lowercasedComposerName = strtolower($composerName);
552  if (!isset($this->composerNameToPackageKeyMap[$lowercasedComposerName])) {
553  return $composerName;
554  }
555  return $this->composerNameToPackageKeyMap[$lowercasedComposerName];
556  }
557 
567  public function getPackage($packageKey)
568  {
569  if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
570  $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
571  }
572  if (!$this->isPackageAvailable($packageKey)) {
573  throw new 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);
574  }
575  return $this->packages[$packageKey];
576  }
577 
586  public function isPackageAvailable($packageKey)
587  {
588  if (isset($this->packageAliasMap[$lowercasedPackageKey = strtolower($packageKey)])) {
589  $packageKey = $this->packageAliasMap[$lowercasedPackageKey];
590  }
591  return isset($this->packages[$packageKey]);
592  }
593 
601  public function isPackageActive($packageKey)
602  {
603  return isset($this->runtimeActivatedPackages[$packageKey]) || isset($this->activePackages[$packageKey]);
604  }
605 
614  public function deactivatePackage($packageKey)
615  {
617 
618  foreach ($this->packageStatesConfiguration['packages'] as $packageStateKey => $packageStateConfiguration) {
619  if ($packageKey === $packageStateKey || empty($packageStateConfiguration['dependencies']) || $packageStateConfiguration['state'] !== 'active') {
620  continue;
621  }
622  if (in_array($packageKey, $packageStateConfiguration['dependencies'], true)) {
623  $this->deactivatePackage($packageStateKey);
624  }
625  }
626 
627  if (!$this->isPackageActive($packageKey)) {
628  return;
629  }
630 
631  $package = $this->getPackage($packageKey);
632  if ($package->isProtected()) {
633  throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be deactivated.', 1308662891);
634  }
635 
636  unset($this->activePackages[$packageKey]);
637  $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'inactive';
638  $this->sortAndSavePackageStates();
639  }
640 
644  public function activatePackage($packageKey)
645  {
646  $package = $this->getPackage($packageKey);
648 
649  if ($this->isPackageActive($packageKey)) {
650  return;
651  }
652 
653  $this->activePackages[$packageKey] = $package;
654  $this->packageStatesConfiguration['packages'][$packageKey]['state'] = 'active';
655  if (!isset($this->packageStatesConfiguration['packages'][$packageKey]['packagePath'])) {
656  $this->packageStatesConfiguration['packages'][$packageKey]['packagePath'] = str_replace($this->packagesBasePath, '', $package->getPackagePath());
657  }
658  $this->sortAndSavePackageStates();
659  }
660 
667  public function activatePackageDuringRuntime($packageKey)
668  {
669  $package = $this->getPackage($packageKey);
670  $this->runtimeActivatedPackages[$package->getPackageKey()] = $package;
671  if (!isset($GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()])) {
672  $loadedExtArrayElement = new LoadedExtensionArrayElement($package);
673  $GLOBALS['TYPO3_LOADED_EXT'][$package->getPackageKey()] = $loadedExtArrayElement->toArray();
674  }
676  }
677 
683  {
685  return;
686  }
687  ClassLoadingInformation::registerTransientClassLoadingInformationForPackage($package);
688  }
689 
699  public function deletePackage($packageKey)
700  {
701  if (!$this->isPackageAvailable($packageKey)) {
702  throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available and cannot be removed.', 1166543253);
703  }
704 
705  $package = $this->getPackage($packageKey);
706  if ($package->isProtected()) {
707  throw new Exception\ProtectedPackageKeyException('The package "' . $packageKey . '" is protected and cannot be removed.', 1220722120);
708  }
709 
710  if ($this->isPackageActive($packageKey)) {
711  $this->deactivatePackage($packageKey);
712  }
713 
714  $this->unregisterPackage($package);
715  $this->sortAndSavePackageStates();
716 
717  $packagePath = $package->getPackagePath();
718  $deletion = GeneralUtility::rmdir($packagePath, true);
719  if ($deletion === false) {
720  throw new Exception('Please check file permissions. The directory "' . $packagePath . '" for package "' . $packageKey . '" could not be removed.', 1301491089);
721  }
722  }
723 
724 
733  public function getActivePackages()
734  {
735  return array_merge($this->activePackages, $this->runtimeActivatedPackages);
736  }
737 
746  {
748 
749  // sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
750  ksort($this->packageStatesConfiguration['packages']);
751  $this->packageStatesConfiguration['packages'] = $this->dependencyResolver->sortPackageStatesConfigurationByDependency($this->packageStatesConfiguration['packages']);
752 
753  // Reorder the packages according to the loading order
754  $newPackages = array();
755  foreach ($this->packageStatesConfiguration['packages'] as $packageKey => $_) {
756  $newPackages[$packageKey] = $this->packages[$packageKey];
757  }
758  $this->packages = $newPackages;
759  }
760 
768  protected function resolvePackageDependencies()
769  {
770  foreach ($this->packages as $packageKey => $package) {
771  $this->packageStatesConfiguration['packages'][$packageKey]['dependencies'] = $this->getDependencyArrayForPackage($packageKey);
772  }
773  foreach ($this->packages as $packageKey => $package) {
774  $this->packageStatesConfiguration['packages'][$packageKey]['suggestions'] = $this->getSuggestionArrayForPackage($packageKey);
775  }
776  }
777 
784  protected function getSuggestionArrayForPackage($packageKey)
785  {
786  if (!isset($this->packages[$packageKey])) {
787  return null;
788  }
789  $suggestedPackageKeys = array();
790  $suggestedPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_SUGGESTS);
791  foreach ($suggestedPackageConstraints as $constraint) {
792  if ($constraint instanceof MetaData\PackageConstraint) {
793  $suggestedPackageKey = $constraint->getValue();
794  if (isset($this->packages[$suggestedPackageKey])) {
795  $suggestedPackageKeys[] = $suggestedPackageKey;
796  }
797  }
798  }
799  return array_reverse($suggestedPackageKeys);
800  }
801 
808  protected function sortAndSavePackageStates()
809  {
811 
812  $this->packageStatesConfiguration['version'] = 4;
813 
814  $fileDescription = "# PackageStates.php\n\n";
815  $fileDescription .= "# This file is maintained by TYPO3's package management. Although you can edit it\n";
816  $fileDescription .= "# manually, you should rather use the extension manager for maintaining packages.\n";
817  $fileDescription .= "# This file will be regenerated automatically if it doesn't exist. Deleting this file\n";
818  $fileDescription .= "# should, however, never become necessary if you use the package commands.\n";
819 
820  // We do not need the dependencies on disk...
821  foreach ($this->packageStatesConfiguration['packages'] as &$packageConfiguration) {
822  if (isset($packageConfiguration['dependencies'])) {
823  unset($packageConfiguration['dependencies']);
824  }
825  }
826  if (!@is_writable($this->packageStatesPathAndFilename)) {
827  // If file does not exists try to create it
828  $fileHandle = @fopen($this->packageStatesPathAndFilename, 'x');
829  if (!$fileHandle) {
830  throw new Exception\PackageStatesFileNotWritableException(
831  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),
832  1382449759
833  );
834  }
835  fclose($fileHandle);
836  }
837  $packageStatesCode = "<?php\n$fileDescription\nreturn " . var_export($this->packageStatesConfiguration, true) . "\n ?>";
838  GeneralUtility::writeFile($this->packageStatesPathAndFilename, $packageStatesCode, true);
839 
841 
842  GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($this->packageStatesPathAndFilename);
843  }
844 
852  public function isPackageKeyValid($packageKey)
853  {
854  return preg_match(PackageInterface::PATTERN_MATCH_PACKAGEKEY, $packageKey) === 1 || preg_match(PackageInterface::PATTERN_MATCH_EXTENSIONKEY, $packageKey) === 1;
855  }
856 
864  public function getAvailablePackages()
865  {
866  return $this->packages;
867  }
868 
876  public function unregisterPackage(PackageInterface $package)
877  {
878  $packageKey = $package->getPackageKey();
879  if (!$this->isPackageAvailable($packageKey)) {
880  throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1338996142);
881  }
882  $this->unregisterPackageByPackageKey($packageKey);
883  }
884 
894  public function reloadPackageInformation($packageKey)
895  {
896  if (!$this->isPackageAvailable($packageKey)) {
897  throw new Exception\InvalidPackageStateException('Package "' . $packageKey . '" is not registered.', 1436201329);
898  }
899 
901  $package = $this->packages[$packageKey];
902  $packagePath = $package->getPackagePath();
903  $newPackage = new Package($this, $packageKey, $packagePath);
904  $this->packages[$packageKey] = $newPackage;
905  unset($package);
906  }
907 
917  protected function scanPackagesInPath($startPath, array $collectedPackagePaths)
918  {
919  foreach (new \DirectoryIterator($startPath) as $fileInfo) {
920  if (!$fileInfo->isDir()) {
921  continue;
922  }
923  $filename = $fileInfo->getFilename();
924  if ($filename[0] !== '.') {
925  $currentPath = $fileInfo->getPathName();
926  $currentPath = PathUtility::sanitizeTrailingSeparator($currentPath);
927  if ($this->hasComposerManifestFile($currentPath)) {
928  $collectedPackagePaths[$currentPath] = $currentPath;
929  }
930  }
931  }
932  return $collectedPackagePaths;
933  }
934 
943  public function getComposerManifest($manifestPath)
944  {
945  $composerManifest = null;
946  if (file_exists($manifestPath . 'composer.json')) {
947  $json = file_get_contents($manifestPath . 'composer.json');
948  $composerManifest = json_decode($json);
949  if (!$composerManifest instanceof \stdClass) {
950  throw new Exception\InvalidPackageManifestException('The composer.json found for extension "' . basename($manifestPath) . '" is invalid!', 1439555561);
951  }
952  }
953 
954  try {
955  $extensionManagerConfiguration = $this->getExtensionEmConf($manifestPath);
956  $composerManifest = $this->mapExtensionManagerConfigurationToComposerManifest(
957  basename($manifestPath),
958  $extensionManagerConfiguration,
959  $composerManifest ?: new \stdClass()
960  );
961  } catch (Exception\InvalidPackageManifestException $e) {
962  if ($composerManifest === null) {
963  throw $e;
964  }
965  }
966 
967  return $composerManifest;
968  }
969 
978  protected function getExtensionEmConf($packagePath)
979  {
980  $packageKey = basename($packagePath);
981  $_EXTKEY = $packageKey;
982  $path = $packagePath . 'ext_emconf.php';
983  $EM_CONF = null;
984  if (@file_exists($path)) {
985  include $path;
986  if (is_array($EM_CONF[$_EXTKEY])) {
987  return $EM_CONF[$_EXTKEY];
988  }
989  }
990  throw new Exception\InvalidPackageManifestException('No valid ext_emconf.php file found for package "' . $packageKey . '".', 1360403545);
991  }
992 
1002  protected function mapExtensionManagerConfigurationToComposerManifest($packageKey, array $extensionManagerConfiguration, \stdClass $composerManifest)
1003  {
1004  $this->setComposerManifestValueIfEmpty($composerManifest, 'name', $packageKey);
1005  $this->setComposerManifestValueIfEmpty($composerManifest, 'type', 'typo3-cms-extension');
1006  $this->setComposerManifestValueIfEmpty($composerManifest, 'description', $extensionManagerConfiguration['title']);
1007  $composerManifest->version = $extensionManagerConfiguration['version'];
1008  if (isset($extensionManagerConfiguration['constraints']['depends']) && is_array($extensionManagerConfiguration['constraints']['depends'])) {
1009  $composerManifest->require = new \stdClass();
1010  foreach ($extensionManagerConfiguration['constraints']['depends'] as $requiredPackageKey => $requiredPackageVersion) {
1011  if (!empty($requiredPackageKey)) {
1012  if ($requiredPackageKey === 'typo3') {
1013  // Add implicit dependency to 'core'
1014  $composerManifest->require->core = $requiredPackageVersion;
1015  } elseif ($requiredPackageKey !== 'php') {
1016  // Skip php dependency
1017  $composerManifest->require->{$requiredPackageKey} = $requiredPackageVersion;
1018  }
1019  } else {
1020  throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in depends section. Extension key is missing!', $packageKey), 1439552058);
1021  }
1022  }
1023  }
1024  if (isset($extensionManagerConfiguration['constraints']['conflicts']) && is_array($extensionManagerConfiguration['constraints']['conflicts'])) {
1025  $composerManifest->conflict = new \stdClass();
1026  foreach ($extensionManagerConfiguration['constraints']['conflicts'] as $conflictingPackageKey => $conflictingPackageVersion) {
1027  if (!empty($conflictingPackageKey)) {
1028  $composerManifest->conflict->$conflictingPackageKey = $conflictingPackageVersion;
1029  } else {
1030  throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in conflicts section. Extension key is missing!', $packageKey), 1439552059);
1031  }
1032  }
1033  }
1034  if (isset($extensionManagerConfiguration['constraints']['suggests']) && is_array($extensionManagerConfiguration['constraints']['suggests'])) {
1035  $composerManifest->suggest = new \stdClass();
1036  foreach ($extensionManagerConfiguration['constraints']['suggests'] as $suggestedPackageKey => $suggestedPackageVersion) {
1037  if (!empty($suggestedPackageKey)) {
1038  $composerManifest->suggest->$suggestedPackageKey = $suggestedPackageVersion;
1039  } else {
1040  throw new Exception\InvalidPackageManifestException(sprintf('The extension "%s" has invalid version constraints in suggests section. Extension key is missing!', $packageKey), 1439552060);
1041  }
1042  }
1043  }
1044  if (isset($extensionManagerConfiguration['autoload'])) {
1045  $composerManifest->autoload = json_decode(json_encode($extensionManagerConfiguration['autoload']));
1046  }
1047  // composer.json autoload-dev information must be discarded, as it may contain information only available after a composer install
1048  unset($composerManifest->{'autoload-dev'});
1049  if (isset($extensionManagerConfiguration['autoload-dev'])) {
1050  $composerManifest->{'autoload-dev'} = json_decode(json_encode($extensionManagerConfiguration['autoload-dev']));
1051  }
1052 
1053  return $composerManifest;
1054  }
1055 
1062  protected function setComposerManifestValueIfEmpty(\stdClass $manifest, $property, $value)
1063  {
1064  if (empty($manifest->{$property})) {
1065  $manifest->{$property} = $value;
1066  }
1067 
1068  return $manifest;
1069  }
1070 
1082  protected function getDependencyArrayForPackage($packageKey, array &$dependentPackageKeys = array(), array $trace = array())
1083  {
1084  if (!isset($this->packages[$packageKey])) {
1085  return null;
1086  }
1087  if (in_array($packageKey, $trace, true) !== false) {
1088  return $dependentPackageKeys;
1089  }
1090  $trace[] = $packageKey;
1091  $dependentPackageConstraints = $this->packages[$packageKey]->getPackageMetaData()->getConstraintsByType(MetaData::CONSTRAINT_TYPE_DEPENDS);
1092  foreach ($dependentPackageConstraints as $constraint) {
1093  if ($constraint instanceof MetaData\PackageConstraint) {
1094  $dependentPackageKey = $constraint->getValue();
1095  if (in_array($dependentPackageKey, $dependentPackageKeys, true) === false && in_array($dependentPackageKey, $trace, true) === false) {
1096  $dependentPackageKeys[] = $dependentPackageKey;
1097  }
1098  $this->getDependencyArrayForPackage($dependentPackageKey, $dependentPackageKeys, $trace);
1099  }
1100  }
1101  return array_reverse($dependentPackageKeys);
1102  }
1103 
1104 
1121  protected function getPackageKeyFromManifest($manifest, $packagePath, $packagesBasePath)
1122  {
1123  if (!is_object($manifest)) {
1124  throw new Exception\InvalidPackageManifestException('Invalid composer manifest in package path: ' . $packagePath, 1348146451);
1125  }
1126  if (isset($manifest->type) && substr($manifest->type, 0, 10) === 'typo3-cms-') {
1127  $relativePackagePath = substr($packagePath, strlen($packagesBasePath));
1128  $packageKey = substr($relativePackagePath, strpos($relativePackagePath, '/') + 1, -1);
1129  return preg_replace('/[^A-Za-z0-9._-]/', '', $packageKey);
1130  } else {
1131  $packageKey = str_replace('/', '.', $manifest->name);
1132  return preg_replace('/[^A-Za-z0-9.]/', '', $packageKey);
1133  }
1134  }
1135 }