[FEATURE] Add feature toggle interface to Settings
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / SettingsController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Install\Controller;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
21 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
22 use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
23 use TYPO3\CMS\Core\Core\Bootstrap;
24 use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
25 use TYPO3\CMS\Core\Database\Connection;
26 use TYPO3\CMS\Core\Database\ConnectionPool;
27 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
28 use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
29 use TYPO3\CMS\Core\Http\JsonResponse;
30 use TYPO3\CMS\Core\Messaging\FlashMessage;
31 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
32 use TYPO3\CMS\Core\Package\PackageManager;
33 use TYPO3\CMS\Core\Utility\ArrayUtility;
34 use TYPO3\CMS\Core\Utility\GeneralUtility;
35 use TYPO3\CMS\Core\Utility\MathUtility;
36 use TYPO3\CMS\Install\Configuration\FeatureManager;
37 use TYPO3\CMS\Install\Service\ExtensionConfigurationService;
38 use TYPO3\CMS\Install\Service\LocalConfigurationValueService;
39
40 /**
41 * Settings controller
42 */
43 class SettingsController extends AbstractController
44 {
45 /**
46 * Main "show the cards" view
47 *
48 * @param ServerRequestInterface $request
49 * @return ResponseInterface
50 */
51 public function cardsAction(ServerRequestInterface $request): ResponseInterface
52 {
53 $view = $this->initializeStandaloneView($request, 'Settings/Cards.html');
54 return new JsonResponse([
55 'success' => true,
56 'html' => $view->render(),
57 ]);
58 }
59
60 /**
61 * Change install tool password
62 *
63 * @param ServerRequestInterface $request
64 * @return ResponseInterface
65 */
66 public function changeInstallToolPasswordGetDataAction(ServerRequestInterface $request): ResponseInterface
67 {
68 $view = $this->initializeStandaloneView($request, 'Settings/ChangeInstallToolPassword.html');
69 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
70 $view->assignMultiple([
71 'changeInstallToolPasswordToken' => $formProtection->generateToken('installTool', 'changeInstallToolPassword'),
72 ]);
73 return new JsonResponse([
74 'success' => true,
75 'html' => $view->render(),
76 ]);
77 }
78
79 /**
80 * Change install tool password
81 *
82 * @param ServerRequestInterface $request
83 * @return ResponseInterface
84 */
85 public function changeInstallToolPasswordAction(ServerRequestInterface $request): ResponseInterface
86 {
87 $password = $request->getParsedBody()['install']['password'] ?? '';
88 $passwordCheck = $request->getParsedBody()['install']['passwordCheck'];
89 $messageQueue = new FlashMessageQueue('install');
90
91 if ($password !== $passwordCheck) {
92 $messageQueue->enqueue(new FlashMessage(
93 'Install tool password not changed. Given passwords do not match.',
94 '',
95 FlashMessage::ERROR
96 ));
97 } elseif (strlen($password) < 8) {
98 $messageQueue->enqueue(new FlashMessage(
99 'Install tool password not changed. Given password must be at least eight characters long.',
100 '',
101 FlashMessage::ERROR
102 ));
103 } else {
104 $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
105 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
106 $configurationManager->setLocalConfigurationValueByPath(
107 'BE/installToolPassword',
108 $hashInstance->getHashedPassword($password)
109 );
110 $messageQueue->enqueue(new FlashMessage('Install tool password changed'));
111 }
112 return new JsonResponse([
113 'success' => true,
114 'status' => $messageQueue,
115 ]);
116 }
117
118 /**
119 * Return a list of possible and active system maintainers
120 *
121 * @param ServerRequestInterface $request
122 * @return ResponseInterface
123 */
124 public function systemMaintainerGetListAction(ServerRequestInterface $request): ResponseInterface
125 {
126 $view = $this->initializeStandaloneView($request, 'Settings/SystemMaintainer.html');
127 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
128 $view->assignMultiple([
129 'systemMaintainerWriteToken' => $formProtection->generateToken('installTool', 'systemMaintainerWrite'),
130 'systemMaintainerIsDevelopmentContext' => GeneralUtility::getApplicationContext()->isDevelopment(),
131 ]);
132
133 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
134
135 // We have to respect the enable fields here by our own because no TCA is loaded
136 $queryBuilder = $connectionPool->getQueryBuilderForTable('be_users');
137 $queryBuilder->getRestrictions()->removeAll();
138 $users = $queryBuilder
139 ->select('uid', 'username', 'disable', 'starttime', 'endtime')
140 ->from('be_users')
141 ->where(
142 $queryBuilder->expr()->andX(
143 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
144 $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
145 $queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('_cli_', \PDO::PARAM_STR))
146 )
147 )
148 ->orderBy('uid')
149 ->execute()
150 ->fetchAll();
151
152 $systemMaintainerList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
153 $systemMaintainerList = array_map('intval', $systemMaintainerList);
154 $currentTime = time();
155 foreach ($users as &$user) {
156 $user['disable'] = $user['disable'] ||
157 ((int)$user['starttime'] !== 0 && $user['starttime'] > $currentTime) ||
158 ((int)$user['endtime'] !== 0 && $user['endtime'] < $currentTime);
159 $user['isSystemMaintainer'] = in_array((int)$user['uid'], $systemMaintainerList, true);
160 }
161 return new JsonResponse([
162 'success' => true,
163 'status' => [],
164 'users' => $users,
165 'html' => $view->render(),
166 ]);
167 }
168
169 /**
170 * Write new system maintainer list
171 *
172 * @param ServerRequestInterface $request
173 * @return ResponseInterface
174 */
175 public function systemMaintainerWriteAction(ServerRequestInterface $request): ResponseInterface
176 {
177 // Sanitize given user list and write out
178 $newUserList = [];
179 $users = $request->getParsedBody()['install']['users'] ?? [];
180 if (is_array($users)) {
181 foreach ($users as $uid) {
182 if (MathUtility::canBeInterpretedAsInteger($uid)) {
183 $newUserList[] = (int)$uid;
184 }
185 }
186 }
187
188 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
189 $queryBuilder->getRestrictions()->removeAll();
190
191 $validatedUserList = $queryBuilder
192 ->select('uid')
193 ->from('be_users')
194 ->where(
195 $queryBuilder->expr()->andX(
196 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
197 $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
198 $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($newUserList, Connection::PARAM_INT_ARRAY))
199 )
200 )->execute()->fetchAll();
201
202 $validatedUserList = array_column($validatedUserList, 'uid');
203 $validatedUserList = array_map('intval', $validatedUserList);
204
205 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
206 $configurationManager->setLocalConfigurationValuesByPathValuePairs(
207 ['SYS/systemMaintainers' => $validatedUserList]
208 );
209
210 $messages = [];
211 if (empty($validatedUserList)) {
212 $messages[] = new FlashMessage(
213 '',
214 'Set system maintainer list to an empty array',
215 FlashMessage::INFO
216 );
217 } else {
218 $messages[] = new FlashMessage(
219 implode(', ', $validatedUserList),
220 'New system maintainer uid list',
221 FlashMessage::INFO
222 );
223 }
224 return new JsonResponse([
225 'success' => true,
226 'status' => $messages
227 ]);
228 }
229
230 /**
231 * Main LocalConfiguration card content
232 *
233 * @param ServerRequestInterface $request
234 * @return ResponseInterface
235 */
236 public function localConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
237 {
238 $localConfigurationValueService = new LocalConfigurationValueService();
239 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
240 $view = $this->initializeStandaloneView($request, 'Settings/LocalConfigurationGetContent.html');
241 $view->assignMultiple([
242 'localConfigurationWriteToken' => $formProtection->generateToken('installTool', 'localConfigurationWrite'),
243 'localConfigurationData' => $localConfigurationValueService->getCurrentConfigurationData(),
244 ]);
245 return new JsonResponse([
246 'success' => true,
247 'html' => $view->render(),
248 ]);
249 }
250
251 /**
252 * Write given LocalConfiguration settings
253 *
254 * @param ServerRequestInterface $request
255 * @return ResponseInterface
256 * @throws \RuntimeException
257 */
258 public function localConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
259 {
260 $settings = $request->getParsedBody()['install']['configurationValues'];
261 if (!is_array($settings) || empty($settings)) {
262 throw new \RuntimeException(
263 'Expected value array not found',
264 1502282283
265 );
266 }
267 $localConfigurationValueService = new LocalConfigurationValueService();
268 $messageQueue = $localConfigurationValueService->updateLocalConfigurationValues($settings);
269 if (empty($messageQueue)) {
270 $messageQueue->enqueue(new FlashMessage(
271 '',
272 'No values changed',
273 FlashMessage::WARNING
274 ));
275 }
276 return new JsonResponse([
277 'success' => true,
278 'status' => $messageQueue,
279 ]);
280 }
281
282 /**
283 * Main preset card content
284 *
285 * @param ServerRequestInterface $request
286 * @return ResponseInterface
287 */
288 public function presetsGetContentAction(ServerRequestInterface $request): ResponseInterface
289 {
290 $view = $this->initializeStandaloneView($request, 'Settings/PresetsGetContent.html');
291 $presetFeatures = GeneralUtility::makeInstance(FeatureManager::class);
292 $presetFeatures = $presetFeatures->getInitializedFeatures($request->getParsedBody()['install']['values'] ?? []);
293 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
294 $view->assignMultiple([
295 'presetsActivateToken' => $formProtection->generateToken('installTool', 'presetsActivate'),
296 // This action is called again from within the card itself if a custom image path is supplied
297 'presetsGetContentToken' => $formProtection->generateToken('installTool', 'presetsGetContent'),
298 'presetFeatures' => $presetFeatures,
299 ]);
300 return new JsonResponse([
301 'success' => true,
302 'html' => $view->render(),
303 ]);
304 }
305
306 /**
307 * Write selected presets
308 *
309 * @param ServerRequestInterface $request
310 * @return ResponseInterface
311 */
312 public function presetsActivateAction(ServerRequestInterface $request): ResponseInterface
313 {
314 $messages = new FlashMessageQueue('install');
315 $configurationManager = new ConfigurationManager();
316 $featureManager = new FeatureManager();
317 $configurationValues = $featureManager->getConfigurationForSelectedFeaturePresets($request->getParsedBody()['install']['values'] ?? []);
318 if (!empty($configurationValues)) {
319 $configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues);
320 $messageBody = [];
321 foreach ($configurationValues as $configurationKey => $configurationValue) {
322 $messageBody[] = '\'' . $configurationKey . '\' => \'' . $configurationValue . '\'';
323 }
324 $messages->enqueue(new FlashMessage(
325 implode(', ', $messageBody),
326 'Configuration written'
327 ));
328 } else {
329 $messages->enqueue(new FlashMessage(
330 '',
331 'No configuration change selected',
332 FlashMessage::INFO
333 ));
334 }
335 return new JsonResponse([
336 'success' => true,
337 'status' => $messages,
338 ]);
339 }
340
341 /**
342 * Render a list of extensions with their configuration form.
343 *
344 * @param ServerRequestInterface $request
345 * @return ResponseInterface
346 */
347 public function extensionConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
348 {
349 // Extension configuration needs initialized $GLOBALS['LANG']
350 Bootstrap::initializeLanguageObject();
351 $extensionConfigurationService = new ExtensionConfigurationService();
352 $extensionsWithConfigurations = [];
353 $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
354 foreach ($activePackages as $extensionKey => $activePackage) {
355 if (@file_exists($activePackage->getPackagePath() . 'ext_conf_template.txt')) {
356 $extensionsWithConfigurations[$extensionKey] = [
357 'packageInfo' => $activePackage,
358 'configuration' => $extensionConfigurationService->getConfigurationPreparedForView($extensionKey),
359 ];
360 }
361 }
362 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
363 $view = $this->initializeStandaloneView($request, 'Settings/ExtensionConfigurationGetContent.html');
364 $view->assignMultiple([
365 'extensionsWithConfigurations' => $extensionsWithConfigurations,
366 'extensionConfigurationWriteToken' => $formProtection->generateToken('installTool', 'extensionConfigurationWrite'),
367 ]);
368 return new JsonResponse([
369 'success' => true,
370 'html' => $view->render(),
371 ]);
372 }
373
374 /**
375 * Write extension configuration
376 *
377 * @param ServerRequestInterface $request
378 * @return ResponseInterface
379 */
380 public function extensionConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
381 {
382 $extensionKey = $request->getParsedBody()['install']['extensionKey'];
383 $configuration = $request->getParsedBody()['install']['extensionConfiguration'];
384 $nestedConfiguration = [];
385 foreach ($configuration as $configKey => $value) {
386 $nestedConfiguration = ArrayUtility::setValueByPath($nestedConfiguration, $configKey, $value, '.');
387 }
388 (new ExtensionConfiguration())->set($extensionKey, '', $nestedConfiguration);
389 $messages = [
390 new FlashMessage(
391 'Successfully saved configuration for extension "' . $extensionKey . '"',
392 '',
393 FlashMessage::OK
394 )
395 ];
396 return new JsonResponse([
397 'success' => true,
398 'status' => $messages,
399 ]);
400 }
401
402 /**
403 * Render feature toggles
404 *
405 * @param ServerRequestInterface $request
406 * @return ResponseInterface
407 */
408 public function featuresGetContentAction(ServerRequestInterface $request): ResponseInterface
409 {
410 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
411 $configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
412 ->load($configurationManager->getDefaultConfigurationDescriptionFileLocation());
413 $allFeatures = $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? [];
414 $features = [];
415 foreach ($allFeatures as $featureName => $featureValue) {
416 // Only features that have a .yml description will be listed. There is currently no
417 // way for extensions to extend this, so feature toggles of non-core extensions are
418 // not listed here.
419 if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
420 $default = $configurationManager->getDefaultConfigurationValueByPath('SYS/features/' . $featureName);
421 $features[] = [
422 'name' => $featureName,
423 'description' => $configurationDescription['SYS']['items']['features']['items'][$featureName]['description'],
424 'default' => $default,
425 'value' => $featureValue,
426 ];
427 }
428 }
429 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
430 $view = $this->initializeStandaloneView($request, 'Settings/FeaturesGetContent.html');
431 $view->assignMultiple([
432 'features' => $features,
433 'featuresSaveToken' => $formProtection->generateToken('installTool', 'featuresSave'),
434 ]);
435 return new JsonResponse([
436 'success' => true,
437 'html' => $view->render(),
438 ]);
439 }
440
441 /**
442 * Update feature toggles state
443 *
444 * @param ServerRequestInterface $request
445 * @return ResponseInterface
446 */
447 public function featuresSaveAction(ServerRequestInterface $request): ResponseInterface
448 {
449 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
450 $enabledFeaturesFromPost = $request->getParsedBody()['install']['values'] ?? [];
451 $allFeatures = array_keys($GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? []);
452 $configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
453 ->load($configurationManager->getDefaultConfigurationDescriptionFileLocation());
454 foreach ($allFeatures as $featureName) {
455 // Only features that have a .yml description will be listed. There is currently no
456 // way for extensions to extend this, so feature toggles of non-core extensions are
457 // not considered.
458 if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
459 if (isset($enabledFeaturesFromPost[$featureName])) {
460 $configurationManager->enableFeature($featureName);
461 } else {
462 $configurationManager->disableFeature($featureName);
463 }
464 }
465 }
466 $messages = [
467 new FlashMessage(
468 'Successfully updated feature toggles',
469 '',
470 FlashMessage::OK
471 )
472 ];
473 return new JsonResponse([
474 'success' => true,
475 'status' => $messages,
476 ]);
477 }
478 }