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