Completed
Push — master ( e28c49...c020e9 )
by
unknown
19:48
created

CoreUpdateService::activateVersion()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 46
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 36
nc 7
nop 1
dl 0
loc 46
rs 8.7217
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'
145
                    . ' results and may be hazardous to your system',
146
                'Automatic TYPO3 CMS core update not possible: Folder structure has errors or warnings',
147
                FlashMessage::ERROR
148
            ));
149
        }
150
151
        // No core update on windows
152
        if (Environment::isWindows()) {
153
            $success = false;
154
            $this->messages->enqueue(new FlashMessage(
155
                '',
156
                'Automatic TYPO3 CMS core update not possible: Update not supported on Windows OS',
157
                FlashMessage::ERROR
158
            ));
159
        }
160
161
        if ($success) {
162
            // Explicit write check to document root
163
            $file = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
164
            $result = @touch($file);
165
            if (!$result) {
166
                $success = false;
167
                $this->messages->enqueue(new FlashMessage(
168
                    'Could not write a file in path "' . Environment::getPublicPath() . '/"!',
169
                    'Automatic TYPO3 CMS core update not possible: No write access to document root',
170
                    FlashMessage::ERROR
171
                ));
172
            } else {
173
                // Check symlink creation
174
                $link = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
175
                @symlink($file, $link);
176
                if (!is_link($link)) {
177
                    $success = false;
178
                    $this->messages->enqueue(new FlashMessage(
179
                        'Could not create a symbolic link in path "' . Environment::getPublicPath() . '/"!',
180
                        'Automatic TYPO3 CMS core update not possible: No symlink creation possible',
181
                        FlashMessage::ERROR
182
                    ));
183
                } else {
184
                    unlink($link);
185
                }
186
                unlink($file);
187
            }
188
189
            if (!$this->checkCoreFilesAvailable($version)) {
190
                // Explicit write check to upper directory of current core location
191
                $coreLocation = @realpath($this->symlinkToCoreFiles . '/../');
192
                $file = $coreLocation . '/' . StringUtility::getUniqueId('install-core-update-test-');
193
                $result = @touch($file);
194
                if (!$result) {
195
                    $success = false;
196
                    $this->messages->enqueue(new FlashMessage(
197
                        'New TYPO3 CMS core should be installed in "' . $coreLocation . '", but this directory is not writable!',
198
                        'Automatic TYPO3 CMS core update not possible: No write access to TYPO3 CMS core location',
199
                        FlashMessage::ERROR
200
                    ));
201
                } else {
202
                    unlink($file);
203
                }
204
            }
205
        }
206
207
        if ($success && !$this->coreVersionService->isInstalledVersionAReleasedVersion()) {
208
            $success = false;
209
            $this->messages->enqueue(new FlashMessage(
210
                'Your current version is specified as ' . $this->coreVersionService->getInstalledVersion() . '.'
211
                    . ' This is a development version and can not be updated automatically. If this is a "git"'
212
                    . ' checkout, please update using git directly.',
213
                'Automatic TYPO3 CMS core update not possible: You are running a development version of TYPO3',
214
                FlashMessage::ERROR
215
            ));
216
        }
217
218
        return $success;
219
    }
220
221
    /**
222
     * Download the specified version
223
     *
224
     * @param string $version A version to download
225
     * @return bool TRUE on success
226
     */
227
    public function downloadVersion($version)
228
    {
229
        $success = true;
230
        if ($this->checkCoreFilesAvailable($version)) {
231
            $this->messages->enqueue(new FlashMessage(
232
                '',
233
                'Skipped download of TYPO3 CMS core. A core source directory already exists in destination path. Using this instead.',
234
                FlashMessage::NOTICE
235
            ));
236
        } else {
237
            $downloadUri = $this->downloadBaseUri . '/' . $version;
238
            $fileLocation = $this->getDownloadTarGzTargetPath($version);
239
240
            if (@file_exists($fileLocation)) {
241
                $success = false;
242
                $this->messages->enqueue(new FlashMessage(
243
                    '',
244
                    'TYPO3 CMS core download exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
245
                    FlashMessage::ERROR
246
                ));
247
            } else {
248
                $fileContent = GeneralUtility::getUrl($downloadUri);
249
                if (!$fileContent) {
250
                    $success = false;
251
                    $this->messages->enqueue(new FlashMessage(
252
                        'Failed to download ' . $downloadUri,
253
                        'Download not successful',
254
                        FlashMessage::ERROR
255
                    ));
256
                } else {
257
                    $fileStoreResult = file_put_contents($fileLocation, $fileContent);
258
                    if (!$fileStoreResult) {
259
                        $success = false;
260
                        $this->messages->enqueue(new FlashMessage(
261
                            '',
262
                            'Unable to store download content',
263
                            FlashMessage::ERROR
264
                        ));
265
                    } else {
266
                        $this->messages->enqueue(new FlashMessage(
267
                            '',
268
                            'TYPO3 CMS core download finished'
269
                        ));
270
                    }
271
                }
272
            }
273
        }
274
        return $success;
275
    }
276
277
    /**
278
     * Verify checksum of downloaded version
279
     *
280
     * @param string $version A downloaded version to check
281
     * @return bool TRUE on success
282
     */
283
    public function verifyFileChecksum($version)
284
    {
285
        $success = true;
286
        if ($this->checkCoreFilesAvailable($version)) {
287
            $this->messages->enqueue(new FlashMessage(
288
                '',
289
                'Verifying existing TYPO3 CMS core checksum is not possible',
290
                FlashMessage::WARNING
291
            ));
292
        } else {
293
            $fileLocation = $this->getDownloadTarGzTargetPath($version);
294
            $expectedChecksum = $this->coreVersionService->getTarGzSha1OfVersion($version);
295
            if (!file_exists($fileLocation)) {
296
                $success = false;
297
                $this->messages->enqueue(new FlashMessage(
298
                    '',
299
                    'Downloaded TYPO3 CMS core not found',
300
                    FlashMessage::ERROR
301
                ));
302
            } else {
303
                $actualChecksum = sha1_file($fileLocation);
304
                if ($actualChecksum !== $expectedChecksum) {
305
                    $success = false;
306
                    $this->messages->enqueue(new FlashMessage(
307
                        'The official TYPO3 CMS version system on https://get.typo3.org expects a sha1 checksum of '
308
                            . $expectedChecksum . ' from the content of the downloaded new TYPO3 CMS core version ' . $version . '.'
309
                            . ' The actual checksum is ' . $actualChecksum . '. The update is stopped. This may be a'
310
                            . ' failed download, an attack, or an issue with the typo3.org infrastructure.',
311
                        'New TYPO3 CMS core checksum mismatch',
312
                        FlashMessage::ERROR
313
                    ));
314
                } else {
315
                    $this->messages->enqueue(new FlashMessage(
316
                        '',
317
                        'Checksum verified'
318
                    ));
319
                }
320
            }
321
        }
322
        return $success;
323
    }
324
325
    /**
326
     * Unpack a downloaded core
327
     *
328
     * @param string $version A version to unpack
329
     * @return bool TRUE on success
330
     */
331
    public function unpackVersion($version)
332
    {
333
        $success = true;
334
        if ($this->checkCoreFilesAvailable($version)) {
335
            $this->messages->enqueue(new FlashMessage(
336
                '',
337
                'Unpacking TYPO3 CMS core files skipped',
338
                FlashMessage::NOTICE
339
            ));
340
        } else {
341
            $fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
342
            if (!@is_file($fileLocation)) {
343
                $success = false;
344
                $this->messages->enqueue(new FlashMessage(
345
                    '',
346
                    'Downloaded TYPO3 CMS core not found',
347
                    FlashMessage::ERROR
348
                ));
349
            } elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
350
                $success = false;
351
                $this->messages->enqueue(new FlashMessage(
352
                    '',
353
                    'Unpacked TYPO3 CMS core exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
354
                    FlashMessage::ERROR
355
                ));
356
            } else {
357
                $unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
358
                exec($unpackCommand, $output, $errorCode);
359
                if ($errorCode) {
360
                    $success = false;
361
                    $this->messages->enqueue(new FlashMessage(
362
                        '',
363
                        'Unpacking TYPO3 CMS core not successful',
364
                        FlashMessage::ERROR
365
                    ));
366
                } else {
367
                    $removePackedFileResult = unlink($fileLocation);
368
                    if (!$removePackedFileResult) {
369
                        $success = false;
370
                        $this->messages->enqueue(new FlashMessage(
371
                            '',
372
                            'Removing packed TYPO3 CMS core not successful',
373
                            FlashMessage::ERROR
374
                        ));
375
                    } else {
376
                        $this->messages->enqueue(new FlashMessage(
377
                            '',
378
                            'Unpacking TYPO3 CMS core successful'
379
                        ));
380
                    }
381
                }
382
            }
383
        }
384
        return $success;
385
    }
386
387
    /**
388
     * Move an unpacked core to its final destination
389
     *
390
     * @param string $version A version to move
391
     * @return bool TRUE on success
392
     */
393
    public function moveVersion($version)
394
    {
395
        $success = true;
396
        if ($this->checkCoreFilesAvailable($version)) {
397
            $this->messages->enqueue(new FlashMessage(
398
                '',
399
                'Moving TYPO3 CMS core files skipped',
400
                FlashMessage::NOTICE
401
            ));
402
        } else {
403
            $downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
404
            $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
405
406
            if (!@is_dir($downloadedCoreLocation)) {
407
                $success = false;
408
                $this->messages->enqueue(new FlashMessage(
409
                    '',
410
                    'Unpacked TYPO3 CMS core not found',
411
                    FlashMessage::ERROR
412
                ));
413
            } else {
414
                $moveResult = rename($downloadedCoreLocation, $newCoreLocation);
415
                if (!$moveResult) {
416
                    $success = false;
417
                    $this->messages->enqueue(new FlashMessage(
418
                        '',
419
                        'Moving TYPO3 CMS core to ' . $newCoreLocation . ' failed',
420
                        FlashMessage::ERROR
421
                    ));
422
                } else {
423
                    $this->messages->enqueue(new FlashMessage(
424
                        '',
425
                        'Moved TYPO3 CMS core to final location'
426
                    ));
427
                }
428
            }
429
        }
430
        return $success;
431
    }
432
433
    /**
434
     * Activate a core version
435
     *
436
     * @param string $version A version to activate
437
     * @return bool TRUE on success
438
     */
439
    public function activateVersion($version)
440
    {
441
        $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
442
        $success = true;
443
        if (!is_dir($newCoreLocation)) {
444
            $success = false;
445
            $this->messages->enqueue(new FlashMessage(
446
                '',
447
                'New TYPO3 CMS core not found',
448
                FlashMessage::ERROR
449
            ));
450
        } elseif (!is_link($this->symlinkToCoreFiles)) {
451
            $success = false;
452
            $this->messages->enqueue(new FlashMessage(
453
                '',
454
                'TYPO3 CMS core source directory (typo3_src) is not a link',
455
                FlashMessage::ERROR
456
            ));
457
        } else {
458
            $isCurrentCoreSymlinkAbsolute = PathUtility::isAbsolutePath(readlink($this->symlinkToCoreFiles));
459
            $unlinkResult = unlink($this->symlinkToCoreFiles);
460
            if (!$unlinkResult) {
461
                $success = false;
462
                $this->messages->enqueue(new FlashMessage(
463
                    '',
464
                    'Removing old symlink failed',
465
                    FlashMessage::ERROR
466
                ));
467
            } else {
468
                if (!$isCurrentCoreSymlinkAbsolute) {
469
                    $newCoreLocation = $this->getRelativePath($newCoreLocation);
470
                }
471
                $symlinkResult = symlink($newCoreLocation, $this->symlinkToCoreFiles);
472
                if ($symlinkResult) {
473
                    GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
474
                } else {
475
                    $success = false;
476
                    $this->messages->enqueue(new FlashMessage(
477
                        '',
478
                        'Linking new TYPO3 CMS core failed',
479
                        FlashMessage::ERROR
480
                    ));
481
                }
482
            }
483
        }
484
        return $success;
485
    }
486
487
    /**
488
     * Absolute path of downloaded .tar.gz
489
     *
490
     * @param string $version A version number
491
     * @return string
492
     */
493
    protected function getDownloadTarGzTargetPath($version)
494
    {
495
        return $this->downloadTargetPath . $version . '.tar.gz';
496
    }
497
498
    /**
499
     * Get relative path to TYPO3 source directory from webroot
500
     *
501
     * @param string $absolutePath to TYPO3 source directory
502
     * @return string relative path to TYPO3 source directory
503
     */
504
    protected function getRelativePath($absolutePath)
505
    {
506
        $sourcePath = explode(DIRECTORY_SEPARATOR, Environment::getPublicPath());
507
        $targetPath = explode(DIRECTORY_SEPARATOR, rtrim($absolutePath, DIRECTORY_SEPARATOR));
508
        while (count($sourcePath) && count($targetPath) && $sourcePath[0] === $targetPath[0]) {
509
            array_shift($sourcePath);
510
            array_shift($targetPath);
511
        }
512
        return str_pad('', count($sourcePath) * 3, '..' . DIRECTORY_SEPARATOR) . implode(DIRECTORY_SEPARATOR, $targetPath);
513
    }
514
515
    /**
516
     * Check if there is are already core files available
517
     * at the download destination.
518
     *
519
     * @param string $version A version number
520
     * @return bool true when core files are available
521
     */
522
    protected function checkCoreFilesAvailable($version)
523
    {
524
        $newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
525
        return @is_dir($newCoreLocation);
526
    }
527
}
528