Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue125-htmlpurifier-cache-path.yaml
T
HeuJung 85e266d361 fix(core,extensions): 확장 쓰기 경로 공통화 · HTMLPurifier 정의 캐시 storage 이전
https://github.com/gnuboard/g7/issues/125 — 상품 상세설명을 HTML 로 저장할 때
HTMLPurifier 가 모듈 vendor 폴더 안에 정의 캐시를 만들려다 실패해 저장이 매번 500 으로
끝나던 문제를 고친다. vendor 를 읽기 전용으로 두는 표준 배포에서 그 쓰기는 예외가 아니라
PHP 경고로 나오고 Laravel 이 이를 ErrorException 으로 승격시킨다. 캐시는 설정 해시당 1회만
기록되므로 캐시가 영영 생기지 않아 재시도해도 같은 결과였다.

캐시 경로를 storage 아래로 옮기고, 그 경로마저 확보하지 못하면 캐시만 끄고 정화는 그대로
수행한다 — 캐시는 성능 장치이고 정화는 보안 장치라, 전자의 실패가 후자를 건너뛰게 만들면
안 된다. 저장은 성공하므로 운영자에게 도달하는 흔적이 로그 하나뿐이라 error 수준으로 남긴다
(출하 기본 로그 수준이 error 라 warning 은 기본 설치 상태에서 파일에 남지 않는다).

그 과정에서 갈라져 있던 두 축을 코어 한 곳으로 모은다.

- 쓰기 디렉토리 확보: 억제 생성·chmod·setgid·소유권 상속·쓰기 판정 절차가 정적 게시와
 정의 캐시 두 곳에 서로 다른 하드닝으로 복제돼 있었다(억제 mkdir·setgid·clearstatcache 가
 사본마다 한쪽씩 빠져 있었다). FilePermissionHelper 의 ensureWritableDirectory 와
 hardenDirectory 로 통합하고, 실패 사유는 out 파라미터로 올려 정책(조용한 성능 저하 대
 시끄러운 실패)은 호출부가 정하게 둔다.

- 확장 저장 경로: storage_path('app/modules/…') 손조립이 30곳에 흩어져 있어 테스트 격리
 분기를 넣으려면 사본마다 복제해야 했고, 한 곳만 빠뜨려도 그 확장의 테스트가 운영 설정
 파일을 덮어쓴다. 디스크 root 를 단일 출처로 읽는 ExtensionStoragePath 로 전환하고 테스트
 분기는 config/filesystems.php 한 줄에서 끝낸다.

함께 고친 것

- 테스트가 운영 라우트 캐시로 부팅해 확장 allowlist 가 라우트 축에서 통째로 무력화되던
 문제. 삭제가 아니라 경로를 돌린다 — 라우트 캐시는 확장 작업 전까지 재생성되지 않아,
 삭제하면 운영 사이트가 그때까지 라우트 파일 스캔 경로로 떨어진다.
- PHPUnit 프로세스가 확장 vendor 의 제3자 composer 패키지를 오토로드하지 않아 그 패키지를
 쓰는 코드 경로가 통째로 테스트 불가였던 문제. 확장 자신의 오토로더를 그대로 쓰면 활성
 디렉토리가 _bundled 를 이기고 base path 유추까지 깨지므로, 생성된 맵에서 제3자 항목만
 골라 별도 로더로 등록한다.
- 게시 폴더가 setgid 를 갖지 않아, 명령줄과 웹이 번갈아 만든 하위 폴더를 다른 쪽이 쓰지
 못하던 문제.
- 관리자 템플릿이 HTML 정화 라이브러리를 직접 지정하지 않아 전이 의존으로 딸려온 구버전이
 쓰이던 문제.

동반 산출물

- 규정 표(·AGENTS.md) 6행 + storage-driver/service-repository/testing-guide 문서
- audit 룰 2종 + coverage 6항목. 저장소가 이미 전량 전환돼 전수 실행이 공허 통과하므로
 판정식은 픽스처 36건이 잠근다
- INSTALL.md 에 설치 후 파일 권한 절 추가 (vendor 쓰기 권한 불요를 명시)
2026-08-28 15:22:22 +09:00

57 lines
3.8 KiB
YAML

feature: HTMLPurifier 정의 캐시 경로 재지정 (공개 #125)
description: |
상품 상세설명을 편집기(HTML)로 작성해 저장하면 정화 구성요소(HTMLPurifier)가 정의 캐시를
자기 설치 폴더(`{module}/vendor/ezyang/htmlpurifier/library/.../DefinitionCache/Serializer/`)에
기록했다. 배포본의 vendor 를 읽기 전용으로 두는 표준 서버에서는 그 쓰기가 예외가 아니라
PHP 경고로 나오고 Laravel 이 이를 ErrorException 으로 승격시켜 상품 등록/수정이 500 으로
끝났다. 캐시는 설정 해시당 1회만 기록되므로 쓰기 불가 환경에서는 캐시가 영영 생기지 않아
매 요청이 같은 실패를 반복했다 (공개 이슈 #125, @lyg-kaban 제보).
정정은 세 갈래다 — ① 정의 캐시 경로를 `storage/app/modules/{identifier}/cache/htmlpurifier`
로 명시 지정하고, ② 그 디렉토리를 우리가 먼저 만든다(HTMLPurifier 는 base 디렉토리가 없으면
경고만 내고 만들지 않는다), ③ 그 경로마저 확보하지 못하면 정의 캐시만 끄고 정화는 그대로
수행한다. 캐시는 성능 장치이고 정화는 보안 장치라, 캐시 실패가 보안 장치를 건너뛰게
만들어서는 안 된다.
③ 의 폴백은 저장을 성공시키므로 운영자에게 도달하는 흔적이 로그 통지 하나뿐이다. 그 통지는
`error` 수준으로 남긴다 — G7 출하 기본 로그 수준(`config/settings/defaults.json` 의
`log_level`)이 `error` 라, `warning` 으로 남기면 기본 설치 상태에서 파일에 기록되지 않아
캐시를 못 쓰는 상태가 흔적 없이 영구히 유지된다.
axes:
description_mode: [text, html]
cache_dir: [writable, occupied_by_file, parent_unusable, already_populated]
exclusions:
- { description_mode: text, cache_dir: occupied_by_file, reason: "text 모드는 HTMLPurifier 를 인스턴스화하지 않아 캐시 상태와 직교" }
- { description_mode: text, cache_dir: parent_unusable, reason: "동일 — text 모드는 purifier 경로에 진입하지 않는다" }
- { description_mode: text, cache_dir: already_populated, reason: "동일 — text 모드는 purifier 경로에 진입하지 않는다" }
effects:
- definition_cache_written_under_storage
- vendor_install_directory_never_written
- text_mode_skips_purifier_entirely
- purify_still_strips_script_when_cache_disabled
- cache_dir_creation_failure_does_not_escalate
- cache_disabled_notice_reaches_default_log_level
- second_save_reuses_cached_definition
test_files:
- modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductDescriptionPurifierCacheTest.php
- modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ProductStoreHtmlDescriptionCacheTest.php
validation:
note: |
거짓 green 경고: 개발 머신의 vendor 에는 이미 `.ser` 가 있고 설정 해시가 같아 파일명이
동일하므로, "vendor 설치 폴더가 쓰이지 않았다" 는 단언만으로는 수정 전에도 통과한다.
결함을 실제로 증명하는 것은 storage 쪽 단언이다 — `Cache.SerializerPath` 값,
`generateFilePath()` 가 가리키는 경로의 실재, 그리고 폴백 경로에서의
`Cache.DefinitionImpl === null`.
이 테스트들은 모듈 전용 composer 패키지(`\HTMLPurifier`)를 실제로 로드해야 성립한다.
PHPUnit 진입점(`autoload-extensions.php`)은 확장 vendor 를 오토로드하지 않으므로 코어
`tests/bootstrap.php` 가 확장 vendor 의 생성된 맵에서 제3자 패키지만 골라 등록하며(확장
자신의 PSR-4 는 제외 — 활성 디렉토리 사본을 검증하게 되는 것을 막는다),
각 테스트는 로드 실패를 조용한 skip 이 아니라 setUp 단언 실패로 드러낸다.