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