[BUGFIX] Catch exception in Upgrade Wizard
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / Action / Tool / UpgradeWizard.php
1 <?php
2 namespace TYPO3\CMS\Install\Controller\Action\Tool;
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\Cache\DatabaseSchemaService;
18 use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
19 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
20 use TYPO3\CMS\Core\Database\Schema\SqlReader;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Core\Utility\VersionNumberUtility;
23 use TYPO3\CMS\Install\Controller\Action;
24 use TYPO3\CMS\Install\Status\ErrorStatus;
25 use TYPO3\CMS\Install\Updates\AbstractUpdate;
26
27 /**
28 * Handle update wizards
29 */
30 class UpgradeWizard extends Action\AbstractAction
31 {
32 /**
33 * There are tables and fields missing in the database
34 *
35 * @var bool
36 */
37 protected $needsInitialUpdateDatabaseSchema = false;
38
39 /**
40 * Executes the tool
41 *
42 * @return string Rendered content
43 */
44 protected function executeAction()
45 {
46 // ext_localconf, db and ext_tables must be loaded for the updates
47 $this->loadExtLocalconfDatabaseAndExtTables();
48
49 if (empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
50 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] = [];
51 }
52
53 $actionMessages = [];
54
55 try {
56 // To make sure DatabaseCharsetUpdate and initialUpdateDatabaseSchema are first wizards, they are added here instead of ext_localconf.php
57 $databaseCharsetUpdateObject = $this->getUpdateObjectInstance(\TYPO3\CMS\Install\Updates\DatabaseCharsetUpdate::class, 'databaseCharsetUpdate');
58 if ($databaseCharsetUpdateObject->shouldRenderWizard()) {
59 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] = array_merge(
60 ['databaseCharsetUpdate' => \TYPO3\CMS\Install\Updates\DatabaseCharsetUpdate::class],
61 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']
62 );
63 }
64 $initialUpdateDatabaseSchemaUpdateObject = $this->getUpdateObjectInstance(\TYPO3\CMS\Install\Updates\InitialDatabaseSchemaUpdate::class, 'initialUpdateDatabaseSchema');
65 if ($initialUpdateDatabaseSchemaUpdateObject->shouldRenderWizard()) {
66 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] = array_merge(
67 ['initialUpdateDatabaseSchema' => \TYPO3\CMS\Install\Updates\InitialDatabaseSchemaUpdate::class],
68 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']
69 );
70 $this->needsInitialUpdateDatabaseSchema = true;
71 }
72
73 // To make sure finalUpdateDatabaseSchema is last wizard, it is added here instead of ext_localconf.php
74 $finalUpdateDatabaseSchemaUpdateObject = $this->getUpdateObjectInstance(\TYPO3\CMS\Install\Updates\FinalDatabaseSchemaUpdate::class, 'finalUpdateDatabaseSchema');
75 if ($finalUpdateDatabaseSchemaUpdateObject->shouldRenderWizard()) {
76 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['finalUpdateDatabaseSchema'] = \TYPO3\CMS\Install\Updates\FinalDatabaseSchemaUpdate::class;
77 }
78 } catch (StatementException $exception) {
79 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
80 $message = GeneralUtility::makeInstance(ErrorStatus::class);
81 $message->setTitle('SQL error');
82 $message->setMessage($exception->getMessage());
83 $actionMessages[] = $message;
84 }
85
86 // Perform silent cache framework table upgrade
87 $this->silentCacheFrameworkTableSchemaMigration();
88
89 if (isset($this->postValues['set']['getUserInput'])) {
90 $actionMessages[] = $this->getUserInputForUpdate();
91 $this->view->assign('updateAction', 'getUserInput');
92 } elseif (isset($this->postValues['set']['performUpdate'])) {
93 $actionMessages[] = $this->performUpdate();
94 $this->view->assign('updateAction', 'performUpdate');
95 } else {
96 $this->listUpdates();
97 $this->view->assign('updateAction', 'listUpdates');
98 }
99
100 $this->view->assign('actionMessages', $actionMessages);
101
102 return $this->view->render();
103 }
104
105 /**
106 * List of available updates
107 *
108 * @return void
109 */
110 protected function listUpdates()
111 {
112 if (empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
113 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
114 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\WarningStatus::class);
115 $message->setTitle('No update wizards registered');
116 return $message;
117 }
118
119 $availableUpdates = [];
120 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
121 $updateObject = $this->getUpdateObjectInstance($className, $identifier);
122 if ($updateObject->shouldRenderWizard()) {
123 // $explanation is changed by reference in Update objects!
124 $explanation = '';
125 $updateObject->checkForUpdate($explanation);
126 $availableUpdates[$identifier] = [
127 'identifier' => $identifier,
128 'title' => $updateObject->getTitle(),
129 'explanation' => $explanation,
130 'renderNext' => false,
131 ];
132 if ($identifier === 'initialUpdateDatabaseSchema') {
133 $availableUpdates['initialUpdateDatabaseSchema']['renderNext'] = $this->needsInitialUpdateDatabaseSchema;
134 // initialUpdateDatabaseSchema is always the first update
135 // we stop immediately here as the remaining updates may
136 // require the new fields to be present in order to avoid SQL errors
137 break;
138 } elseif ($identifier === 'finalUpdateDatabaseSchema') {
139 // Okay to check here because finalUpdateDatabaseSchema is last element in array
140 $availableUpdates['finalUpdateDatabaseSchema']['renderNext'] = count($availableUpdates) === 1;
141 } elseif (!$this->needsInitialUpdateDatabaseSchema && $updateObject->shouldRenderNextButton()) {
142 // There are Updates that only show text and don't want to be executed
143 $availableUpdates[$identifier]['renderNext'] = true;
144 }
145 }
146 }
147
148 $this->view->assign('availableUpdates', $availableUpdates);
149
150 // compute done wizards for statistics
151 $wizardsDone = [];
152 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
153 /** @var AbstractUpdate $updateObject */
154 $updateObject = $this->getUpdateObjectInstance($className, $identifier);
155 if ($updateObject->shouldRenderWizard() !== true) {
156 $wizardsDone[] = $updateObject;
157 }
158 }
159 $this->view->assign('wizardsDone', $wizardsDone);
160
161 $wizardsTotal = (count($wizardsDone) + count($availableUpdates));
162 $this->view->assign('wizardsTotal', $wizardsTotal);
163
164 $this->view->assign('wizardsPercentageDone', floor(($wizardsTotal - count($availableUpdates)) * 100 / $wizardsTotal));
165 }
166
167 /**
168 * Get user input of update wizard
169 *
170 * @return \TYPO3\CMS\Install\Status\StatusInterface
171 */
172 protected function getUserInputForUpdate()
173 {
174 $wizardIdentifier = $this->postValues['values']['identifier'];
175
176 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
177 $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
178 $wizardHtml = '';
179 if (method_exists($updateObject, 'getUserInput')) {
180 $wizardHtml = $updateObject->getUserInput('install[values][' . $wizardIdentifier . ']');
181 }
182
183 $updateData = [
184 'identifier' => $wizardIdentifier,
185 'title' => $updateObject->getTitle(),
186 'wizardHtml' => $wizardHtml,
187 ];
188
189 $this->view->assign('updateData', $updateData);
190
191 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
192 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
193 $message->setTitle('Show wizard options');
194 return $message;
195 }
196
197 /**
198 * Perform update of a specific wizard
199 *
200 * @throws \TYPO3\CMS\Install\Exception
201 * @return \TYPO3\CMS\Install\Status\StatusInterface
202 */
203 protected function performUpdate()
204 {
205 $this->getDatabaseConnection()->store_lastBuiltQuery = true;
206
207 $wizardIdentifier = $this->postValues['values']['identifier'];
208 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
209 $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
210
211 $wizardData = [
212 'identifier' => $wizardIdentifier,
213 'title' => $updateObject->getTitle(),
214 ];
215
216 // $wizardInputErrorMessage is given as reference to wizard object!
217 $wizardInputErrorMessage = '';
218 if (method_exists($updateObject, 'checkUserInput') && !$updateObject->checkUserInput($wizardInputErrorMessage)) {
219 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
220 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
221 $message->setTitle('Input parameter broken');
222 $message->setMessage($wizardInputErrorMessage ?: 'Something went wrong!');
223 $wizardData['wizardInputBroken'] = true;
224 } else {
225 if (!method_exists($updateObject, 'performUpdate')) {
226 throw new \TYPO3\CMS\Install\Exception(
227 'No performUpdate method in update wizard with identifier ' . $wizardIdentifier,
228 1371035200
229 );
230 }
231
232 // Both variables are used by reference in performUpdate()
233 $customOutput = '';
234 $databaseQueries = [];
235 $performResult = $updateObject->performUpdate($databaseQueries, $customOutput);
236
237 if ($performResult) {
238 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
239 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
240 $message->setTitle('Update successful');
241 } else {
242 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
243 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
244 $message->setTitle('Update failed!');
245 if ($customOutput) {
246 $message->setMessage($customOutput);
247 }
248 }
249
250 if ($this->postValues['values']['showDatabaseQueries'] == 1) {
251 $wizardData['queries'] = $databaseQueries;
252 }
253 }
254
255 $this->view->assign('wizardData', $wizardData);
256
257 $this->getDatabaseConnection()->store_lastBuiltQuery = false;
258
259 // Next update wizard, if available
260 $nextUpdate = $this->getNextUpdateInstance($updateObject);
261 $nextUpdateIdentifier = '';
262 if ($nextUpdate) {
263 $nextUpdateIdentifier = $nextUpdate->getIdentifier();
264 }
265 $this->view->assign('nextUpdateIdentifier', $nextUpdateIdentifier);
266
267 return $message;
268 }
269
270 /**
271 * Creates instance of an Update object
272 *
273 * @param string $className The class name
274 * @param string $identifier The identifier of Update object - needed to fetch user input
275 * @return AbstractUpdate Newly instantiated Update object
276 */
277 protected function getUpdateObjectInstance($className, $identifier)
278 {
279 $userInput = $this->postValues['values'][$identifier];
280 $versionAsInt = VersionNumberUtility::convertVersionNumberToInteger(TYPO3_version);
281 return GeneralUtility::makeInstance($className, $identifier, $versionAsInt, $userInput, $this);
282 }
283
284 /**
285 * Returns the next Update object
286 * Used to show the link/button to the next Update
287 *
288 * @param AbstractUpdate $currentUpdate Current Update object
289 * @return AbstractUpdate|NULL
290 */
291 protected function getNextUpdateInstance(AbstractUpdate $currentUpdate)
292 {
293 $isPreviousRecord = true;
294 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
295 // Find the current update wizard, and then start validating the next ones
296 if ($currentUpdate->getIdentifier() === $identifier) {
297 $isPreviousRecord = false;
298 // For the updateDatabaseSchema-wizards verify they do not have to be executed again
299 if ($identifier !== 'initialUpdateDatabaseSchema' && $identifier !== 'finalUpdateDatabaseSchema') {
300 continue;
301 }
302 }
303 if (!$isPreviousRecord) {
304 $nextUpdate = $this->getUpdateObjectInstance($className, $identifier);
305 if ($nextUpdate->shouldRenderWizard()) {
306 return $nextUpdate;
307 }
308 }
309 }
310 return null;
311 }
312
313 /**
314 * Force creation / update of caching framework tables that are needed by some update wizards
315 *
316 * @TODO: See also the other remarks on this topic in the abstract class, this whole area needs improvements
317 * @return void
318 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
319 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
320 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
321 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
322 * @throws \RuntimeException
323 * @throws \Doctrine\DBAL\Schema\SchemaException
324 * @throws \InvalidArgumentException
325 * @throws \Doctrine\DBAL\DBALException
326 */
327 protected function silentCacheFrameworkTableSchemaMigration()
328 {
329 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
330 $cachingFrameworkDatabaseSchemaService = GeneralUtility::makeInstance(DatabaseSchemaService::class);
331 $createTableStatements = $sqlReader->getStatementArray(
332 $cachingFrameworkDatabaseSchemaService->getCachingFrameworkRequiredDatabaseSchema()
333 );
334
335 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
336 $schemaMigrationService->install($createTableStatements);
337 }
338 }