sir.kr 커뮤니티 제보(Xbuilder 님 외)와 전수조사 발굴분을 코어·게시판· 이커머스·페이지·ckeditor5 전반에 반영. - 통합 검색 하이라이트: 코어 SearchHighlighter 로 원문 이스케이프 후 강조 (저장형 XSS 차단, board/ecommerce/page 3중복제 통합) - 공개 최신글·인기글·관리자 대시보드: 읽기권한 없는 게시판·블라인드·비활성 게시판 글 필터. 비밀글은 기존 공개 정책대로 제목 노출 유지(본문만 보호) - 게시판 관리 대시보드 권한 게이트, 이커머스 쿠폰 다운로드·리뷰 대상·주문옵션 확정 스코프 강화, 취소/환불 항목 검증 회원/비회원 대칭 - 만료 토큰 거부(SetLocale), 회원 탈퇴·정보관리 실패 시 내부 오류 원문 마스킹 - 예외→응답 매핑 정비(generic catch 상태코드), ckeditor 업로드 권한 게이트 정리 정책 정합 정정: 착수 계획이 "비밀글 제목 노출=취약점"으로 전제해 공개 최신글· 대시보드에 넣었던 is_secret 필터는, 저장소가 확정한 "제목 공개, 본문만 보호" 정책(2026-01-02/02-04/06-18/06-19)과 배치되어 되돌림. 읽기권한·블라인드 필터는 유지. Chrome MCP 실측(T1~T10) + 회귀 테스트로 확인.
63 lines
2.2 KiB
PHP
63 lines
2.2 KiB
PHP
<?php
|
|
|
|
namespace App\Search;
|
|
|
|
/**
|
|
* 검색 결과의 키워드 하이라이트와 본문 프리뷰 평문화를 담당하는 공유 헬퍼.
|
|
*
|
|
* 하이라이트 필드는 소비 측이 HTML 로 렌더하는 계약이므로, 원문을 반드시
|
|
* 이스케이프한 뒤 검색어만 <mark> 로 감싼다. 이스케이프를 건너뛰면 제목·본문에
|
|
* 삽입된 태그가 그대로 실행 계약으로 나간다(게시판/상품/페이지 검색 공통).
|
|
*/
|
|
class SearchHighlighter
|
|
{
|
|
/**
|
|
* 원문을 HTML 이스케이프한 뒤 검색어만 <mark> 로 감쌉니다.
|
|
*
|
|
* @param string|null $text 원문 텍스트(평문)
|
|
* @param string $keyword 검색어
|
|
* @return string 이스케이프 완료된 안전한 HTML
|
|
*/
|
|
public static function highlight(?string $text, string $keyword): string
|
|
{
|
|
if ($text === null || $text === '') {
|
|
return '';
|
|
}
|
|
|
|
$safe = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
|
|
|
if ($keyword === '') {
|
|
return $safe;
|
|
}
|
|
|
|
// 키워드도 동일하게 이스케이프해, 이스케이프된 원문과 일관되게 매칭한다.
|
|
$safeKeyword = htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8');
|
|
$escapedKeyword = preg_quote($safeKeyword, '/');
|
|
|
|
$result = preg_replace('/('.$escapedKeyword.')/iu', '<mark>$1</mark>', $safe);
|
|
|
|
// 유효하지 않은 UTF-8 등으로 preg_replace 가 null 을 반환하면 이스케이프본을 유지한다.
|
|
return $result ?? $safe;
|
|
}
|
|
|
|
/**
|
|
* HTML 본문을 태그 없는 평문으로 변환합니다.
|
|
*
|
|
* 엔티티를 먼저 디코드한 뒤 태그를 제거해, 엔티티로 인코딩된 태그가
|
|
* 평문화 단계에서 실제 태그로 부활하지 못하게 합니다.
|
|
*
|
|
* @param string|null $html 본문(HTML 또는 평문)
|
|
* @return string 태그가 제거된 평문
|
|
*/
|
|
public static function toPlainText(?string $html): string
|
|
{
|
|
if ($html === null || $html === '') {
|
|
return '';
|
|
}
|
|
|
|
$decoded = html_entity_decode($html, ENT_QUOTES, 'UTF-8');
|
|
|
|
return trim((string) preg_replace('/\s+/', ' ', strip_tags($decoded)));
|
|
}
|
|
}
|