1<?php
2namespace TYPO3\CMS\Install\Service;
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
17use TYPO3\CMS\Core\Configuration\ConfigurationManager;
18use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
19use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
20use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface;
21use TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash;
22use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
23use TYPO3\CMS\Core\Crypto\Random;
24use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
25use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
26use TYPO3\CMS\Core\Utility\GeneralUtility;
27use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
28
29/**
30 * Execute "silent" LocalConfiguration upgrades if needed.
31 *
32 * Some LocalConfiguration settings are obsolete or changed over time.
33 * This class handles upgrades of these settings. It is called by
34 * the step controller at an early point.
35 *
36 * Every change is encapsulated in one method an must throw a ConfigurationChangedException
37 * if new data is written to LocalConfiguration. This is caught by above
38 * step controller to initiate a redirect and start again with adapted configuration.
39 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
40 */
41class SilentConfigurationUpgradeService
42{
43    /**
44     * @var \TYPO3\CMS\Core\Configuration\ConfigurationManager
45     */
46    protected $configurationManager;
47
48    /**
49     * List of obsolete configuration options in LocalConfiguration to be removed
50     * Example:
51     *    // #forge-ticket
52     *    'BE/somesetting',
53     *
54     * @var array
55     */
56    protected $obsoleteLocalConfigurationSettings = [
57        // #72400
58        'BE/spriteIconGenerator_handler',
59        // #72417
60        'SYS/lockingMode',
61        // #72473
62        'FE/secureFormmail',
63        'FE/strictFormmail',
64        'FE/formmailMaxAttachmentSize',
65        // #72337
66        'SYS/t3lib_cs_utils',
67        'SYS/t3lib_cs_convMethod',
68        // #72604
69        'SYS/maxFileNameLength',
70        // #72602
71        'BE/unzip_path',
72        // #72615
73        'BE/notificationPrefix',
74        // #72616
75        'BE/XCLASS',
76        'FE/XCLASS',
77        // #43085
78        'GFX/image_processing',
79        // #70056
80        'SYS/curlUse',
81        'SYS/curlProxyNTLM',
82        'SYS/curlProxyServer',
83        'SYS/curlProxyTunnel',
84        'SYS/curlProxyUserPass',
85        'SYS/curlTimeout',
86        // #75355
87        'BE/niceFlexFormXMLtags',
88        'BE/compactFlexFormXML',
89        // #75625
90        'SYS/clearCacheSystem',
91        // #77411
92        'SYS/caching/cacheConfigurations/extbase_typo3dbbackend_tablecolumns',
93        // #77460
94        'SYS/caching/cacheConfigurations/extbase_typo3dbbackend_queries',
95        // #79513
96        'FE/lockHashKeyWords',
97        'BE/lockHashKeyWords',
98        // #78835
99        'SYS/cookieHttpOnly',
100        // #71095
101        'BE/lang',
102        // #80050
103        'FE/cHashIncludePageId',
104        // #80711
105        'FE/noPHPscriptInclude',
106        'FE/maxSessionDataSize',
107        // #82162
108        'SYS/enable_errorDLOG',
109        'SYS/enable_exceptionDLOG',
110        // #82377
111        'EXT/allowSystemInstall',
112        // #82421
113        'SYS/sqlDebug',
114        'SYS/no_pconnect',
115        'SYS/setDBinit',
116        'SYS/dbClientCompress',
117        // #82430
118        'SYS/syslogErrorReporting',
119        // #82639
120        'SYS/enable_DLOG',
121        'SC_OPTIONS/t3lib/class.t3lib_userauth.php/writeDevLog',
122        'SC_OPTIONS/t3lib/class.t3lib_userauth.php/writeDevLogBE',
123        'SC_OPTIONS/t3lib/class.t3lib_userauth.php/writeDevLogFE',
124        // #82438
125        'SYS/enableDeprecationLog',
126        // #82680
127        'GFX/png_truecolor',
128        // #82803
129        'FE/content_doktypes',
130        // #83081
131        'BE/fileExtensions',
132        // #83768
133        'SYS/doNotCheckReferer',
134        // #83878
135        'SYS/isInitialInstallationInProgress',
136        'SYS/isInitialDatabaseImportDone',
137        // #84810
138        'BE/explicitConfirmationOfTranslation',
139    ];
140
141    public function __construct(ConfigurationManager $configurationManager = null)
142    {
143        $this->configurationManager = $configurationManager ?: GeneralUtility::makeInstance(ConfigurationManager::class);
144    }
145
146    /**
147     * Executed configuration upgrades. Single upgrade methods must throw a
148     * ConfigurationChangedException if something was written to LocalConfiguration.
149     *
150     * @throws ConfigurationChangedException
151     */
152    public function execute()
153    {
154        $this->generateEncryptionKeyIfNeeded();
155        $this->configureBackendLoginSecurity();
156        $this->configureFrontendLoginSecurity();
157        $this->migrateImageProcessorSetting();
158        $this->transferHttpSettings();
159        $this->disableImageMagickDetailSettingsIfImageMagickIsDisabled();
160        $this->setImageMagickDetailSettings();
161        $this->migrateThumbnailsPngSetting();
162        $this->migrateLockSslSetting();
163        $this->migrateDatabaseConnectionSettings();
164        $this->migrateDatabaseConnectionCharset();
165        $this->migrateDatabaseDriverOptions();
166        $this->migrateLangDebug();
167        $this->migrateCacheHashOptions();
168        $this->migrateExceptionErrors();
169        $this->migrateDisplayErrorsSetting();
170        $this->migrateSaltedPasswordsSettings();
171
172        // Should run at the end to prevent obsolete settings are removed before migration
173        $this->removeObsoleteLocalConfigurationSettings();
174    }
175
176    /**
177     * Some settings in LocalConfiguration vanished in DefaultConfiguration
178     * and have no impact on the core anymore.
179     * To keep the configuration clean, those old settings are just silently
180     * removed from LocalConfiguration if set.
181     *
182     * @throws ConfigurationChangedException
183     */
184    protected function removeObsoleteLocalConfigurationSettings()
185    {
186        $removed = $this->configurationManager->removeLocalConfigurationKeysByPath($this->obsoleteLocalConfigurationSettings);
187
188        // If something was changed: Trigger a reload to have new values in next request
189        if ($removed) {
190            $this->throwConfigurationChangedException();
191        }
192    }
193
194    /**
195     * Backend login security is set to rsa if rsaauth
196     * is installed (but not used) otherwise the default value "normal" has to be used.
197     * This forces either 'normal' or 'rsa' to be set in LocalConfiguration.
198     *
199     * @throws ConfigurationChangedException
200     */
201    protected function configureBackendLoginSecurity()
202    {
203        $rsaauthLoaded = ExtensionManagementUtility::isLoaded('rsaauth');
204        try {
205            $currentLoginSecurityLevelValue = $this->configurationManager->getLocalConfigurationValueByPath('BE/loginSecurityLevel');
206            if ($rsaauthLoaded && $currentLoginSecurityLevelValue !== 'rsa') {
207                $this->configurationManager->setLocalConfigurationValueByPath('BE/loginSecurityLevel', 'rsa');
208                $this->throwConfigurationChangedException();
209            } elseif (!$rsaauthLoaded && $currentLoginSecurityLevelValue !== 'normal') {
210                $this->configurationManager->setLocalConfigurationValueByPath('BE/loginSecurityLevel', 'normal');
211                $this->throwConfigurationChangedException();
212            }
213        } catch (MissingArrayPathException $e) {
214            // If an exception is thrown, the value is not set in LocalConfiguration
215            $this->configurationManager->setLocalConfigurationValueByPath(
216                'BE/loginSecurityLevel',
217                $rsaauthLoaded ? 'rsa' : 'normal'
218            );
219            $this->throwConfigurationChangedException();
220        }
221    }
222
223    /**
224     * Frontend login security is set to normal in case
225     * any other value is set while ext:rsaauth is not loaded.
226     *
227     * @throws ConfigurationChangedException
228     */
229    protected function configureFrontendLoginSecurity()
230    {
231        $rsaauthLoaded = ExtensionManagementUtility::isLoaded('rsaauth');
232        try {
233            $currentLoginSecurityLevelValue = $this->configurationManager->getLocalConfigurationValueByPath('FE/loginSecurityLevel');
234            if (!$rsaauthLoaded && $currentLoginSecurityLevelValue !== 'normal') {
235                $this->configurationManager->setLocalConfigurationValueByPath('FE/loginSecurityLevel', 'normal');
236                $this->throwConfigurationChangedException();
237            }
238        } catch (MissingArrayPathException $e) {
239            // no value set, just ignore
240        }
241    }
242
243    /**
244     * The encryption key is crucial for securing form tokens
245     * and the whole TYPO3 link rendering later on. A random key is set here in
246     * LocalConfiguration if it does not exist yet. This might possible happen
247     * during upgrading and will happen during first install.
248     *
249     * @throws ConfigurationChangedException
250     */
251    protected function generateEncryptionKeyIfNeeded()
252    {
253        try {
254            $currentValue = $this->configurationManager->getLocalConfigurationValueByPath('SYS/encryptionKey');
255        } catch (MissingArrayPathException $e) {
256            // If an exception is thrown, the value is not set in LocalConfiguration
257            $currentValue = '';
258        }
259
260        if (empty($currentValue)) {
261            $randomKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
262            $this->configurationManager->setLocalConfigurationValueByPath('SYS/encryptionKey', $randomKey);
263            $this->throwConfigurationChangedException();
264        }
265    }
266
267    /**
268     * Parse old curl and HTTP options and set new HTTP options, related to Guzzle
269     *
270     * @throws ConfigurationChangedException
271     */
272    protected function transferHttpSettings()
273    {
274        $changed = false;
275        $newParameters = [];
276        $obsoleteParameters = [];
277
278        // Remove / migrate options to new options
279        try {
280            // Check if the adapter option is set, if so, set it to the parameters that are obsolete
281            $this->configurationManager->getLocalConfigurationValueByPath('HTTP/adapter');
282            $obsoleteParameters[] = 'HTTP/adapter';
283        } catch (MissingArrayPathException $e) {
284            // Migration done already
285        }
286        try {
287            $newParameters['HTTP/version'] = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/protocol_version');
288            $obsoleteParameters[] = 'HTTP/protocol_version';
289        } catch (MissingArrayPathException $e) {
290            // Migration done already
291        }
292        try {
293            $this->configurationManager->getLocalConfigurationValueByPath('HTTP/ssl_verify_host');
294            $obsoleteParameters[] = 'HTTP/ssl_verify_host';
295        } catch (MissingArrayPathException $e) {
296            // Migration done already
297        }
298        try {
299            $legacyUserAgent = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/userAgent');
300            $newParameters['HTTP/headers/User-Agent'] = $legacyUserAgent;
301            $obsoleteParameters[] = 'HTTP/userAgent';
302        } catch (MissingArrayPathException $e) {
303            // Migration done already
304        }
305
306        // Redirects
307        try {
308            $legacyFollowRedirects = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/follow_redirects');
309            $obsoleteParameters[] = 'HTTP/follow_redirects';
310        } catch (MissingArrayPathException $e) {
311            $legacyFollowRedirects = '';
312        }
313        try {
314            $legacyMaximumRedirects = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/max_redirects');
315            $obsoleteParameters[] = 'HTTP/max_redirects';
316        } catch (MissingArrayPathException $e) {
317            $legacyMaximumRedirects = '';
318        }
319        try {
320            $legacyStrictRedirects = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/strict_redirects');
321            $obsoleteParameters[] = 'HTTP/strict_redirects';
322        } catch (MissingArrayPathException $e) {
323            $legacyStrictRedirects = '';
324        }
325
326        // Check if redirects have been disabled
327        if ($legacyFollowRedirects !== '' && (bool)$legacyFollowRedirects === false) {
328            $newParameters['HTTP/allow_redirects'] = false;
329        } elseif ($legacyMaximumRedirects !== '' || $legacyStrictRedirects !== '') {
330            $newParameters['HTTP/allow_redirects'] = [];
331            if ($legacyMaximumRedirects !== '' && (int)$legacyMaximumRedirects !== 5) {
332                $newParameters['HTTP/allow_redirects']['max'] = (int)$legacyMaximumRedirects;
333            }
334            if ($legacyStrictRedirects !== '' && (bool)$legacyStrictRedirects === true) {
335                $newParameters['HTTP/allow_redirects']['strict'] = true;
336            }
337            // defaults are used, no need to set the option in LocalConfiguration.php
338            if (empty($newParameters['HTTP/allow_redirects'])) {
339                unset($newParameters['HTTP/allow_redirects']);
340            }
341        }
342
343        // Migrate Proxy settings
344        try {
345            // Currently without protocol or port
346            $legacyProxyHost = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/proxy_host');
347            $obsoleteParameters[] = 'HTTP/proxy_host';
348        } catch (MissingArrayPathException $e) {
349            $legacyProxyHost = '';
350        }
351        try {
352            $legacyProxyPort = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/proxy_port');
353            $obsoleteParameters[] = 'HTTP/proxy_port';
354        } catch (MissingArrayPathException $e) {
355            $legacyProxyPort = '';
356        }
357        try {
358            $legacyProxyUser = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/proxy_user');
359            $obsoleteParameters[] = 'HTTP/proxy_user';
360        } catch (MissingArrayPathException $e) {
361            $legacyProxyUser = '';
362        }
363        try {
364            $legacyProxyPassword = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/proxy_password');
365            $obsoleteParameters[] = 'HTTP/proxy_password';
366        } catch (MissingArrayPathException $e) {
367            $legacyProxyPassword = '';
368        }
369        // Auth Scheme: Basic, digest etc.
370        try {
371            $legacyProxyAuthScheme = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/proxy_auth_scheme');
372            $obsoleteParameters[] = 'HTTP/proxy_auth_scheme';
373        } catch (MissingArrayPathException $e) {
374            $legacyProxyAuthScheme = '';
375        }
376
377        if ($legacyProxyHost !== '') {
378            $proxy = 'http://';
379            if ($legacyProxyAuthScheme !== '' && $legacyProxyUser !== '' && $legacyProxyPassword !== '') {
380                $proxy .= $legacyProxyUser . ':' . $legacyProxyPassword . '@';
381            }
382            $proxy .= $legacyProxyHost;
383            if ($legacyProxyPort !== '') {
384                $proxy .= ':' . $legacyProxyPort;
385            }
386            $newParameters['HTTP/proxy'] = $proxy;
387        }
388
389        // Verify peers
390        // see http://docs.guzzlephp.org/en/latest/request-options.html#verify
391        try {
392            $legacySslVerifyPeer = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/ssl_verify_peer');
393            $obsoleteParameters[] = 'HTTP/ssl_verify_peer';
394        } catch (MissingArrayPathException $e) {
395            $legacySslVerifyPeer = '';
396        }
397
398        // Directory holding multiple Certificate Authority files
399        try {
400            $legacySslCaPath = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/ssl_capath');
401            $obsoleteParameters[] = 'HTTP/ssl_capath';
402        } catch (MissingArrayPathException $e) {
403            $legacySslCaPath = '';
404        }
405        // Certificate Authority file to verify the peer with (use when ssl_verify_peer is TRUE)
406        try {
407            $legacySslCaFile = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/ssl_cafile');
408            $obsoleteParameters[] = 'HTTP/ssl_cafile';
409        } catch (MissingArrayPathException $e) {
410            $legacySslCaFile = '';
411        }
412        if ($legacySslVerifyPeer !== '') {
413            if ($legacySslCaFile !== '' && $legacySslCaPath !== '') {
414                $newParameters['HTTP/verify'] = $legacySslCaPath . $legacySslCaFile;
415            } elseif ((bool)$legacySslVerifyPeer === false) {
416                $newParameters['HTTP/verify'] = false;
417            }
418        }
419
420        // SSL Key + Passphrase
421        // Name of a file containing local certificate
422        try {
423            $legacySslLocalCert = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/ssl_local_cert');
424            $obsoleteParameters[] = 'HTTP/ssl_local_cert';
425        } catch (MissingArrayPathException $e) {
426            $legacySslLocalCert = '';
427        }
428
429        // Passphrase with which local certificate was encoded
430        try {
431            $legacySslPassphrase = $this->configurationManager->getLocalConfigurationValueByPath('HTTP/ssl_passphrase');
432            $obsoleteParameters[] = 'HTTP/ssl_passphrase';
433        } catch (MissingArrayPathException $e) {
434            $legacySslPassphrase = '';
435        }
436
437        if ($legacySslLocalCert !== '') {
438            if ($legacySslPassphrase !== '') {
439                $newParameters['HTTP/ssl_key'] = [
440                    $legacySslLocalCert,
441                    $legacySslPassphrase
442                ];
443            } else {
444                $newParameters['HTTP/ssl_key'] = $legacySslLocalCert;
445            }
446        }
447
448        // Update the LocalConfiguration file if obsolete parameters or new parameters are set
449        if (!empty($obsoleteParameters)) {
450            $this->configurationManager->removeLocalConfigurationKeysByPath($obsoleteParameters);
451            $changed = true;
452        }
453        if (!empty($newParameters)) {
454            $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($newParameters);
455            $changed = true;
456        }
457        if ($changed) {
458            $this->throwConfigurationChangedException();
459        }
460    }
461
462    /**
463     * Detail configuration of Image Magick settings must be cleared
464     * if Image Magick handling is disabled.
465     *
466     * "Configuration presets" in install tool is not type safe, so value
467     * comparisons here are not type safe too, to not trigger changes to
468     * LocalConfiguration again.
469     *
470     * @throws ConfigurationChangedException
471     */
472    protected function disableImageMagickDetailSettingsIfImageMagickIsDisabled()
473    {
474        $changedValues = [];
475        try {
476            $currentEnabledValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor_enabled');
477        } catch (MissingArrayPathException $e) {
478            $currentEnabledValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/processor_enabled');
479        }
480
481        try {
482            $currentPathValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor_path');
483        } catch (MissingArrayPathException $e) {
484            $currentPathValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/processor_path');
485        }
486
487        try {
488            $currentPathLzwValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor_path_lzw');
489        } catch (MissingArrayPathException $e) {
490            $currentPathLzwValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/processor_path_lzw');
491        }
492
493        try {
494            $currentImageFileExtValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/imagefile_ext');
495        } catch (MissingArrayPathException $e) {
496            $currentImageFileExtValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/imagefile_ext');
497        }
498
499        try {
500            $currentThumbnailsValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/thumbnails');
501        } catch (MissingArrayPathException $e) {
502            $currentThumbnailsValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/thumbnails');
503        }
504
505        if (!$currentEnabledValue) {
506            if ($currentPathValue != '') {
507                $changedValues['GFX/processor_path'] = '';
508            }
509            if ($currentPathLzwValue != '') {
510                $changedValues['GFX/processor_path_lzw'] = '';
511            }
512            if ($currentImageFileExtValue !== 'gif,jpg,jpeg,png') {
513                $changedValues['GFX/imagefile_ext'] = 'gif,jpg,jpeg,png';
514            }
515            if ($currentThumbnailsValue != 0) {
516                $changedValues['GFX/thumbnails'] = 0;
517            }
518        }
519        if (!empty($changedValues)) {
520            $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($changedValues);
521            $this->throwConfigurationChangedException();
522        }
523    }
524
525    /**
526     * Detail configuration of Image Magick and Graphics Magick settings
527     * depending on main values.
528     *
529     * "Configuration presets" in install tool is not type safe, so value
530     * comparisons here are not type safe too, to not trigger changes to
531     * LocalConfiguration again.
532     *
533     * @throws ConfigurationChangedException
534     */
535    protected function setImageMagickDetailSettings()
536    {
537        $changedValues = [];
538        try {
539            $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor');
540        } catch (MissingArrayPathException $e) {
541            $currentProcessorValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/processor');
542        }
543
544        try {
545            $currentProcessorMaskValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor_allowTemporaryMasksAsPng');
546        } catch (MissingArrayPathException $e) {
547            $currentProcessorMaskValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/processor_allowTemporaryMasksAsPng');
548        }
549
550        try {
551            $currentProcessorEffectsValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor_effects');
552        } catch (MissingArrayPathException $e) {
553            $currentProcessorEffectsValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/processor_effects');
554        }
555
556        if ((string)$currentProcessorValue !== '') {
557            if (!is_bool($currentProcessorEffectsValue)) {
558                $changedValues['GFX/processor_effects'] = (int)$currentProcessorEffectsValue > 0;
559            }
560
561            if ($currentProcessorMaskValue != 0) {
562                $changedValues['GFX/processor_allowTemporaryMasksAsPng'] = 0;
563            }
564        }
565        if (!empty($changedValues)) {
566            $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($changedValues);
567            $this->throwConfigurationChangedException();
568        }
569    }
570
571    /**
572     * Migrate the definition of the image processor from the configuration value
573     * im_version_5 to the setting processor.
574     *
575     * @throws ConfigurationChangedException
576     */
577    protected function migrateImageProcessorSetting()
578    {
579        $changedSettings = [];
580        $settingsToRename = [
581            'GFX/im' => 'GFX/processor_enabled',
582            'GFX/im_version_5' => 'GFX/processor',
583            'GFX/im_v5effects' => 'GFX/processor_effects',
584            'GFX/im_path' => 'GFX/processor_path',
585            'GFX/im_path_lzw' => 'GFX/processor_path_lzw',
586            'GFX/im_mask_temp_ext_gif' => 'GFX/processor_allowTemporaryMasksAsPng',
587            'GFX/im_noScaleUp' => 'GFX/processor_allowUpscaling',
588            'GFX/im_noFramePrepended' => 'GFX/processor_allowFrameSelection',
589            'GFX/im_stripProfileCommand' => 'GFX/processor_stripColorProfileCommand',
590            'GFX/im_useStripProfileByDefault' => 'GFX/processor_stripColorProfileByDefault',
591            'GFX/colorspace' => 'GFX/processor_colorspace',
592        ];
593
594        foreach ($settingsToRename as $oldPath => $newPath) {
595            try {
596                $value = $this->configurationManager->getLocalConfigurationValueByPath($oldPath);
597                $this->configurationManager->setLocalConfigurationValueByPath($newPath, $value);
598                $changedSettings[$oldPath] = true;
599            } catch (MissingArrayPathException $e) {
600                // If an exception is thrown, the value is not set in LocalConfiguration
601                $changedSettings[$oldPath] = false;
602            }
603        }
604
605        if (!empty($changedSettings['GFX/im_version_5'])) {
606            $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_version_5');
607            $newProcessorValue = $currentProcessorValue === 'gm' ? 'GraphicsMagick' : 'ImageMagick';
608            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor', $newProcessorValue);
609        }
610
611        if (!empty($changedSettings['GFX/im_noScaleUp'])) {
612            $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_noScaleUp');
613            $newProcessorValue = !$currentProcessorValue;
614            $this->configurationManager->setLocalConfigurationValueByPath(
615                'GFX/processor_allowUpscaling',
616                $newProcessorValue
617            );
618        }
619
620        if (!empty($changedSettings['GFX/im_noFramePrepended'])) {
621            $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_noFramePrepended');
622            $newProcessorValue = !$currentProcessorValue;
623            $this->configurationManager->setLocalConfigurationValueByPath(
624                'GFX/processor_allowFrameSelection',
625                $newProcessorValue
626            );
627        }
628
629        if (!empty($changedSettings['GFX/im_mask_temp_ext_gif'])) {
630            $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_mask_temp_ext_gif');
631            $newProcessorValue = !$currentProcessorValue;
632            $this->configurationManager->setLocalConfigurationValueByPath(
633                'GFX/processor_allowTemporaryMasksAsPng',
634                $newProcessorValue
635            );
636        }
637
638        if (!empty(array_filter($changedSettings))) {
639            $this->configurationManager->removeLocalConfigurationKeysByPath(array_keys($changedSettings));
640            $this->throwConfigurationChangedException();
641        }
642    }
643
644    /**
645     * Throw exception after configuration change to trigger a redirect.
646     *
647     * @throws ConfigurationChangedException
648     */
649    protected function throwConfigurationChangedException()
650    {
651        throw new ConfigurationChangedException(
652            'Configuration updated, reload needed',
653            1379024938
654        );
655    }
656
657    /**
658     * Migrate the configuration value thumbnails_png to a boolean value.
659     *
660     * @throws ConfigurationChangedException
661     */
662    protected function migrateThumbnailsPngSetting()
663    {
664        $changedValues = [];
665        try {
666            $currentThumbnailsPngValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/thumbnails_png');
667        } catch (MissingArrayPathException $e) {
668            $currentThumbnailsPngValue = $this->configurationManager->getDefaultConfigurationValueByPath('GFX/thumbnails_png');
669        }
670
671        if (is_int($currentThumbnailsPngValue) && $currentThumbnailsPngValue > 0) {
672            $changedValues['GFX/thumbnails_png'] = true;
673        }
674        if (!empty($changedValues)) {
675            $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($changedValues);
676            $this->throwConfigurationChangedException();
677        }
678    }
679
680    /**
681     * Migrate the configuration setting BE/lockSSL to boolean if set in the LocalConfiguration.php file
682     *
683     * @throws ConfigurationChangedException
684     */
685    protected function migrateLockSslSetting()
686    {
687        try {
688            $currentOption = $this->configurationManager->getLocalConfigurationValueByPath('BE/lockSSL');
689            // check if the current option is an integer/string and if it is active
690            if (!is_bool($currentOption) && (int)$currentOption > 0) {
691                $this->configurationManager->setLocalConfigurationValueByPath('BE/lockSSL', true);
692                $this->throwConfigurationChangedException();
693            }
694        } catch (MissingArrayPathException $e) {
695            // no change inside the LocalConfiguration.php found, so nothing needs to be modified
696        }
697    }
698
699    /**
700     * Move the database connection settings to a "Default" connection
701     *
702     * @throws ConfigurationChangedException
703     */
704    protected function migrateDatabaseConnectionSettings()
705    {
706        $confManager = $this->configurationManager;
707
708        $newSettings = [];
709        $removeSettings = [];
710
711        try {
712            $value = $confManager->getLocalConfigurationValueByPath('DB/username');
713            $removeSettings[] = 'DB/username';
714            $newSettings['DB/Connections/Default/user'] = $value;
715        } catch (MissingArrayPathException $e) {
716            // Old setting does not exist, do nothing
717        }
718
719        try {
720            $value = $confManager->getLocalConfigurationValueByPath('DB/password');
721            $removeSettings[] = 'DB/password';
722            $newSettings['DB/Connections/Default/password'] = $value;
723        } catch (MissingArrayPathException $e) {
724            // Old setting does not exist, do nothing
725        }
726
727        try {
728            $value = $confManager->getLocalConfigurationValueByPath('DB/host');
729            $removeSettings[] = 'DB/host';
730            $newSettings['DB/Connections/Default/host'] = $value;
731        } catch (MissingArrayPathException $e) {
732            // Old setting does not exist, do nothing
733        }
734
735        try {
736            $value = $confManager->getLocalConfigurationValueByPath('DB/port');
737            $removeSettings[] = 'DB/port';
738            $newSettings['DB/Connections/Default/port'] = $value;
739        } catch (MissingArrayPathException $e) {
740            // Old setting does not exist, do nothing
741        }
742
743        try {
744            $value = $confManager->getLocalConfigurationValueByPath('DB/socket');
745            $removeSettings[] = 'DB/socket';
746            // Remove empty socket connects
747            if (!empty($value)) {
748                $newSettings['DB/Connections/Default/unix_socket'] = $value;
749            }
750        } catch (MissingArrayPathException $e) {
751            // Old setting does not exist, do nothing
752        }
753
754        try {
755            $value = $confManager->getLocalConfigurationValueByPath('DB/database');
756            $removeSettings[] = 'DB/database';
757            $newSettings['DB/Connections/Default/dbname'] = $value;
758        } catch (MissingArrayPathException $e) {
759            // Old setting does not exist, do nothing
760        }
761
762        try {
763            $value = (bool)$confManager->getLocalConfigurationValueByPath('SYS/dbClientCompress');
764            $removeSettings[] = 'SYS/dbClientCompress';
765            if ($value) {
766                $newSettings['DB/Connections/Default/driverOptions'] = [
767                    'flags' => MYSQLI_CLIENT_COMPRESS,
768                ];
769            }
770        } catch (MissingArrayPathException $e) {
771            // Old setting does not exist, do nothing
772        }
773
774        try {
775            $value = (bool)$confManager->getLocalConfigurationValueByPath('SYS/no_pconnect');
776            $removeSettings[] = 'SYS/no_pconnect';
777            if (!$value) {
778                $newSettings['DB/Connections/Default/persistentConnection'] = true;
779            }
780        } catch (MissingArrayPathException $e) {
781            // Old setting does not exist, do nothing
782        }
783
784        try {
785            $value = $confManager->getLocalConfigurationValueByPath('SYS/setDBinit');
786            $removeSettings[] = 'SYS/setDBinit';
787            $newSettings['DB/Connections/Default/initCommands'] = $value;
788        } catch (MissingArrayPathException $e) {
789            // Old setting does not exist, do nothing
790        }
791
792        try {
793            $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/charset');
794        } catch (MissingArrayPathException $e) {
795            // If there is no charset option yet, add it.
796            $newSettings['DB/Connections/Default/charset'] = 'utf8';
797        }
798
799        try {
800            $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/driver');
801        } catch (MissingArrayPathException $e) {
802            // Use the mysqli driver by default if no value has been provided yet
803            $newSettings['DB/Connections/Default/driver'] = 'mysqli';
804        }
805
806        // Add new settings and remove old ones
807        if (!empty($newSettings)) {
808            $confManager->setLocalConfigurationValuesByPathValuePairs($newSettings);
809        }
810        if (!empty($removeSettings)) {
811            $confManager->removeLocalConfigurationKeysByPath($removeSettings);
812        }
813
814        // Throw redirect if something was changed
815        if (!empty($newSettings) || !empty($removeSettings)) {
816            $this->throwConfigurationChangedException();
817        }
818    }
819
820    /**
821     * Migrate the configuration setting DB/Connections/Default/charset to 'utf8' as
822     * 'utf-8' is not supported by all MySQL versions.
823     *
824     * @throws ConfigurationChangedException
825     */
826    protected function migrateDatabaseConnectionCharset()
827    {
828        $confManager = $this->configurationManager;
829        try {
830            $driver = $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/driver');
831            $charset = $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/charset');
832            if (in_array($driver, ['mysqli', 'pdo_mysql', 'drizzle_pdo_mysql'], true) && $charset === 'utf-8') {
833                $confManager->setLocalConfigurationValueByPath('DB/Connections/Default/charset', 'utf8');
834                $this->throwConfigurationChangedException();
835            }
836        } catch (MissingArrayPathException $e) {
837            // no incompatible charset configuration found, so nothing needs to be modified
838        }
839    }
840
841    /**
842     * Migrate the configuration setting DB/Connections/Default/driverOptions to array type.
843     *
844     * @throws ConfigurationChangedException
845     */
846    protected function migrateDatabaseDriverOptions()
847    {
848        $confManager = $this->configurationManager;
849        try {
850            $options = $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/driverOptions');
851            if (!is_array($options)) {
852                $confManager->setLocalConfigurationValueByPath(
853                    'DB/Connections/Default/driverOptions',
854                    ['flags' => (int)$options]
855                );
856                $this->throwConfigurationChangedException();
857            }
858        } catch (MissingArrayPathException $e) {
859            // no driver options found, nothing needs to be modified
860        }
861    }
862
863    /**
864     * Migrate the configuration setting BE/lang/debug if set in the LocalConfiguration.php file
865     *
866     * @throws ConfigurationChangedException
867     */
868    protected function migrateLangDebug()
869    {
870        $confManager = $this->configurationManager;
871        try {
872            $currentOption = $confManager->getLocalConfigurationValueByPath('BE/lang/debug');
873            // check if the current option is set and boolean
874            if (isset($currentOption) && is_bool($currentOption)) {
875                $confManager->setLocalConfigurationValueByPath('BE/languageDebug', $currentOption);
876                $this->throwConfigurationChangedException();
877            }
878        } catch (MissingArrayPathException $e) {
879            // no change inside the LocalConfiguration.php found, so nothing needs to be modified
880        }
881    }
882
883    /**
884     * Migrate single cache hash related options under "FE" into "FE/cacheHash"
885     *
886     * @throws ConfigurationChangedException
887     */
888    protected function migrateCacheHashOptions()
889    {
890        $confManager = $this->configurationManager;
891        $removeSettings = [];
892        $newSettings = [];
893
894        try {
895            $value = $confManager->getLocalConfigurationValueByPath('FE/cHashOnlyForParameters');
896            $removeSettings[] = 'FE/cHashOnlyForParameters';
897            $newSettings['FE/cacheHash/cachedParametersWhiteList'] = GeneralUtility::trimExplode(',', $value, true);
898        } catch (MissingArrayPathException $e) {
899            // Migration done already
900        }
901
902        try {
903            $value = $confManager->getLocalConfigurationValueByPath('FE/cHashExcludedParameters');
904            $removeSettings[] = 'FE/cHashExcludedParameters';
905            $newSettings['FE/cacheHash/excludedParameters'] = GeneralUtility::trimExplode(',', $value, true);
906        } catch (MissingArrayPathException $e) {
907            // Migration done already
908        }
909
910        try {
911            $value = $confManager->getLocalConfigurationValueByPath('FE/cHashRequiredParameters');
912            $removeSettings[] = 'FE/cHashRequiredParameters';
913            $newSettings['FE/cacheHash/requireCacheHashPresenceParameters'] = GeneralUtility::trimExplode(',', $value, true);
914        } catch (MissingArrayPathException $e) {
915            // Migration done already
916        }
917
918        try {
919            $value = $confManager->getLocalConfigurationValueByPath('FE/cHashExcludedParametersIfEmpty');
920            $removeSettings[] = 'FE/cHashExcludedParametersIfEmpty';
921            if (trim($value) === '*') {
922                $newSettings['FE/cacheHash/excludeAllEmptyParameters'] = true;
923            } else {
924                $newSettings['FE/cacheHash/excludedParametersIfEmpty'] = GeneralUtility::trimExplode(',', $value, true);
925            }
926        } catch (MissingArrayPathException $e) {
927            // Migration done already
928        }
929
930        // Add new settings and remove old ones
931        if (!empty($newSettings)) {
932            $confManager->setLocalConfigurationValuesByPathValuePairs($newSettings);
933        }
934        if (!empty($removeSettings)) {
935            $confManager->removeLocalConfigurationKeysByPath($removeSettings);
936        }
937
938        // Throw redirect if something was changed
939        if (!empty($newSettings) || !empty($removeSettings)) {
940            $this->throwConfigurationChangedException();
941        }
942    }
943
944    /**
945     * Migrate SYS/exceptionalErrors to not contain E_USER_DEPRECATED
946     *
947     * @throws ConfigurationChangedException
948     */
949    protected function migrateExceptionErrors()
950    {
951        $confManager = $this->configurationManager;
952        try {
953            $currentOption = (int)$confManager->getLocalConfigurationValueByPath('SYS/exceptionalErrors');
954            // make sure E_USER_DEPRECATED is not part of the exceptionalErrors
955            if ($currentOption & E_USER_DEPRECATED) {
956                $confManager->setLocalConfigurationValueByPath('SYS/exceptionalErrors', $currentOption & ~E_USER_DEPRECATED);
957                $this->throwConfigurationChangedException();
958            }
959        } catch (MissingArrayPathException $e) {
960            // no change inside the LocalConfiguration.php found, so nothing needs to be modified
961        }
962    }
963
964    /**
965     * Migrate SYS/displayErrors to not contain 2
966     *
967     * @throws ConfigurationChangedException
968     */
969    protected function migrateDisplayErrorsSetting()
970    {
971        $confManager = $this->configurationManager;
972        try {
973            $currentOption = (int)$confManager->getLocalConfigurationValueByPath('SYS/displayErrors');
974            // make sure displayErrors is set to 2
975            if ($currentOption === 2) {
976                $confManager->setLocalConfigurationValueByPath('SYS/displayErrors', -1);
977                $this->throwConfigurationChangedException();
978            }
979        } catch (MissingArrayPathException $e) {
980            // no change inside the LocalConfiguration.php found, so nothing needs to be modified
981        }
982    }
983
984    /**
985     * Migrate salted passwords extension configuration settings to BE/passwordHashing and FE/passwordHashing
986     *
987     * @throws ConfigurationChangedException
988     */
989    protected function migrateSaltedPasswordsSettings()
990    {
991        $confManager = $this->configurationManager;
992        $configsToRemove = [];
993        try {
994            $extensionConfiguration = (array)$confManager->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords');
995            $configsToRemove[] = 'EXTENSIONS/saltedpasswords';
996        } catch (MissingArrayPathException $e) {
997            $extensionConfiguration = [];
998        }
999        try {
1000            // The silent upgrade may be executed before LayoutController synchronized old serialized extConf
1001            // settings to EXTENSIONS if upgrading from v8 to v9.
1002            $extConfConfiguration = (string)$confManager->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords');
1003            $configsToRemove[] = 'EXT/extConf/saltedpasswords';
1004        } catch (MissingArrayPathException $e) {
1005            $extConfConfiguration = [];
1006        }
1007        // Migration already done
1008        if (empty($extensionConfiguration) && empty($extConfConfiguration)) {
1009            return;
1010        }
1011        // Upgrade to best available hash method. This is only done once since that code will no longer be reached
1012        // after first migration because extConf and EXTENSIONS array entries are gone then. Thus, a manual selection
1013        // to some different hash mechanism will not be touched again after first upgrade.
1014        // Phpass is always available, so we have some last fallback if the others don't kick in
1015        $okHashMethods = [
1016            Argon2iPasswordHash::class,
1017            BcryptPasswordHash::class,
1018            Pbkdf2PasswordHash::class,
1019            PhpassPasswordHash::class,
1020        ];
1021        $newMethods = [];
1022        foreach (['BE', 'FE'] as $mode) {
1023            foreach ($okHashMethods as $className) {
1024                /** @var PasswordHashInterface $instance */
1025                $instance = GeneralUtility::makeInstance($className);
1026                if ($instance->isAvailable()) {
1027                    $newMethods[$mode] = $className;
1028                    break;
1029                }
1030            }
1031        }
1032        // We only need to write to LocalConfiguration if method is different than Argon2i from DefaultConfiguration
1033        $newConfig = [];
1034        if ($newMethods['BE'] !== Argon2iPasswordHash::class) {
1035            $newConfig['BE/passwordHashing/className'] = $newMethods['BE'];
1036        }
1037        if ($newMethods['FE'] !== Argon2iPasswordHash::class) {
1038            $newConfig['FE/passwordHashing/className'] = $newMethods['FE'];
1039        }
1040        if (!empty($newConfig)) {
1041            $confManager->setLocalConfigurationValuesByPathValuePairs($newConfig);
1042        }
1043        $confManager->removeLocalConfigurationKeysByPath($configsToRemove);
1044        $this->throwConfigurationChangedException();
1045    }
1046}
1047