1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Extensionmanager\Utility; 17 18use Psr\EventDispatcher\EventDispatcherInterface; 19use TYPO3\CMS\Core\Core\Environment; 20use TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent; 21use TYPO3\CMS\Core\Package\PackageInterface; 22use TYPO3\CMS\Core\Package\PackageManager; 23use TYPO3\CMS\Core\SingletonInterface; 24use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 25use TYPO3\CMS\Core\Utility\GeneralUtility; 26use TYPO3\CMS\Core\Utility\PathUtility; 27use TYPO3\CMS\Core\Utility\VersionNumberUtility; 28use TYPO3\CMS\Extensionmanager\Domain\Model\Extension; 29use TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository; 30 31/** 32 * Utility for dealing with extension list related functions 33 * 34 * @TODO: Refactor this API class: 35 * - The methods depend on each other, they take each others result, that could be done internally 36 * - There is no good wording to distinguish existing and loaded extensions 37 * - The name 'listUtility' is not good, the methods could be moved to some 'extensionInformationUtility', or a repository? 38 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API. 39 */ 40class ListUtility implements SingletonInterface 41{ 42 /** 43 * @var EmConfUtility 44 */ 45 protected $emConfUtility; 46 47 /** 48 * @var ExtensionRepository 49 */ 50 protected $extensionRepository; 51 52 /** 53 * @var InstallUtility 54 */ 55 protected $installUtility; 56 57 /** 58 * @var PackageManager 59 */ 60 protected $packageManager; 61 62 /** 63 * @var array 64 */ 65 protected $availableExtensions; 66 67 /** 68 * @var EventDispatcherInterface 69 */ 70 protected $eventDispatcher; 71 72 public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher) 73 { 74 $this->eventDispatcher = $eventDispatcher; 75 } 76 77 /** 78 * @param EmConfUtility $emConfUtility 79 */ 80 public function injectEmConfUtility(EmConfUtility $emConfUtility) 81 { 82 $this->emConfUtility = $emConfUtility; 83 } 84 85 /** 86 * @param ExtensionRepository $extensionRepository 87 */ 88 public function injectExtensionRepository(ExtensionRepository $extensionRepository) 89 { 90 $this->extensionRepository = $extensionRepository; 91 } 92 93 /** 94 * @param InstallUtility $installUtility 95 */ 96 public function injectInstallUtility(InstallUtility $installUtility) 97 { 98 $this->installUtility = $installUtility; 99 } 100 101 /** 102 * @param PackageManager $packageManager 103 */ 104 public function injectPackageManager(PackageManager $packageManager) 105 { 106 $this->packageManager = $packageManager; 107 } 108 109 /** 110 * Returns the list of available, but not necessarily loaded extensions 111 * 112 * @param string $filter 113 * @return array[] All extensions with info 114 */ 115 public function getAvailableExtensions(string $filter = ''): array 116 { 117 if ($this->availableExtensions === null) { 118 $this->availableExtensions = []; 119 $this->eventDispatcher->dispatch(new PackagesMayHaveChangedEvent()); 120 foreach ($this->packageManager->getAvailablePackages() as $package) { 121 $installationType = $this->getInstallTypeForPackage($package); 122 if ($filter === '' || $filter === $installationType) { 123 $this->availableExtensions[$package->getPackageKey()] = [ 124 'siteRelPath' => str_replace(Environment::getPublicPath() . '/', '', $package->getPackagePath()), 125 'type' => $installationType, 126 'key' => $package->getPackageKey(), 127 'icon' => PathUtility::getAbsoluteWebPath($package->getPackagePath() . ExtensionManagementUtility::getExtensionIcon($package->getPackagePath())), 128 ]; 129 } 130 } 131 } 132 133 return $this->availableExtensions; 134 } 135 136 /** 137 * Reset and reload the available extensions 138 */ 139 public function reloadAvailableExtensions() 140 { 141 $this->availableExtensions = null; 142 $this->packageManager->scanAvailablePackages(); 143 $this->getAvailableExtensions(); 144 } 145 146 /** 147 * @param string $extensionKey 148 * @return \TYPO3\CMS\Core\Package\PackageInterface 149 * @throws \TYPO3\CMS\Core\Package\Exception\UnknownPackageException if the specified package is unknown 150 */ 151 public function getExtension($extensionKey) 152 { 153 return $this->packageManager->getPackage($extensionKey); 154 } 155 156 /** 157 * Returns "System", "Global" or "Local" based on extension position in filesystem. 158 * 159 * @param PackageInterface $package 160 * @return string 161 */ 162 protected function getInstallTypeForPackage(PackageInterface $package) 163 { 164 foreach (Extension::returnInstallPaths() as $installType => $installPath) { 165 if (GeneralUtility::isFirstPartOfStr($package->getPackagePath(), $installPath)) { 166 return $installType; 167 } 168 } 169 return ''; 170 } 171 172 /** 173 * Enrich the output of getAvailableExtensions() with an array key installed = 1 if an extension is loaded. 174 * 175 * @param array $availableExtensions 176 * @return array 177 */ 178 public function getAvailableAndInstalledExtensions(array $availableExtensions) 179 { 180 foreach ($this->packageManager->getActivePackages() as $extKey => $_) { 181 if (isset($availableExtensions[$extKey])) { 182 $availableExtensions[$extKey]['installed'] = true; 183 } 184 } 185 return $availableExtensions; 186 } 187 188 /** 189 * Adds the information from the emconf array to the extension information 190 * 191 * @param array $extensions 192 * @return array 193 */ 194 public function enrichExtensionsWithEmConfInformation(array $extensions) 195 { 196 foreach ($extensions as $extensionKey => $properties) { 197 $emconf = $this->emConfUtility->includeEmConf($extensionKey, $properties); 198 if (is_array($emconf)) { 199 $extensions[$extensionKey] = array_merge($emconf, $properties); 200 } else { 201 unset($extensions[$extensionKey]); 202 } 203 } 204 return $extensions; 205 } 206 207 /** 208 * Adds the information from the emconf array and TER to the extension information 209 * 210 * @param array $extensions 211 * @return array 212 */ 213 public function enrichExtensionsWithEmConfAndTerInformation(array $extensions) 214 { 215 $extensions = $this->enrichExtensionsWithEmConfInformation($extensions); 216 foreach ($extensions as $extensionKey => $properties) { 217 $terObject = $this->getExtensionTerData($extensionKey, $extensions[$extensionKey]['version'] ?? ''); 218 if ($terObject !== null) { 219 $extensions[$extensionKey]['terObject'] = $terObject; 220 $extensions[$extensionKey]['updateAvailable'] = false; 221 $extensions[$extensionKey]['updateToVersion'] = null; 222 $extensionToUpdate = $this->installUtility->getUpdateableVersion($terObject); 223 if ($extensionToUpdate !== false) { 224 $extensions[$extensionKey]['updateAvailable'] = true; 225 $extensions[$extensionKey]['updateToVersion'] = $extensionToUpdate; 226 } 227 } 228 } 229 return $extensions; 230 } 231 232 /** 233 * Tries to find given extension with given version in TER data. 234 * If extension is found but not the given version, we return TER data from highest version with version data set to 235 * given one. 236 * 237 * @param string $extensionKey Key of the extension 238 * @param string $version String representation of version number 239 * @return Extension|null Extension TER object or NULL if nothing found 240 */ 241 protected function getExtensionTerData($extensionKey, $version) 242 { 243 $terObject = $this->extensionRepository->findOneByExtensionKeyAndVersion($extensionKey, $version); 244 if (!$terObject instanceof Extension) { 245 // Version unknown in TER data, try to find extension 246 $terObject = $this->extensionRepository->findHighestAvailableVersion($extensionKey); 247 if ($terObject instanceof Extension) { 248 // Found in TER now, set version information to the known ones, so we can look if there is a newer one 249 // Use a cloned object, otherwise wrong information is stored in persistenceManager 250 $terObject = clone $terObject; 251 $terObject->setVersion($version); 252 $terObject->setIntegerVersion( 253 VersionNumberUtility::convertVersionNumberToInteger($terObject->getVersion()) 254 ); 255 } else { 256 $terObject = null; 257 } 258 } 259 260 return $terObject; 261 } 262 263 /** 264 * Gets all available and installed extension with additional information 265 * from em_conf and TER (if available) 266 * 267 * @param string $filter 268 * @return array 269 */ 270 public function getAvailableAndInstalledExtensionsWithAdditionalInformation(string $filter = ''): array 271 { 272 $availableExtensions = $this->getAvailableExtensions($filter); 273 $availableAndInstalledExtensions = $this->getAvailableAndInstalledExtensions($availableExtensions); 274 return $this->enrichExtensionsWithEmConfAndTerInformation($availableAndInstalledExtensions); 275 } 276} 277