[TASK] Make ExtensionManagementServiceTest notice free
[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 = $this->installDependencies($installQueue);
226
227 return array_merge($downloadedDependencies, $updatedDependencies, $installedDependencies);
228 }
229
230 /**
231 * Returns the unresolved dependency errors
232 *
233 * @return array
234 */
235 public function getDependencyErrors()
236 {
237 return $this->dependencyUtility->getDependencyErrors();
238 }
239
240 /**
241 * @param string $extensionKey
242 * @return Extension
243 * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
244 */
245 public function getExtension($extensionKey)
246 {
247 return $this->extensionModelUtility->mapExtensionArrayToModel(
248 $this->installUtility->enrichExtensionWithDetails($extensionKey)
249 );
250 }
251
252 /**
253 * Checks if an extension is available in the system
254 *
255 * @param string $extensionKey
256 * @return bool
257 */
258 public function isAvailable($extensionKey)
259 {
260 return $this->installUtility->isAvailable($extensionKey);
261 }
262
263 /**
264 * @param string $extensionKey
265 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException if the package isn't available
266 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException if an invalid package key was passed
267 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException if an invalid package path was passed
268 * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException if no extension configuration file could be found
269 */
270 public function reloadPackageInformation($extensionKey)
271 {
272 $this->installUtility->reloadPackageInformation($extensionKey);
273 }
274
275 /**
276 * Download an extension
277 *
278 * @param Extension $extension
279 */
280 protected function downloadExtension(Extension $extension)
281 {
282 $this->downloadMainExtension($extension);
283 $this->setInExtensionRepository($extension->getExtensionKey());
284 }
285
286 /**
287 * Check dependencies for an extension and its required extensions
288 *
289 * @param Extension $extension
290 * @return bool Returns TRUE if all dependencies can be resolved, otherwise FALSE
291 */
292 protected function checkDependencies(Extension $extension)
293 {
294 $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
295 $this->dependencyUtility->checkDependencies($extension);
296
297 return !$this->dependencyUtility->hasDependencyErrors();
298 }
299
300 /**
301 * Sets the path to the repository in an extension
302 * (Initialisation/Extensions) depending on the extension
303 * that is currently installed
304 *
305 * @param string $extensionKey
306 */
307 protected function setInExtensionRepository($extensionKey)
308 {
309 $paths = Extension::returnInstallPaths();
310 $path = $paths[$this->downloadUtility->getDownloadPath()];
311 $localExtensionStorage = $path . $extensionKey . '/Initialisation/Extensions/';
312 $this->dependencyUtility->setLocalExtensionStorage($localExtensionStorage);
313 }
314
315 /**
316 * Copies locally provided extensions to typo3conf/ext
317 *
318 * @param array $copyQueue
319 */
320 protected function copyDependencies(array $copyQueue)
321 {
322 $installPaths = Extension::returnAllowedInstallPaths();
323 foreach ($copyQueue as $extensionKey => $sourceFolder) {
324 $destination = $installPaths['Local'] . $extensionKey;
325 GeneralUtility::mkdir($destination);
326 GeneralUtility::copyDirectory($sourceFolder . $extensionKey, $destination);
327 $this->markExtensionForInstallation($extensionKey);
328 $this->downloadQueue->removeExtensionFromCopyQueue($extensionKey);
329 }
330 }
331
332 /**
333 * Uninstall extensions that will be updated
334 * This is not strictly necessary but cleaner all in all
335 *
336 * @param Extension[] $updateQueue
337 * @return array
338 */
339 protected function uninstallDependenciesToBeUpdated(array $updateQueue)
340 {
341 $resolvedDependencies = [];
342 foreach ($updateQueue as $extensionToUpdate) {
343 $this->installUtility->uninstall($extensionToUpdate->getExtensionKey());
344 $resolvedDependencies['updated'][$extensionToUpdate->getExtensionKey()] = $extensionToUpdate;
345 }
346 return $resolvedDependencies;
347 }
348
349 /**
350 * Install dependent extensions
351 *
352 * @param array $installQueue
353 * @return array
354 */
355 protected function installDependencies(array $installQueue)
356 {
357 if (!empty($installQueue)) {
358 $this->emitWillInstallExtensionsSignal($installQueue);
359 }
360 $resolvedDependencies = [];
361 foreach ($installQueue as $extensionKey => $_) {
362 $this->installUtility->install($extensionKey);
363 $this->emitHasInstalledExtensionSignal($extensionKey);
364 if (!isset($resolvedDependencies['installed']) || !is_array($resolvedDependencies['installed'])) {
365 $resolvedDependencies['installed'] = [];
366 }
367 $resolvedDependencies['installed'][$extensionKey] = $extensionKey;
368 }
369 return $resolvedDependencies;
370 }
371
372 /**
373 * Download dependencies
374 * expects an array of extension objects to download
375 *
376 * @param Extension[] $downloadQueue
377 * @return array
378 */
379 protected function downloadDependencies(array $downloadQueue)
380 {
381 $resolvedDependencies = [];
382 foreach ($downloadQueue as $extensionToDownload) {
383 $this->downloadUtility->download($extensionToDownload);
384 $this->downloadQueue->removeExtensionFromQueue($extensionToDownload);
385 $resolvedDependencies['downloaded'][$extensionToDownload->getExtensionKey()] = $extensionToDownload;
386 $this->markExtensionForInstallation($extensionToDownload->getExtensionKey());
387 }
388 return $resolvedDependencies;
389 }
390
391 /**
392 * Get and resolve dependencies
393 *
394 * @param Extension $extension
395 * @return array
396 */
397 public function getAndResolveDependencies(Extension $extension)
398 {
399 $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
400 $this->dependencyUtility->checkDependencies($extension);
401 $installQueue = $this->downloadQueue->getExtensionInstallStorage();
402 if (is_array($installQueue) && !empty($installQueue)) {
403 $installQueue = ['install' => $installQueue];
404 }
405 return array_merge($this->downloadQueue->getExtensionQueue(), $installQueue);
406 }
407
408 /**
409 * Downloads the extension the user wants to install
410 * This is separated from downloading the dependencies
411 * as an extension is able to provide it's own dependencies
412 *
413 * @param Extension $extension
414 */
415 public function downloadMainExtension(Extension $extension)
416 {
417 // The extension object has a uid if the extension is not present in the system
418 // or an update of a present extension is triggered.
419 if ($extension->getUid()) {
420 $this->downloadUtility->download($extension);
421 }
422 }
423
424 /**
425 * @param array $installQueue
426 */
427 protected function emitWillInstallExtensionsSignal(array $installQueue)
428 {
429 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'willInstallExtensions', [$installQueue]);
430 }
431
432 /**
433 * @param string $extensionKey
434 */
435 protected function emitHasInstalledExtensionSignal($extensionKey)
436 {
437 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'hasInstalledExtensions', [$extensionKey]);
438 }
439
440 /**
441 * Get the SignalSlot dispatcher
442 *
443 * @return Dispatcher
444 */
445 protected function getSignalSlotDispatcher()
446 {
447 if (!isset($this->signalSlotDispatcher)) {
448 $this->signalSlotDispatcher = GeneralUtility::makeInstance(ObjectManager::class)
449 ->get(Dispatcher::class);
450 }
451 return $this->signalSlotDispatcher;
452 }
453 }