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

DependencyUtility::isDependentExtensionAvailable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
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\Utility;
17
18
use TYPO3\CMS\Core\SingletonInterface;
19
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
20
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
21
use TYPO3\CMS\Extensionmanager\Domain\Model\Dependency;
22
use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
23
use TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository;
24
use TYPO3\CMS\Extensionmanager\Exception;
25
use TYPO3\CMS\Extensionmanager\Exception\MissingExtensionDependencyException;
26
use TYPO3\CMS\Extensionmanager\Exception\MissingVersionDependencyException;
27
use TYPO3\CMS\Extensionmanager\Exception\UnresolvedDependencyException;
28
use TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService;
29
30
/**
31
 * Utility for dealing with dependencies
32
 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
33
 */
34
class DependencyUtility implements SingletonInterface
35
{
36
    /**
37
     * @var ExtensionRepository
38
     */
39
    protected $extensionRepository;
40
41
    /**
42
     * @var ListUtility
43
     */
44
    protected $listUtility;
45
46
    /**
47
     * @var EmConfUtility
48
     */
49
    protected $emConfUtility;
50
51
    /**
52
     * @var ExtensionManagementService
53
     */
54
    protected $managementService;
55
56
    /**
57
     * @var array
58
     */
59
    protected $availableExtensions = [];
60
61
    /**
62
     * @var array
63
     */
64
    protected $dependencyErrors = [];
65
66
    /**
67
     * @var bool
68
     */
69
    protected $skipDependencyCheck = false;
70
71
    /**
72
     * @param ExtensionRepository $extensionRepository
73
     */
74
    public function injectExtensionRepository(ExtensionRepository $extensionRepository)
75
    {
76
        $this->extensionRepository = $extensionRepository;
77
    }
78
79
    /**
80
     * @param ListUtility $listUtility
81
     */
82
    public function injectListUtility(ListUtility $listUtility)
83
    {
84
        $this->listUtility = $listUtility;
85
    }
86
87
    /**
88
     * @param EmConfUtility $emConfUtility
89
     */
90
    public function injectEmConfUtility(EmConfUtility $emConfUtility)
91
    {
92
        $this->emConfUtility = $emConfUtility;
93
    }
94
95
    /**
96
     * @param ExtensionManagementService $managementService
97
     */
98
    public function injectManagementService(ExtensionManagementService $managementService)
99
    {
100
        $this->managementService = $managementService;
101
    }
102
103
    /**
104
     * Setter for available extensions
105
     * gets available extensions from list utility if not already done
106
     */
107
    protected function setAvailableExtensions()
108
    {
109
        $this->availableExtensions = $this->listUtility->getAvailableExtensions();
110
    }
111
112
    /**
113
     * @param bool $skipDependencyCheck
114
     */
115
    public function setSkipDependencyCheck($skipDependencyCheck)
116
    {
117
        $this->skipDependencyCheck = $skipDependencyCheck;
118
    }
119
120
    /**
121
     * Checks dependencies for special cases (currently typo3 and php)
122
     *
123
     * @param Extension $extension
124
     */
125
    public function checkDependencies(Extension $extension)
126
    {
127
        $this->dependencyErrors = [];
128
        $dependencies = $extension->getDependencies();
129
        foreach ($dependencies as $dependency) {
130
            /** @var Dependency $dependency */
131
            $identifier = $dependency->getIdentifier();
132
            try {
133
                if (in_array($identifier, Dependency::$specialDependencies)) {
134
                    if ($this->skipDependencyCheck) {
135
                        continue;
136
                    }
137
                    if ($identifier === 'typo3') {
138
                        $this->checkTypo3Dependency($dependency, VersionNumberUtility::getNumericTypo3Version());
139
                    }
140
                    if ($identifier === 'php') {
141
                        $this->checkPhpDependency($dependency, PHP_VERSION);
142
                    }
143
                } elseif ($dependency->getType() === 'depends') {
144
                    $this->checkExtensionDependency($dependency);
145
                }
146
            } catch (UnresolvedDependencyException $e) {
147
                if (in_array($identifier, Dependency::$specialDependencies)) {
148
                    $extensionKey = $extension->getExtensionKey();
149
                } else {
150
                    $extensionKey = $identifier;
151
                }
152
                if (!isset($this->dependencyErrors[$extensionKey])) {
153
                    $this->dependencyErrors[$extensionKey] = [];
154
                }
155
                $this->dependencyErrors[$extensionKey][] = [
156
                    'code' => $e->getCode(),
157
                    'message' => $e->getMessage()
158
                ];
159
            }
160
        }
161
    }
162
163
    /**
164
     * Returns TRUE if a dependency error was found
165
     *
166
     * @return bool
167
     */
168
    public function hasDependencyErrors()
169
    {
170
        return !empty($this->dependencyErrors);
171
    }
172
173
    /**
174
     * Return the dependency errors
175
     *
176
     * @return array
177
     */
178
    public function getDependencyErrors(): array
179
    {
180
        return $this->dependencyErrors;
181
    }
182
183
    /**
184
     * Returns true if current TYPO3 version fulfills extension requirements
185
     *
186
     * @param Dependency $dependency
187
     * @param string $version
188
     * @return bool
189
     * @throws Exception\UnresolvedTypo3DependencyException
190
     */
191
    protected function checkTypo3Dependency(Dependency $dependency, string $version): bool
192
    {
193
        if ($dependency->getIdentifier() === 'typo3') {
194
            if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
195
                throw new Exception\UnresolvedTypo3DependencyException(
196
                    'Your TYPO3 version is lower than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
197
                    1399144499
198
                );
199
            }
200
            if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
201
                throw new Exception\UnresolvedTypo3DependencyException(
202
                    'Your TYPO3 version is higher than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
203
                    1399144521
204
                );
205
            }
206
        } else {
207
            throw new Exception\UnresolvedTypo3DependencyException(
208
                'checkTypo3Dependency can only check TYPO3 dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
209
                1399144551
210
            );
211
        }
212
        return true;
213
    }
214
215
    /**
216
     * Returns true if current php version fulfills extension requirements
217
     *
218
     * @param Dependency $dependency
219
     * @param string $version
220
     * @throws Exception\UnresolvedPhpDependencyException
221
     * @return bool
222
     */
223
    protected function checkPhpDependency(Dependency $dependency, string $version): bool
224
    {
225
        if ($dependency->getIdentifier() === 'php') {
226
            if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
227
                throw new Exception\UnresolvedPhpDependencyException(
228
                    'Your PHP version is lower than necessary. You need at least PHP version ' . $dependency->getLowestVersion(),
229
                    1377977857
230
                );
231
            }
232
            if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
233
                throw new Exception\UnresolvedPhpDependencyException(
234
                    'Your PHP version is higher than allowed. You can use PHP versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
235
                    1377977856
236
                );
237
            }
238
        } else {
239
            throw new Exception\UnresolvedPhpDependencyException(
240
                'checkPhpDependency can only check PHP dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
241
                1377977858
242
            );
243
        }
244
        return true;
245
    }
246
247
    /**
248
     * Main controlling function for checking dependencies
249
     * Dependency check is done in the following way:
250
     * - installed extension in matching version ? - return true
251
     * - available extension in matching version ? - mark for installation
252
     * - remote (TER) extension in matching version? - mark for download
253
     *
254
     * @todo handle exceptions / markForUpload
255
     * @param Dependency $dependency
256
     * @throws Exception\MissingVersionDependencyException
257
     * @return bool
258
     */
259
    protected function checkExtensionDependency(Dependency $dependency)
260
    {
261
        $extensionKey = $dependency->getIdentifier();
262
        $extensionIsLoaded = $this->isDependentExtensionLoaded($extensionKey);
263
        if ($extensionIsLoaded === true) {
264
            if ($this->skipDependencyCheck || $this->isLoadedVersionCompatible($dependency)) {
265
                return true;
266
            }
267
            $extension = $this->listUtility->getExtension($extensionKey);
268
            $loadedVersion = $extension->getPackageMetaData()->getVersion();
269
            if (version_compare($loadedVersion, $dependency->getHighestVersion()) === -1) {
270
                try {
271
                    $this->downloadExtensionFromRemote($extensionKey, $dependency);
272
                } catch (UnresolvedDependencyException $e) {
273
                    throw new MissingVersionDependencyException(
274
                        'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion
275
                            . ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER',
276
                        1396302624
277
                    );
278
                }
279
            } else {
280
                throw new MissingVersionDependencyException(
281
                    'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion .
282
                    ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
283
                    1430561927
284
                );
285
            }
286
        } else {
287
            $extensionIsAvailable = $this->isDependentExtensionAvailable($extensionKey);
288
            if ($extensionIsAvailable === true) {
289
                $isAvailableVersionCompatible = $this->isAvailableVersionCompatible($dependency);
290
                if ($isAvailableVersionCompatible) {
291
                    $unresolvedDependencyErrors = $this->dependencyErrors;
292
                    $this->managementService->markExtensionForInstallation($extensionKey);
293
                    $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors);
294
                } else {
295
                    $extension = $this->listUtility->getExtension($extensionKey);
296
                    $availableVersion = $extension->getPackageMetaData()->getVersion();
297
                    if (version_compare($availableVersion, $dependency->getHighestVersion()) === -1) {
298
                        try {
299
                            $this->downloadExtensionFromRemote($extensionKey, $dependency);
300
                        } catch (MissingExtensionDependencyException $e) {
301
                            if (!$this->skipDependencyCheck) {
302
                                throw new MissingVersionDependencyException(
303
                                    'The extension ' . $extensionKey . ' is available in version ' . $availableVersion
304
                                    . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER',
305
                                    1430560390
306
                                );
307
                            }
308
                        }
309
                    } else {
310
                        if (!$this->skipDependencyCheck) {
311
                            throw new MissingVersionDependencyException(
312
                                'The extension ' . $extensionKey . ' is available in version ' . $availableVersion
313
                                . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
314
                                1430562374
315
                            );
316
                        }
317
                        // Dependency check is skipped and the local version has to be installed
318
                        $this->managementService->markExtensionForInstallation($extensionKey);
319
                    }
320
                }
321
            } else {
322
                $unresolvedDependencyErrors = $this->dependencyErrors;
323
                $this->downloadExtensionFromRemote($extensionKey, $dependency);
324
                $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors);
325
            }
326
        }
327
328
        return false;
329
    }
330
331
    /**
332
     * Handles checks to find a compatible extension version from TER to fulfill given dependency
333
     *
334
     * @param string $extensionKey
335
     * @param Dependency $dependency
336
     * @throws MissingExtensionDependencyException
337
     */
338
    protected function downloadExtensionFromRemote(string $extensionKey, Dependency $dependency)
339
    {
340
        if (!$this->isExtensionDownloadableFromRemote($extensionKey)) {
341
            if (!$this->skipDependencyCheck) {
342
                if ($this->extensionRepository->countAll() > 0) {
343
                    throw new MissingExtensionDependencyException(
344
                        'The extension ' . $extensionKey . ' is not available from TER.',
345
                        1399161266
346
                    );
347
                }
348
                throw new MissingExtensionDependencyException(
349
                    'The extension ' . $extensionKey . ' could not be checked. Please update your Extension-List from TYPO3 Extension Repository (TER).',
350
                    1430580308
351
                );
352
            }
353
            return;
354
        }
355
356
        $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency);
357
        if (!$isDownloadableVersionCompatible) {
358
            if (!$this->skipDependencyCheck) {
359
                throw new MissingVersionDependencyException(
360
                    'No compatible version found for extension ' . $extensionKey,
361
                    1399161284
362
                );
363
            }
364
            return;
365
        }
366
367
        $latestCompatibleExtensionByDependency = $this->getLatestCompatibleExtensionByDependency($dependency);
368
        if (!$latestCompatibleExtensionByDependency instanceof Extension) {
369
            if (!$this->skipDependencyCheck) {
370
                throw new MissingExtensionDependencyException(
371
                    'Could not resolve dependency for "' . $dependency->getIdentifier() . '"',
372
                    1399161302
373
                );
374
            }
375
            return;
376
        }
377
378
        if ($this->isDependentExtensionLoaded($extensionKey)) {
379
            $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByDependency);
380
        } else {
381
            $this->managementService->markExtensionForDownload($latestCompatibleExtensionByDependency);
382
        }
383
    }
384
385
    /**
386
     * @param string $extensionKey
387
     * @return bool
388
     */
389
    protected function isDependentExtensionLoaded($extensionKey)
390
    {
391
        return ExtensionManagementUtility::isLoaded($extensionKey);
392
    }
393
394
    /**
395
     * @param Dependency $dependency
396
     * @return bool
397
     */
398
    protected function isLoadedVersionCompatible(Dependency $dependency): bool
399
    {
400
        $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier());
401
        return $dependency->isVersionCompatible($extensionVersion);
402
    }
403
404
    /**
405
     * Checks whether the needed extension is available
406
     * (not necessarily installed, but present in system)
407
     *
408
     * @param string $extensionKey
409
     * @return bool
410
     */
411
    protected function isDependentExtensionAvailable(string $extensionKey): bool
412
    {
413
        $this->setAvailableExtensions();
414
        return array_key_exists($extensionKey, $this->availableExtensions);
415
    }
416
417
    /**
418
     * Checks whether the available version is compatible
419
     *
420
     * @param Dependency $dependency
421
     * @return bool
422
     */
423
    protected function isAvailableVersionCompatible(Dependency $dependency): bool
424
    {
425
        $this->setAvailableExtensions();
426
        $extensionData = $this->emConfUtility->includeEmConf(
427
            $dependency->getIdentifier(),
428
            $this->availableExtensions[$dependency->getIdentifier()]
429
        );
430
        return $dependency->isVersionCompatible($extensionData['version']);
431
    }
432
433
    /**
434
     * Checks whether a ter extension with $extensionKey exists
435
     *
436
     * @param string $extensionKey
437
     * @return bool
438
     */
439
    protected function isExtensionDownloadableFromRemote(string $extensionKey): bool
440
    {
441
        return $this->extensionRepository->countByExtensionKey($extensionKey) > 0;
0 ignored issues
show
Bug introduced by
The method countByExtensionKey() does not exist on TYPO3\CMS\Extensionmanag...ory\ExtensionRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

441
        return $this->extensionRepository->/** @scrutinizer ignore-call */ countByExtensionKey($extensionKey) > 0;
Loading history...
442
    }
443
444
    /**
445
     * Checks whether a compatible version of the extension exists in TER
446
     *
447
     * @param Dependency $dependency
448
     * @return bool
449
     */
450
    protected function isDownloadableVersionCompatible(Dependency $dependency): bool
451
    {
452
        $count = $this->extensionRepository->countByVersionRangeAndExtensionKey(
453
            $dependency->getIdentifier(),
454
            $dependency->getLowestVersionAsInteger(),
455
            $dependency->getHighestVersionAsInteger()
456
        );
457
        return !empty($count);
458
    }
459
460
    /**
461
     * Get the latest compatible version of an extension that's
462
     * compatible with the current core and PHP version.
463
     *
464
     * @param iterable $extensions
465
     * @return Extension|null
466
     */
467
    protected function getCompatibleExtension(iterable $extensions): ?Extension
468
    {
469
        foreach ($extensions as $extension) {
470
            /** @var Extension $extension */
471
            $this->checkDependencies($extension);
472
            $extensionKey = $extension->getExtensionKey();
473
474
            if (isset($this->dependencyErrors[$extensionKey])) {
475
                // reset dependencyErrors and continue with next version
476
                unset($this->dependencyErrors[$extensionKey]);
477
                continue;
478
            }
479
480
            return $extension;
481
        }
482
483
        return null;
484
    }
485
486
    /**
487
     * Get the latest compatible version of an extension that
488
     * fulfills the given dependency from TER
489
     *
490
     * @param Dependency $dependency
491
     * @return Extension|null
492
     */
493
    protected function getLatestCompatibleExtensionByDependency(Dependency $dependency): ?Extension
494
    {
495
        $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
496
            $dependency->getIdentifier(),
497
            $dependency->getLowestVersionAsInteger(),
498
            $dependency->getHighestVersionAsInteger()
499
        );
500
        return $this->getCompatibleExtension($compatibleDataSets);
501
    }
502
}
503