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