Completed
Push — master ( 031ec6...098ccd )
by
unknown
15:29
created

DependencyUtility::setLocalExtensionStorage()   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\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\Exception\UnresolvedPhpDependencyException;
29
use TYPO3\CMS\Extensionmanager\Exception\UnresolvedTypo3DependencyException;
30
use TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService;
31
32
/**
33
 * Utility for dealing with dependencies
34
 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
35
 */
36
class DependencyUtility implements SingletonInterface
37
{
38
    /**
39
     * @var ExtensionRepository
40
     */
41
    protected $extensionRepository;
42
43
    /**
44
     * @var ListUtility
45
     */
46
    protected $listUtility;
47
48
    /**
49
     * @var EmConfUtility
50
     */
51
    protected $emConfUtility;
52
53
    /**
54
     * @var ExtensionManagementService
55
     */
56
    protected $managementService;
57
58
    /**
59
     * @var array
60
     */
61
    protected $availableExtensions = [];
62
63
    /**
64
     * @var array
65
     */
66
    protected $dependencyErrors = [];
67
68
    /**
69
     * @var bool
70
     */
71
    protected $skipDependencyCheck = false;
72
73
    /**
74
     * @param ExtensionRepository $extensionRepository
75
     */
76
    public function injectExtensionRepository(ExtensionRepository $extensionRepository)
77
    {
78
        $this->extensionRepository = $extensionRepository;
79
    }
80
81
    /**
82
     * @param ListUtility $listUtility
83
     */
84
    public function injectListUtility(ListUtility $listUtility)
85
    {
86
        $this->listUtility = $listUtility;
87
    }
88
89
    /**
90
     * @param EmConfUtility $emConfUtility
91
     */
92
    public function injectEmConfUtility(EmConfUtility $emConfUtility)
93
    {
94
        $this->emConfUtility = $emConfUtility;
95
    }
96
97
    /**
98
     * @param ExtensionManagementService $managementService
99
     */
100
    public function injectManagementService(ExtensionManagementService $managementService)
101
    {
102
        $this->managementService = $managementService;
103
    }
104
105
    /**
106
     * Setter for available extensions
107
     * gets available extensions from list utility if not already done
108
     */
109
    protected function setAvailableExtensions()
110
    {
111
        $this->availableExtensions = $this->listUtility->getAvailableExtensions();
112
    }
113
114
    /**
115
     * @param bool $skipDependencyCheck
116
     */
117
    public function setSkipDependencyCheck($skipDependencyCheck)
118
    {
119
        $this->skipDependencyCheck = $skipDependencyCheck;
120
    }
121
122
    /**
123
     * Checks dependencies for special cases (currently typo3 and php)
124
     *
125
     * @param Extension $extension
126
     */
127
    public function checkDependencies(Extension $extension)
128
    {
129
        $this->dependencyErrors = [];
130
        $dependencies = $extension->getDependencies();
131
        foreach ($dependencies as $dependency) {
132
            /** @var Dependency $dependency */
133
            $identifier = strtolower($dependency->getIdentifier());
134
            try {
135
                if (in_array($identifier, Dependency::$specialDependencies)) {
136
                    if (!$this->skipDependencyCheck) {
137
                        $methodName = 'check' . ucfirst($identifier) . 'Dependency';
138
                        $this->{$methodName}($dependency);
139
                    }
140
                } else {
141
                    if ($dependency->getType() === 'depends') {
142
                        $this->checkExtensionDependency($dependency);
143
                    }
144
                }
145
            } catch (UnresolvedDependencyException $e) {
146
                if (in_array($identifier, Dependency::$specialDependencies)) {
147
                    $extensionKey = $extension->getExtensionKey();
148
                } else {
149
                    $extensionKey = $identifier;
150
                }
151
                if (!isset($this->dependencyErrors[$extensionKey])) {
152
                    $this->dependencyErrors[$extensionKey] = [];
153
                }
154
                $this->dependencyErrors[$extensionKey][] = [
155
                    'code' => $e->getCode(),
156
                    'message' => $e->getMessage()
157
                ];
158
            }
159
        }
160
    }
161
162
    /**
163
     * Returns TRUE if a dependency error was found
164
     *
165
     * @return bool
166
     */
167
    public function hasDependencyErrors()
168
    {
169
        return !empty($this->dependencyErrors);
170
    }
171
172
    /**
173
     * Return the dependency errors
174
     *
175
     * @return array
176
     */
177
    public function getDependencyErrors()
178
    {
179
        return $this->dependencyErrors;
180
    }
181
182
    /**
183
     * Returns true if current TYPO3 version fulfills extension requirements
184
     *
185
     * @param Dependency $dependency
186
     * @throws Exception\UnresolvedTypo3DependencyException
187
     * @return bool
188
     */
189
    protected function checkTypo3Dependency(Dependency $dependency)
190
    {
191
        $lowerCaseIdentifier = strtolower($dependency->getIdentifier());
192
        if ($lowerCaseIdentifier === 'typo3') {
193
            if (!($dependency->getLowestVersion() === '') && version_compare(VersionNumberUtility::getNumericTypo3Version(), $dependency->getLowestVersion()) === -1) {
194
                throw new UnresolvedTypo3DependencyException(
195
                    'Your TYPO3 version is lower than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
196
                    1399144499
197
                );
198
            }
199
            if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), VersionNumberUtility::getNumericTypo3Version()) === -1) {
200
                throw new UnresolvedTypo3DependencyException(
201
                    'Your TYPO3 version is higher than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
202
                    1399144521
203
                );
204
            }
205
        } else {
206
            throw new UnresolvedTypo3DependencyException(
207
                'checkTypo3Dependency can only check TYPO3 dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
208
                1399144551
209
            );
210
        }
211
        return true;
212
    }
213
214
    /**
215
     * Returns true if current php version fulfills extension requirements
216
     *
217
     * @param Dependency $dependency
218
     * @throws Exception\UnresolvedPhpDependencyException
219
     * @return bool
220
     */
221
    protected function checkPhpDependency(Dependency $dependency)
222
    {
223
        $lowerCaseIdentifier = strtolower($dependency->getIdentifier());
224
        if ($lowerCaseIdentifier === 'php') {
225
            if (!($dependency->getLowestVersion() === '') && version_compare(PHP_VERSION, $dependency->getLowestVersion()) === -1) {
226
                throw new UnresolvedPhpDependencyException(
227
                    'Your PHP version is lower than necessary. You need at least PHP version ' . $dependency->getLowestVersion(),
228
                    1377977857
229
                );
230
            }
231
            if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), PHP_VERSION) === -1) {
232
                throw new UnresolvedPhpDependencyException(
233
                    'Your PHP version is higher than allowed. You can use PHP versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
234
                    1377977856
235
                );
236
            }
237
        } else {
238
            throw new UnresolvedPhpDependencyException(
239
                'checkPhpDependency can only check PHP dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
240
                1377977858
241
            );
242
        }
243
        return true;
244
    }
245
246
    /**
247
     * Main controlling function for checking dependencies
248
     * Dependency check is done in the following way:
249
     * - installed extension in matching version ? - return true
250
     * - available extension in matching version ? - mark for installation
251
     * - remote (TER) extension in matching version? - mark for download
252
     *
253
     * @todo handle exceptions / markForUpload
254
     * @param Dependency $dependency
255
     * @throws Exception\MissingVersionDependencyException
256
     * @return bool
257
     */
258
    protected function checkExtensionDependency(Dependency $dependency)
259
    {
260
        $extensionKey = $dependency->getIdentifier();
261
        $extensionIsLoaded = $this->isDependentExtensionLoaded($extensionKey);
262
        if ($extensionIsLoaded === true) {
263
            $isLoadedVersionCompatible = $this->isLoadedVersionCompatible($dependency);
264
            if ($isLoadedVersionCompatible === true || $this->skipDependencyCheck) {
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 Exception\UnresolvedDependencyException
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
        $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency);
368
        if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) {
0 ignored issues
show
introduced by
$latestCompatibleExtensi...ntegerVersionDependency is always a sub-type of TYPO3\CMS\Extensionmanager\Domain\Model\Extension.
Loading history...
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($latestCompatibleExtensionByIntegerVersionDependency);
380
        } else {
381
            $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency);
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)
399
    {
400
        $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier());
401
        return $this->isVersionCompatible($extensionVersion, $dependency);
402
    }
403
404
    /**
405
     * @param string $version
406
     * @param Dependency $dependency
407
     * @return bool
408
     */
409
    protected function isVersionCompatible($version, Dependency $dependency)
410
    {
411
        if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
412
            return false;
413
        }
414
        if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
415
            return false;
416
        }
417
        return true;
418
    }
419
420
    /**
421
     * Checks whether the needed extension is available
422
     * (not necessarily installed, but present in system)
423
     *
424
     * @param string $extensionKey
425
     * @return bool
426
     */
427
    protected function isDependentExtensionAvailable($extensionKey)
428
    {
429
        $this->setAvailableExtensions();
430
        return array_key_exists($extensionKey, $this->availableExtensions);
431
    }
432
433
    /**
434
     * Checks whether the available version is compatible
435
     *
436
     * @param Dependency $dependency
437
     * @return bool
438
     */
439
    protected function isAvailableVersionCompatible(Dependency $dependency)
440
    {
441
        $this->setAvailableExtensions();
442
        $extensionData = $this->emConfUtility->includeEmConf(
443
            $dependency->getIdentifier(),
444
            $this->availableExtensions[$dependency->getIdentifier()]
445
        );
446
        return $this->isVersionCompatible($extensionData['version'], $dependency);
447
    }
448
449
    /**
450
     * Checks whether a ter extension with $extensionKey exists
451
     *
452
     * @param string $extensionKey
453
     * @return bool
454
     */
455
    protected function isExtensionDownloadableFromRemote($extensionKey)
456
    {
457
        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

457
        return $this->extensionRepository->/** @scrutinizer ignore-call */ countByExtensionKey($extensionKey) > 0;
Loading history...
458
    }
459
460
    /**
461
     * Checks whether a compatible version of the extension exists in TER
462
     *
463
     * @param Dependency $dependency
464
     * @return bool
465
     */
466
    protected function isDownloadableVersionCompatible(Dependency $dependency)
467
    {
468
        $versions = $this->getLowestAndHighestIntegerVersions($dependency);
469
        $count = $this->extensionRepository->countByVersionRangeAndExtensionKey(
470
            $dependency->getIdentifier(),
471
            $versions['lowestIntegerVersion'],
472
            $versions['highestIntegerVersion']
473
        );
474
        return !empty($count);
475
    }
476
477
    /**
478
     * Get the latest compatible version of an extension that's
479
     * compatible with the current core and PHP version.
480
     *
481
     * @param iterable $extensions
482
     * @return Extension|null
483
     */
484
    protected function getCompatibleExtension(iterable $extensions): ?Extension
485
    {
486
        foreach ($extensions as $extension) {
487
            /** @var Extension $extension */
488
            $this->checkDependencies($extension);
489
            $extensionKey = $extension->getExtensionKey();
490
491
            if (isset($this->dependencyErrors[$extensionKey])) {
492
                // reset dependencyErrors and continue with next version
493
                unset($this->dependencyErrors[$extensionKey]);
494
                continue;
495
            }
496
497
            return $extension;
498
        }
499
500
        return null;
501
    }
502
503
    /**
504
     * Get the latest compatible version of an extension that
505
     * fulfills the given dependency from TER
506
     *
507
     * @param Dependency $dependency
508
     * @return Extension
509
     */
510
    protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency)
511
    {
512
        $versions = $this->getLowestAndHighestIntegerVersions($dependency);
513
        $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
514
            $dependency->getIdentifier(),
515
            $versions['lowestIntegerVersion'],
516
            $versions['highestIntegerVersion']
517
        );
518
        return $this->getCompatibleExtension($compatibleDataSets);
519
    }
520
521
    /**
522
     * Return array of lowest and highest version of dependency as integer
523
     *
524
     * @param Dependency $dependency
525
     * @return array
526
     */
527
    protected function getLowestAndHighestIntegerVersions(Dependency $dependency)
528
    {
529
        $lowestVersion = $dependency->getLowestVersion();
530
        $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0;
531
        $highestVersion = $dependency->getHighestVersion();
532
        $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0;
533
        return [
534
            'lowestIntegerVersion' => $lowestVersionInteger,
535
            'highestIntegerVersion' => $highestVersionInteger
536
        ];
537
    }
538
539
    /**
540
     * @param string $extensionKey
541
     * @return array
542
     */
543
    public function findInstalledExtensionsThatDependOnMe($extensionKey)
544
    {
545
        $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
546
        $dependentExtensions = [];
547
        foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
548
            if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) {
549
                if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
550
                    $dependentExtensions[] = $availableAndInstalledExtensionKey;
551
                }
552
            }
553
        }
554
        return $dependentExtensions;
555
    }
556
557
    /**
558
     * Get extensions (out of a given list) that are suitable for the current TYPO3 version
559
     *
560
     * @param \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array $extensions List of extensions to check
561
     * @return array List of extensions suitable for current TYPO3 version
562
     */
563
    public function getExtensionsSuitableForTypo3Version($extensions)
564
    {
565
        $suitableExtensions = [];
566
        /** @var Extension $extension */
567
        foreach ($extensions as $extension) {
568
            /** @var Dependency $dependency */
569
            foreach ($extension->getDependencies() as $dependency) {
570
                if ($dependency->getIdentifier() === 'typo3') {
571
                    try {
572
                        if ($this->checkTypo3Dependency($dependency)) {
573
                            $suitableExtensions[] = $extension;
574
                        }
575
                    } catch (UnresolvedTypo3DependencyException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
576
                    }
577
                    break;
578
                }
579
            }
580
        }
581
        return $suitableExtensions;
582
    }
583
584
    /**
585
     * Gets a list of various extensions in various versions and returns
586
     * a filtered list containing the extension-version combination with
587
     * the highest version number.
588
     *
589
     * @param Extension[] $extensions
590
     * @param bool $showUnsuitable
591
     *
592
     * @return Extension[]
593
     */
594
    public function filterYoungestVersionOfExtensionList(array $extensions, $showUnsuitable)
595
    {
596
        if (!$showUnsuitable) {
597
            $extensions = $this->getExtensionsSuitableForTypo3Version($extensions);
598
        }
599
        $filteredExtensions = [];
600
        foreach ($extensions as $extension) {
601
            $extensionKey = $extension->getExtensionKey();
602
            if (!array_key_exists($extensionKey, $filteredExtensions)) {
603
                $filteredExtensions[$extensionKey] = $extension;
604
                continue;
605
            }
606
            $currentVersion = $filteredExtensions[$extensionKey]->getVersion();
607
            $newVersion = $extension->getVersion();
608
            if (version_compare($newVersion, $currentVersion, '>')) {
609
                $filteredExtensions[$extensionKey] = $extension;
610
            }
611
        }
612
        return $filteredExtensions;
613
    }
614
}
615