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