1<?php
2
3namespace TYPO3\CMS\Install\Updates;
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18use Symfony\Component\Console\Output\OutputInterface;
19use TYPO3\CMS\Core\Core\Environment;
20use TYPO3\CMS\Core\Utility\GeneralUtility;
21use TYPO3\CMS\Extbase\Object\ObjectManager;
22use TYPO3\CMS\Extensionmanager\Utility\Connection\TerUtility;
23use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility;
24use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;
25use TYPO3\CMS\Extensionmanager\Utility\ListUtility;
26
27/**
28 * Download extension from TER
29 */
30abstract class AbstractDownloadExtensionUpdate implements UpgradeWizardInterface, ConfirmableInterface, ChattyInterface
31{
32    /**
33     * @var string
34     */
35    protected $repositoryUrl = 'https://typo3.org/fileadmin/ter/@filename';
36
37    /**
38     * @var OutputInterface
39     */
40    protected $output;
41
42    /**
43     * @var \TYPO3\CMS\Install\Updates\ExtensionModel
44     */
45    protected $extension;
46
47    /**
48     * @param OutputInterface $output
49     */
50    public function setOutput(OutputInterface $output): void
51    {
52        $this->output = $output;
53    }
54
55    /**
56     * Execute the update
57     * Called when a wizard reports that an update is necessary
58     *
59     * @return bool
60     */
61    public function executeUpdate(): bool
62    {
63        return $this->installExtension($this->extension);
64    }
65
66    /**
67     * This method can be called to install an extension following all proper processes
68     * (e.g. installing in extList, respecting priority, etc.)
69     *
70     * @param \TYPO3\CMS\Install\Updates\ExtensionModel $extension
71     * @return bool whether the installation worked or not
72     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
73     */
74    protected function installExtension(ExtensionModel $extension): bool
75    {
76        $updateSuccessful = true;
77        /** @var ObjectManager $objectManager */
78        $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
79
80        /** @var ListUtility $extensionListUtility */
81        $extensionListUtility = $objectManager->get(ListUtility::class);
82        $availableExtensions = $extensionListUtility->getAvailableExtensions();
83
84        $extensionKey = $extension->getKey();
85        $isExtensionAvailable = !empty($availableExtensions[$extensionKey]);
86        $isComposerMode = Environment::isComposerMode();
87
88        if (!$isComposerMode && !$isExtensionAvailable) {
89            /** @var TerUtility $extensionTerUtility */
90            $extensionTerUtility = $objectManager->get(TerUtility::class);
91            $t3xContent = $this->fetchExtension($extensionKey, $extension->getVersionString());
92            if (empty($t3xContent)) {
93                $updateSuccessful = false;
94                $this->output->writeln('<error>The extension ' . $extensionKey . ' could not be downloaded.</error>');
95            }
96            $t3xExtracted = $extensionTerUtility->decodeExchangeData($t3xContent);
97            if (empty($t3xExtracted) || !is_array($t3xExtracted) || empty($t3xExtracted['extKey'])) {
98                $updateSuccessful = false;
99                $this->output->writeln('<error>The extension ' . $extensionKey . ' could not be extracted.</error>');
100            }
101
102            /** @var FileHandlingUtility $extensionFileHandlingUtility */
103            $extensionFileHandlingUtility = $objectManager->get(FileHandlingUtility::class);
104            $extensionFileHandlingUtility->unpackExtensionFromExtensionDataArray($t3xExtracted);
105
106            // The listUtility now needs to have the regenerated list of packages
107            $extensionListUtility->reloadAvailableExtensions();
108        }
109
110        if ($isComposerMode && !$isExtensionAvailable) {
111            $updateSuccessful = false;
112            $this->output->writeln(
113                '<warning>The extension ' . $extensionKey
114                . ' can not be downloaded since Composer is used for package management. Please require this '
115                . 'extension as package via Composer: "composer require ' . $extension->getComposerName()
116                . ':^' . $extension->getVersionString() . '"</warning>'
117            );
118        }
119
120        if ($updateSuccessful) {
121            /** @var InstallUtility $extensionInstallUtility */
122            $extensionInstallUtility = $objectManager->get(InstallUtility::class);
123            $extensionInstallUtility->install($extensionKey);
124        }
125
126        return $updateSuccessful;
127    }
128
129    /**
130     * Fetch extension from repository
131     *
132     * @param string $extensionKey The extension key to fetch
133     * @param string $version The version to fetch
134     * @throws \InvalidArgumentException
135     * @return string T3X file content
136     */
137    protected function fetchExtension($extensionKey, $version): string
138    {
139        if (empty($extensionKey) || empty($version)) {
140            throw new \InvalidArgumentException(
141                'No extension key for fetching an extension was given.',
142                1344687432
143            );
144        }
145
146        $filename = $extensionKey[0] . '/' . $extensionKey[1] . '/' . $extensionKey . '_' . $version . '.t3x';
147        $url = str_replace('@filename', $filename, $this->repositoryUrl);
148
149        return $this->fetchUrl($url);
150    }
151
152    /**
153     * Open an URL and return the response
154     * This wrapper method is required to try several download methods if
155     * the configuration is not valid or initially written by the installer.
156     *
157     * @param string $url The URL to file
158     * @throws \Exception
159     * @throws \InvalidArgumentException
160     * @return string File content
161     */
162    protected function fetchUrl($url): string
163    {
164        if (empty($url)) {
165            throw new \InvalidArgumentException(
166                'No URL for downloading an extension given.',
167                1344687436
168            );
169        }
170
171        $fileContent = GeneralUtility::getUrl($url);
172
173        // Can not fetch url, throw an exception
174        if ($fileContent === false) {
175            throw new \RuntimeException(
176                'Can not fetch URL "' . $url . '". Possible reasons are network problems or misconfiguration.',
177                1344685036
178            );
179        }
180
181        return $fileContent;
182    }
183}
184