diff --git a/src/Config/ConfigServiceProvider.php b/src/Config/ConfigServiceProvider.php index e822ab7d48c..8978c08aea7 100644 --- a/src/Config/ConfigServiceProvider.php +++ b/src/Config/ConfigServiceProvider.php @@ -10,11 +10,9 @@ use CraftCms\Cms\Support\Typecast; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables; -use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\File; use Illuminate\Support\ServiceProvider; use Override; -use Throwable; class ConfigServiceProvider extends ServiceProvider { @@ -32,7 +30,15 @@ public function register(): void $this->loadEnvironmentVariablesWhenConfigIsCached(); - $this->app->singleton(GeneralConfig::class, fn () => $this->app->make(ConfigRepository::class)->get('craft.general')); + if (! $this->app->bound('config')) { + return; + } + + $this->app->singleton(GeneralConfig::class, function () { + $repository = $this->app->make(ConfigRepository::class); + + return $this->loadGeneralConfig($repository); + }); collect($this->configFiles)->each(function (string $file) { if ($file === 'general') { @@ -45,8 +51,9 @@ public function register(): void public function boot(): void { + $this->app->make(GeneralConfig::class); + $this->bootPublishables(); - $this->loadGeneralConfig(); $this->loadHtmlSanitizers(); } @@ -76,46 +83,47 @@ private function bootPublishables(): void }); } - private function loadGeneralConfig(): void + private function loadGeneralConfig(ConfigRepository $repository): GeneralConfig { - $generalConfig = Config::get('craft.general', []); + /** @var GeneralConfig|array $staticConfig */ + $staticConfig = $repository->get('craft.general', []); - /** - * When the configuration is a simple array config, load it into - * the GeneralConfig object and replace the configuration key. - */ - if (! $generalConfig instanceof GeneralConfig) { - $generalConfig = GeneralConfig::__set_state($generalConfig); + if ($staticConfig instanceof GeneralConfig) { + $config = clone $staticConfig; + } else { + Typecast::properties(GeneralConfig::class, $staticConfig); + $config = GeneralConfig::create(); - Config::set('craft.general', $generalConfig); + foreach ($staticConfig as $setting => $value) { + $this->applyGeneralConfigSetting($config, $setting, $value); + } } - $configClass = $generalConfig::class; + $envConfig = Env::config($config::class, 'CRAFT_'); + Typecast::properties($config::class, $envConfig); - // Get any environment value overrides - $envConfig = Env::config($configClass, 'CRAFT_'); + foreach ($envConfig as $setting => $value) { + $this->applyGeneralConfigSetting($config, $setting, $value); + } - Typecast::properties($configClass, $envConfig); + foreach ($config->aliases as $name => $value) { + Aliases::set($name, $value); + } - foreach ($envConfig as $name => $value) { - // Use the fluent methods when possible, in case it has any value normalization logic - if (! method_exists($generalConfig, $name)) { - continue; - } + $repository->set('craft.general', $config); - try { - $generalConfig->$name($value); + return $config; + } - continue; - } catch (Throwable) { - } + private function applyGeneralConfigSetting(GeneralConfig $config, string $setting, mixed $value): void + { + if (method_exists($config, $setting)) { + $config->{$setting}($value); - $generalConfig->$name = $value; + return; } - foreach ($generalConfig->aliases as $name => $value) { - Aliases::set($name, $value); - } + $config->{$setting} = $value; } private function loadHtmlSanitizers(): void diff --git a/src/Config/GeneralConfig.php b/src/Config/GeneralConfig.php index 69a0e12e57a..ec694e627d4 100644 --- a/src/Config/GeneralConfig.php +++ b/src/Config/GeneralConfig.php @@ -8,13 +8,9 @@ use CraftCms\Cms\Auth\Enums\CpAuthPath; use CraftCms\Cms\Support\Attributes\EnvName; use CraftCms\Cms\Support\Config as ConfigHelper; -use CraftCms\Cms\Support\DateTimeHelper; use CraftCms\Cms\Support\Env; -use CraftCms\Cms\Support\Facades\Deprecator; use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\PHP; -use DateInterval; -use Illuminate\Support\Facades\Config; use Illuminate\Support\Traits\Conditionable; use InvalidArgumentException; use Override; @@ -37,18 +33,7 @@ class GeneralConfig extends BaseConfig public const string SNAKE_CASE = 'snake'; #[Override] - protected static array $renamedSettings = [ - 'activateAccountFailurePath' => 'invalidUserTokenPath', - 'allowAutoUpdates' => 'allowUpdates', - 'backupDbOnUpdate' => 'backupOnUpdate', - 'defaultFilePermissions' => 'defaultFileMode', - 'defaultFolderPermissions' => 'defaultDirMode', - 'enableGraphQlCaching' => 'enableGraphqlCaching', - 'environmentVariables' => 'aliases', - 'isSystemOn' => 'isSystemLive', - 'restoreDbOnUpdateFailure' => 'restoreOnUpdateFailure', - 'validationKey' => 'securityKey', - ]; + protected static array $renamedSettings = []; /** * @var array The default user accessibility preferences that should be applied to users that haven’t saved their preferences yet. @@ -642,24 +627,6 @@ class GeneralConfig extends BaseConfig */ public ?string $cpTrigger = 'admin'; - /** - * @var string The domain that cookies generated by Craft should be created for. If blank, it will be left up to the browser to determine - * which domain to use (almost always the current). If you want the cookies to work for all subdomains, for example, you could - * set this to `'.my-project.tld'`. - * - * ::: code - * ```php Static Config - * ->defaultCookieDomain('.my-project.tld') - * ``` - * ```shell Environment Override - * CRAFT_DEFAULT_COOKIE_DOMAIN=.my-project.tld - * ``` - * ::: - * - * @group Environment - */ - public string $defaultCookieDomain = ''; - /** * @var string The two-letter country code that addresses will be set to by default. * @@ -2256,28 +2223,6 @@ class GeneralConfig extends BaseConfig */ public mixed $rememberUsernameDuration = 31536000; - /** - * @var mixed The amount of time a user stays logged if “Remember Me” is checked on the login page. - * - * Set to `0` to disable the “Remember Me” feature altogether. - * - * See {@see ConfigHelper::durationInSeconds()} for a list of supported value types. - * - * ::: code - * ```php Static Config - * ->rememberedUserSessionDuration(0) - * ``` - * ```shell Environment Override - * CRAFT_REMEMBERED_USER_SESSION_DURATION=0 - * ``` - * ::: - * - * @group Session - * - * @defaultAlt 14 days - */ - public mixed $rememberedUserSessionDuration = 1209600; - /** * @var string The path to the root directory that should store published control panel resources. * @@ -3110,28 +3055,6 @@ class GeneralConfig extends BaseConfig */ public string|bool $useSslOnTokenizedUrls = 'auto'; - /** - * @var mixed The amount of time a user verification code can be used before expiring. - * - * See {@see ConfigHelper::durationInSeconds()} for a list of supported value types. - * - * ::: code - * ```php Static Config - * // 1 hour - * ->verificationCodeDuration(3600) - * ``` - * ```shell Environment Override - * # 1 hour - * CRAFT_VERIFICATION_CODE_DURATION=3600 - * ``` - * ::: - * - * @group Security - * - * @defaultAlt 1 day - */ - public mixed $verificationCodeDuration = 86400; - /** * @var mixed The URI or URL that Craft should use for email verification links on the front end. * @@ -3191,7 +3114,6 @@ public function __construct() ->purgeUnsavedDraftsDuration($this->purgeUnsavedDraftsDuration) ->rememberUsernameDuration($this->rememberUsernameDuration) ->softDeleteDuration($this->softDeleteDuration) - ->verificationCodeDuration($this->verificationCodeDuration) // locales ->defaultCpLanguage($this->defaultCpLanguage) ->extraAppLocales($this->extraAppLocales) @@ -3782,32 +3704,6 @@ public function cpTrigger(?string $value): self return $this; } - /** - * The domain that cookies generated by Craft should be created for. If blank, it will be left up to the browser to determine - * which domain to use (almost always the current). If you want the cookies to work for all subdomains, for example, you could - * set this to `'.my-project.tld'`. - * - * ```php - * ->defaultCookieDomain('.my-project.tld') - * ``` - * - * @group Environment - * - * @see $defaultCookieDomain - * @since 4.2.0 - */ - public function defaultCookieDomain(string $value): self - { - $this->defaultCookieDomain = $value; - - app()->booting(function () use ($value) { - Deprecator::log('generalConfig.defaultCookieDomain', 'Calling defaultCookieDomain() is deprecated.'); - Config::set('session.domain', $value); - }); - - return $this; - } - /** * The two-letter country code that addresses will be set to by default. * @@ -5510,47 +5406,6 @@ public function rasterizeSvgThumbs(bool $value = true): self return $this; } - /** - * The amount of time a user stays logged if “Remember Me” is checked on the login page. - * - * Set to `0` to disable the “Remember Me” feature altogether. - * - * See {@see ConfigHelper::durationInSeconds()} for a list of supported value types. - * - * ```php - * ->rememberedUserSessionDuration(0) - * ``` - * - * @group Session - * - * @defaultAlt 14 days - * - * @throws RuntimeException - * - * @see $rememberedUserSessionDuration - * @since 4.2.0 - */ - public function rememberedUserSessionDuration(mixed $value): self - { - // Store the DateInterval separately for getRememberedUserSessionDuration() - try { - $interval = DateTimeHelper::toDateInterval($value); - } catch (InvalidArgumentException $e) { - throw new RuntimeException($e->getMessage(), 0, $e); - } - - $this->rememberedUserSessionDuration = $interval ? ConfigHelper::durationInSeconds($interval) : 0; - - app()->booting(function () { - Config::set( - 'auth.guards.craft.remember', - floor($this->rememberedUserSessionDuration / 60), - ); - }); - - return $this; - } - /** * The amount of time Craft will remember a username and pre-populate it on the control panel’s Login page. * @@ -6318,33 +6173,6 @@ public function useSslOnTokenizedUrls(string|bool $value): self return $this; } - /** - * The amount of time a user verification code can be used before expiring. - * - * See {@see ConfigHelper::durationInSeconds()} for a list of supported value types. - * - * ```php - * // 1 hour - * ->verificationCodeDuration(3600) - * ``` - * - * @group Security - * - * @defaultAlt 1 day - * - * @see $verificationCodeDuration - */ - public function verificationCodeDuration(mixed $value): self - { - $this->verificationCodeDuration = ConfigHelper::durationInSeconds($value); - - app()->booted(function () { - Config::set('auth.passwords.craft.expire', $this->verificationCodeDuration); - }); - - return $this; - } - /** * The URI or URL that Craft should use for email verification links on the front end. * @@ -6505,18 +6333,6 @@ public function getPostLogoutRedirect(?string $siteHandle = null): string return ConfigHelper::localizedValue($this->postLogoutRedirect, $siteHandle); } - /** - * Returns the remembered user session duration as a [[\DateInterval]] object, if it’s set. - * - * @since 4.2.1 - */ - public function getRememberedUserSessionDuration(): ?DateInterval - { - return $this->rememberedUserSessionDuration > 0 - ? DateTimeHelper::toDateInterval($this->rememberedUserSessionDuration) - : null; - } - /** * Returns the localized Set Password Path value. * diff --git a/src/Cp/Cp.php b/src/Cp/Cp.php index 593386de44b..403f58e7a15 100644 --- a/src/Cp/Cp.php +++ b/src/Cp/Cp.php @@ -66,7 +66,7 @@ public static function config(): Collection 'cpTrigger' => $generalConfig->cpTrigger, 'baseCpUrl' => Url::cpUrl(), 'defaultCpLocale' => $generalConfig->defaultCpLocale, - 'rememberedUserSessionDuration' => $generalConfig->rememberedUserSessionDuration, + 'rememberedUserSessionDuration' => (int) config('auth.guards.craft.remember', 20160) * 60, 'runQueueAutomatically' => $generalConfig->runQueueAutomatically, ]); } diff --git a/tests/Unit/Config/ConfigServiceProviderTest.php b/tests/Unit/Config/ConfigServiceProviderTest.php index d8ca84fccbc..51be04d9813 100644 --- a/tests/Unit/Config/ConfigServiceProviderTest.php +++ b/tests/Unit/Config/ConfigServiceProviderTest.php @@ -3,13 +3,20 @@ declare(strict_types=1); use CraftCms\Cms\Config\ConfigServiceProvider; +use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Support\Env; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Filesystem\Filesystem; use Illuminate\Foundation\Application; afterEach(function () { unset($_SERVER['CRAFT_CACHED_ENV_TEST']); putenv('CRAFT_CACHED_ENV_TEST'); + + foreach (['CRAFT_CP_TRIGGER', 'CRAFT_DEFAULT_COUNTRY_CODE'] as $name) { + unset($_SERVER[$name]); + putenv($name); + } }); it('loads environment variables when configuration is cached', function () { @@ -35,3 +42,46 @@ $filesystem->deleteDirectory($basePath); } }); + +it('materializes array configuration through fluent setters', function () { + app(ConfigRepository::class)->set('craft.general', [ + 'extraAllowedFileExtensions' => ['CUSTOM'], + ]); + app()->forgetInstance(GeneralConfig::class); + + new ConfigServiceProvider(app())->register(); + + $config = app(GeneralConfig::class); + + expect($config->allowedFileExtensions)->toContain('custom') + ->and(app(ConfigRepository::class)->get('craft.general'))->toBe($config); +}); + +it('applies environment overrides when resolved', function () { + app(ConfigRepository::class)->set('craft.general', [ + 'cpTrigger' => 'control', + ]); + app()->forgetInstance(GeneralConfig::class); + putenv('CRAFT_CP_TRIGGER=adminus'); + + new ConfigServiceProvider(app())->register(); + + expect(app(GeneralConfig::class)->cpTrigger)->toBe('adminus'); +}); + +it('fails when an environment override cannot be normalized', function () { + app(ConfigRepository::class)->set('craft.general', [ + 'defaultCountryCode' => 'US', + ]); + app()->forgetInstance(GeneralConfig::class); + putenv('CRAFT_DEFAULT_COUNTRY_CODE='); + + new ConfigServiceProvider(app())->register(); + + expect(fn () => app(GeneralConfig::class)) + ->toThrow(RuntimeException::class); + + expect(app(ConfigRepository::class)->get('craft.general'))->toBe([ + 'defaultCountryCode' => 'US', + ]); +}); diff --git a/tests/Unit/Config/GeneralConfigTest.php b/tests/Unit/Config/GeneralConfigTest.php index dca846257e3..1d36ffc545d 100644 --- a/tests/Unit/Config/GeneralConfigTest.php +++ b/tests/Unit/Config/GeneralConfigTest.php @@ -3,7 +3,6 @@ declare(strict_types=1); use CraftCms\Cms\Cms; -use CraftCms\Cms\Config\ConfigServiceProvider; use CraftCms\Cms\Config\GeneralConfig; use Illuminate\Support\Facades\Config; @@ -12,37 +11,6 @@ expect(app(GeneralConfig::class))->toBe(Cms::config()); }); -it('can get renamed settings', function () { - $config = GeneralConfig::create(); - - $config->aliases([ - '@webroot' => public_path(), - ]); - - expect($config->environmentVariables)->toBe($config->aliases); -}); - -it('can set renamed settings', function () { - $config = GeneralConfig::create(); - - $config->environmentVariables = [ - '@webroot' => public_path(), - ]; - - expect($config->aliases)->toBe([ - '@webroot' => public_path(), - ]); -}); - -test('env overrides get precedence over config', function () { - putenv('CRAFT_CP_TRIGGER=adminus'); - - // Simulate the application being loaded - app(ConfigServiceProvider::class, ['app' => app()])->boot(); - - expect(Cms::config()->cpTrigger)->toBe('adminus'); -}); - it('can set queueName via fluent setter', function () { $config = GeneralConfig::create()->queueName('custom'); @@ -67,18 +35,6 @@ expect($config->compiledTemplatesPath)->toBe('@storage/custom-compiled-templates'); }); -it('does not expose moved deprecated members on the new class', function () { - $config = GeneralConfig::create(); - - expect(method_exists($config, 'devMode'))->toBeFalse() - ->and(method_exists($config, 'enableCsrfProtection'))->toBeFalse() - ->and(method_exists($config, 'securityKey'))->toBeFalse() - ->and(property_exists($config, 'allowedGraphqlOrigins'))->toBeFalse() - ->and(property_exists($config, 'userSessionDuration'))->toBeFalse() - ->and(method_exists($config, 'pageTrigger'))->toBeTrue() - ->and(property_exists($config, 'pageTrigger'))->toBeTrue(); -}); - it('normalizes pageTrigger on the main config class', function () { $config = GeneralConfig::create(); diff --git a/yii2-adapter/legacy/Craft.php b/yii2-adapter/legacy/Craft.php index 3ec84aeb5da..676358d662d 100644 --- a/yii2-adapter/legacy/Craft.php +++ b/yii2-adapter/legacy/Craft.php @@ -214,7 +214,7 @@ public static function cookieConfig(array $config = [], ?Request $request = null } self::$_baseCookieConfig = [ - 'domain' => $generalConfig->defaultCookieDomain, + 'domain' => config('session.domain'), 'secure' => $generalConfig->useSecureCookies, 'httpOnly' => true, 'sameSite' => $generalConfig->sameSiteCookieValue, diff --git a/yii2-adapter/legacy/config/GeneralConfig.php b/yii2-adapter/legacy/config/GeneralConfig.php index 3b0aade4922..454e634b8fc 100644 --- a/yii2-adapter/legacy/config/GeneralConfig.php +++ b/yii2-adapter/legacy/config/GeneralConfig.php @@ -9,12 +9,16 @@ use craft\services\Config; use CraftCms\Cms\Support\Config as ConfigHelper; +use CraftCms\Cms\Support\DateTimeHelper; use CraftCms\Cms\Support\Facades\Deprecator; +use DateInterval; use Deprecated; use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; use Illuminate\Http\Middleware\TrustProxies; use Illuminate\Http\Request; use Illuminate\Support\Facades\Config as ConfigFacade; +use InvalidArgumentException; +use RuntimeException; use yii\base\InvalidConfigException; use function CraftCms\Cms\t; @@ -28,6 +32,19 @@ */ class GeneralConfig extends \CraftCms\Cms\Config\GeneralConfig { + protected static array $renamedSettings = [ + 'activateAccountFailurePath' => 'invalidUserTokenPath', + 'allowAutoUpdates' => 'allowUpdates', + 'backupDbOnUpdate' => 'backupOnUpdate', + 'defaultFilePermissions' => 'defaultFileMode', + 'defaultFolderPermissions' => 'defaultDirMode', + 'enableGraphQlCaching' => 'enableGraphqlCaching', + 'environmentVariables' => 'aliases', + 'isSystemOn' => 'isSystemLive', + 'restoreDbOnUpdateFailure' => 'restoreOnUpdateFailure', + 'validationKey' => 'securityKey', + ]; + public const string EVENT_DEFINE_BEHAVIORS = 'defineBehaviors'; /** @@ -41,6 +58,27 @@ class GeneralConfig extends \CraftCms\Cms\Config\GeneralConfig */ public string|array|null|false $testToEmailAddress = null; + /** + * @var string The domain that cookies generated by Craft should be created for. + * + * @deprecated 6.0.0. Configure `session.domain` or set the `SESSION_DOMAIN` environment variable instead. + */ + public string $defaultCookieDomain = ''; + + /** + * @var mixed The amount of time a user stays logged in when “Remember Me” is checked. + * + * @deprecated 6.0.0. Configure `auth.guards.craft.remember` instead. + */ + public mixed $rememberedUserSessionDuration = 1209600; + + /** + * @var mixed The amount of time a user verification code can be used before expiring. + * + * @deprecated 6.0.0. Configure `auth.passwords.craft.expire` instead. + */ + public mixed $verificationCodeDuration = 86400; + /** * @inheritdoc */ @@ -461,15 +499,17 @@ public function init(): void ->invalidLoginWindowDuration($this->invalidLoginWindowDuration) ->previewTokenDuration($this->previewTokenDuration ?? $this->defaultTokenDuration); + // legacy-only durations + $this->rememberedUserSessionDuration($this->rememberedUserSessionDuration); + $this->verificationCodeDuration($this->verificationCodeDuration); + $this - // legacy-only durations + // durations ->purgeStaleUserSessionDuration($this->purgeStaleUserSessionDuration) ->purgePendingUsersDuration($this->purgePendingUsersDuration) ->purgeUnsavedDraftsDuration($this->purgeUnsavedDraftsDuration) ->rememberUsernameDuration($this->rememberUsernameDuration) - ->rememberedUserSessionDuration($this->rememberedUserSessionDuration) ->softDeleteDuration($this->softDeleteDuration) - ->verificationCodeDuration($this->verificationCodeDuration) // locales ->defaultCpLanguage($this->defaultCpLanguage) ->extraAppLocales($this->extraAppLocales) @@ -583,6 +623,72 @@ public function devMode(bool $value = true): self return $this; } + /** + * The domain that cookies generated by Craft should be created for. + * + * @deprecated 6.0.0. Configure `session.domain` or set the `SESSION_DOMAIN` environment variable instead. + */ + #[Deprecated(message: 'in 6.0.0. Configure `session.domain` or set the `SESSION_DOMAIN` environment variable instead.')] + public function defaultCookieDomain(string $value): self + { + $this->defaultCookieDomain = $value; + + app()->booting(function() use ($value) { + Deprecator::log('generalConfig.defaultCookieDomain', 'Calling defaultCookieDomain() is deprecated. Configure `session.domain` or set the `SESSION_DOMAIN` environment variable instead.'); + ConfigFacade::set('session.domain', $value); + }); + + return $this; + } + + /** + * The amount of time a user stays logged in when “Remember Me” is checked. + * + * @deprecated 6.0.0. Configure `auth.guards.craft.remember` instead. + */ + #[Deprecated(message: 'in 6.0.0. Configure `auth.guards.craft.remember` instead.')] + public function rememberedUserSessionDuration(mixed $value): self + { + try { + $interval = DateTimeHelper::toDateInterval($value); + } catch (InvalidArgumentException $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + + $this->rememberedUserSessionDuration = $interval ? ConfigHelper::durationInSeconds($interval) : 0; + + app()->booting(function() { + Deprecator::log('generalConfig.rememberedUserSessionDuration', 'Calling rememberedUserSessionDuration() is deprecated. Configure `auth.guards.craft.remember` instead.'); + ConfigFacade::set('auth.guards.craft.remember', floor($this->rememberedUserSessionDuration / 60)); + }); + + return $this; + } + + public function getRememberedUserSessionDuration(): ?DateInterval + { + return $this->rememberedUserSessionDuration > 0 + ? DateTimeHelper::toDateInterval($this->rememberedUserSessionDuration) + : null; + } + + /** + * The amount of time a user verification code can be used before expiring. + * + * @deprecated 6.0.0. Configure `auth.passwords.craft.expire` instead. + */ + #[Deprecated(message: 'in 6.0.0. Configure `auth.passwords.craft.expire` instead.')] + public function verificationCodeDuration(mixed $value): self + { + $this->verificationCodeDuration = ConfigHelper::durationInSeconds($value); + + app()->booting(function() { + Deprecator::log('generalConfig.verificationCodeDuration', 'Calling verificationCodeDuration() is deprecated. Configure `auth.passwords.craft.expire` instead.'); + ConfigFacade::set('auth.passwords.craft.expire', floor($this->verificationCodeDuration / 60)); + }); + + return $this; + } /** * Whether the `@transform` directive should be disabled for the GraphQL API. * diff --git a/yii2-adapter/legacy/services/Config.php b/yii2-adapter/legacy/services/Config.php index 67a0c7f618b..b130126788b 100644 --- a/yii2-adapter/legacy/services/Config.php +++ b/yii2-adapter/legacy/services/Config.php @@ -18,7 +18,6 @@ use Illuminate\Support\Facades\Config as ConfigFacade; use InvalidArgumentException; use RuntimeException; -use Throwable; use yii\base\Component; /** @@ -159,13 +158,10 @@ private function _createConfigObj(string $category, string $filename, BaseConfig foreach ($envConfig as $name => $value) { // Use the fluent methods when possible, in case it has any value normalization logic if (method_exists($config, $name)) { - try { - $config->$name($value); - continue; - } catch (Throwable) { - } - $config->$name = $value; + $config->$name($value); + continue; } + $config->$name = $value; } return $config; diff --git a/yii2-adapter/legacy/services/Users.php b/yii2-adapter/legacy/services/Users.php index 3c0bca57e47..6def5f3088a 100644 --- a/yii2-adapter/legacy/services/Users.php +++ b/yii2-adapter/legacy/services/Users.php @@ -15,7 +15,6 @@ use craft\events\UserPhotoEvent; use CraftCms\Cms\Asset\Exceptions\ImageException; use CraftCms\Cms\Asset\Exceptions\VolumeException; -use CraftCms\Cms\Cms; use CraftCms\Cms\Database\Table; use CraftCms\Cms\Edition; use CraftCms\Cms\Element\Exceptions\InvalidElementException; @@ -305,7 +304,7 @@ public function isVerificationCodeValidForUser(User $user, string $code): bool } // Make sure the verification code isn't expired - $minCodeIssueDate = now()->subSeconds(Cms::config()->verificationCodeDuration)->toDateTime(); + $minCodeIssueDate = now()->subMinutes((int) config('auth.passwords.craft.expire', 1440))->toDateTime(); // Make sure it’s not expired if ($user->verificationCodeIssuedDate < $minCodeIssueDate) { diff --git a/yii2-adapter/src/Config/GeneralConfigCompatibility.php b/yii2-adapter/src/Config/GeneralConfigCompatibility.php new file mode 100644 index 00000000000..77f86f02a42 --- /dev/null +++ b/yii2-adapter/src/Config/GeneralConfigCompatibility.php @@ -0,0 +1,58 @@ +resolve($config, LegacyGeneralConfig::create()); + + if ($overlay === null || $overlay === []) { + return $config; + } + + Deprecator::log('config-general-app-type', 'Using general.web.php and general.console.php configuration files is deprecated.'); + + $overlay = $this->resolve($overlay, $config instanceof GeneralConfig ? $config : LegacyGeneralConfig::create()); + + if ($overlay instanceof GeneralConfig) { + return $overlay; + } + + if (is_array($config)) { + return array_merge($config, $overlay); + } + + Typecast::properties($config::class, $overlay); + + foreach ($overlay as $setting => $value) { + method_exists($config, $setting) + ? $config->{$setting}($value) + : $config->{$setting} = $value; + } + + return $config; + } + + private function resolve(mixed $config, GeneralConfig $default): GeneralConfig|array + { + if (is_callable($config)) { + Deprecator::log('config-general-callable', 'Returning a callable from general.php is deprecated.'); + $config = $config($default); + } + + if ($config instanceof GeneralConfig || is_array($config)) { + return $config; + } + + return []; + } +} diff --git a/yii2-adapter/src/Config/MultiEnvironmentConfigCompatibility.php b/yii2-adapter/src/Config/MultiEnvironmentConfigCompatibility.php index c8dce65fbf4..697fa830439 100644 --- a/yii2-adapter/src/Config/MultiEnvironmentConfigCompatibility.php +++ b/yii2-adapter/src/Config/MultiEnvironmentConfigCompatibility.php @@ -31,8 +31,6 @@ public function register(Application $app): void } if (!is_array($config)) { - Config::set($key, []); - continue; } diff --git a/yii2-adapter/src/Yii2ServiceProvider.php b/yii2-adapter/src/Yii2ServiceProvider.php index d9350325f62..97e553bbf2e 100644 --- a/yii2-adapter/src/Yii2ServiceProvider.php +++ b/yii2-adapter/src/Yii2ServiceProvider.php @@ -14,6 +14,7 @@ use CraftCms\Cms\Support\Env; use CraftCms\Cms\Twig\Variables\CraftVariable; use CraftCms\Cms\View\Events\SiteTemplateRootsResolving; +use CraftCms\Yii2Adapter\Config\GeneralConfigCompatibility; use CraftCms\Yii2Adapter\Config\MultiEnvironmentConfigCompatibility; use CraftCms\Yii2Adapter\Console\AddCategoriesSupportCommand; use CraftCms\Yii2Adapter\Console\AddGlobalSetsSupportCommand; @@ -58,6 +59,12 @@ public function register(): void new ClassAliases()->register(); new MultiEnvironmentConfigCompatibility()->register($this->app); + $appType = $this->app->runningInConsole() ? 'console' : 'web'; + Config::set('craft.general', new GeneralConfigCompatibility()->convert( + Config::get('craft.general', []), + Config::get("craft.general.{$appType}"), + )); + $this->registerConstants(); new LegacyApp()->register($this->app); diff --git a/yii2-adapter/tests-laravel/Legacy/ConfigCompatibilityTest.php b/yii2-adapter/tests-laravel/Legacy/ConfigCompatibilityTest.php new file mode 100644 index 00000000000..0a0d29f351f --- /dev/null +++ b/yii2-adapter/tests-laravel/Legacy/ConfigCompatibilityTest.php @@ -0,0 +1,62 @@ +app->configPath(); + $configPath = sys_get_temp_dir() . '/craft-config-' . bin2hex(random_bytes(8)); + $craftConfigPath = "$configPath/craft"; + mkdir($craftConfigPath, recursive: true); + file_put_contents("$craftConfigPath/general.php", 'app->useConfigPath($configPath); + + $config = fn() => GeneralConfig::create(); + Config::set('craft.general', $config); + + try { + new MultiEnvironmentConfigCompatibility()->register($this->app); + + expect(Config::get('craft.general'))->toBe($config); + } finally { + $this->app->useConfigPath($originalConfigPath); + unlink("$craftConfigPath/general.php"); + rmdir($craftConfigPath); + rmdir($configPath); + } +}); + +it('maps renamed general config settings in the adapter', function(): void { + $config = GeneralConfig::create(); + + $config->allowAutoUpdates = false; + $config->environmentVariables = ['@uploads' => '/path/to/uploads']; + + expect($config->allowUpdates)->toBeFalse() + ->and($config->aliases)->toBe(['@uploads' => '/path/to/uploads']); +}); + +it('supports moved deprecated config settings', function(): void { + $config = GeneralConfig::create() + ->defaultCookieDomain('.example.test') + ->rememberedUserSessionDuration(7200) + ->verificationCodeDuration(1800); + + expect($config->defaultCookieDomain)->toBe('.example.test') + ->and($config->rememberedUserSessionDuration)->toBe(7200) + ->and($config->verificationCodeDuration)->toBe(1800); +}); + +it('converts callable general config and application type overlays', function(): void { + $config = new GeneralConfigCompatibility()->convert( + fn(GeneralConfig $config) => $config->cpTrigger('control'), + ['cpTrigger' => 'console'], + ); + + expect($config)->toBeInstanceOf(GeneralConfig::class) + ->and($config->cpTrigger)->toBe('console'); +});