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