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

decodeExchangeData()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 7
nop 1
dl 0
loc 19
rs 9.5222
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\Controller;
17
18
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
19
use TYPO3\CMS\Core\Core\Environment;
20
use TYPO3\CMS\Core\Messaging\FlashMessage;
21
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
22
use TYPO3\CMS\Core\Utility\GeneralUtility;
23
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
24
use TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository;
25
use TYPO3\CMS\Extensionmanager\Exception\DependencyConfigurationNotFoundException;
26
use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
27
use TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService;
28
use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility;
29
30
/**
31
 * Controller for handling upload of a local extension file
32
 * Handles .t3x or .zip files
33
 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
34
 */
35
class UploadExtensionFileController extends AbstractController
36
{
37
    use BlockSerializationTrait;
38
39
    /**
40
     * @var ExtensionRepository
41
     */
42
    protected $extensionRepository;
43
44
    /**
45
     * @var FileHandlingUtility
46
     */
47
    protected $fileHandlingUtility;
48
49
    /**
50
     * @var ExtensionManagementService
51
     */
52
    protected $managementService;
53
54
    /**
55
     * @var string
56
     */
57
    protected $extensionBackupPath = '';
58
59
    /**
60
     * @var bool
61
     */
62
    protected $removeFromOriginalPath = false;
63
64
    /**
65
     * @param ExtensionRepository $extensionRepository
66
     */
67
    public function injectExtensionRepository(ExtensionRepository $extensionRepository)
68
    {
69
        $this->extensionRepository = $extensionRepository;
70
    }
71
72
    /**
73
     * @param FileHandlingUtility $fileHandlingUtility
74
     */
75
    public function injectFileHandlingUtility(FileHandlingUtility $fileHandlingUtility)
76
    {
77
        $this->fileHandlingUtility = $fileHandlingUtility;
78
    }
79
80
    /**
81
     * @param ExtensionManagementService $managementService
82
     */
83
    public function injectManagementService(ExtensionManagementService $managementService)
84
    {
85
        $this->managementService = $managementService;
86
    }
87
88
    /**
89
     * Remove backup folder before destruction
90
     */
91
    public function __destruct()
92
    {
93
        $this->removeBackupFolder();
94
    }
95
96
    /**
97
     * Render upload extension form
98
     */
99
    public function formAction()
100
    {
101
        if (Environment::isComposerMode()) {
102
            throw new ExtensionManagerException(
103
                'Composer mode is active. You are not allowed to upload any extension file.',
104
                1444725828
105
            );
106
        }
107
    }
108
109
    /**
110
     * Extract an uploaded file and install the matching extension
111
     *
112
     * @param bool $overwrite Overwrite existing extension if TRUE
113
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
114
     */
115
    public function extractAction($overwrite = false)
116
    {
117
        if (Environment::isComposerMode()) {
118
            throw new ExtensionManagerException(
119
                'Composer mode is active. You are not allowed to upload any extension file.',
120
                1444725853
121
            );
122
        }
123
        $file = $_FILES['tx_extensionmanager_tools_extensionmanagerextensionmanager'];
124
        $fileName = pathinfo($file['name']['extensionFile'], PATHINFO_BASENAME);
125
        try {
126
            // If the file name isn't valid an error will be thrown
127
            $this->checkFileName($fileName);
128
            if (!empty($file['tmp_name']['extensionFile'])) {
129
                $tempFile = GeneralUtility::upload_to_tempfile($file['tmp_name']['extensionFile']);
130
            } else {
131
                throw new ExtensionManagerException(
132
                    'Creating temporary file failed. Check your upload_max_filesize and post_max_size limits.',
133
                    1342864339
134
                );
135
            }
136
            $extensionData = $this->extractExtensionFromFile($tempFile, $fileName, $overwrite);
137
            $isAutomaticInstallationEnabled = (bool)GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('extensionmanager', 'automaticInstallation');
138
            if (!$isAutomaticInstallationEnabled) {
139
                $this->addFlashMessage(
140
                    $this->translate('extensionList.uploadFlashMessage.message', [$extensionData['extKey']]),
141
                    $this->translate('extensionList.uploadFlashMessage.title'),
142
                    FlashMessage::OK
143
                );
144
            } else {
145
                if ($this->activateExtension($extensionData['extKey'])) {
146
                    $this->addFlashMessage(
147
                        $this->translate('extensionList.installedFlashMessage.message', [$extensionData['extKey']]),
148
                        '',
149
                        FlashMessage::OK
150
                    );
151
                } else {
152
                    $this->redirect('unresolvedDependencies', 'List', null, ['extensionKey' => $extensionData['extKey']]);
153
                }
154
            }
155
        } catch (StopActionException $exception) {
156
            throw $exception;
157
        } catch (DependencyConfigurationNotFoundException $exception) {
158
            $this->addFlashMessage($exception->getMessage(), '', FlashMessage::ERROR);
159
        } catch (\Exception $exception) {
160
            $this->removeExtensionAndRestoreFromBackup($fileName);
161
            $this->addFlashMessage($exception->getMessage(), '', FlashMessage::ERROR);
162
        }
163
        $this->redirect('index', 'List', null, [
164
            self::TRIGGER_RefreshModuleMenu => true,
165
            self::TRIGGER_RefreshTopbar => true
166
        ]);
167
    }
168
169
    /**
170
     * Validate the filename of an uploaded file
171
     *
172
     * @param string $fileName
173
     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
174
     */
175
    public function checkFileName($fileName)
176
    {
177
        $extension = pathinfo($fileName, PATHINFO_EXTENSION);
178
        if (empty($fileName)) {
179
            throw new ExtensionManagerException('No file given.', 1342858852);
180
        }
181
        if ($extension !== 't3x' && $extension !== 'zip') {
182
            throw new ExtensionManagerException('Wrong file format "' . $extension . '" given. Allowed formats are t3x and zip.', 1342858853);
183
        }
184
    }
185
186
    /**
187
     * Extract a given t3x or zip file
188
     *
189
     * @param string $uploadPath Path to existing extension file
190
     * @param string $fileName Filename of the uploaded file
191
     * @param bool $overwrite If true, extension will be replaced
192
     * @return array Extension data
193
     * @throws ExtensionManagerException
194
     * @throws DependencyConfigurationNotFoundException
195
     */
196
    public function extractExtensionFromFile($uploadPath, $fileName, $overwrite)
197
    {
198
        $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
199
        if ($fileExtension === 't3x') {
200
            $extensionData = $this->getExtensionFromT3xFile($uploadPath, $overwrite);
201
        } else {
202
            $extensionData = $this->getExtensionFromZipFile($uploadPath, $fileName, $overwrite);
203
        }
204
205
        return $extensionData;
206
    }
207
208
    /**
209
     * @param string $extensionKey
210
     * @return bool
211
     */
212
    public function activateExtension($extensionKey)
213
    {
214
        $this->managementService->reloadPackageInformation($extensionKey);
215
        $extension = $this->managementService->getExtension($extensionKey);
216
        return is_array($this->managementService->installExtension($extension));
217
    }
218
219
    /**
220
     * Extracts a given t3x file and installs the extension
221
     *
222
     * @param string $file Path to uploaded file
223
     * @param bool $overwrite Overwrite existing extension if TRUE
224
     * @throws ExtensionManagerException
225
     * @throws DependencyConfigurationNotFoundException
226
     * @return array
227
     */
228
    protected function getExtensionFromT3xFile($file, $overwrite = false)
229
    {
230
        $fileContent = file_get_contents($file);
231
        if (!$fileContent) {
232
            throw new ExtensionManagerException('File had no or wrong content.', 1342859339);
233
        }
234
        $extensionData = $this->decodeExchangeData($fileContent);
235
        if (empty($extensionData['extKey'])) {
236
            throw new ExtensionManagerException('Decoding the file went wrong. No extension key found', 1342864309);
237
        }
238
        $isExtensionAvailable = $this->managementService->isAvailable($extensionData['extKey']);
239
        if (!$overwrite && $isExtensionAvailable) {
240
            throw new ExtensionManagerException($this->translate('extensionList.overwritingDisabled'), 1342864310);
241
        }
242
        if ($isExtensionAvailable) {
243
            $this->copyExtensionFolderToTempFolder($extensionData['extKey']);
244
        }
245
        $this->removeFromOriginalPath = true;
246
        $extension = $this->extensionRepository->findOneByExtensionKeyAndVersion($extensionData['extKey'], $extensionData['EM_CONF']['version']);
247
        $this->fileHandlingUtility->unpackExtensionFromExtensionDataArray($extensionData, $extension);
0 ignored issues
show
Bug introduced by
$extension of type TYPO3\CMS\Extbase\Persis...ryResultInterface|array is incompatible with the type TYPO3\CMS\Extensionmanag...in\Model\Extension|null expected by parameter $extension of TYPO3\CMS\Extensionmanag...romExtensionDataArray(). ( Ignorable by Annotation )

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

247
        $this->fileHandlingUtility->unpackExtensionFromExtensionDataArray($extensionData, /** @scrutinizer ignore-type */ $extension);
Loading history...
248
249
        if (empty($extension)
250
            && empty($extensionData['EM_CONF']['constraints'])
251
            && !isset($extensionData['FILES']['ext_emconf.php'])
252
            && !isset($extensionData['FILES']['/ext_emconf.php'])
253
        ) {
254
            throw new DependencyConfigurationNotFoundException('Extension cannot be installed automatically because no dependencies could be found! Please check dependencies manually (on typo3.org) before installing the extension.', 1439587168);
255
        }
256
257
        return $extensionData;
258
    }
259
260
    /**
261
     * Extracts a given zip file and installs the extension
262
     * As there is no information about the extension key in the zip
263
     * we have to use the file name to get that information
264
     * filename format is expected to be extensionkey_version.zip
265
     *
266
     * @param string $file Path to uploaded file
267
     * @param string $fileName Filename (basename) of uploaded file
268
     * @param bool $overwrite Overwrite existing extension if TRUE
269
     * @return array
270
     * @throws ExtensionManagerException
271
     */
272
    protected function getExtensionFromZipFile($file, $fileName, $overwrite = false)
273
    {
274
        // Remove version and extension from filename to determine the extension key
275
        $extensionKey = $this->getExtensionKeyFromFileName($fileName);
276
        $isExtensionAvailable = $this->managementService->isAvailable($extensionKey);
277
        if (!$overwrite && $isExtensionAvailable) {
278
            throw new ExtensionManagerException('Extension is already available and overwriting is disabled.', 1342864311);
279
        }
280
        if ($isExtensionAvailable) {
281
            $this->copyExtensionFolderToTempFolder($extensionKey);
282
        }
283
        $this->removeFromOriginalPath = true;
284
        $this->fileHandlingUtility->unzipExtensionFromFile($file, $extensionKey);
285
286
        return ['extKey' => $extensionKey];
287
    }
288
289
    /**
290
     * Removes version and file extension from filename to determine extension key
291
     *
292
     * @param string $fileName
293
     * @return string
294
     */
295
    protected function getExtensionKeyFromFileName($fileName)
296
    {
297
        return preg_replace('/_(\\d+)(\\.|\\-)(\\d+)(\\.|\\-)(\\d+).*/i', '', strtolower(substr($fileName, 0, -4)));
298
    }
299
300
    /**
301
     * Copies current extension folder to typo3temp directory as backup
302
     *
303
     * @param string $extensionKey
304
     * @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
305
     */
306
    protected function copyExtensionFolderToTempFolder($extensionKey)
307
    {
308
        $this->extensionBackupPath = Environment::getVarPath() . '/transient/' . $extensionKey . substr(sha1($extensionKey . microtime()), 0, 7) . '/';
309
        GeneralUtility::mkdir($this->extensionBackupPath);
310
        GeneralUtility::copyDirectory(
311
            $this->fileHandlingUtility->getExtensionDir($extensionKey),
312
            $this->extensionBackupPath
313
        );
314
    }
315
316
    /**
317
     * Removes the extension directory and restores the extension from the backup directory
318
     *
319
     * @param string $fileName
320
     * @see UploadExtensionFileController::extractAction
321
     */
322
    protected function removeExtensionAndRestoreFromBackup($fileName)
323
    {
324
        $extDirPath = $this->fileHandlingUtility->getExtensionDir($this->getExtensionKeyFromFileName($fileName));
325
        if ($this->removeFromOriginalPath && is_dir($extDirPath)) {
326
            GeneralUtility::rmdir($extDirPath, true);
327
        }
328
        if (!empty($this->extensionBackupPath)) {
329
            GeneralUtility::mkdir($extDirPath);
330
            GeneralUtility::copyDirectory($this->extensionBackupPath, $extDirPath);
331
        }
332
    }
333
334
    /**
335
     * Removes the backup folder in typo3temp
336
     */
337
    protected function removeBackupFolder()
338
    {
339
        if (!empty($this->extensionBackupPath)) {
340
            GeneralUtility::rmdir($this->extensionBackupPath, true);
341
            $this->extensionBackupPath = '';
342
        }
343
    }
344
345
    /**
346
     * Decodes extension upload array.
347
     * This kind of data is when an extension is uploaded to TER
348
     *
349
     * @param string $stream Data stream
350
     * @throws ExtensionManagerException
351
     * @return array Array with result on success, otherwise an error string.
352
     */
353
    protected function decodeExchangeData(string $stream): array
354
    {
355
        [$expectedHash, $compressionType, $contents] = explode(':', $stream, 3);
356
        if ($compressionType === 'gzcompress') {
357
            if (function_exists('gzuncompress')) {
358
                $contents = (string)gzuncompress($contents);
359
            } else {
360
                throw new ExtensionManagerException('Decoding Error: No decompressor available for compressed content. gzcompress()/gzuncompress() functions are not available!', 1344761814);
361
            }
362
        }
363
        if (hash_equals($expectedHash, md5($contents))) {
364
            $output = unserialize($contents, ['allowed_classes' => false]);
365
            if (!is_array($output)) {
366
                throw new ExtensionManagerException('Error: Content could not be unserialized to an array. Strange (since MD5 hashes match!)', 1344761938);
367
            }
368
        } else {
369
            throw new ExtensionManagerException('Error: MD5 mismatch. Maybe the extension file was downloaded and saved as a text file by the browser and thereby corrupted!? (Always select "All" filetype when saving extensions)', 1344761991);
370
        }
371
        return $output;
372
    }
373
}
374