[BUGFIX] Fix core update if core location exists
[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\Utility\GeneralUtility;
18 use TYPO3\CMS\Core\Utility\OpcodeCacheUtility;
19 use TYPO3\CMS\Core\Utility\PathUtility;
20 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
21 use TYPO3\CMS\Install\Service\Exception\RemoteFetchException;
22 use TYPO3\CMS\Install\Status\ErrorStatus;
23 use TYPO3\CMS\Install\Status\NoticeStatus;
24 use TYPO3\CMS\Install\Status\OkStatus;
25 use TYPO3\CMS\Install\Status\StatusInterface;
26 use TYPO3\CMS\Install\Status\StatusUtility;
27 use TYPO3\CMS\Install\Status\WarningStatus;
28
29 /**
30 * Core update service.
31 * This service handles core updates, all the nasty details are encapsulated
32 * here. The single public methods 'depend' on each other, for example a new
33 * core has to be downloaded before it can be unpacked.
34 *
35 * Each method returns only TRUE of FALSE indicating if it was successful or
36 * not. Detailed information can be fetched with getMessages() and will return
37 * a list of status messages of the previous operation.
38 */
39 class CoreUpdateService {
40
41 /**
42 * @var \TYPO3\CMS\Extbase\Object\ObjectManager
43 * @inject
44 */
45 protected $objectManager;
46
47 /**
48 * @var \TYPO3\CMS\Install\Service\CoreVersionService
49 * @inject
50 */
51 protected $coreVersionService;
52
53 /**
54 * @var StatusInterface[]
55 */
56 protected $messages = array();
57
58 /**
59 * Absolute path to download location
60 *
61 * @var string
62 */
63 protected $downloadTargetPath;
64
65 /**
66 * Absolute path to the symlink pointing to the currently used TYPO3 core files
67 *
68 * @var string
69 */
70 protected $symlinkToCoreFiles;
71
72 /**
73 * Base URI for TYPO3 downloads
74 *
75 * @var string
76 */
77 protected $downloadBaseUri;
78
79 /**
80 * Initialize update paths
81 */
82 public function initializeObject() {
83 $this->setDownloadTargetPath(PATH_site . 'typo3temp/core-update/');
84 $this->symlinkToCoreFiles = $this->discoverCurrentCoreSymlink();
85 $this->downloadBaseUri = $this->coreVersionService->getDownloadBaseUri();
86 }
87
88 /**
89 * Check if this installation wants to enable the core updater
90 *
91 * @return bool
92 */
93 public function isCoreUpdateEnabled() {
94 $coreUpdateDisabled = getenv('TYPO3_DISABLE_CORE_UPDATER') ?: (getenv('REDIRECT_TYPO3_DISABLE_CORE_UPDATER') ?: FALSE);
95 return !$coreUpdateDisabled;
96 }
97
98 /**
99 * In future implementations we might implement some smarter logic here
100 *
101 * @return string
102 */
103 protected function discoverCurrentCoreSymlink() {
104 return PATH_site . 'typo3_src';
105 }
106
107 /**
108 * Create download location in case the folder does not exist
109 * @todo move this to folder structure
110 *
111 * @param string $downloadTargetPath
112 */
113 protected function setDownloadTargetPath($downloadTargetPath) {
114 if (!is_dir($downloadTargetPath)) {
115 GeneralUtility::mkdir_deep($downloadTargetPath);
116 }
117 $this->downloadTargetPath = $downloadTargetPath;
118 }
119
120 /**
121 * Get messages of previous method call
122 *
123 * @return StatusInterface[]
124 */
125 public function getMessages() {
126 return $this->messages;
127 }
128
129 /**
130 * Wrapper method for CoreVersionService
131 *
132 * @return bool TRUE on success
133 */
134 public function updateVersionMatrix() {
135 $success = TRUE;
136 try {
137 $this->coreVersionService->updateVersionMatrix();
138 } catch (RemoteFetchException $e) {
139 $success = FALSE;
140 /** @var $message StatusInterface */
141 $message = $this->objectManager->get(ErrorStatus::class);
142 $message->setTitle('Version matrix could not be fetched from get.typo3.org');
143 $message->setMessage(
144 'Current version specification could not be fetched from http://get.typo3.org/json.'
145 . ' This is probably a network issue, please fix it.'
146 );
147 $this->messages = array($message);
148 }
149 return $success;
150 }
151
152 /**
153 * Check if an update is possible at all
154 *
155 * @return bool TRUE on success
156 */
157 public function checkPreConditions() {
158 $success = TRUE;
159 $messages = array();
160
161 /** @var StatusUtility $statusUtility */
162 $statusUtility = $this->objectManager->get(StatusUtility::class);
163
164 // Folder structure test: Update can be done only if folder structure returns no errors
165 /** @var $folderStructureFacade \TYPO3\CMS\Install\FolderStructure\StructureFacade */
166 $folderStructureFacade = $this->objectManager->get(DefaultFactory::class)->getStructure();
167 $folderStructureErrors = $statusUtility->filterBySeverity($folderStructureFacade->getStatus(), 'error');
168 $folderStructureWarnings = $statusUtility->filterBySeverity($folderStructureFacade->getStatus(), 'warning');
169 if (count($folderStructureErrors) > 0 || count($folderStructureWarnings) > 0) {
170 $success = FALSE;
171 /** @var $message StatusInterface */
172 $message = $this->objectManager->get(ErrorStatus::class);
173 $message->setTitle('Automatic TYPO3 CMS core update not possible: Folder structure has errors or warnings');
174 $message->setMessage(
175 'To perform an update, the folder structure of this TYPO3 CMS instance must'
176 . ' stick to the conventions, or the update process could lead to unexpected'
177 . ' results and may be hazardous to your system'
178 );
179 $messages[] = $message;
180 }
181
182 // No core update on windows
183 if (TYPO3_OS === 'WIN') {
184 $success = FALSE;
185 /** @var $message StatusInterface */
186 $message = $this->objectManager->get(ErrorStatus::class);
187 $message->setTitle('Automatic TYPO3 CMS core update not possible: Update not supported on Windows OS');
188 $messages[] = $message;
189 }
190
191 if ($success) {
192 // Explicit write check to document root
193 $file = PATH_site . uniqid('install-core-update-test-', TRUE);
194 $result = @touch($file);
195 if (!$result) {
196 $success = FALSE;
197 /** @var $message StatusInterface */
198 $message = $this->objectManager->get(ErrorStatus::class);
199 $message->setTitle('Automatic TYPO3 CMS core update not possible: No write access to document root');
200 $message->setMessage('Could not write a file in path "' . PATH_site . '"!');
201 $messages[] = $message;
202 } else {
203 unlink($file);
204 }
205
206 // Explicit write check to upper directory of current core location
207 $coreLocation = @realPath($this->symlinkToCoreFiles . '/../');
208 $file = $coreLocation . '/' . uniqid('install-core-update-test-', TRUE);
209 $result = @touch($file);
210 if (!$result) {
211 $success = FALSE;
212 /** @var $message StatusInterface */
213 $message = $this->objectManager->get(ErrorStatus::class);
214 $message->setTitle('Automatic TYPO3 CMS core update not possible: No write access to TYPO3 CMS core location');
215 $message->setMessage(
216 'New TYPO3 CMS core should be installed in "' . $coreLocation . '", but this directory is not writable!'
217 );
218 $messages[] = $message;
219 } else {
220 unlink($file);
221 }
222 }
223
224 if ($success && !$this->coreVersionService->isInstalledVersionAReleasedVersion()) {
225 $success = FALSE;
226 /** @var $message StatusInterface */
227 $message = $this->objectManager->get(ErrorStatus::class);
228 $message->setTitle('Automatic TYPO3 CMS core update not possible: You are running a development version of TYPO3');
229 $message->setMessage(
230 'Your current version is specified as ' . $this->coreVersionService->getInstalledVersion() . '.'
231 . ' This is a development version and can not be updated automatically. If this is a "git"'
232 . ' checkout, please update using git directly.'
233 );
234 $messages[] = $message;
235 }
236
237 $this->messages = $messages;
238 return $success;
239 }
240
241 /**
242 * Download the specified version
243 *
244 * @param string $version A version to download
245 * @return bool TRUE on success
246 */
247 public function downloadVersion($version) {
248 $messages = array();
249 $success = TRUE;
250
251 if ($this->checkCoreFilesAvailable($version)) {
252 /** @var $message StatusInterface */
253 $message = $this->objectManager->get(NoticeStatus::class);
254 $message->setTitle('Skipped download of TYPO3 CMS core. A core source directory already exists in destination path. Using this instead.');
255 $messages[] = $message;
256 } else {
257 $downloadUri = $this->downloadBaseUri . $version;
258 $fileLocation = $this->getDownloadTarGzTargetPath($version);
259
260 if (@file_exists($fileLocation)) {
261 $success = FALSE;
262 /** @var $message StatusInterface */
263 $message = $this->objectManager->get(ErrorStatus::class);
264 $message->setTitle('TYPO3 CMS core download exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath));
265 $messages[] = $message;
266 } else {
267 $fileContent = GeneralUtility::getUrl($downloadUri);
268 if (!$fileContent) {
269 $success = FALSE;
270 /** @var $message StatusInterface */
271 $message = $this->objectManager->get(ErrorStatus::class);
272 $message->setTitle('Download not successful');
273 $messages[] = $message;
274 } else {
275 $fileStoreResult = file_put_contents($fileLocation, $fileContent);
276 if (!$fileStoreResult) {
277 $success = FALSE;
278 /** @var $message StatusInterface */
279 $message = $this->objectManager->get(ErrorStatus::class);
280 $message->setTitle('Unable to store download content');
281 $messages[] = $message;
282 } else {
283 $message = $this->objectManager->get(OkStatus::class);
284 $message->setTitle('TYPO3 CMS core download finished');
285 $messages[] = $message;
286 }
287 }
288 }
289 }
290 $this->messages = $messages;
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 $messages = array();
302 $success = TRUE;
303
304 if ($this->checkCoreFilesAvailable($version)) {
305 /** @var $message StatusInterface */
306 $message = $this->objectManager->get(WarningStatus::class);
307 $message->setTitle('Verifying existing TYPO3 CMS core checksum is not possible');
308 $messages[] = $message;
309 } else {
310 $fileLocation = $this->getDownloadTarGzTargetPath($version);
311 $expectedChecksum = $this->coreVersionService->getTarGzSha1OfVersion($version);
312
313 if (!file_exists($fileLocation)) {
314 $success = FALSE;
315 /** @var $message StatusInterface */
316 $message = $this->objectManager->get(ErrorStatus::class);
317 $message->setTitle('Downloaded TYPO3 CMS core not found');
318 $messages[] = $message;
319 } else {
320 $actualChecksum = sha1_file($fileLocation);
321 if ($actualChecksum !== $expectedChecksum) {
322 $success = FALSE;
323 /** @var $message StatusInterface */
324 $message = $this->objectManager->get(ErrorStatus::class);
325 $message->setTitle('New TYPO3 CMS core checksum mismatch');
326 $message->setMessage(
327 'The official TYPO3 CMS version system on https://get.typo3.org expects a sha1 checksum of '
328 . $expectedChecksum . ' from the content of the downloaded new TYPO3 CMS core version ' . $version . '.'
329 . ' The actual checksum is ' . $actualChecksum . '. The update is stopped. This may be a'
330 . ' failed download, an attack, or an issue with the typo3.org infrastructure.'
331 );
332 $messages[] = $message;
333 } else {
334 $message = $this->objectManager->get(OkStatus::class);
335 $message->setTitle('Checksum verified');
336 $messages[] = $message;
337 }
338 }
339 }
340 $this->messages = $messages;
341 return $success;
342 }
343
344 /**
345 * Unpack a downloaded core
346 *
347 * @param string $version A version to unpack
348 * @return bool TRUE on success
349 */
350 public function unpackVersion($version) {
351 $messages = array();
352 $success = TRUE;
353
354 if ($this->checkCoreFilesAvailable($version)) {
355 /** @var $message StatusInterface */
356 $message = $this->objectManager->get(NoticeStatus::class);
357 $message->setTitle('Unpacking TYPO3 CMS core files skipped');
358 $messages[] = $message;
359 } else {
360 $fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
361
362 if (!@is_file($fileLocation)) {
363 $success = FALSE;
364 /** @var $message StatusInterface */
365 $message = $this->objectManager->get(ErrorStatus::class);
366 $message->setTitle('Downloaded TYPO3 CMS core not found');
367 $messages[] = $message;
368 } elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
369 $success = FALSE;
370 /** @var $message StatusInterface */
371 $message = $this->objectManager->get(ErrorStatus::class);
372 $message->setTitle('Unpacked TYPO3 CMS core exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath));
373 $messages[] = $message;
374 } else {
375 $unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
376 exec($unpackCommand, $output, $errorCode);
377 if ($errorCode) {
378 $success = FALSE;
379 /** @var $message StatusInterface */
380 $message = $this->objectManager->get(ErrorStatus::class);
381 $message->setTitle('Unpacking TYPO3 CMS core not successful');
382 $messages[] = $message;
383 } else {
384 $removePackedFileResult = unlink($fileLocation);
385 if (!$removePackedFileResult) {
386 $success = FALSE;
387 /** @var $message StatusInterface */
388 $message = $this->objectManager->get(ErrorStatus::class);
389 $message->setTitle('Removing packed TYPO3 CMS core not successful');
390 $messages[] = $message;
391 } else {
392 $message = $this->objectManager->get(OkStatus::class);
393 $message->setTitle('Unpacking TYPO3 CMS core successful');
394 $messages[] = $message;
395 }
396 }
397 }
398 }
399 $this->messages = $messages;
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 $messages = array();
411 $success = TRUE;
412
413 if ($this->checkCoreFilesAvailable($version)) {
414 /** @var $message StatusInterface */
415 $message = $this->objectManager->get(NoticeStatus::class);
416 $message->setTitle('Moving TYPO3 CMS core files skipped');
417 $messages[] = $message;
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 /** @var $message StatusInterface */
425 $message = $this->objectManager->get(ErrorStatus::class);
426 $message->setTitle('Unpacked TYPO3 CMS core not found');
427 $messages[] = $message;
428 } else {
429 $moveResult = rename($downloadedCoreLocation, $newCoreLocation);
430 if (!$moveResult) {
431 $success = FALSE;
432 /** @var $message StatusInterface */
433 $message = $this->objectManager->get(ErrorStatus::class);
434 $message->setTitle('Moving TYPO3 CMS core to ' . $newCoreLocation . ' failed');
435 $messages[] = $message;
436 } else {
437 $message = $this->objectManager->get(OkStatus::class);
438 $message->setTitle('Moved TYPO3 CMS core to final location');
439 $messages[] = $message;
440 }
441 }
442 }
443
444 $this->messages = $messages;
445 return $success;
446 }
447
448 /**
449 * Activate a core version
450 *
451 * @param string $version A version to activate
452 * @return bool TRUE on success
453 */
454 public function activateVersion($version) {
455 $newCoreLocation = @realPath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
456
457 $messages = array();
458 $success = TRUE;
459
460 if (!is_dir($newCoreLocation)) {
461 $success = FALSE;
462 /** @var $message StatusInterface */
463 $message = $this->objectManager->get(ErrorStatus::class);
464 $message->setTitle('New TYPO3 CMS core not found');
465 $messages[] = $message;
466 } elseif (!is_link($this->symlinkToCoreFiles)) {
467 $success = FALSE;
468 /** @var $message StatusInterface */
469 $message = $this->objectManager->get(ErrorStatus::class);
470 $message->setTitle('TYPO3 CMS core source directory (typo3_src) is not a link');
471 $messages[] = $message;
472 } else {
473 $isCurrentCoreSymlinkAbsolute = PathUtility::isAbsolutePath(readlink($this->symlinkToCoreFiles));
474 $unlinkResult = unlink($this->symlinkToCoreFiles);
475 if (!$unlinkResult) {
476 $success = FALSE;
477 /** @var $message StatusInterface */
478 $message = $this->objectManager->get(ErrorStatus::class);
479 $message->setTitle('Removing old symlink failed');
480 $messages[] = $message;
481 } else {
482 if (!$isCurrentCoreSymlinkAbsolute) {
483 $newCoreLocation = $this->getRelativePath($newCoreLocation);
484 }
485 $symlinkResult = symlink($newCoreLocation, $this->symlinkToCoreFiles);
486 if ($symlinkResult) {
487 OpcodeCacheUtility::clearAllActive();
488 } else {
489 $success = FALSE;
490 /** @var $message StatusInterface */
491 $message = $this->objectManager->get(ErrorStatus::class);
492 $message->setTitle('Linking new TYPO3 CMS core failed');
493 $messages[] = $message;
494 }
495 }
496 }
497
498 $this->messages = $messages;
499 return $success;
500 }
501
502 /**
503 * Absolute path of downloaded .tar.gz
504 *
505 * @param string $version A version number
506 * @return string
507 */
508 protected function getDownloadTarGzTargetPath($version) {
509 return $this->downloadTargetPath . $version . '.tar.gz';
510 }
511
512 /**
513 * Get relative path to TYPO3 source directory from webroot
514 *
515 * @param string $absolutePath to TYPO3 source directory
516 * @return string relative path to TYPO3 source directory
517 */
518 protected function getRelativePath($absolutePath) {
519 $sourcePath = explode(DIRECTORY_SEPARATOR, rtrim(PATH_site, DIRECTORY_SEPARATOR));
520 $targetPath = explode(DIRECTORY_SEPARATOR, rtrim($absolutePath, DIRECTORY_SEPARATOR));
521 while (count($sourcePath) && count($targetPath) && $sourcePath[0] === $targetPath[0]) {
522 array_shift($sourcePath);
523 array_shift($targetPath);
524 }
525 return str_pad('', count($sourcePath) * 3, '..' . DIRECTORY_SEPARATOR) . implode(DIRECTORY_SEPARATOR, $targetPath);
526 }
527
528 /**
529 * Check if there is are already core files available
530 * at the download destination.
531 *
532 * @param string $version A version number
533 * @return bool true when core files are available
534 */
535 protected function checkCoreFilesAvailable($version) {
536 $newCoreLocation = @realPath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
537 return @is_dir($newCoreLocation);
538 }
539
540 }