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