self::DEFAULT_SCORE_THRESHOLD, ]; protected function casts(): array { return [ 'provider' => CaptchaProvider::class, 'secret_key' => 'encrypted', 'score_threshold' => 'float', ]; } public static function for(CaptchaProvider $provider): self { return static::query()->firstOrNew(['provider' => $provider->value]); } /** * Every provider, configured or not, in enum order — so the settings * screen always renders the same cards and a provider added to the * enum needs no seeding. * * @return array */ public static function allProviders(): array { $stored = static::query()->get()->keyBy(fn (self $row): string => $row->provider->value); $rows = []; foreach (CaptchaProvider::cases() as $provider) { $rows[$provider->value] = $stored->get($provider->value) ?? static::for($provider); } return $rows; } /** * Whether this provider can actually be used. * * Incomplete configuration behaves exactly as "switched off" rather * than throwing on the first visitor to reach a login form — the same * rule SocialSettings::usable() and LdapSettings::usable() follow, and * for the same reason: an administrator can save a half-filled form. */ public function usable(): bool { return $this->filled('site_key') && $this->filled('secret_key'); } public function threshold(): float { // Not `?:` — 0.0 is a meaningful threshold ("accept every score"), // and v1's `!empty()` check silently turned it back into 0.5. return $this->score_threshold ?? self::DEFAULT_SCORE_THRESHOLD; } protected static function booted(): void { // Any write to this table can change what the browser is told, // including one that clears a key. static::saved(fn () => Cache::forget(self::DISPLAY_CACHE_KEY)); static::deleted(fn () => Cache::forget(self::DISPLAY_CACHE_KEY)); } private function filled(string $attribute): bool { $value = $this->getAttribute($attribute); return is_string($value) && trim($value) !== ''; } }