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