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