[FEATURE] ext:install Core update
[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($this->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->downloadTargetPath . $version . '.tar.gz';
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 * Unpack a downloaded core
278 *
279 * @param string $version A version to unpack
280 * @return boolean TRUE on success
281 */
282 public function unpackVersion($version) {
283 $fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
284
285 $messages = array();
286 $success = TRUE;
287
288 if (!@is_file($fileLocation)) {
289 $success = FALSE;
290 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
291 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
292 $message->setTitle('Downloaded core not found');
293 $messages[] = $message;
294 } elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
295 $success = FALSE;
296 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
297 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
298 $message->setTitle('Unpacked core exists in download location: ' . substr($this->downloadTargetPath, strlen(PATH_site)));
299 $messages[] = $message;
300 } else {
301 $unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
302 exec($unpackCommand, $output, $errorCode);
303 if ($errorCode) {
304 $success = FALSE;
305 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
306 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
307 $message->setTitle('Unpacking core not successful');
308 $messages[] = $message;
309 } else {
310 $removePackedFileResult = unlink($fileLocation);
311 if (!$removePackedFileResult) {
312 $success = FALSE;
313 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
314 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
315 $message->setTitle('Removing packed core not successful');
316 $messages[] = $message;
317 }
318 }
319 }
320 $this->messages = $messages;
321 return $success;
322 }
323
324 /**
325 * Move an unpacked core to its final destination
326 *
327 * @param string $version A version to move
328 * @return boolean TRUE on success
329 */
330 public function moveVersion($version) {
331 $downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
332 $newCoreLocation = @realPath($this->currentCoreLocation . '/../') . '/typo3_src-' . $version;
333
334 $messages = array();
335 $success = TRUE;
336
337 if (!@is_dir($downloadedCoreLocation)) {
338 $success = FALSE;
339 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
340 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
341 $message->setTitle('Unpacked core not found');
342 $messages[] = $message;
343 } elseif (@is_dir($newCoreLocation)) {
344 $success = FALSE;
345 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
346 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
347 $message->setTitle('Another core source directory already exists in path ' . $newCoreLocation);
348 $messages[] = $message;
349 } else {
350 $moveResult = rename($downloadedCoreLocation, $newCoreLocation);
351 if (!$moveResult) {
352 $success = FALSE;
353 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
354 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
355 $message->setTitle('Moving core to ' . $newCoreLocation . ' failed');
356 $messages[] = $message;
357 }
358 }
359
360 $this->messages = $messages;
361 return $success;
362 }
363
364 /**
365 * Activate a core version
366 *
367 * @param string $version A version to activate
368 * @return boolean TRUE on success
369 */
370 public function activateVersion($version) {
371 $newCoreLocation = @realPath($this->currentCoreLocation . '/../') . '/typo3_src-' . $version;
372
373 $messages = array();
374 $success = TRUE;
375
376 if (!is_dir($newCoreLocation)) {
377 $success = FALSE;
378 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
379 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
380 $message->setTitle('New core not found');
381 $messages[] = $message;
382 } elseif (!is_link($this->currentCoreLocation)) {
383 $success = FALSE;
384 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
385 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
386 $message->setTitle('TYPO3 source directory (typo3_src) is not a link');
387 $messages[] = $message;
388 } else {
389 $unlinkResult = unlink($this->currentCoreLocation);
390 if (!$unlinkResult) {
391 $success = FALSE;
392 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
393 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
394 $message->setTitle('Removing old symlink failed');
395 $messages[] = $message;
396 } else {
397 $symlinkResult = symlink($newCoreLocation, $this->currentCoreLocation);
398 if (!$symlinkResult) {
399 $success = FALSE;
400 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
401 $message = $this->objectManager->get('TYPO3\\CMS\\Install\\Status\\ErrorStatus');
402 $message->setTitle('Linking new core failed');
403 $messages[] = $message;
404 }
405 }
406 }
407
408 $this->messages = $messages;
409 return $success;
410 }
411 }