공개 제보 — 파일 스토리지에서 S3 를 선택해 저장해도 실제 파일 저장이 동작하지 않던 결함의 전면 수정. - S3 어댑터(league/flysystem-aws-s3-v3)·predis 를 코어 기본 의존성으로 포함 — 어댑터 부재 즉사, phpredis 확장 없는 서버의 redis 선택 전면 다운 차단 (부트 시 확장 부재 감지 → predis 자동 폴백) - storage_driver=s3 저장 시 코어 첨부 업로드 디스크를 s3 로 전환 (ATTACHMENT_DISK env 명시가 항상 우선, 기존 행은 저장 당시 disk 로 서빙) - 첨부·템플릿 레이아웃 첨부 서빙을 행 disk 를 따르는 스토리지 스트림으로 교체 — 로컬 절대 경로 전제 fileResponse 는 S3 행에서 filemtime stat 500 (streamedFileResponse: 행 메타 기반 ETag/304/Cache-Control) - S3 호환 스토리지(R2/MinIO/NCP) 연결 지원: 엔드포인트 URL·path-style 설정 신설, 리전 목록 선택 → 자유 입력 전환, 연결 테스트를 실제 저장 경로와 동일 설정(endpoint/path-style)으로 정렬 - 사용 불능 드라이버(어댑터·PHP 확장 부재)의 저장/테스트 요청을 사유와 함께 422 로 차단하는 서버 게이트 신설 (DriverRegistryService 능력 판정) - 웹소켓 연결 테스트에 서버(백엔드 발송용) endpoint 검사 추가 — 클라이언트만 검사해 테스트 성공 + 실제 발송 실패가 가능하던 비대칭 해소 - env 빈 값(`KEY=`) 함정 정규화: AWS_URL/AWS_ENDPOINT/ATTACHMENT_DISK 빈 문자열을 미설정으로 취급 (config 정규화 + 예시 파일 주석 처리) - 플러그인 드라이버 폴백의 log 카테고리 죽은 키(logging.default) 정정 및 websocket 유령 설정 키 제거 - 실 AWS S3 종단 검증 완료 (설정 저장 → 업로드 S3 실저장 → 서빙 200/304)
979 lines
62 KiB
PHP
979 lines
62 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Requests\Settings;
|
|
|
|
use App\Extension\HookManager;
|
|
use App\Models\Attachment;
|
|
use App\Search\Engines\DatabaseFulltextEngine;
|
|
use App\Services\DriverRegistryService;
|
|
use App\Support\AllowedExtensions;
|
|
use App\Support\AssetUrl;
|
|
use Illuminate\Contracts\Validation\ValidationRule;
|
|
use Illuminate\Contracts\Validation\Validator;
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class SaveSettingsRequest extends FormRequest
|
|
{
|
|
/**
|
|
* 지원되는 메일러 목록
|
|
*/
|
|
private const SUPPORTED_MAILERS = ['smtp', 'mailgun', 'ses'];
|
|
|
|
/**
|
|
* 지원되는 암호화 방식 목록
|
|
*/
|
|
private const SUPPORTED_ENCRYPTIONS = ['tls', 'ssl', ''];
|
|
|
|
/**
|
|
* 지원되는 스토리지 드라이버 목록
|
|
*/
|
|
private const SUPPORTED_STORAGE_DRIVERS = ['local', 's3'];
|
|
|
|
/**
|
|
* S3 리전 형식 (AWS 리전 코드 + S3 호환 스토리지 임의값 허용 — R2 의 `auto` 등)
|
|
*/
|
|
private const S3_REGION_FORMAT = 'regex:/^[a-z0-9-]+$/';
|
|
|
|
/**
|
|
* 지원되는 캐시 드라이버 목록
|
|
*/
|
|
private const SUPPORTED_CACHE_DRIVERS = ['file', 'redis', 'memcached'];
|
|
|
|
/**
|
|
* 지원되는 세션 드라이버 목록
|
|
*/
|
|
private const SUPPORTED_SESSION_DRIVERS = ['file', 'database', 'redis'];
|
|
|
|
/**
|
|
* 지원되는 큐 드라이버 목록
|
|
*/
|
|
private const SUPPORTED_QUEUE_DRIVERS = ['sync', 'database', 'redis'];
|
|
|
|
/**
|
|
* 지원되는 웹소켓 프로토콜 목록
|
|
*/
|
|
private const SUPPORTED_WEBSOCKET_SCHEMES = ['http', 'https'];
|
|
|
|
/**
|
|
* 지원되는 로그 드라이버 목록
|
|
*/
|
|
private const SUPPORTED_LOG_DRIVERS = ['single', 'daily'];
|
|
|
|
/**
|
|
* 지원되는 로그 레벨 목록
|
|
*/
|
|
private const SUPPORTED_LOG_LEVELS = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'];
|
|
|
|
/**
|
|
* Determine if the user is authorized to make this request.
|
|
*
|
|
* @return bool 권한 체크는 permission 미들웨어 체인이 담당하므로 항상 true 반환
|
|
*/
|
|
public function authorize(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 검증 전 입력 데이터를 정규화합니다.
|
|
*
|
|
* site_logo 필드가 Attachment 객체 배열로 전송될 수 있으므로
|
|
* 정수 ID 배열로 변환합니다.
|
|
*/
|
|
protected function prepareForValidation(): void
|
|
{
|
|
$siteLogo = $this->input('general.site_logo');
|
|
|
|
if (is_array($siteLogo)) {
|
|
$this->merge([
|
|
'general' => array_merge($this->input('general', []), [
|
|
'site_logo' => array_map(
|
|
fn ($item) => is_array($item) ? ($item['id'] ?? $item) : $item,
|
|
$siteLogo
|
|
),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
// GeoIP 토글 필드: 신규 필드라 초기값이 settings에 없을 수 있음.
|
|
// Toggle 컴포넌트가 undefined → true 전환 시 비표준 타입을 보낼 수 있으므로
|
|
// boolean으로 명시 캐스팅하여 Laravel boolean 검증 호환 보장.
|
|
$this->castAdvancedBooleanFields([
|
|
'advanced.geoip_enabled',
|
|
'advanced.geoip_auto_update_enabled',
|
|
]);
|
|
|
|
$this->normalizeAllowedExtensions();
|
|
}
|
|
|
|
/**
|
|
* 허용 확장자를 저장 형태(소문자 확장자 배열)로 정규화합니다.
|
|
*
|
|
* 화면 입력 칸은 콤마로 구분한 한 줄 텍스트라 이 값은 문자열로 들어온다. 문자열 그대로
|
|
* 저장하면 업로드 제한을 적용하는 지점이 그 값을 쓰지 못해, 저장은 성공하는데 제한은
|
|
* 종전 목록으로 남는다. 형태 판정은 저장 경로 진입 전 이 한 곳에서만 수행한다.
|
|
*/
|
|
private function normalizeAllowedExtensions(): void
|
|
{
|
|
$upload = $this->input('upload', []);
|
|
|
|
if (! is_array($upload) || ! array_key_exists('allowed_extensions', $upload)) {
|
|
return;
|
|
}
|
|
|
|
$upload['allowed_extensions'] = AllowedExtensions::normalize($upload['allowed_extensions']);
|
|
|
|
$this->merge(['upload' => $upload]);
|
|
}
|
|
|
|
/**
|
|
* advanced 탭의 boolean 필드를 PHP boolean으로 캐스팅합니다.
|
|
*
|
|
* Toggle 컴포넌트가 초기값 미존재(undefined) 상태에서 전환 시
|
|
* 비표준 타입(문자열 "true", null 등)을 전송할 수 있으므로
|
|
* prepareForValidation 단계에서 명시 캐스팅합니다.
|
|
*
|
|
* @param array<int, string> $fields dot-notation 필드 경로 배열
|
|
*/
|
|
private function castAdvancedBooleanFields(array $fields): void
|
|
{
|
|
$advanced = $this->input('advanced', []);
|
|
$changed = false;
|
|
|
|
foreach ($fields as $dotPath) {
|
|
$key = str_replace('advanced.', '', $dotPath);
|
|
if (array_key_exists($key, $advanced)) {
|
|
$advanced[$key] = filter_var($advanced[$key], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
|
|
$changed = true;
|
|
}
|
|
}
|
|
|
|
if ($changed) {
|
|
$this->merge(['advanced' => $advanced]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the validation rules that apply to the request.
|
|
*
|
|
* nested 구조로 요청됩니다:
|
|
* { _tab: 'general', general: { site_name: '...', ... } }
|
|
*
|
|
* @return array<string, ValidationRule|array<mixed>|string>
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
$tab = $this->input('_tab', 'general');
|
|
|
|
$rules = [
|
|
// 탭 식별자
|
|
'_tab' => ['nullable', 'string', Rule::in(['general', 'mail', 'upload', 'seo', 'security', 'drivers', 'advanced', 'notifications', 'identity'])],
|
|
|
|
// 각 탭의 컨테이너
|
|
'general' => ['sometimes', 'array'],
|
|
'mail' => ['sometimes', 'array'],
|
|
'upload' => ['sometimes', 'array'],
|
|
'seo' => ['sometimes', 'array'],
|
|
'security' => ['sometimes', 'array'],
|
|
'drivers' => ['sometimes', 'array'],
|
|
'advanced' => ['sometimes', 'array'],
|
|
'notifications' => ['sometimes', 'array'],
|
|
'identity' => ['sometimes', 'array'],
|
|
'notifications.channels' => ['sometimes', 'array'],
|
|
'notifications.channels.*.id' => ['required_with:notifications.channels', 'string', 'max:50'],
|
|
'notifications.channels.*.is_active' => ['required_with:notifications.channels', 'boolean'],
|
|
'notifications.channels.*.sort_order' => ['nullable', 'integer', 'min:0'],
|
|
|
|
// 일반 설정
|
|
'general.site_name' => $this->getTabRules($tab, 'general', 'string|max:100'),
|
|
'general.site_url' => $this->getTabRules($tab, 'general', 'url|max:255'),
|
|
'general.site_description' => ['nullable', 'string', 'max:500'],
|
|
'general.admin_email' => $this->getTabRules($tab, 'general', 'email|max:255'),
|
|
'general.timezone' => $this->getTabRules($tab, 'general', ['timezone']),
|
|
'general.language' => $this->getTabRules($tab, 'general', [Rule::in(config('app.supported_locales', ['ko', 'en']))]),
|
|
'general.currency' => ['nullable', 'string', 'max:10'],
|
|
'general.maintenance_mode' => ['nullable', 'boolean'],
|
|
// 자산 URL 방식 — 정적 최적화 서버 대응 (이슈 #486).
|
|
// 임의 문자열이 들어오면 AssetUrl::mode() 가 기본값으로 폴백하지만,
|
|
// 저장 단계에서 막아 설정 파일에 뜻 모를 값이 남지 않게 한다.
|
|
'general.asset_url_mode' => ['nullable', 'string', Rule::in([
|
|
AssetUrl::MODE_EXTENSION,
|
|
AssetUrl::MODE_EXTENSIONLESS,
|
|
])],
|
|
'general.site_logo' => ['nullable', 'array'],
|
|
'general.site_logo.*' => ['integer', Rule::exists(Attachment::class, 'id')],
|
|
|
|
// 메일 설정
|
|
'mail.mailer' => $this->getTabRules($tab, 'mail', [Rule::in(self::SUPPORTED_MAILERS)]),
|
|
'mail.host' => $this->getMailerRules($tab, 'smtp', 'string|max:255'),
|
|
'mail.port' => $this->getMailerRules($tab, 'smtp', 'integer|min:1|max:65535'),
|
|
'mail.username' => ['nullable', 'string', 'max:255'],
|
|
'mail.password' => ['nullable', 'string', 'max:255'],
|
|
'mail.encryption' => ['nullable', Rule::in(self::SUPPORTED_ENCRYPTIONS)],
|
|
'mail.mailgun_domain' => $this->getMailerRules($tab, 'mailgun', 'string|max:255'),
|
|
'mail.mailgun_secret' => $this->getMailerRules($tab, 'mailgun', 'string|max:255'),
|
|
'mail.mailgun_endpoint' => ['nullable', 'string', 'max:255'],
|
|
'mail.ses_key' => $this->getMailerRules($tab, 'ses', 'string|max:255'),
|
|
'mail.ses_secret' => $this->getMailerRules($tab, 'ses', 'string|max:255'),
|
|
'mail.ses_region' => $this->getMailerRules($tab, 'ses', 'string|max:255'),
|
|
'mail.from_address' => $this->getTabRules($tab, 'mail', 'email|max:255'),
|
|
'mail.from_name' => $this->getTabRules($tab, 'mail', 'string|max:255'),
|
|
|
|
// 업로드 설정
|
|
'upload.max_file_size' => $this->getTabRules($tab, 'upload', 'integer|min:'.config('core.settings_limits.upload_max_file_size_min', 1).'|max:'.config('core.settings_limits.upload_max_file_size_max', 1024)),
|
|
'upload.allowed_extensions' => $this->getAllowedExtensionsRules($tab),
|
|
'upload.image_max_width' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_image_max_width_min', 100), 'max:'.config('core.settings_limits.upload_image_max_width_max', 10000)],
|
|
'upload.image_max_height' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_image_max_height_min', 100), 'max:'.config('core.settings_limits.upload_image_max_height_max', 10000)],
|
|
'upload.image_quality' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_image_quality_min', 1), 'max:'.config('core.settings_limits.upload_image_quality_max', 100)],
|
|
|
|
// SEO 설정
|
|
'seo.meta_title_suffix' => ['nullable', 'string', 'max:100'],
|
|
'seo.meta_description' => ['nullable', 'string', 'max:160'],
|
|
'seo.meta_keywords' => ['nullable', 'string', 'max:255'],
|
|
'seo.google_analytics_id' => ['nullable', 'string', 'max:50'],
|
|
'seo.google_site_verification' => ['nullable', 'string', 'max:100'],
|
|
'seo.naver_site_verification' => ['nullable', 'string', 'max:100'],
|
|
'seo.bot_user_agents' => ['nullable', 'array'],
|
|
'seo.bot_user_agents.*' => ['string', 'max:100'],
|
|
'seo.bot_detection_enabled' => ['nullable', 'boolean'],
|
|
'seo.bot_detection_library_enabled' => ['nullable', 'boolean'],
|
|
'seo.og_default_site_name' => ['nullable', 'string', 'max:200'],
|
|
'seo.og_image_default_width' => ['nullable', 'integer', 'min:'.config('core.settings_limits.seo_og_image_default_width_min', 0), 'max:'.config('core.settings_limits.seo_og_image_default_width_max', 8000)],
|
|
'seo.og_image_default_height' => ['nullable', 'integer', 'min:'.config('core.settings_limits.seo_og_image_default_height_min', 0), 'max:'.config('core.settings_limits.seo_og_image_default_height_max', 8000)],
|
|
'seo.twitter_default_card' => ['nullable', 'string', Rule::in(['summary', 'summary_large_image', 'app', 'player', ''])],
|
|
'seo.twitter_default_site' => ['nullable', 'string', 'max:50'],
|
|
'seo.cache_enabled' => ['nullable', 'boolean'],
|
|
'seo.cache_ttl' => ['nullable', 'integer', 'min:'.config('core.settings_limits.seo_cache_ttl_min', 60), 'max:'.config('core.settings_limits.seo_cache_ttl_max', 86400)],
|
|
'seo.sitemap_enabled' => ['nullable', 'boolean'],
|
|
'seo.sitemap_cache_ttl' => ['nullable', 'integer', 'min:'.config('core.settings_limits.seo_sitemap_cache_ttl_min', 3600), 'max:'.config('core.settings_limits.seo_sitemap_cache_ttl_max', 604800)],
|
|
// 분할 기준. 상한 50000 은 sitemaps.org 프로토콜 제한이며 SitemapWriter 가 동일 값으로 클램프합니다.
|
|
'seo.sitemap_urls_per_file' => ['nullable', 'integer', 'min:'.config('core.settings_limits.seo_sitemap_urls_per_file_min', 1000), 'max:'.config('core.settings_limits.seo_sitemap_urls_per_file_max', 50000)],
|
|
'seo.sitemap_gzip' => ['nullable', 'boolean'],
|
|
'seo.sitemap_serve_stale_on_miss' => ['nullable', 'boolean'],
|
|
'seo.sitemap_max_urls_per_contributor' => ['nullable', 'integer', 'min:0'],
|
|
'seo.sitemap_hreflang_enabled' => ['nullable', 'boolean'],
|
|
'seo.sitemap_schedule' => ['nullable', 'string', Rule::in(['hourly', 'daily', 'weekly'])],
|
|
'seo.sitemap_schedule_time' => ['nullable', 'string', 'regex:/^\d{2}:\d{2}$/'],
|
|
'seo.generator_enabled' => ['nullable', 'boolean'],
|
|
'seo.generator_content' => ['nullable', 'string', 'max:200'],
|
|
|
|
// 보안 설정
|
|
'security.force_https' => $this->getTabRules($tab, 'security', 'boolean'),
|
|
'security.login_attempt_enabled' => $this->getTabRules($tab, 'security', 'boolean'),
|
|
'security.auth_token_lifetime' => $this->getSecurityTabAuthTokenRules($tab),
|
|
'security.max_login_attempts' => ['nullable', 'integer', 'min:'.config('core.settings_limits.security_max_login_attempts_min', 0), 'max:'.config('core.settings_limits.security_max_login_attempts_max', 100)],
|
|
'security.login_lockout_time' => ['nullable', 'integer', 'min:'.config('core.settings_limits.security_login_lockout_time_min', 0), 'max:'.config('core.settings_limits.security_login_lockout_time_max', 1440)],
|
|
// 비밀번호 정책 — 저장 규칙이 없어 whitelist 에서 탈락하던 고아 설정을 배선.
|
|
// 경계값은 config('core.settings_limits') 가 SSoT — 설정 화면도 같은 값을 받아 쓴다.
|
|
'security.password_min_length' => [
|
|
'nullable',
|
|
'integer',
|
|
'min:'.config('core.settings_limits.security_password_min_length_min', 6),
|
|
'max:'.config('core.settings_limits.security_password_min_length_max', 64),
|
|
],
|
|
'security.require_password_special_char' => ['nullable', 'boolean'],
|
|
'security.two_factor_auth' => ['nullable', 'boolean'],
|
|
// 신규 설정이므로 미전송(기존 클라이언트)을 허용한다 — 미전송 시 기본값 false 유지
|
|
'security.allow_internal_outbound_urls' => ['nullable', 'boolean'],
|
|
|
|
// 캐시 설정 (advanced 탭)
|
|
'advanced.cache_enabled' => $this->getTabRules($tab, 'advanced', 'boolean'),
|
|
'advanced.layout_cache_enabled' => $this->getTabRules($tab, 'advanced', 'boolean'),
|
|
'advanced.layout_cache_ttl' => $this->getAdvancedTabCacheTtlRules($tab),
|
|
'advanced.stats_cache_enabled' => $this->getTabRules($tab, 'advanced', 'boolean'),
|
|
'advanced.stats_cache_ttl' => $this->getAdvancedTabCacheTtlRules($tab),
|
|
'advanced.seo_cache_enabled' => $this->getTabRules($tab, 'advanced', 'boolean'),
|
|
'advanced.seo_cache_ttl' => $this->getAdvancedTabCacheTtlRules($tab),
|
|
// 신규 설정이므로 미전송(기존 클라이언트)을 허용한다 — 미전송/미지정 시 코드 기본값 유지.
|
|
// 범위는 SEO 탭의 오버라이드 칸(seo.sitemap_cache_ttl)과 동일하게 맞춘다.
|
|
'advanced.seo_sitemap_cache_ttl' => ['nullable', 'integer', 'min:'.config('core.settings_limits.advanced_seo_sitemap_cache_ttl_min', 3600), 'max:'.config('core.settings_limits.advanced_seo_sitemap_cache_ttl_max', 604800)],
|
|
|
|
// 디버그 설정 (advanced 탭)
|
|
'advanced.debug_mode' => $this->getTabRules($tab, 'advanced', 'boolean'),
|
|
'advanced.sql_query_log' => $this->getTabRules($tab, 'advanced', 'boolean'),
|
|
|
|
// 코어 업데이트 설정 (advanced 탭)
|
|
'advanced.core_update_github_url' => ['nullable', 'url', 'max:500'],
|
|
'advanced.core_update_github_token' => ['nullable', 'string', 'max:500'],
|
|
|
|
// GeoIP 설정 (advanced 탭)
|
|
// 마스터 토글 OFF 시 하위 필드(license_key, auto_update)는 collapse 되어 미전송됨
|
|
// → nullable 필수 (기존 cache/debug 필드와 달리 조건부 렌더링 내부에 있음)
|
|
'advanced.geoip_enabled' => ['nullable', 'boolean'],
|
|
'advanced.geoip_license_key' => ['nullable', 'string', 'max:200', 'regex:/^[A-Za-z0-9_]+$/'],
|
|
'advanced.geoip_auto_update_enabled' => ['nullable', 'boolean'],
|
|
|
|
// 목록 한계값 (advanced 탭) — 0 은 무제한
|
|
'advanced.pagination_result_cap' => ['nullable', 'integer', 'min:'.config('core.settings_limits.advanced_pagination_result_cap_min', 0), 'max:'.config('core.settings_limits.advanced_pagination_result_cap_max', 1000000)],
|
|
'advanced.pagination_max_page' => ['nullable', 'integer', 'min:'.config('core.settings_limits.advanced_pagination_max_page_min', 0), 'max:'.config('core.settings_limits.advanced_pagination_max_page_max', 100000)],
|
|
|
|
// 드라이버 설정 (drivers 탭)
|
|
'drivers.storage_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_STORAGE_DRIVERS), $this->getDriverUsabilityRule('storage')]),
|
|
'drivers.s3_bucket' => ['nullable', 'string', 'max:255'],
|
|
'drivers.s3_region' => ['nullable', 'string', 'max:64', self::S3_REGION_FORMAT],
|
|
'drivers.s3_access_key' => ['nullable', 'string', 'max:255'],
|
|
'drivers.s3_secret_key' => ['nullable', 'string', 'max:255'],
|
|
'drivers.s3_url' => ['nullable', 'url', 'max:500'],
|
|
'drivers.s3_endpoint' => ['nullable', 'url', 'max:500'],
|
|
'drivers.s3_use_path_style' => ['nullable', 'boolean'],
|
|
'drivers.cache_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_CACHE_DRIVERS), $this->getDriverUsabilityRule('cache')]),
|
|
'drivers.redis_host' => ['nullable', 'string', 'max:255'],
|
|
'drivers.redis_port' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_redis_port_min', 1), 'max:'.config('core.settings_limits.drivers_redis_port_max', 65535)],
|
|
'drivers.redis_password' => ['nullable', 'string', 'max:255'],
|
|
'drivers.redis_database' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_redis_database_min', 0), 'max:'.config('core.settings_limits.drivers_redis_database_max', 15)],
|
|
'drivers.memcached_host' => ['nullable', 'string', 'max:255'],
|
|
'drivers.memcached_port' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_memcached_port_min', 1), 'max:'.config('core.settings_limits.drivers_memcached_port_max', 65535)],
|
|
'drivers.session_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_SESSION_DRIVERS), $this->getDriverUsabilityRule('session')]),
|
|
'drivers.session_lifetime' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_session_lifetime_min', 1), 'max:'.config('core.settings_limits.drivers_session_lifetime_max', 43200)], // 1분 ~ 30일
|
|
'drivers.queue_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_QUEUE_DRIVERS), $this->getDriverUsabilityRule('queue')]),
|
|
'drivers.websocket_enabled' => ['nullable', 'boolean'],
|
|
'drivers.websocket_app_id' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'],
|
|
'drivers.websocket_app_key' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'],
|
|
'drivers.websocket_app_secret' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'],
|
|
'drivers.websocket_host' => ['nullable', 'string', 'max:255'],
|
|
'drivers.websocket_port' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_websocket_port_min', 1), 'max:'.config('core.settings_limits.drivers_websocket_port_max', 65535)],
|
|
'drivers.websocket_scheme' => ['nullable', Rule::in(self::SUPPORTED_WEBSOCKET_SCHEMES)],
|
|
'drivers.websocket_verify_ssl' => ['nullable', 'boolean'],
|
|
'drivers.websocket_server_host' => ['nullable', 'string', 'max:255'],
|
|
'drivers.websocket_server_port' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_websocket_server_port_min', 1), 'max:'.config('core.settings_limits.drivers_websocket_server_port_max', 65535)],
|
|
'drivers.websocket_server_scheme' => ['nullable', Rule::in(self::SUPPORTED_WEBSOCKET_SCHEMES)],
|
|
'drivers.search_engine_driver' => ['nullable', Rule::in($this->getSupportedSearchEngineDrivers())],
|
|
'drivers.log_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_LOG_DRIVERS)]),
|
|
'drivers.log_level' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_LOG_LEVELS)]),
|
|
'drivers.log_days' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_log_days_min', 1), 'max:'.config('core.settings_limits.drivers_log_days_max', 365)],
|
|
|
|
// 본인인증(IDV) provider 기술 파라미터 — 정책 분기는 IdentityPolicy 로 흡수됨
|
|
'identity.default_provider' => ['nullable', 'string', 'max:100'],
|
|
'identity.purpose_providers' => ['sometimes', 'array'],
|
|
'identity.challenge_ttl_minutes' => $this->getTabRules($tab, 'identity', 'integer|min:'.config('core.settings_limits.identity_challenge_ttl_minutes_min', 1).'|max:'.config('core.settings_limits.identity_challenge_ttl_minutes_max', 1440)),
|
|
'identity.max_attempts' => $this->getTabRules($tab, 'identity', 'integer|min:'.config('core.settings_limits.identity_max_attempts_min', 1).'|max:'.config('core.settings_limits.identity_max_attempts_max', 20)),
|
|
];
|
|
|
|
// 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 훅 제공
|
|
return HookManager::applyFilters('core.settings.save_validation_rules', $rules, $this);
|
|
}
|
|
|
|
/**
|
|
* 특정 탭의 필수 필드에 대한 규칙을 반환합니다.
|
|
*
|
|
* @param string $currentTab 현재 활성 탭
|
|
* @param string $targetTab 규칙이 적용될 타겟 탭
|
|
* @param string|array $additionalRules 추가 규칙
|
|
* @return array 검증 규칙 배열
|
|
*/
|
|
private function getTabRules(string $currentTab, string $targetTab, string|array $additionalRules): array
|
|
{
|
|
$baseRules = $currentTab === $targetTab ? ['required'] : ['nullable'];
|
|
|
|
if (is_string($additionalRules)) {
|
|
return array_merge($baseRules, explode('|', $additionalRules));
|
|
}
|
|
|
|
return array_merge($baseRules, $additionalRules);
|
|
}
|
|
|
|
/**
|
|
* 메일러 드라이버별 조건부 필드 규칙을 반환합니다.
|
|
*
|
|
* mail 탭이 활성 상태이고 현재 선택된 메일러가 대상 메일러와 일치하면 필수,
|
|
* 그렇지 않으면 nullable로 처리합니다.
|
|
*
|
|
* @param string $currentTab 현재 활성 탭
|
|
* @param string $targetMailer 규칙이 적용될 대상 메일러 (smtp, mailgun, ses)
|
|
* @param string|array $additionalRules 추가 규칙
|
|
* @return array 검증 규칙 배열
|
|
*/
|
|
private function getMailerRules(string $currentTab, string $targetMailer, string|array $additionalRules): array
|
|
{
|
|
$mailer = $this->input('mail.mailer', 'smtp');
|
|
$isRequired = $currentTab === 'mail' && $mailer === $targetMailer;
|
|
$baseRules = $isRequired ? ['required'] : ['nullable'];
|
|
|
|
if (is_string($additionalRules)) {
|
|
return array_merge($baseRules, explode('|', $additionalRules));
|
|
}
|
|
|
|
return array_merge($baseRules, $additionalRules);
|
|
}
|
|
|
|
/**
|
|
* 인증 토큰 유지시간에 대한 검증 규칙을 반환합니다.
|
|
* 0이면 무한대, 30~3600 분 사이 값만 허용
|
|
*
|
|
* @param string $currentTab 현재 탭
|
|
* @return array 검증 규칙 배열
|
|
*/
|
|
private function getSecurityTabAuthTokenRules(string $currentTab): array
|
|
{
|
|
return [
|
|
'nullable',
|
|
'integer',
|
|
'min:'.config('core.settings_limits.security_auth_token_lifetime_min', 0),
|
|
'max:'.config('core.settings_limits.security_auth_token_lifetime_max', 3600),
|
|
function ($attribute, $value, $fail) {
|
|
// 0은 무한대를 의미하므로 허용, 그 외에는 30 이상이어야 함
|
|
if ($value !== null && $value !== 0 && $value < 30) {
|
|
$fail(__('validation.settings.auth_token_lifetime_range'));
|
|
}
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 허용 확장자에 대한 검증 규칙을 반환합니다.
|
|
* 문자열(콤마 구분) 또는 배열 형태 모두 허용합니다.
|
|
*
|
|
* @param string $currentTab 현재 탭
|
|
* @return array 검증 규칙 배열
|
|
*/
|
|
private function getAllowedExtensionsRules(string $currentTab): array
|
|
{
|
|
$baseRules = $currentTab === 'upload' ? ['required'] : ['nullable'];
|
|
|
|
return array_merge($baseRules, [
|
|
function ($attribute, $value, $fail) {
|
|
// 배열인 경우
|
|
if (is_array($value)) {
|
|
$joined = implode(',', $value);
|
|
if (strlen($joined) > 500) {
|
|
$fail(__('validation.settings.allowed_extensions_max'));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 문자열인 경우
|
|
if (is_string($value)) {
|
|
if (strlen($value) > 500) {
|
|
$fail(__('validation.settings.allowed_extensions_max'));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 그 외 타입은 에러
|
|
$fail(__('validation.settings.allowed_extensions_invalid_type'));
|
|
},
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 캐시 TTL에 대한 검증 규칙을 반환합니다.
|
|
* 0이면 무한대, 1~14400 초 사이 값만 허용
|
|
*
|
|
* @param string $currentTab 현재 탭
|
|
* @return array 검증 규칙 배열
|
|
*/
|
|
private function getAdvancedTabCacheTtlRules(string $currentTab): array
|
|
{
|
|
$baseRules = $currentTab === 'advanced' ? ['required'] : ['nullable'];
|
|
|
|
return array_merge($baseRules, [
|
|
'integer',
|
|
'min:'.config('core.settings_limits.advanced_cache_ttl_min', 0),
|
|
'max:'.config('core.settings_limits.advanced_cache_ttl_max', 14400),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 드라이버 가용성 검증 rule 을 반환합니다.
|
|
*
|
|
* 선택된 드라이버가 현재 서버에서 실제로 동작 가능한지(어댑터 클래스·PHP 확장 존재)를
|
|
* 저장 시점에 검사합니다. 사용 불능 드라이버가 저장되면 사이트 전면 다운으로 이어질 수
|
|
* 있으므로(예: phpredis 확장 없는 서버의 redis) 서버 게이트로 차단합니다.
|
|
*
|
|
* @param string $category 드라이버 카테고리 (storage, cache, session, queue)
|
|
* @return \Closure 검증 클로저
|
|
*/
|
|
private function getDriverUsabilityRule(string $category): \Closure
|
|
{
|
|
return function ($attribute, $value, $fail) use ($category) {
|
|
if (empty($value)) {
|
|
return;
|
|
}
|
|
|
|
$registry = app(DriverRegistryService::class);
|
|
|
|
if (! $registry->isDriverUsable($category, $value)) {
|
|
$fail(__('validation.settings.driver_unusable', [
|
|
'driver' => $value,
|
|
'reason' => $registry->usabilityFailureReason($category, $value),
|
|
]));
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 추가 검증을 위해 validator after 콜백을 등록합니다.
|
|
*
|
|
* 목적별 프로바이더(purpose_providers) 매핑은 purpose id 에 점(.)이 포함될 수
|
|
* 있어 단일 깊이 와일드카드 룰로는 leaf 값을 검증할 수 없습니다.
|
|
* 임의 깊이의 leaf 값을 재귀 순회하며 string|max:100 으로 검증합니다.
|
|
*
|
|
* @param Validator $validator 검증기 인스턴스
|
|
*/
|
|
public function withValidator($validator): void
|
|
{
|
|
$validator->after(function ($validator) {
|
|
$purposeProviders = $this->input('identity.purpose_providers');
|
|
|
|
if (is_array($purposeProviders)) {
|
|
$this->validatePurposeProviderLeaves(
|
|
$purposeProviders,
|
|
'identity.purpose_providers',
|
|
$validator
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 목적별 프로바이더 매핑을 재귀 순회하며 leaf 값을 검증합니다.
|
|
*
|
|
* purpose id 에 점(.)이 포함될 수 있습니다 (예: KG이니시스 플러그인의
|
|
* `inicis.adult_verification`). 프론트엔드 폼 자동 바인딩이 dot-notation name
|
|
* (`identity.purpose_providers.inicis.adult_verification`) 을 중첩 객체로 풀어
|
|
* `purpose_providers.inicis = { adult_verification: '...' }` 형태로 전송하므로,
|
|
* 깊이에 무관하게 각 leaf 값을 검증하고 위반 시 전체 dot-path 키로 에러를 부착합니다.
|
|
* 백엔드 조회(IdentityVerificationManager::resolveForPurpose) 가 config dot-path
|
|
* 라 이 중첩 구조 자체는 정상입니다.
|
|
*
|
|
* @param array<string, mixed> $node 검증할 노드 (purpose_providers 또는 중첩 하위)
|
|
* @param string $path 현재 노드의 dot-path (에러 키 prefix)
|
|
* @param Validator $validator 검증기 인스턴스
|
|
*/
|
|
private function validatePurposeProviderLeaves(array $node, string $path, $validator): void
|
|
{
|
|
foreach ($node as $key => $value) {
|
|
$childPath = $path.'.'.$key;
|
|
|
|
if (is_array($value)) {
|
|
$this->validatePurposeProviderLeaves($value, $childPath, $validator);
|
|
|
|
continue;
|
|
}
|
|
|
|
// null / '' (기본 프로바이더 사용) 은 허용
|
|
if ($value === null || $value === '') {
|
|
continue;
|
|
}
|
|
|
|
if (! is_string($value)) {
|
|
$validator->errors()->add($childPath, __('validation.settings.identity_purpose_provider_string'));
|
|
|
|
continue;
|
|
}
|
|
|
|
if (mb_strlen($value) > 100) {
|
|
$validator->errors()->add($childPath, __('validation.settings.identity_purpose_provider_max'));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 지원되는 검색엔진 드라이버 목록을 반환합니다.
|
|
*
|
|
* 플러그인이 `core.search.engine_drivers` 필터 훅으로 등록한 드라이버를 포함합니다.
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
private function getSupportedSearchEngineDrivers(): array
|
|
{
|
|
$drivers = HookManager::applyFilters(
|
|
'core.search.engine_drivers',
|
|
['mysql-fulltext' => DatabaseFulltextEngine::class]
|
|
);
|
|
|
|
return array_keys($drivers);
|
|
}
|
|
|
|
/**
|
|
* Get custom messages for validator errors.
|
|
*
|
|
* nested 구조에 맞게 'category.field.rule' 형식으로 정의합니다.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public function messages(): array
|
|
{
|
|
return [
|
|
// 일반 설정 - 필수 필드
|
|
'general.site_name.required' => __('validation.settings.site_name_required'),
|
|
'general.site_name.max' => __('validation.settings.site_name_max'),
|
|
'general.site_url.required' => __('validation.settings.site_url_required'),
|
|
'general.site_url.url' => __('validation.settings.site_url_invalid'),
|
|
'general.site_url.max' => __('validation.settings.site_url_max'),
|
|
'general.site_description.max' => __('validation.settings.site_description_max'),
|
|
'general.admin_email.required' => __('validation.settings.admin_email_required'),
|
|
'general.admin_email.email' => __('validation.settings.admin_email_invalid'),
|
|
'general.admin_email.max' => __('validation.settings.admin_email_max'),
|
|
'general.timezone.required' => __('validation.settings.timezone_required'),
|
|
'general.timezone.in' => __('validation.settings.timezone_invalid'),
|
|
'general.language.required' => __('validation.settings.language_required'),
|
|
'general.language.in' => __('validation.settings.language_invalid'),
|
|
'general.currency.max' => __('validation.settings.currency_max'),
|
|
'general.maintenance_mode.boolean' => __('validation.settings.maintenance_mode_boolean'),
|
|
|
|
// 메일 설정
|
|
'mail.mailer.required' => __('validation.settings.mailer_required'),
|
|
'mail.mailer.in' => __('validation.settings.mailer_invalid'),
|
|
'mail.host.required' => __('validation.settings.host_required'),
|
|
'mail.host.max' => __('validation.settings.host_max'),
|
|
'mail.port.required' => __('validation.settings.port_required'),
|
|
'mail.port.integer' => __('validation.settings.port_integer'),
|
|
'mail.port.min' => __('validation.settings.port_min'),
|
|
'mail.port.max' => __('validation.settings.port_max'),
|
|
'mail.username.max' => __('validation.settings.username_max'),
|
|
'mail.password.max' => __('validation.settings.password_max'),
|
|
'mail.encryption.in' => __('validation.settings.encryption_invalid'),
|
|
'mail.from_address.required' => __('validation.settings.from_address_required'),
|
|
'mail.from_address.email' => __('validation.settings.from_address_invalid'),
|
|
'mail.from_address.max' => __('validation.settings.from_address_max'),
|
|
'mail.from_name.required' => __('validation.settings.from_name_required'),
|
|
'mail.from_name.max' => __('validation.settings.from_name_max'),
|
|
|
|
// 업로드 설정
|
|
'upload.max_file_size.required' => __('validation.settings.max_file_size_required'),
|
|
'upload.max_file_size.integer' => __('validation.settings.max_file_size_integer'),
|
|
'upload.max_file_size.min' => __('validation.settings.max_file_size_min'),
|
|
'upload.max_file_size.max' => __('validation.settings.max_file_size_max'),
|
|
'upload.allowed_extensions.required' => __('validation.settings.allowed_extensions_required'),
|
|
'upload.allowed_extensions.max' => __('validation.settings.allowed_extensions_max'),
|
|
'upload.image_max_width.integer' => __('validation.settings.image_max_width_integer'),
|
|
'upload.image_max_width.min' => __('validation.settings.image_max_width_min'),
|
|
'upload.image_max_width.max' => __('validation.settings.image_max_width_max'),
|
|
'upload.image_max_height.integer' => __('validation.settings.image_max_height_integer'),
|
|
'upload.image_max_height.min' => __('validation.settings.image_max_height_min'),
|
|
'upload.image_max_height.max' => __('validation.settings.image_max_height_max'),
|
|
'upload.image_quality.integer' => __('validation.settings.image_quality_integer'),
|
|
'upload.image_quality.min' => __('validation.settings.image_quality_min'),
|
|
'upload.image_quality.max' => __('validation.settings.image_quality_max'),
|
|
|
|
// SEO 설정
|
|
'seo.meta_title_suffix.max' => __('validation.settings.meta_title_suffix_max'),
|
|
'seo.meta_description.max' => __('validation.settings.meta_description_max'),
|
|
'seo.meta_keywords.max' => __('validation.settings.meta_keywords_max'),
|
|
'seo.google_analytics_id.max' => __('validation.settings.google_analytics_id_max'),
|
|
'seo.google_site_verification.max' => __('validation.settings.google_site_verification_max'),
|
|
'seo.naver_site_verification.max' => __('validation.settings.naver_site_verification_max'),
|
|
'seo.generator_enabled.boolean' => __('validation.settings.generator_enabled_boolean'),
|
|
'seo.generator_content.string' => __('validation.settings.generator_content_string'),
|
|
'seo.generator_content.max' => __('validation.settings.generator_content_max'),
|
|
'seo.bot_user_agents.array' => __('validation.settings.bot_user_agents_array'),
|
|
'seo.bot_user_agents.*.string' => __('validation.settings.bot_user_agents_item_string'),
|
|
'seo.bot_user_agents.*.max' => __('validation.settings.bot_user_agents_item_max'),
|
|
'seo.bot_detection_enabled.boolean' => __('validation.settings.bot_detection_enabled_boolean'),
|
|
'seo.bot_detection_library_enabled.boolean' => __('validation.settings.bot_detection_library_enabled_boolean'),
|
|
'seo.og_default_site_name.string' => __('validation.settings.og_default_site_name_string'),
|
|
'seo.og_default_site_name.max' => __('validation.settings.og_default_site_name_max'),
|
|
'seo.og_image_default_width.integer' => __('validation.settings.og_image_default_width_integer'),
|
|
'seo.og_image_default_width.min' => __('validation.settings.og_image_default_width_min'),
|
|
'seo.og_image_default_width.max' => __('validation.settings.og_image_default_width_max'),
|
|
'seo.og_image_default_height.integer' => __('validation.settings.og_image_default_height_integer'),
|
|
'seo.og_image_default_height.min' => __('validation.settings.og_image_default_height_min'),
|
|
'seo.og_image_default_height.max' => __('validation.settings.og_image_default_height_max'),
|
|
'seo.twitter_default_card.string' => __('validation.settings.twitter_default_card_string'),
|
|
'seo.twitter_default_card.in' => __('validation.settings.twitter_default_card_in'),
|
|
'seo.twitter_default_site.string' => __('validation.settings.twitter_default_site_string'),
|
|
'seo.twitter_default_site.max' => __('validation.settings.twitter_default_site_max'),
|
|
'seo.cache_enabled.boolean' => __('validation.settings.seo_cache_enabled_boolean'),
|
|
'seo.cache_ttl.integer' => __('validation.settings.seo_cache_ttl_integer'),
|
|
'seo.cache_ttl.min' => __('validation.settings.seo_cache_ttl_min'),
|
|
'seo.cache_ttl.max' => __('validation.settings.seo_cache_ttl_max'),
|
|
'seo.sitemap_enabled.boolean' => __('validation.settings.sitemap_enabled_boolean'),
|
|
'seo.sitemap_cache_ttl.integer' => __('validation.settings.sitemap_cache_ttl_integer'),
|
|
'seo.sitemap_cache_ttl.min' => __('validation.settings.sitemap_cache_ttl_min'),
|
|
'seo.sitemap_cache_ttl.max' => __('validation.settings.sitemap_cache_ttl_max'),
|
|
'seo.sitemap_urls_per_file.integer' => __('validation.settings.sitemap_urls_per_file_integer'),
|
|
'seo.sitemap_urls_per_file.min' => __('validation.settings.sitemap_urls_per_file_min'),
|
|
'seo.sitemap_urls_per_file.max' => __('validation.settings.sitemap_urls_per_file_max'),
|
|
'seo.sitemap_gzip.boolean' => __('validation.settings.sitemap_gzip_boolean'),
|
|
'seo.sitemap_serve_stale_on_miss.boolean' => __('validation.settings.sitemap_serve_stale_on_miss_boolean'),
|
|
'seo.sitemap_max_urls_per_contributor.integer' => __('validation.settings.sitemap_max_urls_per_contributor_integer'),
|
|
'seo.sitemap_max_urls_per_contributor.min' => __('validation.settings.sitemap_max_urls_per_contributor_min'),
|
|
'seo.sitemap_hreflang_enabled.boolean' => __('validation.settings.sitemap_hreflang_enabled_boolean'),
|
|
'seo.sitemap_schedule.in' => __('validation.settings.sitemap_schedule_invalid'),
|
|
'seo.sitemap_schedule_time.regex' => __('validation.settings.sitemap_schedule_time_invalid'),
|
|
|
|
// 보안 설정
|
|
'security.force_https.required' => __('validation.settings.force_https_required'),
|
|
'security.force_https.boolean' => __('validation.settings.force_https_boolean'),
|
|
'security.login_attempt_enabled.required' => __('validation.settings.login_attempt_enabled_required'),
|
|
'security.login_attempt_enabled.boolean' => __('validation.settings.login_attempt_enabled_boolean'),
|
|
'security.auth_token_lifetime.integer' => __('validation.settings.auth_token_lifetime_integer'),
|
|
'security.auth_token_lifetime.min' => __('validation.settings.auth_token_lifetime_min'),
|
|
'security.auth_token_lifetime.max' => __('validation.settings.auth_token_lifetime_max'),
|
|
'security.allow_internal_outbound_urls.boolean' => __('validation.settings.allow_internal_outbound_urls_boolean'),
|
|
'security.max_login_attempts.integer' => __('validation.settings.max_login_attempts_integer'),
|
|
'security.max_login_attempts.min' => __('validation.settings.max_login_attempts_min'),
|
|
'security.max_login_attempts.max' => __('validation.settings.max_login_attempts_max'),
|
|
'security.login_lockout_time.integer' => __('validation.settings.login_lockout_time_integer'),
|
|
'security.login_lockout_time.min' => __('validation.settings.login_lockout_time_min'),
|
|
'security.login_lockout_time.max' => __('validation.settings.login_lockout_time_max'),
|
|
|
|
// 캐시 설정
|
|
'advanced.cache_enabled.required' => __('validation.settings.cache_enabled_required'),
|
|
'advanced.cache_enabled.boolean' => __('validation.settings.cache_enabled_boolean'),
|
|
'advanced.layout_cache_enabled.required' => __('validation.settings.layout_cache_enabled_required'),
|
|
'advanced.layout_cache_enabled.boolean' => __('validation.settings.layout_cache_enabled_boolean'),
|
|
'advanced.layout_cache_ttl.required' => __('validation.settings.layout_cache_ttl_required'),
|
|
'advanced.layout_cache_ttl.integer' => __('validation.settings.layout_cache_ttl_integer'),
|
|
'advanced.layout_cache_ttl.min' => __('validation.settings.layout_cache_ttl_min'),
|
|
'advanced.layout_cache_ttl.max' => __('validation.settings.layout_cache_ttl_max'),
|
|
'advanced.stats_cache_enabled.required' => __('validation.settings.stats_cache_enabled_required'),
|
|
'advanced.stats_cache_enabled.boolean' => __('validation.settings.stats_cache_enabled_boolean'),
|
|
'advanced.stats_cache_ttl.required' => __('validation.settings.stats_cache_ttl_required'),
|
|
'advanced.stats_cache_ttl.integer' => __('validation.settings.stats_cache_ttl_integer'),
|
|
'advanced.stats_cache_ttl.min' => __('validation.settings.stats_cache_ttl_min'),
|
|
'advanced.stats_cache_ttl.max' => __('validation.settings.stats_cache_ttl_max'),
|
|
'advanced.seo_cache_enabled.required' => __('validation.settings.advanced_seo_cache_enabled_required'),
|
|
'advanced.seo_cache_enabled.boolean' => __('validation.settings.advanced_seo_cache_enabled_boolean'),
|
|
'advanced.seo_cache_ttl.required' => __('validation.settings.advanced_seo_cache_ttl_required'),
|
|
'advanced.seo_cache_ttl.integer' => __('validation.settings.advanced_seo_cache_ttl_integer'),
|
|
'advanced.seo_cache_ttl.min' => __('validation.settings.advanced_seo_cache_ttl_min'),
|
|
'advanced.seo_cache_ttl.max' => __('validation.settings.advanced_seo_cache_ttl_max'),
|
|
'advanced.seo_sitemap_cache_ttl.integer' => __('validation.settings.seo_sitemap_cache_ttl_integer'),
|
|
'advanced.seo_sitemap_cache_ttl.min' => __('validation.settings.seo_sitemap_cache_ttl_min'),
|
|
'advanced.seo_sitemap_cache_ttl.max' => __('validation.settings.seo_sitemap_cache_ttl_max'),
|
|
|
|
// 디버그 설정
|
|
'advanced.debug_mode.required' => __('validation.settings.debug_mode_required'),
|
|
'advanced.debug_mode.boolean' => __('validation.settings.debug_mode_boolean'),
|
|
'advanced.sql_query_log.required' => __('validation.settings.sql_query_log_required'),
|
|
'advanced.sql_query_log.boolean' => __('validation.settings.sql_query_log_boolean'),
|
|
|
|
// 목록 한계값
|
|
'advanced.pagination_result_cap.integer' => __('validation.settings.pagination_result_cap_integer'),
|
|
'advanced.pagination_result_cap.min' => __('validation.settings.pagination_result_cap_min'),
|
|
'advanced.pagination_result_cap.max' => __('validation.settings.pagination_result_cap_max'),
|
|
'advanced.pagination_max_page.integer' => __('validation.settings.pagination_max_page_integer'),
|
|
'advanced.pagination_max_page.min' => __('validation.settings.pagination_max_page_min'),
|
|
'advanced.pagination_max_page.max' => __('validation.settings.pagination_max_page_max'),
|
|
|
|
// 코어 업데이트 설정
|
|
'advanced.core_update_github_url.url' => __('validation.settings.core_update_github_url_invalid'),
|
|
'advanced.core_update_github_url.max' => __('validation.settings.core_update_github_url_max'),
|
|
'advanced.core_update_github_token.max' => __('validation.settings.core_update_github_token_max'),
|
|
|
|
// 드라이버 설정
|
|
'drivers.storage_driver.required' => __('validation.settings.storage_driver_required'),
|
|
'drivers.storage_driver.in' => __('validation.settings.storage_driver_invalid'),
|
|
'drivers.s3_bucket.max' => __('validation.settings.s3_bucket_max'),
|
|
'drivers.s3_region.regex' => __('validation.settings.s3_region_invalid'),
|
|
'drivers.s3_region.max' => __('validation.settings.s3_region_max'),
|
|
'drivers.s3_access_key.max' => __('validation.settings.s3_access_key_max'),
|
|
'drivers.s3_secret_key.max' => __('validation.settings.s3_secret_key_max'),
|
|
'drivers.s3_url.url' => __('validation.settings.s3_url_invalid'),
|
|
'drivers.s3_url.max' => __('validation.settings.s3_url_max'),
|
|
'drivers.s3_endpoint.url' => __('validation.settings.s3_endpoint_invalid'),
|
|
'drivers.s3_endpoint.max' => __('validation.settings.s3_endpoint_max'),
|
|
'drivers.s3_use_path_style.boolean' => __('validation.settings.s3_use_path_style_boolean'),
|
|
'drivers.cache_driver.required' => __('validation.settings.cache_driver_required'),
|
|
'drivers.cache_driver.in' => __('validation.settings.cache_driver_invalid'),
|
|
'drivers.redis_host.max' => __('validation.settings.redis_host_max'),
|
|
'drivers.redis_port.integer' => __('validation.settings.redis_port_integer'),
|
|
'drivers.redis_port.min' => __('validation.settings.redis_port_min'),
|
|
'drivers.redis_port.max' => __('validation.settings.redis_port_max'),
|
|
'drivers.redis_password.max' => __('validation.settings.redis_password_max'),
|
|
'drivers.redis_database.integer' => __('validation.settings.redis_database_integer'),
|
|
'drivers.redis_database.min' => __('validation.settings.redis_database_min'),
|
|
'drivers.redis_database.max' => __('validation.settings.redis_database_max'),
|
|
'drivers.memcached_host.max' => __('validation.settings.memcached_host_max'),
|
|
'drivers.memcached_port.integer' => __('validation.settings.memcached_port_integer'),
|
|
'drivers.memcached_port.min' => __('validation.settings.memcached_port_min'),
|
|
'drivers.memcached_port.max' => __('validation.settings.memcached_port_max'),
|
|
'drivers.session_driver.required' => __('validation.settings.session_driver_required'),
|
|
'drivers.session_driver.in' => __('validation.settings.session_driver_invalid'),
|
|
'drivers.session_lifetime.integer' => __('validation.settings.session_lifetime_integer'),
|
|
'drivers.session_lifetime.min' => __('validation.settings.session_lifetime_min'),
|
|
'drivers.session_lifetime.max' => __('validation.settings.session_lifetime_max'),
|
|
'drivers.queue_driver.required' => __('validation.settings.queue_driver_required'),
|
|
'drivers.queue_driver.in' => __('validation.settings.queue_driver_invalid'),
|
|
'drivers.websocket_enabled.boolean' => __('validation.settings.websocket_enabled_boolean'),
|
|
'drivers.websocket_app_id.required' => __('validation.settings.websocket_app_id_required'),
|
|
'drivers.websocket_app_id.max' => __('validation.settings.websocket_app_id_max'),
|
|
'drivers.websocket_app_key.required' => __('validation.settings.websocket_app_key_required'),
|
|
'drivers.websocket_app_key.max' => __('validation.settings.websocket_app_key_max'),
|
|
'drivers.websocket_app_secret.required' => __('validation.settings.websocket_app_secret_required'),
|
|
'drivers.websocket_app_secret.max' => __('validation.settings.websocket_app_secret_max'),
|
|
'drivers.websocket_host.max' => __('validation.settings.websocket_host_max'),
|
|
'drivers.websocket_port.integer' => __('validation.settings.websocket_port_integer'),
|
|
'drivers.websocket_port.min' => __('validation.settings.websocket_port_min'),
|
|
'drivers.websocket_port.max' => __('validation.settings.websocket_port_max'),
|
|
'drivers.websocket_scheme.in' => __('validation.settings.websocket_scheme_invalid'),
|
|
'drivers.websocket_verify_ssl.boolean' => __('validation.settings.websocket_verify_ssl_boolean'),
|
|
'drivers.websocket_server_host.max' => __('validation.settings.websocket_server_host_max'),
|
|
'drivers.websocket_server_port.integer' => __('validation.settings.websocket_server_port_integer'),
|
|
'drivers.websocket_server_port.min' => __('validation.settings.websocket_server_port_min'),
|
|
'drivers.websocket_server_port.max' => __('validation.settings.websocket_server_port_max'),
|
|
'drivers.websocket_server_scheme.in' => __('validation.settings.websocket_server_scheme_invalid'),
|
|
'drivers.search_engine_driver.in' => __('validation.settings.search_engine_driver_invalid'),
|
|
'drivers.log_driver.required' => __('validation.settings.log_driver_required'),
|
|
'drivers.log_driver.in' => __('validation.settings.log_driver_invalid'),
|
|
'drivers.log_level.required' => __('validation.settings.log_level_required'),
|
|
'drivers.log_level.in' => __('validation.settings.log_level_invalid'),
|
|
'drivers.log_days.integer' => __('validation.settings.log_days_integer'),
|
|
'drivers.log_days.min' => __('validation.settings.log_days_min'),
|
|
'drivers.log_days.max' => __('validation.settings.log_days_max'),
|
|
|
|
// 본인인증(IDV) 설정
|
|
'identity.default_provider.string' => __('validation.settings.identity_default_provider_string'),
|
|
'identity.default_provider.max' => __('validation.settings.identity_default_provider_max'),
|
|
'identity.purpose_providers.array' => __('validation.settings.identity_purpose_providers_array'),
|
|
// 목적별 프로바이더 leaf 값(점 포함 purpose id 대응)은 withValidator() 에서
|
|
// validation.settings.identity_purpose_provider_string / _max 를 직접 부착합니다.
|
|
'identity.challenge_ttl_minutes.required' => __('validation.settings.identity_challenge_ttl_required'),
|
|
'identity.challenge_ttl_minutes.integer' => __('validation.settings.identity_challenge_ttl_integer'),
|
|
'identity.challenge_ttl_minutes.min' => __('validation.settings.identity_challenge_ttl_min'),
|
|
'identity.challenge_ttl_minutes.max' => __('validation.settings.identity_challenge_ttl_max'),
|
|
'identity.max_attempts.required' => __('validation.settings.identity_max_attempts_required'),
|
|
'identity.max_attempts.integer' => __('validation.settings.identity_max_attempts_integer'),
|
|
'identity.max_attempts.min' => __('validation.settings.identity_max_attempts_min'),
|
|
'identity.max_attempts.max' => __('validation.settings.identity_max_attempts_max'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get custom attributes for validator errors.
|
|
*
|
|
* nested 구조에 맞게 'category.field' 형식으로 정의합니다.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public function attributes(): array
|
|
{
|
|
return [
|
|
'general.site_name' => __('validation.attributes.site_name'),
|
|
'general.site_url' => __('validation.attributes.site_url'),
|
|
'general.site_description' => __('validation.attributes.site_description'),
|
|
'general.admin_email' => __('validation.attributes.admin_email'),
|
|
'general.timezone' => __('validation.attributes.timezone'),
|
|
'general.language' => __('validation.attributes.language'),
|
|
// 본인인증(IDV) 필드
|
|
'identity.default_provider' => __('validation.attributes.identity_default_provider'),
|
|
'identity.purpose_providers' => __('validation.attributes.identity_purpose_providers'),
|
|
'identity.challenge_ttl_minutes' => __('validation.attributes.identity_challenge_ttl_minutes'),
|
|
'identity.max_attempts' => __('validation.attributes.identity_max_attempts'),
|
|
// notifications
|
|
'notifications.channels' => __('validation.attributes.channels'),
|
|
// general
|
|
'general.currency' => __('validation.attributes.currency'),
|
|
'general.maintenance_mode' => __('validation.attributes.maintenance_mode'),
|
|
'general.asset_url_mode' => __('validation.attributes.asset_url_mode'),
|
|
'general.site_logo' => __('validation.attributes.site_logo'),
|
|
// mail
|
|
'mail.mailer' => __('validation.attributes.mailer'),
|
|
'mail.host' => __('validation.attributes.host'),
|
|
'mail.port' => __('validation.attributes.port'),
|
|
'mail.username' => __('validation.attributes.username'),
|
|
'mail.password' => __('validation.attributes.password'),
|
|
'mail.encryption' => __('validation.attributes.encryption'),
|
|
'mail.mailgun_domain' => __('validation.attributes.mailgun_domain'),
|
|
'mail.mailgun_secret' => __('validation.attributes.mailgun_secret'),
|
|
'mail.mailgun_endpoint' => __('validation.attributes.mailgun_endpoint'),
|
|
'mail.ses_key' => __('validation.attributes.ses_key'),
|
|
'mail.ses_secret' => __('validation.attributes.ses_secret'),
|
|
'mail.ses_region' => __('validation.attributes.ses_region'),
|
|
'mail.from_address' => __('validation.attributes.from_address'),
|
|
'mail.from_name' => __('validation.attributes.from_name'),
|
|
// upload
|
|
'upload.max_file_size' => __('validation.attributes.max_file_size'),
|
|
'upload.allowed_extensions' => __('validation.attributes.allowed_extensions'),
|
|
'upload.image_max_width' => __('validation.attributes.image_max_width'),
|
|
'upload.image_max_height' => __('validation.attributes.image_max_height'),
|
|
'upload.image_quality' => __('validation.attributes.image_quality'),
|
|
// seo
|
|
'seo.meta_title_suffix' => __('validation.attributes.meta_title_suffix'),
|
|
'seo.meta_description' => __('validation.attributes.meta_description'),
|
|
'seo.meta_keywords' => __('validation.attributes.meta_keywords'),
|
|
'seo.google_analytics_id' => __('validation.attributes.google_analytics_id'),
|
|
'seo.google_site_verification' => __('validation.attributes.google_site_verification'),
|
|
'seo.naver_site_verification' => __('validation.attributes.naver_site_verification'),
|
|
'seo.bot_user_agents' => __('validation.attributes.bot_user_agents'),
|
|
'seo.bot_detection_enabled' => __('validation.attributes.bot_detection_enabled'),
|
|
'seo.bot_detection_library_enabled' => __('validation.attributes.bot_detection_library_enabled'),
|
|
'seo.og_default_site_name' => __('validation.attributes.og_default_site_name'),
|
|
'seo.og_image_default_width' => __('validation.attributes.og_image_default_width'),
|
|
'seo.og_image_default_height' => __('validation.attributes.og_image_default_height'),
|
|
'seo.twitter_default_card' => __('validation.attributes.twitter_default_card'),
|
|
'seo.twitter_default_site' => __('validation.attributes.twitter_default_site'),
|
|
'seo.cache_enabled' => __('validation.attributes.seo_page_cache_enabled'),
|
|
'seo.cache_ttl' => __('validation.attributes.cache_ttl'),
|
|
'seo.sitemap_enabled' => __('validation.attributes.sitemap_enabled'),
|
|
'seo.sitemap_cache_ttl' => __('validation.attributes.sitemap_cache_ttl'),
|
|
'seo.sitemap_urls_per_file' => __('validation.attributes.sitemap_urls_per_file'),
|
|
'seo.sitemap_gzip' => __('validation.attributes.sitemap_gzip'),
|
|
'seo.sitemap_serve_stale_on_miss' => __('validation.attributes.sitemap_serve_stale_on_miss'),
|
|
'seo.sitemap_max_urls_per_contributor' => __('validation.attributes.sitemap_max_urls_per_contributor'),
|
|
'seo.sitemap_hreflang_enabled' => __('validation.attributes.sitemap_hreflang_enabled'),
|
|
'seo.sitemap_schedule' => __('validation.attributes.sitemap_schedule'),
|
|
'seo.sitemap_schedule_time' => __('validation.attributes.sitemap_schedule_time'),
|
|
'seo.generator_enabled' => __('validation.attributes.generator_enabled'),
|
|
'seo.generator_content' => __('validation.attributes.generator_content'),
|
|
// security
|
|
'security.force_https' => __('validation.attributes.force_https'),
|
|
'security.login_attempt_enabled' => __('validation.attributes.login_attempt_enabled'),
|
|
'security.auth_token_lifetime' => __('validation.attributes.auth_token_lifetime'),
|
|
'security.max_login_attempts' => __('validation.attributes.max_login_attempts'),
|
|
'security.login_lockout_time' => __('validation.attributes.login_lockout_time'),
|
|
'security.password_min_length' => __('validation.attributes.password_min_length'),
|
|
'security.require_password_special_char' => __('validation.attributes.require_password_special_char'),
|
|
'security.two_factor_auth' => __('validation.attributes.two_factor_auth'),
|
|
'security.allow_internal_outbound_urls' => __('validation.attributes.allow_internal_outbound_urls'),
|
|
// advanced
|
|
'advanced.cache_enabled' => __('validation.attributes.advanced_cache_enabled'),
|
|
'advanced.layout_cache_enabled' => __('validation.attributes.layout_cache_enabled'),
|
|
'advanced.layout_cache_ttl' => __('validation.attributes.layout_cache_ttl'),
|
|
'advanced.stats_cache_enabled' => __('validation.attributes.stats_cache_enabled'),
|
|
'advanced.stats_cache_ttl' => __('validation.attributes.stats_cache_ttl'),
|
|
'advanced.seo_cache_enabled' => __('validation.attributes.seo_cache_enabled'),
|
|
'advanced.seo_cache_ttl' => __('validation.attributes.seo_cache_ttl'),
|
|
'advanced.seo_sitemap_cache_ttl' => __('validation.attributes.seo_sitemap_cache_ttl'),
|
|
'advanced.debug_mode' => __('validation.attributes.debug_mode'),
|
|
'advanced.sql_query_log' => __('validation.attributes.sql_query_log'),
|
|
'advanced.core_update_github_url' => __('validation.attributes.core_update_github_url'),
|
|
'advanced.core_update_github_token' => __('validation.attributes.core_update_github_token'),
|
|
'advanced.geoip_enabled' => __('validation.attributes.geoip_enabled'),
|
|
'advanced.geoip_license_key' => __('validation.attributes.geoip_license_key'),
|
|
'advanced.geoip_auto_update_enabled' => __('validation.attributes.geoip_auto_update_enabled'),
|
|
'advanced.pagination_result_cap' => __('validation.attributes.pagination_result_cap'),
|
|
'advanced.pagination_max_page' => __('validation.attributes.pagination_max_page'),
|
|
// drivers
|
|
'drivers.storage_driver' => __('validation.attributes.storage_driver'),
|
|
'drivers.s3_bucket' => __('validation.attributes.s3_bucket'),
|
|
'drivers.s3_region' => __('validation.attributes.s3_region'),
|
|
'drivers.s3_access_key' => __('validation.attributes.s3_access_key'),
|
|
'drivers.s3_secret_key' => __('validation.attributes.s3_secret_key'),
|
|
'drivers.s3_url' => __('validation.attributes.s3_url'),
|
|
'drivers.s3_endpoint' => __('validation.attributes.s3_endpoint'),
|
|
'drivers.s3_use_path_style' => __('validation.attributes.s3_use_path_style'),
|
|
'drivers.cache_driver' => __('validation.attributes.cache_driver'),
|
|
'drivers.redis_host' => __('validation.attributes.redis_host'),
|
|
'drivers.redis_port' => __('validation.attributes.redis_port'),
|
|
'drivers.redis_password' => __('validation.attributes.redis_password'),
|
|
'drivers.redis_database' => __('validation.attributes.redis_database'),
|
|
'drivers.memcached_host' => __('validation.attributes.memcached_host'),
|
|
'drivers.memcached_port' => __('validation.attributes.memcached_port'),
|
|
'drivers.session_driver' => __('validation.attributes.session_driver'),
|
|
'drivers.session_lifetime' => __('validation.attributes.session_lifetime'),
|
|
'drivers.queue_driver' => __('validation.attributes.queue_driver'),
|
|
'drivers.websocket_enabled' => __('validation.attributes.websocket_enabled'),
|
|
'drivers.websocket_app_id' => __('validation.attributes.websocket_app_id'),
|
|
'drivers.websocket_app_key' => __('validation.attributes.websocket_app_key'),
|
|
'drivers.websocket_app_secret' => __('validation.attributes.websocket_app_secret'),
|
|
'drivers.websocket_host' => __('validation.attributes.websocket_host'),
|
|
'drivers.websocket_port' => __('validation.attributes.websocket_port'),
|
|
'drivers.websocket_scheme' => __('validation.attributes.websocket_scheme'),
|
|
'drivers.websocket_verify_ssl' => __('validation.attributes.websocket_verify_ssl'),
|
|
'drivers.websocket_server_host' => __('validation.attributes.websocket_server_host'),
|
|
'drivers.websocket_server_port' => __('validation.attributes.websocket_server_port'),
|
|
'drivers.websocket_server_scheme' => __('validation.attributes.websocket_server_scheme'),
|
|
'drivers.search_engine_driver' => __('validation.attributes.search_engine_driver'),
|
|
'drivers.log_driver' => __('validation.attributes.log_driver'),
|
|
'drivers.log_level' => __('validation.attributes.log_level'),
|
|
'drivers.log_days' => __('validation.attributes.log_days'),
|
|
];
|
|
}
|
|
}
|