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