Passed
Push — master ( fcbfb0...64542d )
by
unknown
13:36
created

injectExtensionModelUtility()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
16
namespace TYPO3\CMS\Extensionmanager\Service;
17
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
20
use TYPO3\CMS\Core\Core\Environment;
21
use TYPO3\CMS\Core\Package\Event\BeforePackageActivationEvent;
22
use TYPO3\CMS\Core\SingletonInterface;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
use TYPO3\CMS\Extensionmanager\Domain\Model\DownloadQueue;
25
use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
26
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
27
use TYPO3\CMS\Extensionmanager\Remote\RemoteRegistry;
28
use TYPO3\CMS\Extensionmanager\Utility\DependencyUtility;
29
use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility;
30
use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;
31
32
/**
33
 * Service class for managing multiple step processes (dependencies for example)
34
 */
35
class ExtensionManagementService implements SingletonInterface
36
{
37
    /**
38
     * @var DownloadQueue
39
     */
40
    protected $downloadQueue;
41
42
    /**
43
     * @var DependencyUtility
44
     */
45
    protected $dependencyUtility;
46
47
    /**
48
     * @var InstallUtility
49
     */
50
    protected $installUtility;
51
52
    /**
53
     * @var bool
54
     */
55
    protected $automaticInstallationEnabled = true;
56
57
    /**
58
     * @var bool
59
     */
60
    protected $skipDependencyCheck = false;
61
62
    /**
63
     * @var EventDispatcherInterface
64
     */
65
    protected $eventDispatcher;
66
67
    /**
68
     * @var FileHandlingUtility
69
     */
70
    protected $fileHandlingUtility;
71
72
    /**
73
     * @var RemoteRegistry
74
     */
75
    protected $remoteRegistry;
76
77
    /**
78
     * @var string
79
     */
80
    protected $downloadPath = 'Local';
81
82
    public function __construct(RemoteRegistry $remoteRegistry, FileHandlingUtility $fileHandlingUtility)
83
    {
84
        $this->remoteRegistry = $remoteRegistry;
85
        $this->fileHandlingUtility = $fileHandlingUtility;
86
    }
87
88
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
89
    {
90
        $this->eventDispatcher = $eventDispatcher;
91
    }
92
93
    /**
94
     * @param DownloadQueue $downloadQueue
95
     */
96
    public function injectDownloadQueue(DownloadQueue $downloadQueue)
97
    {
98
        $this->downloadQueue = $downloadQueue;
99
    }
100
101
    /**
102
     * @param DependencyUtility $dependencyUtility
103
     */
104
    public function injectDependencyUtility(DependencyUtility $dependencyUtility)
105
    {
106
        $this->dependencyUtility = $dependencyUtility;
107
    }
108
109
    /**
110
     * @param InstallUtility $installUtility
111
     */
112
    public function injectInstallUtility(InstallUtility $installUtility)
113
    {
114
        $this->installUtility = $installUtility;
115
    }
116
117
    /**
118
     * @param string $extensionKey
119
     */
120
    public function markExtensionForInstallation($extensionKey)
121
    {
122
        // We have to check for dependencies of the extension first, before marking it for installation
123
        // because this extension might have dependencies, which need to be installed first
124
        $this->installUtility->reloadAvailableExtensions();
125
        $extension = $this->getExtension($extensionKey);
126
        $this->dependencyUtility->checkDependencies($extension);
127
        $this->downloadQueue->addExtensionToInstallQueue($extension);
128
    }
129
130
    /**
131
     * Mark an extension for download
132
     *
133
     * @param Extension $extension
134
     */
135
    public function markExtensionForDownload(Extension $extension)
136
    {
137
        // We have to check for dependencies of the extension first, before marking it for download
138
        // because this extension might have dependencies, which need to be downloaded and installed first
139
        $this->dependencyUtility->checkDependencies($extension);
140
        if (!$this->dependencyUtility->hasDependencyErrors()) {
141
            $this->downloadQueue->addExtensionToQueue($extension);
142
        }
143
    }
144
145
    /**
146
     * @param Extension $extension
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->downloadMainExtension($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->isQueueEmpty('download')
196
            || !$this->downloadQueue->isQueueEmpty('update')
197
        ) {
198
            $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
199
            // Get download and update information
200
            $queue = $this->downloadQueue->resetExtensionQueue();
201
            if (!empty($queue['download'])) {
202
                $downloadedDependencies = array_merge($downloadedDependencies, $this->downloadDependencies($queue['download']));
203
            }
204
            $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
205
            if ($this->automaticInstallationEnabled) {
206
                if (!empty($queue['update'])) {
207
                    $this->downloadDependencies($queue['update']);
208
                    $updatedDependencies = array_merge($updatedDependencies, $this->uninstallDependenciesToBeUpdated($queue['update']));
209
                }
210
                $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
211
            }
212
        }
213
214
        // If there were any dependency errors we have to abort here
215
        if ($this->dependencyUtility->hasDependencyErrors()) {
216
            return false;
217
        }
218
219
        // Attach extension to install queue
220
        $this->downloadQueue->addExtensionToInstallQueue($extension);
221
        $installQueue += $this->downloadQueue->resetExtensionInstallStorage();
222
        $installedDependencies = [];
223
        if ($this->automaticInstallationEnabled) {
224
            $installedDependencies = $this->installDependencies($installQueue);
225
        }
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 Extension::createFromExtensionArray(
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
     * Check dependencies for an extension and its required extensions
277
     *
278
     * @param Extension $extension
279
     * @return bool Returns TRUE if all dependencies can be resolved, otherwise FALSE
280
     */
281
    protected function checkDependencies(Extension $extension)
282
    {
283
        $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
284
        $this->dependencyUtility->checkDependencies($extension);
285
286
        return !$this->dependencyUtility->hasDependencyErrors();
287
    }
288
289
    /**
290
     * Uninstall extensions that will be updated
291
     * This is not strictly necessary but cleaner all in all
292
     *
293
     * @param Extension[] $updateQueue
294
     * @return array
295
     */
296
    protected function uninstallDependenciesToBeUpdated(array $updateQueue)
297
    {
298
        $resolvedDependencies = [];
299
        foreach ($updateQueue as $extensionToUpdate) {
300
            $this->installUtility->uninstall($extensionToUpdate->getExtensionKey());
301
            $resolvedDependencies['updated'][$extensionToUpdate->getExtensionKey()] = $extensionToUpdate;
302
        }
303
        return $resolvedDependencies;
304
    }
305
306
    /**
307
     * Install dependent extensions
308
     *
309
     * @param array $installQueue
310
     * @return array
311
     */
312
    protected function installDependencies(array $installQueue)
313
    {
314
        if (empty($installQueue)) {
315
            return [];
316
        }
317
        $this->eventDispatcher->dispatch(new BeforePackageActivationEvent($installQueue));
318
        $resolvedDependencies = [];
319
        $this->installUtility->install(...array_keys($installQueue));
320
        foreach ($installQueue as $extensionKey => $_) {
321
            if (!isset($resolvedDependencies['installed']) || !is_array($resolvedDependencies['installed'])) {
322
                $resolvedDependencies['installed'] = [];
323
            }
324
            $resolvedDependencies['installed'][$extensionKey] = $extensionKey;
325
        }
326
        return $resolvedDependencies;
327
    }
328
329
    /**
330
     * Download dependencies
331
     * expects an array of extension objects to download
332
     *
333
     * @param Extension[] $downloadQueue
334
     * @return array
335
     */
336
    protected function downloadDependencies(array $downloadQueue)
337
    {
338
        $resolvedDependencies = [];
339
        foreach ($downloadQueue as $extensionToDownload) {
340
            $this->rawDownload($extensionToDownload);
341
            $this->downloadQueue->removeExtensionFromQueue($extensionToDownload);
342
            $resolvedDependencies['downloaded'][$extensionToDownload->getExtensionKey()] = $extensionToDownload;
343
            $this->markExtensionForInstallation($extensionToDownload->getExtensionKey());
344
        }
345
        return $resolvedDependencies;
346
    }
347
348
    /**
349
     * Get and resolve dependencies
350
     *
351
     * @param Extension $extension
352
     * @return array
353
     */
354
    public function getAndResolveDependencies(Extension $extension)
355
    {
356
        $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
357
        $this->dependencyUtility->checkDependencies($extension);
358
        $installQueue = $this->downloadQueue->getExtensionInstallStorage();
359
        if (is_array($installQueue) && !empty($installQueue)) {
360
            $installQueue = ['install' => $installQueue];
361
        }
362
        return array_merge($this->downloadQueue->getExtensionQueue(), $installQueue);
363
    }
364
365
    /**
366
     * Downloads the extension the user wants to install
367
     * This is separated from downloading the dependencies
368
     * as an extension is able to provide it's own dependencies
369
     *
370
     * @param Extension $extension
371
     */
372
    public function downloadMainExtension(Extension $extension)
373
    {
374
        // The extension object has a uid if the extension is not present in the system
375
        // or an update of a present extension is triggered.
376
        if ($extension->getUid()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extension->getUid() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
377
            $this->rawDownload($extension);
378
        }
379
    }
380
381
    protected function rawDownload(Extension $extension): void
382
    {
383
        if (
384
            Environment::isComposerMode()
385
            || (bool)GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('extensionmanager', 'offlineMode')
386
        ) {
387
            throw new ExtensionManagerException('Extension Manager is in offline mode. No TER connection available.', 1437078620);
388
        }
389
390
        $remoteIdentifier = $extension->getRemoteIdentifier();
391
392
        if ($this->remoteRegistry->hasRemote($remoteIdentifier)) {
393
            $this->remoteRegistry
394
                ->getRemote($remoteIdentifier)
395
                ->downloadExtension(
396
                    $extension->getExtensionKey(),
397
                    $extension->getVersion(),
398
                    $this->fileHandlingUtility,
399
                    $extension->getMd5hash(),
400
                    $this->downloadPath
401
                );
402
        }
403
    }
404
405
    /**
406
     * Set the download path
407
     *
408
     * @param string $downloadPath
409
     * @throws ExtensionManagerException
410
     */
411
    public function setDownloadPath(string $downloadPath): void
412
    {
413
        if (!in_array($downloadPath, Extension::returnAllowedInstallTypes(), true)) {
414
            throw new ExtensionManagerException($downloadPath . ' not in allowed download paths', 1344766387);
415
        }
416
        $this->downloadPath = $downloadPath;
417
    }
418
}
419