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