[BUGFIX] Do not mark wizards implementing RepeatableInterface done
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Command / UpgradeWizardRunCommand.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Install\Command;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use Symfony\Component\Console\Command\Command;
20 use Symfony\Component\Console\Input\InputArgument;
21 use Symfony\Component\Console\Input\InputInterface;
22 use Symfony\Component\Console\Output\OutputInterface;
23 use Symfony\Component\Console\Question\ConfirmationQuestion;
24 use Symfony\Component\Console\Style\SymfonyStyle;
25 use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
26 use TYPO3\CMS\Core\Core\Bootstrap;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
29 use TYPO3\CMS\Install\Updates\ChattyInterface;
30 use TYPO3\CMS\Install\Updates\ConfirmableInterface;
31 use TYPO3\CMS\Install\Updates\PrerequisiteCollection;
32 use TYPO3\CMS\Install\Updates\RepeatableInterface;
33 use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
34
35 /**
36 * Upgrade wizard command for running wizards
37 *
38 * @internal
39 */
40 class UpgradeWizardRunCommand extends Command
41 {
42 /**
43 * @var UpgradeWizardsService
44 */
45 private $upgradeWizardsService;
46
47 /**
48 * @var OutputInterface|\Symfony\Component\Console\Style\StyleInterface
49 */
50 private $output;
51
52 /**
53 * @var InputInterface
54 */
55 private $input;
56
57 /**
58 * Bootstrap running of upgrade wizard,
59 * ensure database is utf-8
60 */
61 protected function bootstrap(): void
62 {
63 Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
64 Bootstrap::unsetReservedGlobalVariables();
65 Bootstrap::loadBaseTca(false);
66 Bootstrap::loadExtTables(false);
67 Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
68 Bootstrap::initializeBackendAuthentication();
69 $this->upgradeWizardsService = new UpgradeWizardsService();
70 $this->upgradeWizardsService->isDatabaseCharsetUtf8() ?: $this->upgradeWizardsService->setDatabaseCharsetUtf8();
71 $this->upgradeWizardsService->silentCacheFrameworkTableSchemaMigration();
72 }
73
74 /**
75 * Configure the command by defining the name, options and arguments
76 */
77 protected function configure()
78 {
79 $this->setDescription('Run upgrade wizard. Without arguments all available wizards will be run.')
80 ->addArgument(
81 'wizardName',
82 InputArgument::OPTIONAL
83 )->setHelp(
84 'This command allows running upgrade wizards on CLI. To run a single wizard add the ' .
85 'identifier of the wizard as argument. The identifier of the wizard is the name it is ' .
86 'registered with in ext_localconf.'
87 );
88 }
89
90 /**
91 * Update language packs of all active languages for all active extensions
92 *
93 * @param InputInterface $input
94 * @param \Symfony\Component\Console\Output\OutputInterface $output
95 * @return int
96 */
97 protected function execute(InputInterface $input, OutputInterface $output)
98 {
99 $this->output = new SymfonyStyle($input, $output);
100 $this->input = $input;
101 $this->bootstrap();
102
103 $result = 0;
104 if ($input->getArgument('wizardName')) {
105 $wizardToExecute = $input->getArgument('wizardName');
106 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardToExecute])) {
107 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardToExecute];
108 $upgradeWizard = $this->getWizard($className, $wizardToExecute);
109 if ($upgradeWizard !== null) {
110 $prerequisitesFulfilled = $this->handlePrerequisites([$upgradeWizard]);
111 if ($prerequisitesFulfilled === true) {
112 $result = $this->runSingleWizard($upgradeWizard);
113 } else {
114 $result = 1;
115 }
116 }
117 } else {
118 $this->output->error('No such wizard: ' . $wizardToExecute);
119 $result = 1;
120 }
121 } else {
122 $result = $this->runAllWizards();
123 }
124 return $result;
125 }
126
127 /**
128 * Get Wizard instance by class name and identifier
129 * Returns null if wizard is already done
130 *
131 * @param $className
132 * @param $identifier
133 * @return \TYPO3\CMS\Install\Updates\UpgradeWizardInterface|null
134 */
135 protected function getWizard(string $className, string $identifier): ?UpgradeWizardInterface
136 {
137 // already done
138 if ($this->upgradeWizardsService->isWizardDone($identifier)) {
139 return null;
140 }
141
142 $wizardInstance = GeneralUtility::makeInstance($className);
143 if ($wizardInstance instanceof ChattyInterface) {
144 $wizardInstance->setOutput($this->output);
145 }
146
147 if (!($wizardInstance instanceof UpgradeWizardInterface)) {
148 $this->output->writeln(
149 'Wizard ' .
150 $identifier .
151 ' needs to be manually run from the install tool, as it does not implement ' .
152 UpgradeWizardInterface::class
153 );
154 return null;
155 }
156
157 if ($wizardInstance->updateNecessary()) {
158 return $wizardInstance;
159 }
160 if ($wizardInstance instanceof RepeatableInterface) {
161 $this->output->note('Wizard ' . $identifier . ' does not need to make changes.');
162 } else {
163 $this->output->note('Wizard ' . $identifier . ' does not need to make changes. Marking wizard as done.');
164 $this->upgradeWizardsService->markWizardAsDone($identifier);
165 }
166 return null;
167 }
168
169 /**
170 * Handles prerequisites of update wizards, allows a more flexible definition and declaration of dependencies
171 * Currently implemented prerequisites include "database needs to be up-to-date" and "referenceIndex needs to be up-
172 * to-date"
173 * At the moment the install tool automatically displays the database updates when necessary but can't do more
174 * prerequisites
175 *
176 * @param UpgradeWizardInterface[] $instances
177 * @return bool
178 */
179 protected function handlePrerequisites(array $instances): bool
180 {
181 $prerequisites = GeneralUtility::makeInstance(PrerequisiteCollection::class);
182 foreach ($instances as $instance) {
183 foreach ($instance->getPrerequisites() as $prerequisite) {
184 $prerequisites->add($prerequisite);
185 }
186 }
187 $result = true;
188 foreach ($prerequisites as $prerequisite) {
189 if ($prerequisite instanceof ChattyInterface) {
190 $prerequisite->setOutput($this->output);
191 }
192 if (!$prerequisite->isFulfilled()) {
193 $this->output->writeln('Prerequisite "' . $prerequisite->getTitle() . '" not fulfilled, will ensure.');
194 $result = $prerequisite->ensure();
195 if ($result === false) {
196 $this->output->error(
197 '<error>Error running ' .
198 $prerequisite->getTitle() .
199 '. Please ensure this prerequisite manually and try again.</error>'
200 );
201 break;
202 }
203 } else {
204 $this->output->writeln('Prerequisite "' . $prerequisite->getTitle() . '" fulfilled.');
205 }
206 }
207 return $result;
208 }
209
210 /**
211 * @param UpgradeWizardInterface $instance
212 * @return int
213 */
214 protected function runSingleWizard(
215 UpgradeWizardInterface $instance
216 ): int {
217 $this->output->title('Running Wizard ' . $instance->getTitle());
218 if ($instance instanceof ConfirmableInterface) {
219 $confirmation = $instance->getConfirmation();
220 $defaultString = $confirmation->getDefaultValue() ? '(Y/n)' : '(y/N)';
221 $question = new ConfirmationQuestion(
222 sprintf(
223 '<info>%s</info>' . LF . '%s %s',
224 $confirmation->getTitle(),
225 $confirmation->getMessage(),
226 $defaultString
227 ),
228 $confirmation->getDefaultValue()
229 );
230 $helper = $this->getHelper('question');
231 if (!$helper->ask($this->input, $this->output, $question)) {
232 if ($instance instanceof RepeatableInterface) {
233 $this->output->note('No changes applied.');
234 } else {
235 $this->upgradeWizardsService->markWizardAsDone($instance->getIdentifier());
236 $this->output->note('No changes applied, marking wizard as done.');
237 }
238 return 0;
239 }
240 }
241 if ($instance->executeUpdate()) {
242 $this->output->success('Successfully ran wizard ' . $instance->getTitle());
243 if (!$instance instanceof RepeatableInterface) {
244 $this->upgradeWizardsService->markWizardAsDone($instance->getIdentifier());
245 }
246 return 0;
247 }
248 $this->output->error('<error>Something went wrong while running ' . $instance->getTitle() . '</error>');
249 return 1;
250 }
251
252 /**
253 * Get list of registered upgrade wizards.
254 *
255 * @return int 0 if all wizards were successful, 1 on error
256 */
257 public function runAllWizards(): int
258 {
259 $returnCode = 1;
260 $wizardInstances = [];
261 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $class) {
262 $wizardInstances[] = $this->getWizard($class, $identifier);
263 }
264 $wizardInstances = array_filter($wizardInstances);
265 if (count($wizardInstances) > 0) {
266 $prerequisitesResult = $this->handlePrerequisites($wizardInstances);
267 if ($prerequisitesResult === false) {
268 $returnCode = 1;
269 $this->output->error('Error handling prerequisites, aborting.');
270 } else {
271 $this->output->title('Found ' . count($wizardInstances) . ' wizard(s) to run.');
272 foreach ($wizardInstances as $wizardInstance) {
273 $result = $this->runSingleWizard($wizardInstance);
274 if ($result > 0) {
275 $returnCode = 1;
276 }
277 }
278 }
279 } else {
280 $this->output->success('No wizards left to run.');
281 }
282 return $returnCode;
283 }
284 }