Completed
Push — master ( 031ec6...098ccd )
by
unknown
15:29
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\ExtensionModelUtility;
30
use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility;
31
use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;
32
33
/**
34
 * Service class for managing multiple step processes (dependencies for example)
35
 */
36
class ExtensionManagementService implements SingletonInterface
37
{
38
    /**
39
     * @var DownloadQueue
40
     */
41
    protected $downloadQueue;
42
43
    /**
44
     * @var DependencyUtility
45
     */
46
    protected $dependencyUtility;
47
48
    /**
49
     * @var InstallUtility
50
     */
51
    protected $installUtility;
52
53
    /**
54
     * @var ExtensionModelUtility
55
     */
56
    protected $extensionModelUtility;
57
58
    /**
59
     * @var bool
60
     */
61
    protected $automaticInstallationEnabled = true;
62
63
    /**
64
     * @var bool
65
     */
66
    protected $skipDependencyCheck = false;
67
68
    /**
69
     * @var EventDispatcherInterface
70
     */
71
    protected $eventDispatcher;
72
73
    /**
74
     * @var FileHandlingUtility
75
     */
76
    protected $fileHandlingUtility;
77
78
    /**
79
     * @var RemoteRegistry
80
     */
81
    protected $remoteRegistry;
82
83
    /**
84
     * @var string
85
     */
86
    protected $downloadPath = 'Local';
87
88
    public function __construct(RemoteRegistry $remoteRegistry, FileHandlingUtility $fileHandlingUtility)
89
    {
90
        $this->remoteRegistry = $remoteRegistry;
91
        $this->fileHandlingUtility = $fileHandlingUtility;
92
    }
93
94
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
95
    {
96
        $this->eventDispatcher = $eventDispatcher;
97
    }
98
99
    /**
100
     * @param DownloadQueue $downloadQueue
101
     */
102
    public function injectDownloadQueue(DownloadQueue $downloadQueue)
103
    {
104
        $this->downloadQueue = $downloadQueue;
105
    }
106
107
    /**
108
     * @param DependencyUtility $dependencyUtility
109
     */
110
    public function injectDependencyUtility(DependencyUtility $dependencyUtility)
111
    {
112
        $this->dependencyUtility = $dependencyUtility;
113
    }
114
115
    /**
116
     * @param InstallUtility $installUtility
117
     */
118
    public function injectInstallUtility(InstallUtility $installUtility)
119
    {
120
        $this->installUtility = $installUtility;
121
    }
122
123
    /**
124
     * @param ExtensionModelUtility $extensionModelUtility
125
     */
126
    public function injectExtensionModelUtility(ExtensionModelUtility $extensionModelUtility)
127
    {
128
        $this->extensionModelUtility = $extensionModelUtility;
129
    }
130
131
    /**
132
     * @param string $extensionKey
133
     */
134
    public function markExtensionForInstallation($extensionKey)
135
    {
136
        // We have to check for dependencies of the extension first, before marking it for installation
137
        // because this extension might have dependencies, which need to be installed first
138
        $this->installUtility->reloadAvailableExtensions();
139
        $extension = $this->getExtension($extensionKey);
140
        $this->dependencyUtility->checkDependencies($extension);
141
        $this->downloadQueue->addExtensionToInstallQueue($extension);
142
    }
143
144
    /**
145
     * Mark an extension for download
146
     *
147
     * @param Extension $extension
148
     */
149
    public function markExtensionForDownload(Extension $extension)
150
    {
151
        // We have to check for dependencies of the extension first, before marking it for download
152
        // because this extension might have dependencies, which need to be downloaded and installed first
153
        $this->dependencyUtility->checkDependencies($extension);
154
        if (!$this->dependencyUtility->hasDependencyErrors()) {
155
            $this->downloadQueue->addExtensionToQueue($extension);
156
        }
157
    }
158
159
    /**
160
     * @param Extension $extension
161
     */
162
    public function markExtensionForUpdate(Extension $extension)
163
    {
164
        // We have to check for dependencies of the extension first, before marking it for download
165
        // because this extension might have dependencies, which need to be downloaded and installed first
166
        $this->dependencyUtility->checkDependencies($extension);
167
        $this->downloadQueue->addExtensionToQueue($extension, 'update');
168
    }
169
170
    /**
171
     * Enables or disables the dependency check for system environment (PHP, TYPO3) before extension installation
172
     *
173
     * @param bool $skipDependencyCheck
174
     */
175
    public function setSkipDependencyCheck($skipDependencyCheck)
176
    {
177
        $this->skipDependencyCheck = $skipDependencyCheck;
178
    }
179
180
    /**
181
     * @param bool $automaticInstallationEnabled
182
     */
183
    public function setAutomaticInstallationEnabled($automaticInstallationEnabled)
184
    {
185
        $this->automaticInstallationEnabled = (bool)$automaticInstallationEnabled;
186
    }
187
188
    /**
189
     * Install the extension
190
     *
191
     * @param Extension $extension
192
     * @return bool|array Returns FALSE if dependencies cannot be resolved, otherwise array with installation information
193
     */
194
    public function installExtension(Extension $extension)
195
    {
196
        $this->downloadMainExtension($extension);
197
        if (!$this->checkDependencies($extension)) {
198
            return false;
199
        }
200
201
        $downloadedDependencies = [];
202
        $updatedDependencies = [];
203
        $installQueue = [];
204
205
        // First resolve all dependencies and the sub-dependencies until all queues are empty as new extensions might be
206
        // added each time
207
        // Extensions have to be installed in reverse order. Extensions which were added at last are dependencies of
208
        // earlier ones and need to be available before
209
        while (!$this->downloadQueue->isQueueEmpty('download')
210
            || !$this->downloadQueue->isQueueEmpty('update')
211
        ) {
212
            $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
213
            // Get download and update information
214
            $queue = $this->downloadQueue->resetExtensionQueue();
215
            if (!empty($queue['download'])) {
216
                $downloadedDependencies = array_merge($downloadedDependencies, $this->downloadDependencies($queue['download']));
217
            }
218
            $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
219
            if ($this->automaticInstallationEnabled) {
220
                if (!empty($queue['update'])) {
221
                    $this->downloadDependencies($queue['update']);
222
                    $updatedDependencies = array_merge($updatedDependencies, $this->uninstallDependenciesToBeUpdated($queue['update']));
223
                }
224
                $installQueue = array_merge($this->downloadQueue->resetExtensionInstallStorage(), $installQueue);
225
            }
226
        }
227
228
        // If there were any dependency errors we have to abort here
229
        if ($this->dependencyUtility->hasDependencyErrors()) {
230
            return false;
231
        }
232
233
        // Attach extension to install queue
234
        $this->downloadQueue->addExtensionToInstallQueue($extension);
235
        $installQueue += $this->downloadQueue->resetExtensionInstallStorage();
236
        $installedDependencies = [];
237
        if ($this->automaticInstallationEnabled) {
238
            $installedDependencies = $this->installDependencies($installQueue);
239
        }
240
241
        return array_merge($downloadedDependencies, $updatedDependencies, $installedDependencies);
242
    }
243
244
    /**
245
     * Returns the unresolved dependency errors
246
     *
247
     * @return array
248
     */
249
    public function getDependencyErrors()
250
    {
251
        return $this->dependencyUtility->getDependencyErrors();
252
    }
253
254
    /**
255
     * @param string $extensionKey
256
     * @return Extension
257
     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
258
     */
259
    public function getExtension($extensionKey)
260
    {
261
        return $this->extensionModelUtility->mapExtensionArrayToModel(
262
            $this->installUtility->enrichExtensionWithDetails($extensionKey)
263
        );
264
    }
265
266
    /**
267
     * Checks if an extension is available in the system
268
     *
269
     * @param string $extensionKey
270
     * @return bool
271
     */
272
    public function isAvailable($extensionKey)
273
    {
274
        return $this->installUtility->isAvailable($extensionKey);
275
    }
276
277
    /**
278
     * @param string $extensionKey
279
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException if the package isn't available
280
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException if an invalid package key was passed
281
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException if an invalid package path was passed
282
     * @throws \TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException if no extension configuration file could be found
283
     */
284
    public function reloadPackageInformation($extensionKey)
285
    {
286
        $this->installUtility->reloadPackageInformation($extensionKey);
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
     * Uninstall extensions that will be updated
305
     * This is not strictly necessary but cleaner all in all
306
     *
307
     * @param Extension[] $updateQueue
308
     * @return array
309
     */
310
    protected function uninstallDependenciesToBeUpdated(array $updateQueue)
311
    {
312
        $resolvedDependencies = [];
313
        foreach ($updateQueue as $extensionToUpdate) {
314
            $this->installUtility->uninstall($extensionToUpdate->getExtensionKey());
315
            $resolvedDependencies['updated'][$extensionToUpdate->getExtensionKey()] = $extensionToUpdate;
316
        }
317
        return $resolvedDependencies;
318
    }
319
320
    /**
321
     * Install dependent extensions
322
     *
323
     * @param array $installQueue
324
     * @return array
325
     */
326
    protected function installDependencies(array $installQueue)
327
    {
328
        if (empty($installQueue)) {
329
            return [];
330
        }
331
        $this->eventDispatcher->dispatch(new BeforePackageActivationEvent($installQueue));
332
        $resolvedDependencies = [];
333
        $this->installUtility->install(...array_keys($installQueue));
334
        foreach ($installQueue as $extensionKey => $_) {
335
            if (!isset($resolvedDependencies['installed']) || !is_array($resolvedDependencies['installed'])) {
336
                $resolvedDependencies['installed'] = [];
337
            }
338
            $resolvedDependencies['installed'][$extensionKey] = $extensionKey;
339
        }
340
        return $resolvedDependencies;
341
    }
342
343
    /**
344
     * Download dependencies
345
     * expects an array of extension objects to download
346
     *
347
     * @param Extension[] $downloadQueue
348
     * @return array
349
     */
350
    protected function downloadDependencies(array $downloadQueue)
351
    {
352
        $resolvedDependencies = [];
353
        foreach ($downloadQueue as $extensionToDownload) {
354
            $this->rawDownload($extensionToDownload);
355
            $this->downloadQueue->removeExtensionFromQueue($extensionToDownload);
356
            $resolvedDependencies['downloaded'][$extensionToDownload->getExtensionKey()] = $extensionToDownload;
357
            $this->markExtensionForInstallation($extensionToDownload->getExtensionKey());
358
        }
359
        return $resolvedDependencies;
360
    }
361
362
    /**
363
     * Get and resolve dependencies
364
     *
365
     * @param Extension $extension
366
     * @return array
367
     */
368
    public function getAndResolveDependencies(Extension $extension)
369
    {
370
        $this->dependencyUtility->setSkipDependencyCheck($this->skipDependencyCheck);
371
        $this->dependencyUtility->checkDependencies($extension);
372
        $installQueue = $this->downloadQueue->getExtensionInstallStorage();
373
        if (is_array($installQueue) && !empty($installQueue)) {
374
            $installQueue = ['install' => $installQueue];
375
        }
376
        return array_merge($this->downloadQueue->getExtensionQueue(), $installQueue);
377
    }
378
379
    /**
380
     * Downloads the extension the user wants to install
381
     * This is separated from downloading the dependencies
382
     * as an extension is able to provide it's own dependencies
383
     *
384
     * @param Extension $extension
385
     */
386
    public function downloadMainExtension(Extension $extension)
387
    {
388
        // The extension object has a uid if the extension is not present in the system
389
        // or an update of a present extension is triggered.
390
        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...
391
            $this->rawDownload($extension);
392
        }
393
    }
394
395
    protected function rawDownload(Extension $extension): void
396
    {
397
        if (
398
            Environment::isComposerMode()
399
            || (bool)GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('extensionmanager', 'offlineMode')
400
        ) {
401
            throw new ExtensionManagerException('Extension Manager is in offline mode. No TER connection available.', 1437078620);
402
        }
403
404
        $remoteIdentifier = $extension->getRemoteIdentifier();
405
406
        if ($this->remoteRegistry->hasRemote($remoteIdentifier)) {
407
            $this->remoteRegistry
408
                ->getRemote($remoteIdentifier)
409
                ->downloadExtension(
410
                    $extension->getExtensionKey(),
411
                    $extension->getVersion(),
412
                    $this->fileHandlingUtility,
413
                    $extension->getMd5hash(),
414
                    $this->downloadPath
415
                );
416
        }
417
    }
418
419
    /**
420
     * Set the download path
421
     *
422
     * @param string $downloadPath
423
     * @throws ExtensionManagerException
424
     */
425
    public function setDownloadPath(string $downloadPath): void
426
    {
427
        if (!in_array($downloadPath, Extension::returnAllowedInstallTypes(), true)) {
428
            throw new ExtensionManagerException($downloadPath . ' not in allowed download paths', 1344766387);
429
        }
430
        $this->downloadPath = $downloadPath;
431
    }
432
}
433