Files
Gnuboard7/modules/_bundled/sirsoft-board/src/Http/Requests/StoreCommentRequest.php
T
HeuJung abb40ad210 fix(core,board,ecommerce,page): 중첩 리소스 스코프·계층 무결성 결함 12건 일괄 수정
중첩 라우트의 상위 리소스 ID 를 조회 스코프에 반영하지 않아 A 의 경로로 B 의
하위 리소스를 읽거나 변경할 수 있었다. 방어 위치를 Repository where 절(SSoT)로
통일하고 컨트롤러 사후 비교에 의존하지 않게 했다. 같은 리소스의 형제 엔드포인트가
서로 다른 강도로 검증하던 부분도 맞췄다.

계층 리소스는 자기 자신·자손을 부모로 지정하는 순환을 검증 계층에서 차단하고,
검증을 우회하는 경로(시더/훅/오염 데이터)를 위해 path 재귀에 방문 ID 가드를 뒀다.
설정값이 정하는 깊이 상한을 Service 가 리터럴로 재클램프하던 부분을 제거해
게시판 설정이 실제로 적용되게 했다.

쓰기 경로는 validated 기준으로 좁혀 FormRequest 미정의 필드가 fillable 로
새지 않게 했다. 기존 오염 데이터는 업그레이드 스텝이 정리한다(삭제 없이 최상위로 이동).

재발 방지로 audit 룰 3종을 추가하고, 규정 문서와 트러블슈팅 사례를 함께 갱신했다.

공개 이슈: gnuboard/g7
2026-07-24 16:17:44 +09:00

154 lines
5.7 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Http\Requests;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Modules\Sirsoft\Board\Enums\PostStatus;
use Modules\Sirsoft\Board\Models\Board;
use Modules\Sirsoft\Board\Rules\BlockedKeywordsRule;
use Modules\Sirsoft\Board\Rules\CommentValidationRule;
use Modules\Sirsoft\Board\Rules\CooldownRule;
/**
* 댓글 생성 요청 폼 검증
*/
class StoreCommentRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인
*
* @return bool 권한 검증 결과 (권한 체크는 미들웨어 위임, 항상 true)
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 전 데이터 준비
*
* route 파라미터를 검증 데이터에 merge합니다.
*/
protected function prepareForValidation(): void
{
$this->merge([
'post_id' => $this->route('postId'),
]);
}
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, mixed>
*/
public function rules(): array
{
$slug = $this->route('slug');
$board = Board::where('slug', $slug)->first();
if (! $board) {
return [];
}
// 비회원 여부 확인 (request()->user()를 사용해야 PermissionMiddleware에서 설정한 사용자를 인식)
$isGuest = ! $this->user();
// 스팸/보안 설정에서 쿨다운 값 가져오기
$spamSecurity = g7_module_settings('sirsoft-board', 'spam_security', []);
$commentCooldown = (int) ($spamSecurity['comment_cooldown_seconds'] ?? 0);
// 금지 키워드 목록 가져오기 (게시글과 동일하게 게시판 설정 기준)
$blockedKeywords = $board->blocked_keywords ?? [];
$rules = [
'content' => [
'required',
'string',
'min:'.($board->min_comment_length ?? 2),
'max:'.($board->max_comment_length ?? 1000),
new BlockedKeywordsRule($blockedKeywords),
new CooldownRule('comment', $commentCooldown, $slug),
],
'post_id' => ['bail', 'required', 'integer', new CommentValidationRule($slug, 'post')],
'parent_id' => [
'nullable',
'integer',
// 부모 댓글은 요청 경로의 게시글에 속해야 한다 (교차 게시글 부모 차단)
new CommentValidationRule($slug, 'parent_comment', (int) $this->route('postId')),
],
// depth는 서비스에서 자동 계산되므로 요청 데이터에서 받지 않음 (보안상 이유)
'is_secret' => ['boolean'],
'status' => ['nullable', 'string', Rule::in(PostStatus::values())],
// 비회원인 경우 author_name, password 필수
'author_name' => [$isGuest ? 'required' : 'nullable', 'string', 'max:50'],
'password' => [$isGuest ? 'required' : 'nullable', 'string', 'min:4', 'max:20'],
];
// 훅: 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 필터 제공
return HookManager::applyFilters('sirsoft-board.comment.store_validation_rules', $rules, $this);
}
/**
* 검증 오류 메시지 커스터마이징
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'content.required' => __('sirsoft-board::validation.comment.content.required'),
'content.min' => __('sirsoft-board::validation.comment.content.min'),
'content.max' => __('sirsoft-board::validation.comment.content.max'),
'parent_id.integer' => __('sirsoft-board::validation.comment.parent_id.integer'),
'author_name.required' => __('sirsoft-board::validation.comment.author_name.required'),
'author_name.max' => __('sirsoft-board::validation.comment.author_name.max'),
'password.required' => __('sirsoft-board::validation.comment.password.required'),
'password.min' => __('sirsoft-board::validation.comment.password.min'),
];
}
/**
* 검증할 필드의 이름을 커스터마이징
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'content' => __('sirsoft-board::validation.attributes.comment.content'),
'parent_id' => __('sirsoft-board::validation.attributes.comment.parent_id'),
'is_secret' => __('sirsoft-board::validation.attributes.comment.is_secret'),
'status' => __('sirsoft-board::validation.attributes.comment.status'),
'author_name' => __('sirsoft-board::validation.attributes.comment.author_name'),
'password' => __('sirsoft-board::validation.attributes.comment.password'),
];
}
/**
* 검증 통과 후 비밀번호를 해싱합니다.
*
* @param string|null $key 반환할 특정 검증 값의 키 (null 이면 전체)
* @param mixed $default 키가 없을 때 반환할 기본값
* @return array<string, mixed>|mixed 검증된 데이터 (키 지정 시 해당 값)
*/
public function validated($key = null, $default = null): mixed
{
$validated = parent::validated($key, $default);
// key가 지정된 경우 특정 값 반환
if ($key !== null) {
return $validated;
}
// 비밀번호가 있으면 해싱
if (! empty($validated['password'])) {
$validated['password'] = Hash::make($validated['password']);
}
return $validated;
}
}