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