1<?php
2namespace TYPO3\CMS\Extensionmanager\Utility;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
18use TYPO3\CMS\Core\Utility\VersionNumberUtility;
19use TYPO3\CMS\Extensionmanager\Domain\Model\Dependency;
20use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
21use TYPO3\CMS\Extensionmanager\Exception;
22
23/**
24 * Utility for dealing with dependencies
25 * @internal This class is a specific ExtensionManager implementation and is not part of the Public TYPO3 API.
26 */
27class DependencyUtility implements \TYPO3\CMS\Core\SingletonInterface
28{
29    /**
30     * @var \TYPO3\CMS\Extbase\Object\ObjectManager
31     */
32    protected $objectManager;
33
34    /**
35     * @var \TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository
36     */
37    protected $extensionRepository;
38
39    /**
40     * @var \TYPO3\CMS\Extensionmanager\Utility\ListUtility
41     */
42    protected $listUtility;
43
44    /**
45     * @var \TYPO3\CMS\Extensionmanager\Utility\EmConfUtility
46     */
47    protected $emConfUtility;
48
49    /**
50     * @var \TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService
51     */
52    protected $managementService;
53
54    /**
55     * @var array
56     */
57    protected $availableExtensions = [];
58
59    /**
60     * @var string
61     */
62    protected $localExtensionStorage = '';
63
64    /**
65     * @var array
66     */
67    protected $dependencyErrors = [];
68
69    /**
70     * @var bool
71     */
72    protected $skipDependencyCheck = false;
73
74    /**
75     * @param \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager
76     */
77    public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManager $objectManager)
78    {
79        $this->objectManager = $objectManager;
80    }
81
82    /**
83     * @param \TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository $extensionRepository
84     */
85    public function injectExtensionRepository(\TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository $extensionRepository)
86    {
87        $this->extensionRepository = $extensionRepository;
88    }
89
90    /**
91     * @param \TYPO3\CMS\Extensionmanager\Utility\ListUtility $listUtility
92     */
93    public function injectListUtility(\TYPO3\CMS\Extensionmanager\Utility\ListUtility $listUtility)
94    {
95        $this->listUtility = $listUtility;
96    }
97
98    /**
99     * @param \TYPO3\CMS\Extensionmanager\Utility\EmConfUtility $emConfUtility
100     */
101    public function injectEmConfUtility(\TYPO3\CMS\Extensionmanager\Utility\EmConfUtility $emConfUtility)
102    {
103        $this->emConfUtility = $emConfUtility;
104    }
105
106    /**
107     * @param \TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService $managementService
108     */
109    public function injectManagementService(\TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService $managementService)
110    {
111        $this->managementService = $managementService;
112    }
113
114    /**
115     * @param string $localExtensionStorage
116     */
117    public function setLocalExtensionStorage($localExtensionStorage)
118    {
119        $this->localExtensionStorage = $localExtensionStorage;
120    }
121
122    /**
123     * Setter for available extensions
124     * gets available extensions from list utility if not already done
125     */
126    protected function setAvailableExtensions()
127    {
128        $this->availableExtensions = $this->listUtility->getAvailableExtensions();
129    }
130
131    /**
132     * @param bool $skipDependencyCheck
133     */
134    public function setSkipDependencyCheck($skipDependencyCheck)
135    {
136        $this->skipDependencyCheck = $skipDependencyCheck;
137    }
138
139    /**
140     * Checks dependencies for special cases (currently typo3 and php)
141     *
142     * @param Extension $extension
143     */
144    public function checkDependencies(Extension $extension)
145    {
146        $this->dependencyErrors = [];
147        $dependencies = $extension->getDependencies();
148        foreach ($dependencies as $dependency) {
149            /** @var Dependency $dependency */
150            $identifier = strtolower($dependency->getIdentifier());
151            try {
152                if (in_array($identifier, Dependency::$specialDependencies)) {
153                    if (!$this->skipDependencyCheck) {
154                        $methodName = 'check' . ucfirst($identifier) . 'Dependency';
155                        $this->{$methodName}($dependency);
156                    }
157                } else {
158                    if ($dependency->getType() === 'depends') {
159                        $this->checkExtensionDependency($dependency);
160                    }
161                }
162            } catch (Exception\UnresolvedDependencyException $e) {
163                if (in_array($identifier, Dependency::$specialDependencies)) {
164                    $extensionKey = $extension->getExtensionKey();
165                } else {
166                    $extensionKey = $identifier;
167                }
168                if (!isset($this->dependencyErrors[$extensionKey])) {
169                    $this->dependencyErrors[$extensionKey] = [];
170                }
171                $this->dependencyErrors[$extensionKey][] = [
172                    'code' => $e->getCode(),
173                    'message' => $e->getMessage()
174                ];
175            }
176        }
177    }
178
179    /**
180     * Returns TRUE if a dependency error was found
181     *
182     * @return bool
183     */
184    public function hasDependencyErrors()
185    {
186        return !empty($this->dependencyErrors);
187    }
188
189    /**
190     * Return the dependency errors
191     *
192     * @return array
193     */
194    public function getDependencyErrors()
195    {
196        return $this->dependencyErrors;
197    }
198
199    /**
200     * Returns true if current TYPO3 version fulfills extension requirements
201     *
202     * @param Dependency $dependency
203     * @throws Exception\UnresolvedTypo3DependencyException
204     * @return bool
205     */
206    protected function checkTypo3Dependency(Dependency $dependency)
207    {
208        $lowerCaseIdentifier = strtolower($dependency->getIdentifier());
209        if ($lowerCaseIdentifier === 'typo3') {
210            if (!($dependency->getLowestVersion() === '') && version_compare(VersionNumberUtility::getNumericTypo3Version(), $dependency->getLowestVersion()) === -1) {
211                throw new Exception\UnresolvedTypo3DependencyException(
212                    'Your TYPO3 version is lower than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
213                    1399144499
214                );
215            }
216            if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), VersionNumberUtility::getNumericTypo3Version()) === -1) {
217                throw new Exception\UnresolvedTypo3DependencyException(
218                    'Your TYPO3 version is higher than this extension requires. It requires TYPO3 versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
219                    1399144521
220                );
221            }
222        } else {
223            throw new Exception\UnresolvedTypo3DependencyException(
224                'checkTypo3Dependency can only check TYPO3 dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
225                1399144551
226            );
227        }
228        return true;
229    }
230
231    /**
232     * Returns true if current php version fulfills extension requirements
233     *
234     * @param Dependency $dependency
235     * @throws Exception\UnresolvedPhpDependencyException
236     * @return bool
237     */
238    protected function checkPhpDependency(Dependency $dependency)
239    {
240        $lowerCaseIdentifier = strtolower($dependency->getIdentifier());
241        if ($lowerCaseIdentifier === 'php') {
242            if (!($dependency->getLowestVersion() === '') && version_compare(PHP_VERSION, $dependency->getLowestVersion()) === -1) {
243                throw new Exception\UnresolvedPhpDependencyException(
244                    'Your PHP version is lower than necessary. You need at least PHP version ' . $dependency->getLowestVersion(),
245                    1377977857
246                );
247            }
248            if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), PHP_VERSION) === -1) {
249                throw new Exception\UnresolvedPhpDependencyException(
250                    'Your PHP version is higher than allowed. You can use PHP versions ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
251                    1377977856
252                );
253            }
254        } else {
255            throw new Exception\UnresolvedPhpDependencyException(
256                'checkPhpDependency can only check PHP dependencies. Found dependency with identifier "' . $dependency->getIdentifier() . '"',
257                1377977858
258            );
259        }
260        return true;
261    }
262
263    /**
264     * Main controlling function for checking dependencies
265     * Dependency check is done in the following way:
266     * - installed extension in matching version ? - return true
267     * - available extension in matching version ? - mark for installation
268     * - remote (TER) extension in matching version? - mark for download
269     *
270     * @todo handle exceptions / markForUpload
271     * @param Dependency $dependency
272     * @throws Exception\MissingVersionDependencyException
273     * @return bool
274     */
275    protected function checkExtensionDependency(Dependency $dependency)
276    {
277        $extensionKey = $dependency->getIdentifier();
278        $extensionIsLoaded = $this->isDependentExtensionLoaded($extensionKey);
279        if ($extensionIsLoaded === true) {
280            $isLoadedVersionCompatible = $this->isLoadedVersionCompatible($dependency);
281            if ($isLoadedVersionCompatible === true || $this->skipDependencyCheck) {
282                return true;
283            }
284            $extension = $this->listUtility->getExtension($extensionKey);
285            $loadedVersion = $extension->getPackageMetaData()->getVersion();
286            if (version_compare($loadedVersion, $dependency->getHighestVersion()) === -1) {
287                try {
288                    $this->getExtensionFromRepository($extensionKey, $dependency);
289                } catch (Exception\UnresolvedDependencyException $e) {
290                    throw new Exception\MissingVersionDependencyException(
291                        'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion
292                            . ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER',
293                        1396302624
294                    );
295                }
296            } else {
297                throw new Exception\MissingVersionDependencyException(
298                    'The extension ' . $extensionKey . ' is installed in version ' . $loadedVersion .
299                    ' but needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
300                    1430561927
301                );
302            }
303        } else {
304            $extensionIsAvailable = $this->isDependentExtensionAvailable($extensionKey);
305            if ($extensionIsAvailable === true) {
306                $isAvailableVersionCompatible = $this->isAvailableVersionCompatible($dependency);
307                if ($isAvailableVersionCompatible) {
308                    $unresolvedDependencyErrors = $this->dependencyErrors;
309                    $this->managementService->markExtensionForInstallation($extensionKey);
310                    $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors);
311                } else {
312                    $extension = $this->listUtility->getExtension($extensionKey);
313                    $availableVersion = $extension->getPackageMetaData()->getVersion();
314                    if (version_compare($availableVersion, $dependency->getHighestVersion()) === -1) {
315                        try {
316                            $this->getExtensionFromRepository($extensionKey, $dependency);
317                        } catch (Exception\MissingExtensionDependencyException $e) {
318                            if (!$this->skipDependencyCheck) {
319                                throw new Exception\MissingVersionDependencyException(
320                                    'The extension ' . $extensionKey . ' is available in version ' . $availableVersion
321                                    . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion() . ' and could not be fetched from TER',
322                                    1430560390
323                                );
324                            }
325                        }
326                    } else {
327                        if (!$this->skipDependencyCheck) {
328                            throw new Exception\MissingVersionDependencyException(
329                                'The extension ' . $extensionKey . ' is available in version ' . $availableVersion
330                                . ' but is needed in version ' . $dependency->getLowestVersion() . ' - ' . $dependency->getHighestVersion(),
331                                1430562374
332                            );
333                        }
334                        // Dependency check is skipped and the local version has to be installed
335                        $this->managementService->markExtensionForInstallation($extensionKey);
336                    }
337                }
338            } else {
339                $unresolvedDependencyErrors = $this->dependencyErrors;
340                $this->getExtensionFromRepository($extensionKey, $dependency);
341                $this->dependencyErrors = array_merge($unresolvedDependencyErrors, $this->dependencyErrors);
342            }
343        }
344
345        return false;
346    }
347
348    /**
349     * Get an extension from a repository
350     * (might be in the extension itself or the TER)
351     *
352     * @param string $extensionKey
353     * @param Dependency $dependency
354     * @throws Exception\UnresolvedDependencyException
355     */
356    protected function getExtensionFromRepository($extensionKey, Dependency $dependency)
357    {
358        if (!$this->getExtensionFromInExtensionRepository($extensionKey)) {
359            $this->getExtensionFromTer($extensionKey, $dependency);
360        }
361    }
362
363    /**
364     * Gets an extension from the in extension repository
365     * (the local extension storage)
366     *
367     * @param string $extensionKey
368     * @return bool
369     */
370    protected function getExtensionFromInExtensionRepository($extensionKey)
371    {
372        if ($this->localExtensionStorage !== '' && is_dir($this->localExtensionStorage)) {
373            $extList = \TYPO3\CMS\Core\Utility\GeneralUtility::get_dirs($this->localExtensionStorage);
374            if (in_array($extensionKey, $extList)) {
375                $this->managementService->markExtensionForCopy($extensionKey, $this->localExtensionStorage);
376                return true;
377            }
378        }
379        return false;
380    }
381
382    /**
383     * Handles checks to find a compatible extension version from TER to fulfill given dependency
384     *
385     * @todo unit tests
386     * @param string $extensionKey
387     * @param Dependency $dependency
388     * @throws Exception\UnresolvedDependencyException
389     */
390    protected function getExtensionFromTer($extensionKey, Dependency $dependency)
391    {
392        $isExtensionDownloadableFromTer = $this->isExtensionDownloadableFromTer($extensionKey);
393        if (!$isExtensionDownloadableFromTer) {
394            if (!$this->skipDependencyCheck) {
395                if ($this->extensionRepository->countAll() > 0) {
396                    throw new Exception\MissingExtensionDependencyException(
397                        'The extension ' . $extensionKey . ' is not available from TER.',
398                        1399161266
399                    );
400                }
401                throw new Exception\MissingExtensionDependencyException(
402                    'The extension ' . $extensionKey . ' could not be checked. Please update your Extension-List from TYPO3 Extension Repository (TER).',
403                    1430580308
404                );
405            }
406            return;
407        }
408
409        $isDownloadableVersionCompatible = $this->isDownloadableVersionCompatible($dependency);
410        if (!$isDownloadableVersionCompatible) {
411            if (!$this->skipDependencyCheck) {
412                throw new Exception\MissingVersionDependencyException(
413                    'No compatible version found for extension ' . $extensionKey,
414                    1399161284
415                );
416            }
417            return;
418        }
419
420        $latestCompatibleExtensionByIntegerVersionDependency = $this->getLatestCompatibleExtensionByIntegerVersionDependency($dependency);
421        if (!$latestCompatibleExtensionByIntegerVersionDependency instanceof Extension) {
422            if (!$this->skipDependencyCheck) {
423                throw new Exception\MissingExtensionDependencyException(
424                    'Could not resolve dependency for "' . $dependency->getIdentifier() . '"',
425                    1399161302
426                );
427            }
428            return;
429        }
430
431        if ($this->isDependentExtensionLoaded($extensionKey)) {
432            $this->managementService->markExtensionForUpdate($latestCompatibleExtensionByIntegerVersionDependency);
433        } else {
434            $this->managementService->markExtensionForDownload($latestCompatibleExtensionByIntegerVersionDependency);
435        }
436    }
437
438    /**
439     * @param string $extensionKey
440     * @return bool
441     */
442    protected function isDependentExtensionLoaded($extensionKey)
443    {
444        return ExtensionManagementUtility::isLoaded($extensionKey);
445    }
446
447    /**
448     * @param Dependency $dependency
449     * @return bool
450     */
451    protected function isLoadedVersionCompatible(Dependency $dependency)
452    {
453        $extensionVersion = ExtensionManagementUtility::getExtensionVersion($dependency->getIdentifier());
454        return $this->isVersionCompatible($extensionVersion, $dependency);
455    }
456
457    /**
458     * @param string $version
459     * @param Dependency $dependency
460     * @return bool
461     */
462    protected function isVersionCompatible($version, Dependency $dependency)
463    {
464        if (!($dependency->getLowestVersion() === '') && version_compare($version, $dependency->getLowestVersion()) === -1) {
465            return false;
466        }
467        if (!($dependency->getHighestVersion() === '') && version_compare($dependency->getHighestVersion(), $version) === -1) {
468            return false;
469        }
470        return true;
471    }
472
473    /**
474     * Checks whether the needed extension is available
475     * (not necessarily installed, but present in system)
476     *
477     * @param string $extensionKey
478     * @return bool
479     */
480    protected function isDependentExtensionAvailable($extensionKey)
481    {
482        $this->setAvailableExtensions();
483        return array_key_exists($extensionKey, $this->availableExtensions);
484    }
485
486    /**
487     * Checks whether the available version is compatible
488     *
489     * @param Dependency $dependency
490     * @return bool
491     */
492    protected function isAvailableVersionCompatible(Dependency $dependency)
493    {
494        $this->setAvailableExtensions();
495        $extensionData = $this->emConfUtility->includeEmConf($this->availableExtensions[$dependency->getIdentifier()]);
496        return $this->isVersionCompatible($extensionData['version'], $dependency);
497    }
498
499    /**
500     * Checks whether a ter extension with $extensionKey exists
501     *
502     * @param string $extensionKey
503     * @return bool
504     */
505    protected function isExtensionDownloadableFromTer($extensionKey)
506    {
507        return $this->extensionRepository->countByExtensionKey($extensionKey) > 0;
508    }
509
510    /**
511     * Checks whether a compatible version of the extension exists in TER
512     *
513     * @param Dependency $dependency
514     * @return bool
515     */
516    protected function isDownloadableVersionCompatible(Dependency $dependency)
517    {
518        $versions = $this->getLowestAndHighestIntegerVersions($dependency);
519        $count = $this->extensionRepository->countByVersionRangeAndExtensionKey(
520            $dependency->getIdentifier(),
521            $versions['lowestIntegerVersion'],
522            $versions['highestIntegerVersion']
523        );
524        return !empty($count);
525    }
526
527    /**
528     * Get the latest compatible version of an extension that's
529     * compatible with the current core and PHP version.
530     *
531     * @param iterable $extensions
532     * @return Extension|null
533     */
534    protected function getCompatibleExtension(iterable $extensions): ?Extension
535    {
536        foreach ($extensions as $extension) {
537            /** @var Extension $extension */
538            $this->checkDependencies($extension);
539            $extensionKey = $extension->getExtensionKey();
540
541            if (isset($this->dependencyErrors[$extensionKey])) {
542                // reset dependencyErrors and continue with next version
543                unset($this->dependencyErrors[$extensionKey]);
544                continue;
545            }
546
547            return $extension;
548        }
549
550        return null;
551    }
552
553    /**
554     * Get the latest compatible version of an extension that
555     * fulfills the given dependency from TER
556     *
557     * @param Dependency $dependency
558     * @return Extension
559     */
560    protected function getLatestCompatibleExtensionByIntegerVersionDependency(Dependency $dependency)
561    {
562        $versions = $this->getLowestAndHighestIntegerVersions($dependency);
563        $compatibleDataSets = $this->extensionRepository->findByVersionRangeAndExtensionKeyOrderedByVersion(
564            $dependency->getIdentifier(),
565            $versions['lowestIntegerVersion'],
566            $versions['highestIntegerVersion']
567        );
568        return $this->getCompatibleExtension($compatibleDataSets);
569    }
570
571    /**
572     * Return array of lowest and highest version of dependency as integer
573     *
574     * @param Dependency $dependency
575     * @return array
576     */
577    protected function getLowestAndHighestIntegerVersions(Dependency $dependency)
578    {
579        $lowestVersion = $dependency->getLowestVersion();
580        $lowestVersionInteger = $lowestVersion ? VersionNumberUtility::convertVersionNumberToInteger($lowestVersion) : 0;
581        $highestVersion = $dependency->getHighestVersion();
582        $highestVersionInteger = $highestVersion ? VersionNumberUtility::convertVersionNumberToInteger($highestVersion) : 0;
583        return [
584            'lowestIntegerVersion' => $lowestVersionInteger,
585            'highestIntegerVersion' => $highestVersionInteger
586        ];
587    }
588
589    /**
590     * @param string $extensionKey
591     * @return array
592     */
593    public function findInstalledExtensionsThatDependOnMe($extensionKey)
594    {
595        $availableAndInstalledExtensions = $this->listUtility->getAvailableAndInstalledExtensionsWithAdditionalInformation();
596        $dependentExtensions = [];
597        foreach ($availableAndInstalledExtensions as $availableAndInstalledExtensionKey => $availableAndInstalledExtension) {
598            if (isset($availableAndInstalledExtension['installed']) && $availableAndInstalledExtension['installed'] === true) {
599                if (is_array($availableAndInstalledExtension['constraints']) && is_array($availableAndInstalledExtension['constraints']['depends']) && array_key_exists($extensionKey, $availableAndInstalledExtension['constraints']['depends'])) {
600                    $dependentExtensions[] = $availableAndInstalledExtensionKey;
601                }
602            }
603        }
604        return $dependentExtensions;
605    }
606
607    /**
608     * Get extensions (out of a given list) that are suitable for the current TYPO3 version
609     *
610     * @param \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array $extensions List of extensions to check
611     * @return array List of extensions suitable for current TYPO3 version
612     */
613    public function getExtensionsSuitableForTypo3Version($extensions)
614    {
615        $suitableExtensions = [];
616        /** @var Extension $extension */
617        foreach ($extensions as $extension) {
618            /** @var Dependency $dependency */
619            foreach ($extension->getDependencies() as $dependency) {
620                if ($dependency->getIdentifier() === 'typo3') {
621                    try {
622                        if ($this->checkTypo3Dependency($dependency)) {
623                            $suitableExtensions[] = $extension;
624                        }
625                    } catch (Exception\UnresolvedTypo3DependencyException $e) {
626                    }
627                    break;
628                }
629            }
630        }
631        return $suitableExtensions;
632    }
633
634    /**
635     * Gets a list of various extensions in various versions and returns
636     * a filtered list containing the extension-version combination with
637     * the highest version number.
638     *
639     * @param Extension[] $extensions
640     * @param bool $showUnsuitable
641     *
642     * @return \TYPO3\CMS\Extensionmanager\Domain\Model\Extension[]
643     */
644    public function filterYoungestVersionOfExtensionList(array $extensions, $showUnsuitable)
645    {
646        if (!$showUnsuitable) {
647            $extensions = $this->getExtensionsSuitableForTypo3Version($extensions);
648        }
649        $filteredExtensions = [];
650        foreach ($extensions as $extension) {
651            $extensionKey = $extension->getExtensionKey();
652            if (!array_key_exists($extensionKey, $filteredExtensions)) {
653                $filteredExtensions[$extensionKey] = $extension;
654                continue;
655            }
656            $currentVersion = $filteredExtensions[$extensionKey]->getVersion();
657            $newVersion = $extension->getVersion();
658            if (version_compare($newVersion, $currentVersion, '>')) {
659                $filteredExtensions[$extensionKey] = $extension;
660            }
661        }
662        return $filteredExtensions;
663    }
664}
665