CoreUpdateService::verifyFileChecksum()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 40
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 31
nc 4
nop 1
dl 0
loc 40
rs 9.424
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\Install\Service;
17
18
use TYPO3\CMS\Core\Core\Environment;
19
use TYPO3\CMS\Core\Messaging\FlashMessage;
20
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
21
use TYPO3\CMS\Core\Service\OpcodeCacheService;
22
use TYPO3\CMS\Core\Utility\GeneralUtility;
23
use TYPO3\CMS\Core\Utility\PathUtility;
24
use TYPO3\CMS\Core\Utility\StringUtility;
25
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
26
27
/**
28
 * Core update service.
29
 * This service handles core updates, all the nasty details are encapsulated
30
 * here. The single public methods 'depend' on each other, for example a new
31
 * core has to be downloaded before it can be unpacked.
32
 *
33
 * Each method returns only TRUE of FALSE indicating if it was successful or
34
 * not. Detailed information can be fetched with getMessages() and will return
35
 * a list of status messages of the previous operation.
36
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
37
 */
38
class CoreUpdateService
39
{
40
    /**
41
     * @var CoreVersionService
42
     */
43
    protected $coreVersionService;
44
45
    /**
46
     * @var FlashMessageQueue
47
     */
48
    protected $messages;
49
50
    /**
51
     * Absolute path to download location
52
     *
53
     * @var string
54
     */
55
    protected $downloadTargetPath;
56
57
    /**
58
     * Absolute path to the symlink pointing to the currently used TYPO3 core files
59
     *
60
     * @var string
61
     */
62
    protected $symlinkToCoreFiles;
63
64
    /**
65
     * Base URI for TYPO3 downloads
66
     *
67
     * @var string
68
     */
69
    protected $downloadBaseUri;
70
71
    public function __construct(CoreVersionService $coreVersionService)
72
    {
73
        $this->coreVersionService = $coreVersionService;
74
        $this->setDownloadTargetPath(Environment::getVarPath() . '/transient/');
75
        $this->symlinkToCoreFiles = $this->discoverCurrentCoreSymlink();
76
        $this->downloadBaseUri = 'https://get.typo3.org';
77
        $this->messages = new FlashMessageQueue('install');
78
    }
79
80
    /**
81
     * Check if this installation wants to enable the core updater
82
     *
83
     * @return bool
84
     */
85
    public function isCoreUpdateEnabled()
86
    {
87
        $coreUpdateDisabled = getenv('TYPO3_DISABLE_CORE_UPDATER') ?: (getenv('REDIRECT_TYPO3_DISABLE_CORE_UPDATER') ?: false);
88
        return !Environment::isComposerMode() && !$coreUpdateDisabled;
89
    }
90
91
    /**
92
     * In future implementations we might implement some smarter logic here
93
     *
94
     * @return string
95
     */
96
    protected function discoverCurrentCoreSymlink()
97
    {
98
        return Environment::getPublicPath() . '/typo3_src';
99
    }
100
101
    /**
102
     * Create download location in case the folder does not exist
103
     * @todo move this to folder structure
104
     *
105
     * @param string $downloadTargetPath
106
     */
107
    protected function setDownloadTargetPath($downloadTargetPath)
108
    {
109
        if (!is_dir($downloadTargetPath)) {
110
            GeneralUtility::mkdir_deep($downloadTargetPath);
111
        }
112
        $this->downloadTargetPath = $downloadTargetPath;
113
    }
114
115
    /**
116
     * Get messages of previous method call
117
     *
118
     * @return FlashMessageQueue
119
     */
120
    public function getMessages(): FlashMessageQueue
121
    {
122
        return $this->messages;
123
    }
124
125
    /**
126
     * Check if an update is possible at all
127
     *
128
     * @param string $version The target version number
129
     * @return bool TRUE on success
130
     */
131
    public function checkPreConditions($version)
132
    {
133
        $success = true;
134
135
        // Folder structure test: Update can be done only if folder structure returns no errors
136
        $folderStructureFacade = GeneralUtility::makeInstance(DefaultFactory::class)->getStructure();
137
        $folderStructureMessageQueue = $folderStructureFacade->getStatus();
138
        $folderStructureErrors = $folderStructureMessageQueue->getAllMessages(FlashMessage::ERROR);
139
        $folderStructureWarnings = $folderStructureMessageQueue->getAllMessages(FlashMessage::WARNING);
140
        if (!empty($folderStructureErrors) || !empty($folderStructureWarnings) || !is_link(Environment::getPublicPath() . '/typo3_src')) {
141
            $success = false;
142
            $this->messages->enqueue(new FlashMessage(
143
                'To perform an update, the folder structure of this TYPO3 CMS instance must'
144
                . ' stick to the conventions, or the update process could lead to unexpected results'
145
                . ' and may be hazardous to your system. Please check your directory status in the'
146
                . ' “Environment” module under “Directory Status”.',
147
                'Automatic TYPO3 CMS core update not possible: Folder structure has errors or warnings',
148
                FlashMessage::ERROR
149
            ));
150
        }
151
152
        // No core update on windows
153
        if (Environment::isWindows()) {
154
            $success = false;
155
            $this->messages->enqueue(new FlashMessage(
156
                '',
157
                'Automatic TYPO3 CMS core update not possible: Update not supported on Windows OS',
158
                FlashMessage::ERROR
159
            ));
160
        }
161
162
        if ($success) {
163
            // Explicit write check to document root
164
            $file = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
165
            $result = @touch($file);
166
            if (!$result) {
167
                $success = false;
168
                $this->messages->enqueue(new FlashMessage(
169
                    'Could not write a file in path "' . Environment::getPublicPath() . '/"!'
170
                    . ' Please check your directory status in the “Environment” module under “Directory Status”.',
171
                    'Automatic TYPO3 CMS core update not possible: No write access to document root',
172
                    FlashMessage::ERROR
173
                ));
174
            } else {
175
                // Check symlink creation
176
                $link = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
177
                @symlink($file, $link);
178
                if (!is_link($link)) {
179
                    $success = false;
180
                    $this->messages->enqueue(new FlashMessage(
181
                        'Could not create a symbolic link in path "' . Environment::getPublicPath() . '/"!'
182
                        . ' Please check your directory status in the “Environment” module under “Directory Status”.',
183
                        'Automatic TYPO3 CMS core update not possible: No symlink creation possible',
184
                        FlashMessage::ERROR
185
                    ));
186
                } else {
187
                    unlink($link);
188
                }
189
                unlink($file);
190
            }
191
192
            if (!$this->checkCoreFilesAvailable($version)) {
193
                // Explicit write check to upper directory of current core location
194
                $coreLocation = @realpath($this->symlinkToCoreFiles . '/../');
195
                $file = $coreLocation . '/' . StringUtility::getUniqueId('install-core-update-test-');
0 ignored issues
show
Bug introduced by
Are you sure $coreLocation of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

195
                $file = /** @scrutinizer ignore-type */ $coreLocation . '/' . StringUtility::getUniqueId('install-core-update-test-');
Loading history...
196
                $result = @touch($file);
197
                if (!$result) {
198
                    $success = false;
199
                    $this->messages->enqueue(new FlashMessage(
200
                        'New TYPO3 CMS core should be installed in "' . $coreLocation . '", but this directory is not writable!'
201
                        . ' Please check your directory status in the “Environment” module under “Directory Status”.',
202
                        'Automatic TYPO3 CMS core update not possible: No write access to TYPO3 CMS core location',
203
                        FlashMessage::ERROR
204
                    ));
205
                } else {
206
                    unlink($file);
207
                }
208
            }
209
        }
210
211
        if ($success && !$this->coreVersionService->isInstalledVersionAReleasedVersion()) {
212
            $success = false;
213
            $this->messages->enqueue(new FlashMessage(
214
                'Your current version is specified as ' . $this->coreVersionService->getInstalledVersion() . '.'
215
                    . ' This is a development version and can not be updated automatically. If this is a "git"'
216
                    . ' checkout, please update using git directly.',
217
                'Automatic TYPO3 CMS core update not possible: You are running a development version of TYPO3',
218
                FlashMessage::ERROR
219
            ));
220
        }
221
222
        return $success;
223
    }
224
225
    /**
226
     * Download the specified version
227
     *
228
     * @param string $version A version to download
229
     * @return bool TRUE on success
230
     */
231
    public function downloadVersion($version)
232
    {
233
        $success = true;
234
        if ($this->checkCoreFilesAvailable($version)) {
235
            $this->messages->enqueue(new FlashMessage(
236
                '',
237
                'Skipped download of TYPO3 CMS core. A core source directory already exists in destination path. Using this instead.',
238
                FlashMessage::NOTICE
239
            ));
240
        } else {
241
            $downloadUri = $this->downloadBaseUri . '/' . $version;
242
            $fileLocation = $this->getDownloadTarGzTargetPath($version);
243
244
            if (@file_exists($fileLocation)) {
245
                $success = false;
246
                $this->messages->enqueue(new FlashMessage(
247
                    '',
248
                    'TYPO3 CMS core download exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
249
                    FlashMessage::ERROR
250
                ));
251
            } else {
252
                $fileContent = GeneralUtility::getUrl($downloadUri);
253
                if (!$fileContent) {
254
                    $success = false;
255
                    $this->messages->enqueue(new FlashMessage(
256
                        'Failed to download ' . $downloadUri,
257
                        'Download not successful',
258
                        FlashMessage::ERROR
259
                    ));
260
                } else {
261
                    $fileStoreResult = file_put_contents($fileLocation, $fileContent);
262
                    if (!$fileStoreResult) {
263
                        $success = false;
264
                        $this->messages->enqueue(new FlashMessage(
265
                            '',
266
                            'Unable to store download content',
267
                            FlashMessage::ERROR
268
                        ));
269
                    } else {
270
                        $this->messages->enqueue(new FlashMessage(
271
                            '',
272
                            'TYPO3 CMS core download finished'
273
                        ));
274
                    }
275
                }
276
            }
277
        }
278
        return $success;
279
    }
280
281
    /**
282
     * Verify checksum of downloaded version
283
     *
284
     * @param string $version A downloaded version to check
285
     * @return bool TRUE on success
286
     */
287
    public function verifyFileChecksum($version)
288
    {
289
        $success = true;
290
        if ($this->checkCoreFilesAvailable($version)) {
291
            $this->messages->enqueue(new FlashMessage(
292
                '',
293
                'Verifying existing TYPO3 CMS core checksum is not possible',
294
                FlashMessage::WARNING
295
            ));
296
        } else {
297
            $fileLocation = $this->getDownloadTarGzTargetPath($version);
298
            $expectedChecksum = $this->coreVersionService->getTarGzSha1OfVersion($version);
299
            if (!file_exists($fileLocation)) {
300
                $success = false;
301
                $this->messages->enqueue(new FlashMessage(
302
                    '',
303
                    'Downloaded TYPO3 CMS core not found',
304
                    FlashMessage::ERROR
305
                ));
306
            } else {
307
                $actualChecksum = sha1_file($fileLocation);
308
                if ($actualChecksum !== $expectedChecksum) {
309
                    $success = false;
310
                    $this->messages->enqueue(new FlashMessage(
311
                        'The official TYPO3 CMS version system on https://get.typo3.org expects a sha1 checksum of '
312
                            . $expectedChecksum . ' from the content of the downloaded new TYPO3 CMS core version ' . $version . '.'
313
                            . ' The actual checksum is ' . $actualChecksum . '. The update is stopped. This may be a'
314
                            . ' failed download, an attack, or an issue with the typo3.org infrastructure.',
315
                        'New TYPO3 CMS core checksum mismatch',
316
                        FlashMessage::ERROR
317
                    ));
318
                } else {
319
                    $this->messages->enqueue(new FlashMessage(
320
                        '',
321
                        'Checksum verified'
322
                    ));
323
                }
324
            }
325
        }
326
        return $success;
327
    }
328
329
    /**
330
     * Unpack a downloaded core
331
     *
332
     * @param string $version A version to unpack
333
     * @return bool TRUE on success
334
     */
335
    public function unpackVersion($version)
336
    {
337
        $success = true;
338
        if ($this->checkCoreFilesAvailable($version)) {
339
            $this->messages->enqueue(new FlashMessage(
340
                '',
341
                'Unpacking TYPO3 CMS core files skipped',
342
                FlashMessage::NOTICE
343
            ));
344
        } else {
345
            $fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
346
            if (!@is_file($fileLocation)) {
347
                $success = false;
348
                $this->messages->enqueue(new FlashMessage(
349
                    '',
350
                    'Downloaded TYPO3 CMS core not found',
351
                    FlashMessage::ERROR
352
                ));
353
            } elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
354
                $success = false;
355
                $this->messages->enqueue(new FlashMessage(
356
                    '',
357
                    'Unpacked TYPO3 CMS core exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
358
                    FlashMessage::ERROR
359
                ));
360
            } else {
361
                $unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
362
                exec($unpackCommand, $output, $errorCode);
363
                if ($errorCode) {
364
                    $success = false;
365
                    $this->messages->enqueue(new FlashMessage(
366
                        '',
367
                        'Unpacking TYPO3 CMS core not successful',
368
                        FlashMessage::ERROR
369
                    ));
370
                } else {
371
                    $removePackedFileResult = unlink($fileLocation);
372
                    if (!$removePackedFileResult) {
373
                        $success = false;
374
                        $this->messages->enqueue(new FlashMessage(
375
                            '',
376
                            'Removing packed TYPO3 CMS core not successful',
377
                            FlashMessage::ERROR
378
                        ));
379
                    } else {
380
                        $this->messages->enqueue(new FlashMessage(
381
                            '',
382
                            'Unpacking TYPO3 CMS core successful'
383
                        ));
384
                    }
385
                }
386
            }
387
        }
388
        return $success;
389
    }
390
391
    /**
392
     * Move an unpacked core to its final destination
393
     *
394
     * @param string $version A version to move
395
     * @return bool TRUE on success
396
     */
397
    public function moveVersion($version)
398
    {
399
        $success = true;
400
        if ($this->checkCoreFilesAvailable($version)) {
401
            $this->messages->enqueue(new FlashMessage(
402
                '',
403
                'Moving TYPO3 CMS core files skipped',
404
                FlashMessage::NOTICE
405
            ));
406
        } else {
407
            $downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
408
            $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
0 ignored issues
show
Bug introduced by
Are you sure @realpath($this->symlinkToCoreFiles . '/../') of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

408
            $newCoreLocation = /** @scrutinizer ignore-type */ @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
Loading history...
409
410
            if (!@is_dir($downloadedCoreLocation)) {
411
                $success = false;
412
                $this->messages->enqueue(new FlashMessage(
413
                    '',
414
                    'Unpacked TYPO3 CMS core not found',
415
                    FlashMessage::ERROR
416
                ));
417
            } else {
418
                $moveResult = rename($downloadedCoreLocation, $newCoreLocation);
419
                if (!$moveResult) {
420
                    $success = false;
421
                    $this->messages->enqueue(new FlashMessage(
422
                        '',
423
                        'Moving TYPO3 CMS core to ' . $newCoreLocation . ' failed',
424
                        FlashMessage::ERROR
425
                    ));
426
                } else {
427
                    $this->messages->enqueue(new FlashMessage(
428
                        '',
429
                        'Moved TYPO3 CMS core to final location'
430
                    ));
431
                }
432
            }
433
        }
434
        return $success;
435
    }
436
437
    /**
438
     * Activate a core version
439
     *
440
     * @param string $version A version to activate
441
     * @return bool TRUE on success
442
     */
443
    public function activateVersion($version)
444
    {
445
        $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
0 ignored issues
show
Bug introduced by
Are you sure @realpath($this->symlinkToCoreFiles . '/../') of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

445
        $newCoreLocation = /** @scrutinizer ignore-type */ @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
Loading history...
446
        $success = true;
447
        if (!is_dir($newCoreLocation)) {
448
            $success = false;
449
            $this->messages->enqueue(new FlashMessage(
450
                '',
451
                'New TYPO3 CMS core not found',
452
                FlashMessage::ERROR
453
            ));
454
        } elseif (!is_link($this->symlinkToCoreFiles)) {
455
            $success = false;
456
            $this->messages->enqueue(new FlashMessage(
457
                '',
458
                'TYPO3 CMS core source directory (typo3_src) is not a link',
459
                FlashMessage::ERROR
460
            ));
461
        } else {
462
            $isCurrentCoreSymlinkAbsolute = PathUtility::isAbsolutePath((string)readlink($this->symlinkToCoreFiles));
463
            $unlinkResult = unlink($this->symlinkToCoreFiles);
464
            if (!$unlinkResult) {
465
                $success = false;
466
                $this->messages->enqueue(new FlashMessage(
467
                    '',
468
                    'Removing old symlink failed',
469
                    FlashMessage::ERROR
470
                ));
471
            } else {
472
                if (!$isCurrentCoreSymlinkAbsolute) {
473
                    $newCoreLocation = $this->getRelativePath($newCoreLocation);
474
                }
475
                $symlinkResult = symlink($newCoreLocation, $this->symlinkToCoreFiles);
476
                if ($symlinkResult) {
477
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
478
                } else {
479
                    $success = false;
480
                    $this->messages->enqueue(new FlashMessage(
481
                        '',
482
                        'Linking new TYPO3 CMS core failed',
483
                        FlashMessage::ERROR
484
                    ));
485
                }
486
            }
487
        }
488
        return $success;
489
    }
490
491
    /**
492
     * Absolute path of downloaded .tar.gz
493
     *
494
     * @param string $version A version number
495
     * @return string
496
     */
497
    protected function getDownloadTarGzTargetPath($version)
498
    {
499
        return $this->downloadTargetPath . $version . '.tar.gz';
500
    }
501
502
    /**
503
     * Get relative path to TYPO3 source directory from webroot
504
     *
505
     * @param string $absolutePath to TYPO3 source directory
506
     * @return string relative path to TYPO3 source directory
507
     */
508
    protected function getRelativePath($absolutePath)
509
    {
510
        $sourcePath = explode(DIRECTORY_SEPARATOR, Environment::getPublicPath());
511
        $targetPath = explode(DIRECTORY_SEPARATOR, rtrim($absolutePath, DIRECTORY_SEPARATOR));
512
        while (count($sourcePath) && count($targetPath) && $sourcePath[0] === $targetPath[0]) {
513
            array_shift($sourcePath);
514
            array_shift($targetPath);
515
        }
516
        return str_pad('', count($sourcePath) * 3, '..' . DIRECTORY_SEPARATOR) . implode(DIRECTORY_SEPARATOR, $targetPath);
517
    }
518
519
    /**
520
     * Check if there is are already core files available
521
     * at the download destination.
522
     *
523
     * @param string $version A version number
524
     * @return bool true when core files are available
525
     */
526
    protected function checkCoreFilesAvailable($version)
527
    {
528
        $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
0 ignored issues
show
Bug introduced by
Are you sure @realpath($this->symlinkToCoreFiles . '/../') of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

528
        $newCoreLocation = /** @scrutinizer ignore-type */ @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
Loading history...
529
        return @is_dir($newCoreLocation);
530
    }
531
}
532