Completed
Push — master ( c92ef6...3f980f )
by
unknown
13:33
created

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