Revert "[TASK] Avoid slow array functions in loops"
[Packages/TYPO3.CMS.git] / typo3 / sysext / extensionmanager / Classes / Service / ExtensionManagementService.php
1 <?php
2 namespace TYPO3\CMS\Extensionmanager\Service;
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
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Extbase\Object\ObjectManager;
19 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
20 use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
21
22 /**
23 * Service class for managing multiple step processes (dependencies for example)
24 */
25 class ExtensionManagementService implements \TYPO3\CMS\Core\SingletonInterface
26 {
27 /**
28 * @var \TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue
29 */
30 protected $downloadQueue;
31
32 /**
33 * @var \TYPO3\CMS\Extensionmanager\Utility\DependencyUtility
34 */
35 protected $dependencyUtility;
36
37 /**
38 * @var \TYPO3\CMS\Extensionmanager\Utility\InstallUtility
39 */
40 protected $installUtility;
41
42 /**
43 * @var \TYPO3\CMS\Extensionmanager\Utility\ExtensionModelUtility
44 */
45 protected $extensionModelUtility;
46
47 /**
48 * @var \TYPO3\CMS\Extensionmanager\Utility\DownloadUtility
49 */
50 protected $downloadUtility;
51
52 /**
53 * @var bool
54 */
55 protected $automaticInstallationEnabled = true;
56
57 /**
58 * @var bool
59 */
60 protected $skipDependencyCheck = false;
61
62 /**
63 * @param \TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue $downloadQueue
64 */
65 public function injectDownloadQueue(\TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue $downloadQueue)
66 {
67 $this->downloadQueue = $downloadQueue;
68 }
69
70 /**
71 * @param \TYPO3\CMS\Extensionmanager\Utility\DependencyUtility $dependencyUtility
72 */
73 public function injectDependencyUtility(\TYPO3\CMS\Extensionmanager\Utility\DependencyUtility $dependencyUtility)
74 {
75 $this->dependencyUtility = $dependencyUtility;
76 }
77
78 /**
79 * @param \TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility
80 */
81 public function injectInstallUtility(\TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility)
82 {
83 $this->installUtility = $installUtility;
84 }
85
86 /**
87 * @param \TYPO3\CMS\Extensionmanager\Utility\ExtensionModelUtility $extensionModelUtility
88 */
89 public function injectExtensionModelUtility(\TYPO3\CMS\Extensionmanager\Utility\ExtensionModelUtility $extensionModelUtility)
90 {
91 $this->extensionModelUtility = $extensionModelUtility;
92 }
93
94 /**
95 * @param \TYPO3\CMS\Extensionmanager\Utility\DownloadUtility $downloadUtility
96 */
97 public function injectDownloadUtility(\TYPO3\CMS\Extensionmanager\Utility\DownloadUtility $downloadUtility)
98 {
99 $this->downloadUtility = $downloadUtility;
100 }
101
102 /**
103 * @param string $extensionKey
104 */
105 public function markExtensionForInstallation($extensionKey)
106 {
107 // We have to check for dependencies of the extension first, before marking it for installation
108 // because this extension might have dependencies, which need to be installed first
109 $this->installUtility->reloadAvailableExtensions();
110 $extension = $this->getExtension($extensionKey);
111 $this->dependencyUtility->checkDependencies($extension);
112 $this->downloadQueue->addExtensionToInstallQueue($extension);
113 }
114
115 /**
116 * Mark an extension for copy
117 *
118 * @param string $extensionKey
119 * @param string $sourceFolder
120 */
121 public function markExtensionForCopy($extensionKey, $sourceFolder)
122 {
123 $this->downloadQueue->addExtensionToCopyQueue($extensionKey, $sourceFolder);
124 }
125
126 /**
127 * Mark an extension for download
128 *
129 * @param Extension $extension
130 */
131 public function markExtensionForDownload(Extension $extension)
132 {
133 // We have to check for dependencies of the extension first, before marking it for download
134 // because this extension might have dependencies, which need to be downloaded and installed first
135 $this->dependencyUtility->checkDependencies($extension);
136 if (!$this->dependencyUtility->hasDependencyErrors()) {
137 $this->downloadQueue->addExtensionToQueue($extension);
138 }
139 }
140
141 /**
142 * @param Extension $extension
143 */
144 public function markExtensionForUpdate(Extension $extension)
145 {
146 // We have to check for dependencies of the extension first, before marking it for download
147 // because this extension might have dependencies, which need to be downloaded and installed first
148 $this->dependencyUtility->checkDependencies($extension);
149 $this->downloadQueue->addExtensionToQueue($extension, 'update');
150 }
151
152 /**
153 * Enables or disables the dependency check for system environment (PHP, TYPO3) before extension installation
154 *
155 * @param bool $skipDependencyCheck
156 */
157 public function setSkipDependencyCheck($skipDependencyCheck)
158 {
159 $this->skipDependencyCheck = $skipDependencyCheck;
160 }
161
162 /**
163 * @param bool $automaticInstallationEnabled
164 */
165 public function setAutomaticInstallationEnabled($automaticInstallationEnabled)
166 {
167 $this->automaticInstallationEnabled = (bool)$automaticInstallationEnabled;
168 }
169
170 /**
171 * Install the extension
172 *
173 * @param Extension $extension
174 * @return bool|array Returns FALSE if dependencies cannot be resolved, otherwise array with installation information
175 */
176 public function installExtension(Extension $extension)
177 {
178 $this->downloadExtension($extension);
179 if (!$this->checkDependencies($extension)) {
180 return false;
181 }
182
183 $downloadedDependencies = [];
184 $updatedDependencies = [];
185 $installQueue = [];
186
187 // First resolve all dependencies and the sub-dependencies until all queues are empty as new extensions might be
188 // added each time
189 // Extensions have to be installed in reverse order. Extensions which were added at last are dependencies of
190 // earlier ones and need to be available before
191 while (!$this->downloadQueue->isCopyQueueEmpty()
192 || !$this->downloadQueue->isQueueEmpty('download')
193 || !$this->downloadQueue->isQueueEmpty('update')
194 ) {
195 // First copy all available extension
196 // This might change other queues again
197 $copyQueue = $this->downloadQueue->resetExtensionCopyStorage();
198 if (!empty($copyQueue)) {
199 $this->copyDependencies($copyQueue);
200 }
201 $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
202 // Get download and update information
203 $queue = $this->downloadQueue->resetExtensionQueue();
204 if (!empty($queue['download'])) {
205 $downloadedDependencies = array_merge($downloadedDependencies, $this->downloadDependencies($queue['download']));
206 }
207 $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
208 if ($this->automaticInstallationEnabled) {
209 if (!empty($queue['update'])) {
210 $this->downloadDependencies($queue['update']);
211 $updatedDependencies = array_merge($updatedDependencies, $this->uninstallDependenciesToBeUpdated($queue['update']));
212 }
213 $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
214 }
215 }
216
217 // If there were any dependency errors we have to abort here
218 if ($this->dependencyUtility->hasDependencyErrors()) {
219 return false;
220 }
221
222 // Attach extension to install queue
223 $this->downloadQueue->addExtensionToInstallQueue($extension);
224 $installQueue += $this->downloadQueue->resetExtensionInstallStorage();
225 $installedDependencies = [];
226 if ($this->automaticInstallationEnabled) {
227 $installedDependencies = $this->installDependencies($installQueue);
228 }
229
230 return array_merge($downloadedDependencies, $updatedDependencies, $installedDependencies);
231 }
232
233 /**
234 * Returns the unresolved dependency errors
235 *
236 * @return array
237 */
238 public function getDependencyErrors()
239 {
240 return $this->dependencyUtility->getDependencyErrors();
241 }
242
243 /**
244 * @param string $extensionKey
245 * @return Extension
246 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
247 */
248 public function getExtension($extensionKey)
249 {
250 return $this->extensionModelUtility->mapExtensionArrayToModel(
251 $this->installUtility->enrichExtensionWithDetails($extensionKey)
252 );
253 }
254
255 /**
256 * Checks if an extension is available in the system
257 *
258 * @param string $extensionKey
259 * @return bool
260 */
261 public function isAvailable($extensionKey)
262 {
263 return $this->installUtility->isAvailable($extensionKey);
264 }
265
266 /**
267 * @param string $extensionKey
268 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException if the package isn't available
269 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException if an invalid package key was passed
270 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException if an invalid package path was passed
271 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException if no extension configuration file could be found
272 */
273 public function reloadPackageInformation($extensionKey)
274 {
275 $this->installUtility->reloadPackageInformation($extensionKey);
276 }
277
278 /**
279 * Download an extension
280 *
281 * @param Extension $extension
282 */
283 protected function downloadExtension(Extension $extension)
284 {
285 $this->downloadMainExtension($extension);
286 $this->setInExtensionRepository($extension->getExtensionKey());
287 }
288
289 /**
290 * Check dependencies for an extension and its required extensions
291 *
292 * @param Extension $extension
293 * @return bool Returns TRUE if all dependencies can be resolved, otherwise FALSE
294 */
295 protected function checkDependencies(Extension $extension)
296 {
297 $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
298 $this->dependencyUtility->checkDependencies($extension);
299
300 return !$this->dependencyUtility->hasDependencyErrors();
301 }
302
303 /**
304 * Sets the path to the repository in an extension
305 * (Initialisation/Extensions) depending on the extension
306 * that is currently installed
307 *
308 * @param string $extensionKey
309 */
310 protected function setInExtensionRepository($extensionKey)
311 {
312 $paths = Extension::returnInstallPaths();
313 $path = $paths[$this->downloadUtility->getDownloadPath()];
314 $localExtensionStorage = $path . $extensionKey . '/Initialisation/Extensions/';
315 $this->dependencyUtility->setLocalExtensionStorage($localExtensionStorage);
316 }
317
318 /**
319 * Copies locally provided extensions to typo3conf/ext
320 *
321 * @param array $copyQueue
322 */
323 protected function copyDependencies(array $copyQueue)
324 {
325 $installPaths = Extension::returnAllowedInstallPaths();
326 foreach ($copyQueue as $extensionKey => $sourceFolder) {
327 $destination = $installPaths['Local'] . $extensionKey;
328 GeneralUtility::mkdir($destination);
329 GeneralUtility::copyDirectory($sourceFolder . $extensionKey, $destination);
330 $this->markExtensionForInstallation($extensionKey);
331 $this->downloadQueue->removeExtensionFromCopyQueue($extensionKey);
332 }
333 }
334
335 /**
336 * Uninstall extensions that will be updated
337 * This is not strictly necessary but cleaner all in all
338 *
339 * @param Extension[] $updateQueue
340 * @return array
341 */
342 protected function uninstallDependenciesToBeUpdated(array $updateQueue)
343 {
344 $resolvedDependencies = [];
345 foreach ($updateQueue as $extensionToUpdate) {
346 $this->installUtility->uninstall($extensionToUpdate->getExtensionKey());
347 $resolvedDependencies['updated'][$extensionToUpdate->getExtensionKey()] = $extensionToUpdate;
348 }
349 return $resolvedDependencies;
350 }
351
352 /**
353 * Install dependent extensions
354 *
355 * @param array $installQueue
356 * @return array
357 */
358 protected function installDependencies(array $installQueue)
359 {
360 if (empty($installQueue)) {
361 return [];
362 }
363 $this->emitWillInstallExtensionsSignal($installQueue);
364 $resolvedDependencies = [];
365 $this->installUtility->install(...array_keys($installQueue));
366 foreach ($installQueue as $extensionKey => $_) {
367 if (!isset($resolvedDependencies['installed']) || !is_array($resolvedDependencies['installed'])) {
368 $resolvedDependencies['installed'] = [];
369 }
370 $resolvedDependencies['installed'][$extensionKey] = $extensionKey;
371 }
372 return $resolvedDependencies;
373 }
374
375 /**
376 * Download dependencies
377 * expects an array of extension objects to download
378 *
379 * @param Extension[] $downloadQueue
380 * @return array
381 */
382 protected function downloadDependencies(array $downloadQueue)
383 {
384 $resolvedDependencies = [];
385 foreach ($downloadQueue as $extensionToDownload) {
386 $this->downloadUtility->download($extensionToDownload);
387 $this->downloadQueue->removeExtensionFromQueue($extensionToDownload);
388 $resolvedDependencies['downloaded'][$extensionToDownload->getExtensionKey()] = $extensionToDownload;
389 $this->markExtensionForInstallation($extensionToDownload->getExtensionKey());
390 }
391 return $resolvedDependencies;
392 }
393
394 /**
395 * Get and resolve dependencies
396 *
397 * @param Extension $extension
398 * @return array
399 */
400 public function getAndResolveDependencies(Extension $extension)
401 {
402 $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
403 $this->dependencyUtility->checkDependencies($extension);
404 $installQueue = $this->downloadQueue->getExtensionInstallStorage();
405 if (is_array($installQueue) && !empty($installQueue)) {
406 $installQueue = ['install' => $installQueue];
407 }
408 return array_merge($this->downloadQueue->getExtensionQueue(), $installQueue);
409 }
410
411 /**
412 * Downloads the extension the user wants to install
413 * This is separated from downloading the dependencies
414 * as an extension is able to provide it's own dependencies
415 *
416 * @param Extension $extension
417 */
418 public function downloadMainExtension(Extension $extension)
419 {
420 // The extension object has a uid if the extension is not present in the system
421 // or an update of a present extension is triggered.
422 if ($extension->getUid()) {
423 $this->downloadUtility->download($extension);
424 }
425 }
426
427 /**
428 * @param array $installQueue
429 */
430 protected function emitWillInstallExtensionsSignal(array $installQueue)
431 {
432 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'willInstallExtensions', [$installQueue]);
433 }
434
435 /**
436 * Get the SignalSlot dispatcher
437 *
438 * @return Dispatcher
439 */
440 protected function getSignalSlotDispatcher()
441 {
442 if (!isset($this->signalSlotDispatcher)) {
443 $this->signalSlotDispatcher = GeneralUtility::makeInstance(ObjectManager::class)
444 ->get(Dispatcher::class);
445 }
446 return $this->signalSlotDispatcher;
447 }
448 }