[CLEANUP] The correct case must be used for standard PHP types in phpdoc
[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 protected function listUpdates()
130 {
131 $availableUpdates = [];
132 $markedWizardsDoneInRegistry = [];
133 $markedWizardsDoneByCallingShouldRenderWizard = [];
134 $registry = GeneralUtility::makeInstance(Registry::class);
135 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
136 $updateObject = $this->getUpdateObjectInstance($className, $identifier);
137 $markedDoneInRegistry = $registry->get('installUpdate', $className, false);
138 if ($markedDoneInRegistry) {
139 $markedWizardsDoneInRegistry[] = [
140 'identifier' => $identifier,
141 'title' => $updateObject->getTitle(),
142 ];
143 } else {
144 if ($updateObject->shouldRenderWizard()) {
145 // $explanation is changed by reference in Update objects!
146 $explanation = '';
147 $updateObject->checkForUpdate($explanation);
148 $availableUpdates[$identifier] = [
149 'identifier' => $identifier,
150 'title' => $updateObject->getTitle(),
151 'explanation' => $explanation,
152 'renderNext' => false,
153 ];
154 if ($identifier === 'initialUpdateDatabaseSchema') {
155 $availableUpdates['initialUpdateDatabaseSchema']['renderNext'] = $this->needsInitialUpdateDatabaseSchema;
156 // initialUpdateDatabaseSchema is always the first update
157 // we stop immediately here as the remaining updates may
158 // require the new fields to be present in order to avoid SQL errors
159 break;
160 }
161 if ($identifier === 'finalUpdateDatabaseSchema') {
162 // Okay to check here because finalUpdateDatabaseSchema is last element in array
163 $availableUpdates['finalUpdateDatabaseSchema']['renderNext'] = count($availableUpdates) === 1;
164 } elseif (!$this->needsInitialUpdateDatabaseSchema && $updateObject->shouldRenderNextButton()) {
165 // There are Updates that only show text and don't want to be executed
166 $availableUpdates[$identifier]['renderNext'] = true;
167 }
168 } else {
169 $markedWizardsDoneByCallingShouldRenderWizard[] = [
170 'identifier' => $identifier,
171 'title' => $updateObject->getTitle(),
172 ];
173 }
174 }
175 }
176
177 // List of row updaters marked as done from "DatabaseRowsUpdateWizard"
178 $rowUpdatersDoneClassNames = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
179 $rowUpdatersDone = [];
180 foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
181 // Silently skip non existing DatabaseRowsUpdateWizard's
182 if (!class_exists($rowUpdaterClassName)) {
183 continue;
184 }
185 /** @var RowUpdaterInterface $rowUpdater */
186 $rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
187 if (!$rowUpdater instanceof RowUpdaterInterface) {
188 throw new \RuntimeException(
189 'Row updater must implement RowUpdaterInterface',
190 1484152906
191 );
192 }
193 $rowUpdatersDone[] = [
194 'identifier' => $rowUpdaterClassName,
195 'title' => $rowUpdater->getTitle(),
196 ];
197 }
198
199 $wizardsTotal = (count($markedWizardsDoneInRegistry) + count($markedWizardsDoneByCallingShouldRenderWizard) + count($availableUpdates));
200 $percentageDone = floor(($wizardsTotal - count($availableUpdates)) * 100 / $wizardsTotal);
201
202 $this->view->assign('wizardsDone', $markedWizardsDoneInRegistry);
203 $this->view->assign('rowUpdatersDone', $rowUpdatersDone);
204 $this->view->assign('availableUpdates', $availableUpdates);
205 $this->view->assign('wizardsTotal', $wizardsTotal);
206 $this->view->assign('wizardsPercentageDone', $percentageDone);
207 }
208
209 /**
210 * Get user input of update wizard
211 *
212 * @return StatusInterface
213 */
214 protected function getUserInputForUpdate()
215 {
216 $wizardIdentifier = $this->postValues['values']['identifier'];
217
218 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
219 $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
220 $wizardHtml = '';
221 if (method_exists($updateObject, 'getUserInput')) {
222 $wizardHtml = $updateObject->getUserInput('install[values][' . $wizardIdentifier . ']');
223 }
224
225 $updateData = [
226 'identifier' => $wizardIdentifier,
227 'title' => $updateObject->getTitle(),
228 'wizardHtml' => $wizardHtml,
229 ];
230
231 $this->view->assign('updateData', $updateData);
232
233 /** @var $message StatusInterface */
234 $message = GeneralUtility::makeInstance(OkStatus::class);
235 $message->setTitle('Show wizard options');
236 return $message;
237 }
238
239 /**
240 * Rechecks the chosen wizards and row updaters to mark them as "was not executed" again.
241 *
242 * @return StatusInterface
243 */
244 protected function recheckWizardsAndRowUpdaters()
245 {
246 if (empty($this->postValues['values']['recheck']) && empty($this->postValues['values']['recheckRowUpdater'])) {
247 $message = GeneralUtility::makeInstance(NoticeStatus::class);
248 $message->setTitle('No wizards selected to recheck');
249 return $message;
250 }
251 $registry = GeneralUtility::makeInstance(Registry::class);
252 if (!empty($this->postValues['values']['recheck'])) {
253 foreach ($this->postValues['values']['recheck'] as $wizardIdentifier => $value) {
254 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
255 $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
256 $registry->set('installUpdate', get_class($updateObject), 0);
257 }
258 }
259 if (!empty($this->postValues['values']['recheckRowUpdater'])) {
260 $rowUpdatersToRecheck = $this->postValues['values']['recheckRowUpdater'];
261 $rowUpdatersMarkedAsDone = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
262 foreach ($rowUpdatersToRecheck as $rowUpdaterToReCheckClassName => $value) {
263 foreach ($rowUpdatersMarkedAsDone as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
264 if ($rowUpdaterMarkedAsDone === $rowUpdaterToReCheckClassName) {
265 unset($rowUpdatersMarkedAsDone[$rowUpdaterMarkedAsDonePosition]);
266 break;
267 }
268 }
269 }
270 $registry->set('installUpdateRows', 'rowUpdatersDone', $rowUpdatersMarkedAsDone);
271 }
272
273 $message = GeneralUtility::makeInstance(OkStatus::class);
274 $message->setTitle('Successfully rechecked');
275 return $message;
276 }
277
278 /**
279 * Perform update of a specific wizard
280 *
281 * @throws \TYPO3\CMS\Install\Exception
282 * @return StatusInterface
283 */
284 protected function performUpdate()
285 {
286 $wizardIdentifier = $this->postValues['values']['identifier'];
287 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
288 $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
289
290 $wizardData = [
291 'identifier' => $wizardIdentifier,
292 'title' => $updateObject->getTitle(),
293 ];
294
295 // $wizardInputErrorMessage is given as reference to wizard object!
296 $wizardInputErrorMessage = '';
297 if (method_exists($updateObject, 'checkUserInput') && !$updateObject->checkUserInput($wizardInputErrorMessage)) {
298 /** @var $message StatusInterface */
299 $message = GeneralUtility::makeInstance(ErrorStatus::class);
300 $message->setTitle('Input parameter broken');
301 $message->setMessage($wizardInputErrorMessage ?: 'Something went wrong!');
302 $wizardData['wizardInputBroken'] = true;
303 } else {
304 if (!method_exists($updateObject, 'performUpdate')) {
305 throw new \TYPO3\CMS\Install\Exception(
306 'No performUpdate method in update wizard with identifier ' . $wizardIdentifier,
307 1371035200
308 );
309 }
310
311 // Both variables are used by reference in performUpdate()
312 $customOutput = '';
313 $databaseQueries = [];
314 $performResult = $updateObject->performUpdate($databaseQueries, $customOutput);
315
316 if ($performResult) {
317 /** @var $message StatusInterface */
318 $message = GeneralUtility::makeInstance(OkStatus::class);
319 $message->setTitle('Update successful');
320 } else {
321 /** @var $message StatusInterface */
322 $message = GeneralUtility::makeInstance(ErrorStatus::class);
323 $message->setTitle('Update failed!');
324 if ($customOutput) {
325 $message->setMessage($customOutput);
326 }
327 }
328
329 if ($this->postValues['values']['showDatabaseQueries'] == 1) {
330 $wizardData['queries'] = $databaseQueries;
331 }
332 }
333
334 $this->view->assign('wizardData', $wizardData);
335
336 // Next update wizard, if available
337 $nextUpdate = $this->getNextUpdateInstance($updateObject);
338 $nextUpdateIdentifier = '';
339 if ($nextUpdate) {
340 $nextUpdateIdentifier = $nextUpdate->getIdentifier();
341 }
342 $this->view->assign('nextUpdateIdentifier', $nextUpdateIdentifier);
343
344 return $message;
345 }
346
347 /**
348 * Creates instance of an Update object
349 *
350 * @param string $className The class name
351 * @param string $identifier The identifier of Update object - needed to fetch user input
352 * @return AbstractUpdate Newly instantiated Update object
353 */
354 protected function getUpdateObjectInstance($className, $identifier)
355 {
356 $userInput = $this->postValues['values'][$identifier];
357 $versionAsInt = VersionNumberUtility::convertVersionNumberToInteger(TYPO3_version);
358 return GeneralUtility::makeInstance($className, $identifier, $versionAsInt, $userInput, $this);
359 }
360
361 /**
362 * Returns the next Update object
363 * Used to show the link/button to the next Update
364 *
365 * @param AbstractUpdate $currentUpdate Current Update object
366 * @return AbstractUpdate|null
367 */
368 protected function getNextUpdateInstance(AbstractUpdate $currentUpdate)
369 {
370 $isPreviousRecord = true;
371 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
372 // Find the current update wizard, and then start validating the next ones
373 if ($currentUpdate->getIdentifier() === $identifier) {
374 $isPreviousRecord = false;
375 // For the updateDatabaseSchema-wizards verify they do not have to be executed again
376 if ($identifier !== 'initialUpdateDatabaseSchema' && $identifier !== 'finalUpdateDatabaseSchema') {
377 continue;
378 }
379 }
380 if (!$isPreviousRecord) {
381 $nextUpdate = $this->getUpdateObjectInstance($className, $identifier);
382 if ($nextUpdate->shouldRenderWizard()) {
383 return $nextUpdate;
384 }
385 }
386 }
387 return null;
388 }
389
390 /**
391 * Force creation / update of caching framework tables that are needed by some update wizards
392 *
393 * @TODO: See also the other remarks on this topic in the abstract class, this whole area needs improvements
394 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
395 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
396 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
397 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
398 * @throws \RuntimeException
399 * @throws \Doctrine\DBAL\Schema\SchemaException
400 * @throws \InvalidArgumentException
401 * @throws \Doctrine\DBAL\DBALException
402 */
403 protected function silentCacheFrameworkTableSchemaMigration()
404 {
405 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
406 $cachingFrameworkDatabaseSchemaService = GeneralUtility::makeInstance(DatabaseSchemaService::class);
407 $createTableStatements = $sqlReader->getStatementArray(
408 $cachingFrameworkDatabaseSchemaService->getCachingFrameworkRequiredDatabaseSchema()
409 );
410
411 if (!empty($createTableStatements)) {
412 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
413 $schemaMigrationService->install($createTableStatements);
414 }
415 }
416 }