Completed
Push — master ( 1ee726...01048e )
by
unknown
26:37 queued 10:59
created

CoreUpdateService::checkCoreFilesAvailable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Install\Service;
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
17
use TYPO3\CMS\Core\Core\Environment;
18
use TYPO3\CMS\Core\Messaging\FlashMessage;
19
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
20
use TYPO3\CMS\Core\Service\OpcodeCacheService;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Core\Utility\PathUtility;
23
use TYPO3\CMS\Core\Utility\StringUtility;
24
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
25
26
/**
27
 * Core update service.
28
 * This service handles core updates, all the nasty details are encapsulated
29
 * here. The single public methods 'depend' on each other, for example a new
30
 * core has to be downloaded before it can be unpacked.
31
 *
32
 * Each method returns only TRUE of FALSE indicating if it was successful or
33
 * not. Detailed information can be fetched with getMessages() and will return
34
 * a list of status messages of the previous operation.
35
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
36
 */
37
class CoreUpdateService
38
{
39
    /**
40
     * @var \TYPO3\CMS\Install\Service\CoreVersionService
41
     */
42
    protected $coreVersionService;
43
44
    /**
45
     * @var FlashMessageQueue
46
     */
47
    protected $messages;
48
49
    /**
50
     * Absolute path to download location
51
     *
52
     * @var string
53
     */
54
    protected $downloadTargetPath;
55
56
    /**
57
     * Absolute path to the symlink pointing to the currently used TYPO3 core files
58
     *
59
     * @var string
60
     */
61
    protected $symlinkToCoreFiles;
62
63
    /**
64
     * Base URI for TYPO3 downloads
65
     *
66
     * @var string
67
     */
68
    protected $downloadBaseUri;
69
70
    /**
71
     * @param CoreVersionService $coreVersionService
72
     */
73
    public function __construct(CoreVersionService $coreVersionService = null)
74
    {
75
        $this->coreVersionService = $coreVersionService ?: GeneralUtility::makeInstance(CoreVersionService::class);
76
        $this->setDownloadTargetPath(Environment::getVarPath() . '/transient/');
77
        $this->symlinkToCoreFiles = $this->discoverCurrentCoreSymlink();
78
        $this->downloadBaseUri = 'https://get.typo3.org';
79
        $this->messages = new FlashMessageQueue('install');
80
    }
81
82
    /**
83
     * Check if this installation wants to enable the core updater
84
     *
85
     * @return bool
86
     */
87
    public function isCoreUpdateEnabled()
88
    {
89
        $coreUpdateDisabled = getenv('TYPO3_DISABLE_CORE_UPDATER') ?: (getenv('REDIRECT_TYPO3_DISABLE_CORE_UPDATER') ?: false);
90
        return !Environment::isComposerMode() && !$coreUpdateDisabled;
91
    }
92
93
    /**
94
     * In future implementations we might implement some smarter logic here
95
     *
96
     * @return string
97
     */
98
    protected function discoverCurrentCoreSymlink()
99
    {
100
        return Environment::getPublicPath() . '/typo3_src';
101
    }
102
103
    /**
104
     * Create download location in case the folder does not exist
105
     * @todo move this to folder structure
106
     *
107
     * @param string $downloadTargetPath
108
     */
109
    protected function setDownloadTargetPath($downloadTargetPath)
110
    {
111
        if (!is_dir($downloadTargetPath)) {
112
            GeneralUtility::mkdir_deep($downloadTargetPath);
113
        }
114
        $this->downloadTargetPath = $downloadTargetPath;
115
    }
116
117
    /**
118
     * Get messages of previous method call
119
     *
120
     * @return FlashMessageQueue
121
     */
122
    public function getMessages(): FlashMessageQueue
123
    {
124
        return $this->messages;
125
    }
126
127
    /**
128
     * Check if an update is possible at all
129
     *
130
     * @param string $version The target version number
131
     * @return bool TRUE on success
132
     */
133
    public function checkPreConditions($version)
134
    {
135
        $success = true;
136
137
        // Folder structure test: Update can be done only if folder structure returns no errors
138
        $folderStructureFacade = GeneralUtility::makeInstance(DefaultFactory::class)->getStructure();
139
        $folderStructureMessageQueue = $folderStructureFacade->getStatus();
140
        $folderStructureErrors = $folderStructureMessageQueue->getAllMessages(FlashMessage::ERROR);
141
        $folderStructureWarnings = $folderStructureMessageQueue->getAllMessages(FlashMessage::WARNING);
142
        if (!empty($folderStructureErrors) || !empty($folderStructureWarnings) || !is_link(Environment::getPublicPath() . '/typo3_src')) {
143
            $success = false;
144
            $this->messages->enqueue(new FlashMessage(
145
                'To perform an update, the folder structure of this TYPO3 CMS instance must'
146
                    . ' stick to the conventions, or the update process could lead to unexpected'
147
                    . ' results and may be hazardous to your system',
148
                'Automatic TYPO3 CMS core update not possible: Folder structure has errors or warnings',
149
                FlashMessage::ERROR
150
            ));
151
        }
152
153
        // No core update on windows
154
        if (Environment::isWindows()) {
155
            $success = false;
156
            $this->messages->enqueue(new FlashMessage(
157
                '',
158
                'Automatic TYPO3 CMS core update not possible: Update not supported on Windows OS',
159
                FlashMessage::ERROR
160
            ));
161
        }
162
163
        if ($success) {
164
            // Explicit write check to document root
165
            $file = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
166
            $result = @touch($file);
167
            if (!$result) {
168
                $success = false;
169
                $this->messages->enqueue(new FlashMessage(
170
                    'Could not write a file in path "' . Environment::getPublicPath() . '/"!',
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
                        'Automatic TYPO3 CMS core update not possible: No symlink creation possible',
183
                        FlashMessage::ERROR
184
                    ));
185
                } else {
186
                    unlink($link);
187
                }
188
                unlink($file);
189
            }
190
191
            if (!$this->checkCoreFilesAvailable($version)) {
192
                // Explicit write check to upper directory of current core location
193
                $coreLocation = @realpath($this->symlinkToCoreFiles . '/../');
194
                $file = $coreLocation . '/' . StringUtility::getUniqueId('install-core-update-test-');
195
                $result = @touch($file);
196
                if (!$result) {
197
                    $success = false;
198
                    $this->messages->enqueue(new FlashMessage(
199
                        'New TYPO3 CMS core should be installed in "' . $coreLocation . '", but this directory is not writable!',
200
                        'Automatic TYPO3 CMS core update not possible: No write access to TYPO3 CMS core location',
201
                        FlashMessage::ERROR
202
                    ));
203
                } else {
204
                    unlink($file);
205
                }
206
            }
207
        }
208
209
        if ($success && !$this->coreVersionService->isInstalledVersionAReleasedVersion()) {
210
            $success = false;
211
            $this->messages->enqueue(new FlashMessage(
212
                'Your current version is specified as ' . $this->coreVersionService->getInstalledVersion() . '.'
213
                    . ' This is a development version and can not be updated automatically. If this is a "git"'
214
                    . ' checkout, please update using git directly.',
215
                'Automatic TYPO3 CMS core update not possible: You are running a development version of TYPO3',
216
                FlashMessage::ERROR
217
            ));
218
        }
219
220
        return $success;
221
    }
222
223
    /**
224
     * Download the specified version
225
     *
226
     * @param string $version A version to download
227
     * @return bool TRUE on success
228
     */
229
    public function downloadVersion($version)
230
    {
231
        $success = true;
232
        if ($this->checkCoreFilesAvailable($version)) {
233
            $this->messages->enqueue(new FlashMessage(
234
                '',
235
                'Skipped download of TYPO3 CMS core. A core source directory already exists in destination path. Using this instead.',
236
                FlashMessage::NOTICE
237
            ));
238
        } else {
239
            $downloadUri = $this->downloadBaseUri . '/' . $version;
240
            $fileLocation = $this->getDownloadTarGzTargetPath($version);
241
242
            if (@file_exists($fileLocation)) {
243
                $success = false;
244
                $this->messages->enqueue(new FlashMessage(
245
                    '',
246
                    'TYPO3 CMS core download exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
247
                    FlashMessage::ERROR
248
                ));
249
            } else {
250
                $fileContent = GeneralUtility::getUrl($downloadUri);
251
                if (!$fileContent) {
252
                    $success = false;
253
                    $this->messages->enqueue(new FlashMessage(
254
                        'Failed to download ' . $downloadUri,
255
                        'Download not successful',
256
                        FlashMessage::ERROR
257
                    ));
258
                } else {
259
                    $fileStoreResult = file_put_contents($fileLocation, $fileContent);
260
                    if (!$fileStoreResult) {
261
                        $success = false;
262
                        $this->messages->enqueue(new FlashMessage(
263
                            '',
264
                            'Unable to store download content',
265
                            FlashMessage::ERROR
266
                        ));
267
                    } else {
268
                        $this->messages->enqueue(new FlashMessage(
269
                            '',
270
                            'TYPO3 CMS core download finished'
271
                        ));
272
                    }
273
                }
274
            }
275
        }
276
        return $success;
277
    }
278
279
    /**
280
     * Verify checksum of downloaded version
281
     *
282
     * @param string $version A downloaded version to check
283
     * @return bool TRUE on success
284
     */
285
    public function verifyFileChecksum($version)
286
    {
287
        $success = true;
288
        if ($this->checkCoreFilesAvailable($version)) {
289
            $this->messages->enqueue(new FlashMessage(
290
                '',
291
                'Verifying existing TYPO3 CMS core checksum is not possible',
292
                FlashMessage::WARNING
293
            ));
294
        } else {
295
            $fileLocation = $this->getDownloadTarGzTargetPath($version);
296
            $expectedChecksum = $this->coreVersionService->getTarGzSha1OfVersion($version);
297
            if (!file_exists($fileLocation)) {
298
                $success = false;
299
                $this->messages->enqueue(new FlashMessage(
300
                    '',
301
                    'Downloaded TYPO3 CMS core not found',
302
                    FlashMessage::ERROR
303
                ));
304
            } else {
305
                $actualChecksum = sha1_file($fileLocation);
306
                if ($actualChecksum !== $expectedChecksum) {
307
                    $success = false;
308
                    $this->messages->enqueue(new FlashMessage(
309
                        'The official TYPO3 CMS version system on https://get.typo3.org expects a sha1 checksum of '
310
                            . $expectedChecksum . ' from the content of the downloaded new TYPO3 CMS core version ' . $version . '.'
311
                            . ' The actual checksum is ' . $actualChecksum . '. The update is stopped. This may be a'
312
                            . ' failed download, an attack, or an issue with the typo3.org infrastructure.',
313
                        'New TYPO3 CMS core checksum mismatch',
314
                        FlashMessage::ERROR
315
                    ));
316
                } else {
317
                    $this->messages->enqueue(new FlashMessage(
318
                        '',
319
                        'Checksum verified'
320
                    ));
321
                }
322
            }
323
        }
324
        return $success;
325
    }
326
327
    /**
328
     * Unpack a downloaded core
329
     *
330
     * @param string $version A version to unpack
331
     * @return bool TRUE on success
332
     */
333
    public function unpackVersion($version)
334
    {
335
        $success = true;
336
        if ($this->checkCoreFilesAvailable($version)) {
337
            $this->messages->enqueue(new FlashMessage(
338
                '',
339
                'Unpacking TYPO3 CMS core files skipped',
340
                FlashMessage::NOTICE
341
            ));
342
        } else {
343
            $fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
344
            if (!@is_file($fileLocation)) {
345
                $success = false;
346
                $this->messages->enqueue(new FlashMessage(
347
                    '',
348
                    'Downloaded TYPO3 CMS core not found',
349
                    FlashMessage::ERROR
350
                ));
351
            } elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
352
                $success = false;
353
                $this->messages->enqueue(new FlashMessage(
354
                    '',
355
                    'Unpacked TYPO3 CMS core exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
356
                    FlashMessage::ERROR
357
                ));
358
            } else {
359
                $unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
360
                exec($unpackCommand, $output, $errorCode);
361
                if ($errorCode) {
362
                    $success = false;
363
                    $this->messages->enqueue(new FlashMessage(
364
                        '',
365
                        'Unpacking TYPO3 CMS core not successful',
366
                        FlashMessage::ERROR
367
                    ));
368
                } else {
369
                    $removePackedFileResult = unlink($fileLocation);
370
                    if (!$removePackedFileResult) {
371
                        $success = false;
372
                        $this->messages->enqueue(new FlashMessage(
373
                            '',
374
                            'Removing packed TYPO3 CMS core not successful',
375
                            FlashMessage::ERROR
376
                        ));
377
                    } else {
378
                        $this->messages->enqueue(new FlashMessage(
379
                            '',
380
                            'Unpacking TYPO3 CMS core successful'
381
                        ));
382
                    }
383
                }
384
            }
385
        }
386
        return $success;
387
    }
388
389
    /**
390
     * Move an unpacked core to its final destination
391
     *
392
     * @param string $version A version to move
393
     * @return bool TRUE on success
394
     */
395
    public function moveVersion($version)
396
    {
397
        $success = true;
398
        if ($this->checkCoreFilesAvailable($version)) {
399
            $this->messages->enqueue(new FlashMessage(
400
                '',
401
                'Moving TYPO3 CMS core files skipped',
402
                FlashMessage::NOTICE
403
            ));
404
        } else {
405
            $downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
406
            $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
407
408
            if (!@is_dir($downloadedCoreLocation)) {
409
                $success = false;
410
                $this->messages->enqueue(new FlashMessage(
411
                    '',
412
                    'Unpacked TYPO3 CMS core not found',
413
                    FlashMessage::ERROR
414
                ));
415
            } else {
416
                $moveResult = rename($downloadedCoreLocation, $newCoreLocation);
417
                if (!$moveResult) {
418
                    $success = false;
419
                    $this->messages->enqueue(new FlashMessage(
420
                        '',
421
                        'Moving TYPO3 CMS core to ' . $newCoreLocation . ' failed',
422
                        FlashMessage::ERROR
423
                    ));
424
                } else {
425
                    $this->messages->enqueue(new FlashMessage(
426
                        '',
427
                        'Moved TYPO3 CMS core to final location'
428
                    ));
429
                }
430
            }
431
        }
432
        return $success;
433
    }
434
435
    /**
436
     * Activate a core version
437
     *
438
     * @param string $version A version to activate
439
     * @return bool TRUE on success
440
     */
441
    public function activateVersion($version)
442
    {
443
        $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
444
        $success = true;
445
        if (!is_dir($newCoreLocation)) {
446
            $success = false;
447
            $this->messages->enqueue(new FlashMessage(
448
                '',
449
                'New TYPO3 CMS core not found',
450
                FlashMessage::ERROR
451
            ));
452
        } elseif (!is_link($this->symlinkToCoreFiles)) {
453
            $success = false;
454
            $this->messages->enqueue(new FlashMessage(
455
                '',
456
                'TYPO3 CMS core source directory (typo3_src) is not a link',
457
                FlashMessage::ERROR
458
            ));
459
        } else {
460
            $isCurrentCoreSymlinkAbsolute = PathUtility::isAbsolutePath(readlink($this->symlinkToCoreFiles));
461
            $unlinkResult = unlink($this->symlinkToCoreFiles);
462
            if (!$unlinkResult) {
463
                $success = false;
464
                $this->messages->enqueue(new FlashMessage(
465
                    '',
466
                    'Removing old symlink failed',
467
                    FlashMessage::ERROR
468
                ));
469
            } else {
470
                if (!$isCurrentCoreSymlinkAbsolute) {
471
                    $newCoreLocation = $this->getRelativePath($newCoreLocation);
472
                }
473
                $symlinkResult = symlink($newCoreLocation, $this->symlinkToCoreFiles);
474
                if ($symlinkResult) {
475
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
476
                } else {
477
                    $success = false;
478
                    $this->messages->enqueue(new FlashMessage(
479
                        '',
480
                        'Linking new TYPO3 CMS core failed',
481
                        FlashMessage::ERROR
482
                    ));
483
                }
484
            }
485
        }
486
        return $success;
487
    }
488
489
    /**
490
     * Absolute path of downloaded .tar.gz
491
     *
492
     * @param string $version A version number
493
     * @return string
494
     */
495
    protected function getDownloadTarGzTargetPath($version)
496
    {
497
        return $this->downloadTargetPath . $version . '.tar.gz';
498
    }
499
500
    /**
501
     * Get relative path to TYPO3 source directory from webroot
502
     *
503
     * @param string $absolutePath to TYPO3 source directory
504
     * @return string relative path to TYPO3 source directory
505
     */
506
    protected function getRelativePath($absolutePath)
507
    {
508
        $sourcePath = explode(DIRECTORY_SEPARATOR, Environment::getPublicPath());
509
        $targetPath = explode(DIRECTORY_SEPARATOR, rtrim($absolutePath, DIRECTORY_SEPARATOR));
510
        while (count($sourcePath) && count($targetPath) && $sourcePath[0] === $targetPath[0]) {
511
            array_shift($sourcePath);
512
            array_shift($targetPath);
513
        }
514
        return str_pad('', count($sourcePath) * 3, '..' . DIRECTORY_SEPARATOR) . implode(DIRECTORY_SEPARATOR, $targetPath);
515
    }
516
517
    /**
518
     * Check if there is are already core files available
519
     * at the download destination.
520
     *
521
     * @param string $version A version number
522
     * @return bool true when core files are available
523
     */
524
    protected function checkCoreFilesAvailable($version)
525
    {
526
        $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
527
        return @is_dir($newCoreLocation);
528
    }
529
}
530