08acd8d2080b389c03fbb34c2cc795bf441f00cd
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Service / CoreUpdateService.php
1 <?php
2 namespace TYPO3\CMS\Install\Service;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Christian Kuhn <lolli@schwarzbu.ch>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * A copy is found in the textfile GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31
32 /**
33 * Core update service.
34 * This service handles core updates, all the nasty details are encapsulated
35 * here. The single public methods 'depend' on each other, for example a new
36 * core has to be downloaded before it can be unpacked.
37 *
38 * Each method returns only TRUE of FALSE indicating if it was successful or
39 * not. Detailed information can be fetched with getMessages() and will return
40 * a list of status messages of the previous operation.
41 */
42 class CoreUpdateService {
43
44 /**
45 * @var \TYPO3\CMS\Extbase\Object\ObjectManager
46 * @inject
47 */
48 protected $objectManager;
49
50 /**
51 * @var \TYPO3\CMS\Install\Service\CoreVersionService
52 * @inject
53 */
54 protected $coreVersionService;
55
56 /**
57 * @var array<\TYPO3\CMS\Install\Status\StatusInterface>
58 */
59 protected $messages = array();
60
61 /**
62 * Absolute path to download location
63 *
64 * @var string
65 */
66 protected $downloadTargetPath;
67
68 /**
69 * Absolute path to the current core files
70 *
71 * @var string
72 */
73 protected $currentCoreLocation;
74
75 /**
76 * Base URI for TYPO3 downloads
77 *
78 * @var string
79 */
80 protected $downloadBaseUri;
81
82 /**
83 * Initialize update paths
84 */
85 public function initializeObject() {
86 $this->setDownloadTargetPath(PATH_site . 'typo3temp/core-update/');
87 $this->currentCoreLocation = $this->discoverCurrentCoreLocation();
88 $this->downloadBaseUri = $this->coreVersionService->getDownloadBaseUri();
89 }
90
91 /**
92 * In future implementations we might implement some sarter logic here
93 *
94 * @return string
95 */
96 protected function discoverCurrentCoreLocation() {
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 boolean 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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 boolean 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');
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')->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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 commit hazard 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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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-');
187 $result = @touch($file);
188 if (!$result) {
189 $success = FALSE;
190 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
191 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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->currentCoreLocation . '/../');
201 $file = $coreLocation . '/' . uniqid('install-core-update-test-');
202 $result = @touch($file);
203 if (!$result) {
204 $success = FALSE;
205 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
206 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 boolean 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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
251 $message->setTitle('Core download exists in download location: ' . substr($this->downloadTargetPath, strlen(PATH_site)));
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 boolean 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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 boolean 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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
337 $message->setTitle('Unpacked core exists in download location: ' . substr($this->downloadTargetPath, strlen(PATH_site)));
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 boolean TRUE on success
368 */
369 public function moveVersion($version) {
370 $downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
371 $newCoreLocation = @realPath($this->currentCoreLocation . '/../') . '/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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
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 boolean TRUE on success
408 */
409 public function activateVersion($version) {
410 $newCoreLocation = @realPath($this->currentCoreLocation . '/../') . '/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('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
419 $message->setTitle('New core not found');
420 $messages[] = $message;
421 } elseif (!is_link($this->currentCoreLocation)) {
422 $success = FALSE;
423 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
424 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
425 $message->setTitle('TYPO3 source directory (typo3_src) is not a link');
426 $messages[] = $message;
427 } else {
428 $unlinkResult = unlink($this->currentCoreLocation);
429 if (!$unlinkResult) {
430 $success = FALSE;
431 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
432 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
433 $message->setTitle('Removing old symlink failed');
434 $messages[] = $message;
435 } else {
436 $symlinkResult = symlink($newCoreLocation, $this->currentCoreLocation);
437 if (!$symlinkResult) {
438 $success = FALSE;
439 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
440 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
441 $message->setTitle('Linking new core failed');
442 $messages[] = $message;
443 }
444 }
445 }
446
447 $this->messages = $messages;
448 return $success;
449 }
450
451 /**
452 * Absolute path of downloaded .tar.gz
453 *
454 * @param string $version A version number
455 * @return string
456 */
457 protected function getDownloadTarGzTargetPath($version) {
458 return $this->downloadTargetPath . $version . '.tar.gz';
459 }
460 }