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