output = new StreamOutput($stream, Output::VERBOSITY_NORMAL, false); } /** * @return array List of wizards marked as done in registry */ public function listOfWizardsDone(): array { $wizardsDoneInRegistry = []; foreach ($this->upgradeWizardRegistry->getUpgradeWizards() as $identifier => $serviceName) { if ($this->registry->get('installUpdate', $serviceName, false)) { $wizardsDoneInRegistry[] = [ 'class' => $serviceName, 'identifier' => $identifier, // @todo fetching the service to get the title should be improved 'title' => $this->upgradeWizardRegistry->getUpgradeWizard($identifier)->getTitle(), ]; } } return $wizardsDoneInRegistry; } /** * @return array List of row updaters marked as done in registry * @throws \RuntimeException */ public function listOfRowUpdatersDone(): array { $rowUpdatersDoneClassNames = $this->registry->get('installUpdateRows', 'rowUpdatersDone', []); $rowUpdatersDone = []; foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) { // Silently skip non-existing DatabaseRowsUpdateWizards if (!class_exists($rowUpdaterClassName)) { continue; } $rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName); if (!$rowUpdater instanceof RowUpdaterInterface) { throw new \RuntimeException( 'Row updater must implement RowUpdaterInterface', 1484152906 ); } $rowUpdatersDone[] = [ 'class' => $rowUpdaterClassName, 'identifier' => $rowUpdaterClassName, 'title' => $rowUpdater->getTitle(), ]; } return $rowUpdatersDone; } /** * Mark one wizard as undone. This can be a "casual" wizard * or a single "row updater". * * @param string $identifier Wizard or RowUpdater identifier * @return bool True if wizard has been marked as undone * @throws \RuntimeException */ public function markWizardUndone(string $identifier): bool { $this->assertIdentifierIsValid($identifier); $aWizardHasBeenMarkedUndone = false; foreach ($this->listOfWizardsDone() as $wizard) { if ($wizard['identifier'] === $identifier) { $aWizardHasBeenMarkedUndone = true; $this->registry->set('installUpdate', $wizard['class'], 0); } } if (!$aWizardHasBeenMarkedUndone) { $rowUpdatersDoneList = $this->listOfRowUpdatersDone(); $registryArray = $this->registry->get('installUpdateRows', 'rowUpdatersDone', []); foreach ($rowUpdatersDoneList as $rowUpdater) { if ($rowUpdater['identifier'] === $identifier) { $aWizardHasBeenMarkedUndone = true; foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) { if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) { unset($registryArray[$rowUpdaterMarkedAsDonePosition]); break; } } $this->registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray); } } } return $aWizardHasBeenMarkedUndone; } /** * Get list of registered upgrade wizards not marked done. * * @return array List of upgrade wizards in correct order with detail information */ public function getUpgradeWizardsList(): array { $wizards = []; foreach (array_keys($this->upgradeWizardRegistry->getUpgradeWizards()) as $identifier) { if ($this->isWizardDone($identifier)) { continue; } $wizards[] = $this->getWizardInformationByIdentifier($identifier); } return $wizards; } public function getWizardInformationByIdentifier(string $identifier): array { $this->assertIdentifierIsValid($identifier); if (is_subclass_of($identifier, RowUpdaterInterface::class)) { return [ 'class' => $identifier, 'identifier' => $identifier, 'title' => $identifier, 'shouldRenderWizard' => false, 'explanation' => '', ]; } $wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier); if ($wizard instanceof ChattyInterface) { $wizard->setOutput($this->output); } return [ 'class' => $wizard::class, 'identifier' => $identifier, 'title' => $wizard->getTitle(), 'shouldRenderWizard' => $wizard->updateNecessary(), 'explanation' => $wizard->getDescription(), ]; } /** * Execute the "get user input" step of a wizard * * @throws \RuntimeException */ public function getWizardUserInput(string $identifier): array { $this->assertIdentifierIsValid($identifier); $wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier); $wizardHtml = ''; if ($wizard instanceof ConfirmableInterface) { $markup = []; $radioAttributes = [ 'type' => 'radio', 'class' => 'btn-check', 'name' => 'install[values][' . $identifier . '][install]', 'value' => '0', ]; $markup[] = '
'; $markup[] = '
'; $markup[] = htmlspecialchars($wizard->getConfirmation()->getTitle()); $markup[] = '
'; $markup[] = '
'; $markup[] = '

' . nl2br(htmlspecialchars($wizard->getConfirmation()->getMessage())) . '

'; $markup[] = '
'; if (!$wizard->getConfirmation()->isRequired()) { $denyChecked = $wizard->getConfirmation()->getDefaultValue() === false ? ' checked' : ''; $markup[] = ' '; $markup[] = ' '; } $radioAttributes['value'] = '1'; $confirmChecked = $wizard->getConfirmation()->getDefaultValue() === true ? ' checked' : ''; $markup[] = ' '; $markup[] = ' '; $markup[] = '
'; $markup[] = '
'; $markup[] = '
'; $wizardHtml = implode('', $markup); } $result = [ 'identifier' => $identifier, 'title' => $wizard->getTitle(), 'description' => $wizard->getDescription(), 'wizardHtml' => $wizardHtml, ]; return $result; } /** * Execute a single update wizard * * @throws \RuntimeException */ public function executeWizard(string $identifier, array $values): FlashMessageQueue { $performResult = false; $this->assertIdentifierIsValid($identifier); $wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier); if ($wizard instanceof ChattyInterface) { $wizard->setOutput($this->output); } $messages = new FlashMessageQueue('install'); if ($wizard instanceof ConfirmableInterface) { // value is set in request but is empty $isSetButEmpty = isset($values[$identifier]['install']) && empty($values[$identifier]['install']); $checkValue = (int)$values[$identifier]['install']; if ($checkValue === 1) { // confirmation = yes, we do the update $performResult = $wizard->executeUpdate(); } elseif ($wizard->getConfirmation()->isRequired()) { // confirmation = no, but is required, we do *not* the update and fail $performResult = false; } elseif ($isSetButEmpty) { // confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done $this->output->writeln('No changes applied, marking wizard as done.'); // confirmation was set to "no" $performResult = true; } } else { // confirmation yes or non-confirmable $performResult = $wizard->executeUpdate(); } $stream = $this->output->getStream(); rewind($stream); if ($performResult) { if (!$wizard instanceof RepeatableInterface) { // mark wizard as done if it's not repeatable and was successful $this->markWizardAsDone($wizard); } $messages->enqueue( new FlashMessage( (string)stream_get_contents($stream), 'Update successful' ) ); } else { $messages->enqueue( new FlashMessage( (string)stream_get_contents($stream), 'Update failed!', ContextualFeedbackSeverity::ERROR ) ); } return $messages; } /** * Marks some wizard as being "seen" so that it not shown again. * Writes the info in system/settings.php */ public function markWizardAsDone(UpgradeWizardInterface $upgradeWizard): void { $this->registry->set('installUpdate', $upgradeWizard::class, 1); } /** * Checks if this wizard has been "done" before * * @return bool TRUE if wizard has been done before, FALSE otherwise * @throws \RuntimeException */ public function isWizardDone(string $identifier): bool { $this->assertIdentifierIsValid($identifier); return (bool)$this->registry->get( 'installUpdate', $this->upgradeWizardRegistry->getUpgradeWizard($identifier)::class, false ); } /** * Wrapper to catch \UnexpectedValueException for backwards compatibility reasons */ public function getUpgradeWizard(string $identifier): ?UpgradeWizardInterface { try { return $this->upgradeWizardRegistry->getUpgradeWizard($identifier); } catch (\UnexpectedValueException) { return null; } } public function getUpgradeWizardIdentifiers(): array { return array_keys($this->upgradeWizardRegistry->getUpgradeWizards()); } public function getNonRepeatableUpgradeWizards(): array { $nonRepeatableUpgradeWizards = []; foreach ($this->upgradeWizardRegistry->getUpgradeWizards() as $identifier => $updateClassName) { if (!in_array(RepeatableInterface::class, class_implements($updateClassName) ?: [], true)) { $nonRepeatableUpgradeWizards[$identifier] = $updateClassName; } } return $nonRepeatableUpgradeWizards; } /** * Validate identifier exists in upgrade wizard list * * @throws \RuntimeException */ private function assertIdentifierIsValid(string $identifier): void { if ($identifier === '') { throw new \RuntimeException('Empty upgrade wizard identifier given', 1650579934); } if (!is_subclass_of($identifier, RowUpdaterInterface::class) && !$this->upgradeWizardRegistry->hasUpgradeWizard($identifier) ) { throw new \RuntimeException( 'The upgrade wizard identifier "' . $identifier . '" must either be registered as upgrade wizard or it must implement TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface', 1650546252 ); } } }