fix(core,gdpr): 신규 설치 시 확장 설정 기본값 단계의 "Class not found" 중단 수정

- 설치 흐름(installPlugin/installModule)이 파일 복사 직후 그 확장의 PSR-4 경로를
 등록하도록 앞당김 (종전에는 시더 직전에만 등록해 검증·설정 초기화 단계가 사각)
- gdpr: 잠금 집합을 진입 파일 Plugin::lockedNecessaryStorage 가 소유하고
 Support\NecessaryAllowlist::locked 는 위임 — plugin.php 가 src/ 오토로드에 비의존
- 회귀 테스트: EntryFileAutoloadIndependenceTest, ExtensionInstallAutoloadOrderTest
- CHANGELOG 정책 정리: 코어 항목에서 확장·쇼핑몰 문구 제거, 쇼핑몰 증상은
 이커머스 1.2.1 로 이동, gdpr 1.0.4 항목·AGENTS.md 갱신
This commit is contained in:
HeuJung
2026-09-06 22:23:15 +09:00
parent d8426c5341
commit 7946410ba6
10 changed files with 173 additions and 15 deletions
+2 -1
View File
@@ -52,6 +52,7 @@
### Fixed ### Fixed
- 새로 설치할 때 일부 확장의 설치가 "Class not found" 오류로 중단되던 문제를 수정했습니다. 확장의 설정 기본값을 읽는 단계가 그 확장의 클래스 경로 등록보다 먼저 실행된 것이 원인이며, 이제 설치는 확장 파일을 복사한 직후 클래스 경로를 먼저 등록합니다. 이미 설치된 사이트의 업데이트에는 영향이 없습니다.
- 설정 캐시가 만들어져 있는 사이트(설치 마법사·환경설정 저장·확장 업데이트를 한 번이라도 거친 대부분의 사이트)에서 코어 업데이트가 업그레이드 스텝 단계에서 "수동 재개" 안내와 함께 멈추던 문제를 수정했습니다. 새 버전으로 실행되는 스텝 프로세스가 이전 버전의 설정 캐시를 읽어 자기 자신을 옛 버전으로 오판한 것이 원인이며, 실행할 스텝이 하나도 없는 버전으로 올릴 때도 같은 안내가 나왔습니다. 같은 원인으로 새 버전이 추가한 쓰기 폴더가 `sudo` 업데이트의 권한 정리에서 빠지던 문제도 함께 바로잡았고, 이제 업데이트는 스텝 실행 전에 이전 설정 캐시를 비웁니다. - 설정 캐시가 만들어져 있는 사이트(설치 마법사·환경설정 저장·확장 업데이트를 한 번이라도 거친 대부분의 사이트)에서 코어 업데이트가 업그레이드 스텝 단계에서 "수동 재개" 안내와 함께 멈추던 문제를 수정했습니다. 새 버전으로 실행되는 스텝 프로세스가 이전 버전의 설정 캐시를 읽어 자기 자신을 옛 버전으로 오판한 것이 원인이며, 실행할 스텝이 하나도 없는 버전으로 올릴 때도 같은 안내가 나왔습니다. 같은 원인으로 새 버전이 추가한 쓰기 폴더가 `sudo` 업데이트의 권한 정리에서 빠지던 문제도 함께 바로잡았고, 이제 업데이트는 스텝 실행 전에 이전 설정 캐시를 비웁니다.
- 명령줄로 업그레이드 스텝을 직접 재실행하거나 관리자 「시스템 최적화」를 실행한 뒤, 그 실행이 예약한 초기 화면 정적 파일 재생성이 조용히 건너뛰어지던 문제를 수정했습니다. - 명령줄로 업그레이드 스텝을 직접 재실행하거나 관리자 「시스템 최적화」를 실행한 뒤, 그 실행이 예약한 초기 화면 정적 파일 재생성이 조용히 건너뛰어지던 문제를 수정했습니다.
- 아웃바운드 프록시를 지정한 사이트에서 글·상품 저장이 수십 초씩 걸리던 문제를 수정했습니다. 사이트가 자기 자신에게 보내는 내부 요청까지 프록시로 나가고 있었고, 프록시가 응답하지 않으면 그 요청이 연결 실패 시각까지 매달렸습니다. 실패는 화면에 드러나지 않고 저장만 느려져 원인을 알기 어려웠습니다. 이제 사이트 자기 주소와 로컬 주소는 운영자가 예외 목록에 적지 않아도 항상 프록시를 거치지 않습니다. - 아웃바운드 프록시를 지정한 사이트에서 글·상품 저장이 수십 초씩 걸리던 문제를 수정했습니다. 사이트가 자기 자신에게 보내는 내부 요청까지 프록시로 나가고 있었고, 프록시가 응답하지 않으면 그 요청이 연결 실패 시각까지 매달렸습니다. 실패는 화면에 드러나지 않고 저장만 느려져 원인을 알기 어려웠습니다. 이제 사이트 자기 주소와 로컬 주소는 운영자가 예외 목록에 적지 않아도 항상 프록시를 거치지 않습니다.
@@ -64,7 +65,7 @@
- 실제 화면 동작에 쓰이는 라이브러리(axios·laravel-echo·pusher-js)가 개발용으로 분류돼 있어 보안 점검에서 빠지던 문제를 수정했습니다. 이제 점검 대상에 포함되며, 함께 확인된 axios 취약점도 1.20.0 으로 올려 해소했습니다. (#126 @jiwonpapa 님께서 제보해주셨습니다.) - 실제 화면 동작에 쓰이는 라이브러리(axios·laravel-echo·pusher-js)가 개발용으로 분류돼 있어 보안 점검에서 빠지던 문제를 수정했습니다. 이제 점검 대상에 포함되며, 함께 확인된 axios 취약점도 1.20.0 으로 올려 해소했습니다. (#126 @jiwonpapa 님께서 제보해주셨습니다.)
- 글을 쓰다 브라우저 창 크기가 바뀌면 저장 시 본문이 사라지던 문제를 수정했습니다. 새 글은 「내용은 필수입니다」로 저장에 실패했고, 글 수정에서는 저장에 성공한 것처럼 보이면서 그때까지 고친 내용이 사라졌습니다. 창 크기를 조금만 바꿔도(20픽셀 이내) 발생했으므로, 휴대폰에서 주소창이 숨겨지거나 키보드가 올라오거나 화면을 돌리는 것도 같은 상황입니다. 게시판 글쓰기(사용자·관리자), 페이지 본문, 상품 상세설명, 상품 공통정보 화면이 대상입니다. (#130 @jiwonpapa 님께서 제보해주셨습니다.) - 글을 쓰다 브라우저 창 크기가 바뀌면 저장 시 본문이 사라지던 문제를 수정했습니다. 새 글은 「내용은 필수입니다」로 저장에 실패했고, 글 수정에서는 저장에 성공한 것처럼 보이면서 그때까지 고친 내용이 사라졌습니다. 창 크기를 조금만 바꿔도(20픽셀 이내) 발생했으므로, 휴대폰에서 주소창이 숨겨지거나 키보드가 올라오거나 화면을 돌리는 것도 같은 상황입니다. 게시판 글쓰기(사용자·관리자), 페이지 본문, 상품 상세설명, 상품 공통정보 화면이 대상입니다. (#130 @jiwonpapa 님께서 제보해주셨습니다.)
- 창 크기가 바뀐 뒤 본문을 고치고 제목 등 다른 입력칸을 건드리면, 저장 시 본문이 고치기 전 내용으로 되돌아가던 문제를 수정했습니다. 새 글은 「내용은 필수입니다」로 저장에 실패했고, 글 수정에서는 저장에 성공한 것처럼 보이면서 그때까지 고친 내용이 사라졌습니다. 편집기에는 고친 내용이 그대로 보였기 때문에 저장 후 다시 열어보기 전까지는 알 수 없었습니다. 게시판 글쓰기(사용자·관리자), 페이지 본문, 상품 상세설명, 상품 공통정보 화면이 대상입니다. - 창 크기가 바뀐 뒤 본문을 고치고 제목 등 다른 입력칸을 건드리면, 저장 시 본문이 고치기 전 내용으로 되돌아가던 문제를 수정했습니다. 새 글은 「내용은 필수입니다」로 저장에 실패했고, 글 수정에서는 저장에 성공한 것처럼 보이면서 그때까지 고친 내용이 사라졌습니다. 편집기에는 고친 내용이 그대로 보였기 때문에 저장 후 다시 열어보기 전까지는 알 수 없었습니다. 게시판 글쓰기(사용자·관리자), 페이지 본문, 상품 상세설명, 상품 공통정보 화면이 대상입니다.
- 상품 상세에서 옵션을 고른 뒤 「바로 구매」·「장바구니 담기」가 동작하지 않던 문제를 수정했습니다. 화면에는 고른 옵션이 목록에 담긴 것으로 보이는데 실제 요청에는 아무 옵션도 실리지 않아, 「바로 구매」는 오류로 끝나고 「장바구니 담기」는 「옵션을 선택해주세요」 안내만 반복됐습니다. 옵션 조합을 두 번 담으면 먼저 담은 조합이 사라지는 것도 같은 원인입니다. 상품 추가옵션(각인·포장 등) 선택도 함께 정상화됐습니다. - 화면에서 목록에 담은 항목(선택한 옵션 등)이 저장·전송 요청에 실리지 않던 문제를 수정했습니다. 화면에는 담긴 것으로 보이는데 실제 요청에는 빈 목록이 전송되어, 상황에 따라 오류로 끝나거나 「선택해주세요」 안내만 반복됐습니다. 화면 상태를 두 곳에 나눠 보관하는 구조에서 한쪽에만 기록되던 것이 원인입니다.
- 서버측 라이브러리 guzzle·commonmark 의 알려진 취약점 12건을 해소했습니다. 결제·본인인증·알림 발송처럼 외부와 통신하는 경로가 이 라이브러리를 사용합니다. (#126 @jiwonpapa 님께서 제보해주셨습니다.) - 서버측 라이브러리 guzzle·commonmark 의 알려진 취약점 12건을 해소했습니다. 결제·본인인증·알림 발송처럼 외부와 통신하는 경로가 이 라이브러리를 사용합니다. (#126 @jiwonpapa 님께서 제보해주셨습니다.)
- 초기 화면 파일을 명령줄과 웹이 번갈아 만들 때, 나중에 생기는 하위 폴더가 한쪽 계정 전용으로 남아 다른 쪽이 쓰지 못하던 문제를 수정했습니다. 게시 폴더가 그룹 권한을 하위 폴더에 물려주도록 정리합니다(Linux·macOS). - 초기 화면 파일을 명령줄과 웹이 번갈아 만들 때, 나중에 생기는 하위 폴더가 한쪽 계정 전용으로 남아 다른 쪽이 쓰지 못하던 문제를 수정했습니다. 게시 폴더가 그룹 권한을 하위 폴더에 물려주도록 정리합니다(Linux·macOS).
- 템플릿을 업데이트해도 레이아웃이 바뀌지 않은 릴리스(다국어·라우트·컴포넌트 정의·에셋만 바뀐 경우)에서는 다국어·라우트·에셋 캐시가 갱신되지 않아 이전 내용이 계속 표시되던 문제를 수정했습니다. 모듈·플러그인 업데이트와 같이 항상 갱신됩니다. - 템플릿을 업데이트해도 레이아웃이 바뀌지 않은 릴리스(다국어·라우트·컴포넌트 정의·에셋만 바뀐 경우)에서는 다국어·라우트·에셋 캐시가 갱신되지 않아 이전 내용이 계속 표시되던 문제를 수정했습니다. 모듈·플러그인 업데이트와 같이 항상 갱신됩니다.
+7
View File
@@ -384,6 +384,13 @@ class ModuleManager implements ModuleManagerInterface
$onProgress?->__invoke('copy', '파일 복사 중...'); $onProgress?->__invoke('copy', '파일 복사 중...');
$this->copyFromPendingOrBundled($moduleName, $onProgress, $force); $this->copyFromPendingOrBundled($moduleName, $onProgress, $force);
// 설치 시점에는 autoload-extensions.php 가 아직 갱신되지 않았다. module.php 의
// getConfigValues()/getSettingsSchema() 등이 자기 src/ 클래스를 호출하면 그 클래스가
// 해석되지 않아 "Class not found" 로 설치가 중단된다 (업그레이드 경로는 기설치본의
// 매핑이 이미 있어 재현되지 않는다). 시더 실행 직전이 아니라 진입 파일을 로드하기
// 전에 그 확장의 PSR-4 매핑을 등록한다.
ExtensionManager::registerExtensionAutoloadPaths('modules', $moduleName);
try { try {
// 모듈이 활성 디렉토리에 있지 않으면 로드 시도 // 모듈이 활성 디렉토리에 있지 않으면 로드 시도
$module = $this->getModule($moduleName); $module = $this->getModule($moduleName);
+7
View File
@@ -368,6 +368,13 @@ class PluginManager implements PluginManagerInterface
$onProgress?->__invoke('copy', '파일 복사 중...'); $onProgress?->__invoke('copy', '파일 복사 중...');
$this->copyFromPendingOrBundled($pluginName, $onProgress, $force); $this->copyFromPendingOrBundled($pluginName, $onProgress, $force);
// 설치 시점에는 autoload-extensions.php 가 아직 갱신되지 않았다. plugin.php 의
// getConfigValues()/getSettingsSchema() 등이 자기 src/ 클래스를 호출하면 그 클래스가
// 해석되지 않아 "Class not found" 로 설치가 중단된다 (업그레이드 경로는 기설치본의
// 매핑이 이미 있어 재현되지 않는다). 시더 실행 직전이 아니라 진입 파일을 로드하기
// 전에 그 확장의 PSR-4 매핑을 등록한다.
ExtensionManager::registerExtensionAutoloadPaths('plugins', $pluginName);
try { try {
// 플러그인이 활성 디렉토리에 있지 않으면 로드 시도 // 플러그인이 활성 디렉토리에 있지 않으면 로드 시도
$plugin = $this->getPlugin($pluginName); $plugin = $this->getPlugin($pluginName);
@@ -8,6 +8,7 @@
### Fixed ### Fixed
- 상품 상세에서 옵션을 고른 뒤 「바로 구매」·「장바구니 담기」가 동작하지 않던 문제가 코어 7.0.10 과 함께 해소됩니다. 화면에는 고른 옵션이 목록에 담긴 것으로 보이는데 실제 요청에는 아무 옵션도 실리지 않아, 「바로 구매」는 오류로 끝나고 「장바구니 담기」는 「옵션을 선택해주세요」 안내만 반복됐습니다. 원인은 코어 화면 엔진에 있었으며 이 모듈의 코드는 바뀌지 않았습니다.
- `sudo` 로 코어를 업데이트할 때 업그레이드 단계가 저장하는 설정 파일이 root 소유로 남을 수 있던 문제를 수정했습니다. 저장 뒤 폴더 소유자와 같게 맞춥니다. - `sudo` 로 코어를 업데이트할 때 업그레이드 단계가 저장하는 설정 파일이 root 소유로 남을 수 있던 문제를 수정했습니다. 저장 뒤 폴더 소유자와 같게 맞춥니다.
### Changed ### Changed
+9 -3
View File
@@ -7,7 +7,7 @@
```text ```text
1. 유형: 플러그인 (sirsoft-gdpr) — 쿠키 동의 배너·동의 이력·GDPR/개인정보보호법 대응을 소유 1. 유형: 플러그인 (sirsoft-gdpr) — 쿠키 동의 배너·동의 이력·GDPR/개인정보보호법 대응을 소유
2. 확장 방식: `sirsoft-gdpr.consent.granted`/`revoked` 훅 구독, `data-gdpr-category` HTML 속성으로 자체 호스팅 자원 등록 2. 확장 방식: `sirsoft-gdpr.consent.granted`/`revoked` 훅 구독, `data-gdpr-category` HTML 속성으로 자체 호스팅 자원 등록
3. 건드리면 안 되는 것: 필수 허용목록의 **잠금 항목**(`Support\NecessaryAllowlist::locked()`), 동의 이력(immutable append-only) 직접 수정 3. 건드리면 안 되는 것: 필수 허용목록의 **잠금 항목**(`Plugin::lockedNecessaryStorage()`, `Support\NecessaryAllowlist::locked()` 가 위임), 동의 이력(immutable append-only) 직접 수정
4. 작업 위치: `plugins/_bundled/sirsoft-gdpr` — 활성 디렉토리 직접 수정 금지 4. 작업 위치: `plugins/_bundled/sirsoft-gdpr` — 활성 디렉토리 직접 수정 금지
5. 반영: `php artisan plugin:update sirsoft-gdpr --force` 5. 반영: `php artisan plugin:update sirsoft-gdpr --force`
``` ```
@@ -47,6 +47,11 @@ immutable append-only)으로 이중 기록합니다 — 지금 상태 조회와
**잠금 항목만 설정 밖입니다** — `auth_token` · `XSRF-TOKEN` · 세션 쿠키(런타임 해석) · **잠금 항목만 설정 밖입니다** — `auth_token` · `XSRF-TOKEN` · 세션 쿠키(런타임 해석) ·
`gdpr_session` 넷은 없으면 사이트가 서지 못하므로 코드가 정하고, 판정에는 언제나 운영자 목록과 `gdpr_session` 넷은 없으면 사이트가 서지 못하므로 코드가 정하고, 판정에는 언제나 운영자 목록과
합집합으로 얹힙니다. 설정에 담으면 저장 요청 한 번으로 지워져 잠금이 아니게 됩니다. 합집합으로 얹힙니다. 설정에 담으면 저장 요청 한 번으로 지워져 잠금이 아니게 됩니다.
그 정의는 진입 파일 `plugin.php`(`Plugin::lockedNecessaryStorage()`)가 소유하고 `Support\NecessaryAllowlist::locked()` 는
위임만 합니다. `getConfigValues()` 가 신규 설치 흐름에서 이 플러그인의 클래스 경로 등록보다 먼저 호출되기
때문입니다 — 진입 파일이 `src/` 클래스를 부르면 새로 설치하는 사이트에서만 "Class not found" 로 설치가
중단되고, 업그레이드는 기설치본 매핑이 있어 통과합니다.
<!-- @intent END --> <!-- @intent END -->
## 2. 디렉토리 지도 ## 2. 디렉토리 지도
@@ -160,7 +165,8 @@ immutable append-only)으로 이중 기록합니다 — 지금 상태 조회와
| 회원탈퇴(`after_withdraw`)에서 신원 정보(user_id 등)를 제거 | 활성 동의만 철회 처리, 신원은 완전삭제(`before_delete`) 시점에만 익명화 | 두 이벤트를 섞으면 탈퇴 회원의 재가입·이력 조회가 깨진다 | | 회원탈퇴(`after_withdraw`)에서 신원 정보(user_id 등)를 제거 | 활성 동의만 철회 처리, 신원은 완전삭제(`before_delete`) 시점에만 익명화 | 두 이벤트를 섞으면 탈퇴 회원의 재가입·이력 조회가 깨진다 |
| 운영자가 등록하지 않은 functional 쿠키를 화이트리스트에 추가 | 필수 허용목록(운영자 설정) ∪ 잠금 집합만 예외 | GDPR 은 "동의 전 전면 차단"이 원칙이지 "등록된 것만 차단"이 아니다 | | 운영자가 등록하지 않은 functional 쿠키를 화이트리스트에 추가 | 필수 허용목록(운영자 설정) ∪ 잠금 집합만 예외 | GDPR 은 "동의 전 전면 차단"이 원칙이지 "등록된 것만 차단"이 아니다 |
| 소비자(인터셉터·정리기·미들웨어)가 자기 목록 사본을 들고 판정 | 넷 다 같은 설정을 같은 함수로 읽는다 | 사본은 갈라지는데 그 어긋남은 "그 항목만 안 되는" 상태로만 나타난다 | | 소비자(인터셉터·정리기·미들웨어)가 자기 목록 사본을 들고 판정 | 넷 다 같은 설정을 같은 함수로 읽는다 | 사본은 갈라지는데 그 어긋남은 "그 항목만 안 되는" 상태로만 나타난다 |
| 잠금 항목을 설정(`necessary_storage_allowlist`)에 넣기 | 코드(`NecessaryAllowlist::locked()`)가 정하고 판정에서 합집합 | 설정에 있으면 저장 요청 한 번으로 지워져 잠금이 아니게 된다 | | 잠금 항목을 설정(`necessary_storage_allowlist`)에 넣기 | 코드(`Plugin::lockedNecessaryStorage()`)가 정하고 판정에서 합집합 | 설정에 있으면 저장 요청 한 번으로 지워져 잠금이 아니게 된다 |
| `plugin.php` 의 `getConfigValues()`/`getSettingsSchema()` 등이 `src/` 클래스를 직접 호출(정적 메서드·`new`·상수) | 값은 진입 파일이 소유하고 `src/` 쪽이 위임한다 (`::class` 참조는 무방) | 신규 설치 흐름은 클래스 경로 등록 전에 진입 파일을 읽어 "Class not found" 로 멈춘다. 업그레이드에서는 재현되지 않아 출시 전 검증을 통과한다 (`tests/Unit/EntryFileAutoloadIndependenceTest.php` 가 잠근다) |
| 세션 쿠키 이름을 `'laravel_session'` 으로 하드코딩 | `config('session.cookie')` 런타임 해석 | `SESSION_COOKIE` 를 지정한 사이트에서 그 항목이 죽고, 그 사실이 어디에도 드러나지 않는다 | | 세션 쿠키 이름을 `'laravel_session'` 으로 하드코딩 | `config('session.cookie')` 런타임 해석 | `SESSION_COOKIE` 를 지정한 사이트에서 그 항목이 죽고, 그 사실이 어디에도 드러나지 않는다 |
| 새 확장의 저장 키를 플러그인이 관측·수집해 자동 등재 | 운영자가 화면에서 직접 추가 | 저장 키 관측은 그 자체가 추적이다 — 동의 없이 하는 관측을 이 플러그인이 할 수는 없다 | | 새 확장의 저장 키를 플러그인이 관측·수집해 자동 등재 | 운영자가 화면에서 직접 추가 | 저장 키 관측은 그 자체가 추적이다 — 동의 없이 하는 관측을 이 플러그인이 할 수는 없다 |
| 정책 버전 발행을 코드/배치로 자동화 | 운영자가 매번 명시적으로 "+ 새 버전 발행" 클릭 | 자동화하면 사소한 문구 수정에도 전 회원이 재동의 화면을 보게 된다 | | 정책 버전 발행을 코드/배치로 자동화 | 운영자가 매번 명시적으로 "+ 새 버전 발행" 클릭 | 자동화하면 사소한 문구 수정에도 전 회원이 재동의 화면을 보게 된다 |
@@ -172,7 +178,7 @@ immutable append-only)으로 이중 기록합니다 — 지금 상태 조회와
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 --> <!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 종류 | 개수 | 위치 | | 종류 | 개수 | 위치 |
|---|---|---| |---|---|---|
| PHPUnit | 25개 | `plugins/_bundled/sirsoft-gdpr/tests` | | PHPUnit | 26개 | `plugins/_bundled/sirsoft-gdpr/tests` |
| Vitest | 13개 | `vitest.config.ts` | | Vitest | 13개 | `vitest.config.ts` |
| Playwright | 4개 | `tests/Playwright` | | Playwright | 4개 | `tests/Playwright` |
| 시나리오 매니페스트 | 6개 | `tests/scenarios` | | 시나리오 매니페스트 | 6개 | `tests/scenarios` |
@@ -14,6 +14,7 @@
- GDPR 설정 화면에 「필수 저장 항목」 카드를 추가했습니다. 기능 쿠키에 동의하지 않은 방문자에게도 저장이 허용되는 항목을 브라우저 저장소·세션 저장소·쿠키로 나누어 직접 편집할 수 있습니다. 새로 설치한 확장이 저장하는 항목이 동의 전에 지워진다면, 이제 이 화면에서 추가하면 됩니다. - GDPR 설정 화면에 「필수 저장 항목」 카드를 추가했습니다. 기능 쿠키에 동의하지 않은 방문자에게도 저장이 허용되는 항목을 브라우저 저장소·세션 저장소·쿠키로 나누어 직접 편집할 수 있습니다. 새로 설치한 확장이 저장하는 항목이 동의 전에 지워진다면, 이제 이 화면에서 추가하면 됩니다.
- 항목 이름 끝에 `*` 를 붙이면 앞부분이 같은 항목을 모두 포함합니다. 예를 들어 `g7_filters_*` 는 `g7_filters_orders_1` 을 함께 포함합니다. - 항목 이름 끝에 `*` 를 붙이면 앞부분이 같은 항목을 모두 포함합니다. 예를 들어 `g7_filters_*` 는 `g7_filters_orders_1` 을 함께 포함합니다.
- 사이트 동작에 반드시 필요한 항목(로그인 토큰, CSRF 토큰, 세션 쿠키, 쿠키 동의 기록)은 「잠금 항목」으로 카드 위쪽에 표시되며 삭제할 수 없습니다. - 사이트 동작에 반드시 필요한 항목(로그인 토큰, CSRF 토큰, 세션 쿠키, 쿠키 동의 기록)은 「잠금 항목」으로 카드 위쪽에 표시되며 삭제할 수 없습니다.
- 잠금 항목의 정의는 플러그인 진입 파일이 소유합니다. 설정 기본값을 만드는 시점에 다른 클래스를 불러오지 않으므로, 새로 설치하는 사이트에서도 설치가 중단되지 않습니다.
- 이미 설치된 사이트는 업데이트 시 현재 기본 목록이 설정으로 옮겨집니다. 이미 편집한 값이 있으면 그대로 둡니다. - 이미 설치된 사이트는 업데이트 시 현재 기본 목록이 설정으로 옮겨집니다. 이미 편집한 값이 있으면 그대로 둡니다.
### Fixed ### Fixed
+25 -3
View File
@@ -13,7 +13,6 @@ use Plugins\Sirsoft\Gdpr\Listeners\GdprAuthConsentListener;
use Plugins\Sirsoft\Gdpr\Listeners\GdprAuthLogoutListener; use Plugins\Sirsoft\Gdpr\Listeners\GdprAuthLogoutListener;
use Plugins\Sirsoft\Gdpr\Listeners\GdprUserDeleteListener; use Plugins\Sirsoft\Gdpr\Listeners\GdprUserDeleteListener;
use Plugins\Sirsoft\Gdpr\Listeners\GdprUserWithdrawListener; use Plugins\Sirsoft\Gdpr\Listeners\GdprUserWithdrawListener;
use Plugins\Sirsoft\Gdpr\Support\NecessaryAllowlist;
/** /**
* GDPR (일반 데이터 보호 규정) 플러그인 * GDPR (일반 데이터 보호 규정) 플러그인
@@ -37,7 +36,7 @@ class Plugin extends AbstractPlugin
* *
* 잠금 항목(`auth_token` / `XSRF-TOKEN` / 세션 쿠키 / `gdpr_session`)은 여기 넣지 * 잠금 항목(`auth_token` / `XSRF-TOKEN` / 세션 쿠키 / `gdpr_session`)은 여기 넣지
* 않습니다 — 설정에 담기면 API 로 지울 수 있어 잠금이 아니게 됩니다 * 않습니다 — 설정에 담기면 API 로 지울 수 있어 잠금이 아니게 됩니다
* (Support\NecessaryAllowlist::locked()). * (self::lockedNecessaryStorage()).
* *
* @var array<string, array<int, string>> * @var array<string, array<int, string>>
*/ */
@@ -458,6 +457,29 @@ class Plugin extends AbstractPlugin
* *
* @return array 설정 값 배열 * @return array 설정 값 배열
*/ */
/**
* strictly necessary 잠금 항목 — 운영자가 지울 수 없는 최소 집합.
*
* 설정에 넣지 않는다. 설정에 넣으면 API 로 지울 수 있어 잠금이 아니게 된다.
* 세션 쿠키 이름은 `session.cookie` 가 정하는 런타임 값이라 상수로 둘 수 없다 —
* 이름을 하드코딩하면 `SESSION_COOKIE` 를 지정한 사이트에서 그 항목이 죽는다.
*
* 이 정의는 `src/` 가 아니라 진입 파일에 둔다. `getConfigValues()` 는 신규 설치 흐름에서
* 이 플러그인의 PSR-4 오토로드가 등록되기 전에 호출되므로, `src/` 클래스를 부르면
* "Class not found" 로 설치가 중단된다 (업그레이드 경로는 기설치본 매핑이 있어 재현되지
* 않는다). `Support\NecessaryAllowlist::locked()` 는 이 메서드에 위임한다.
*
* @return array<string, array<int, string>> 스코프 => 항목 배열
*/
public static function lockedNecessaryStorage(): array
{
return [
'localStorage' => ['auth_token'],
'sessionStorage' => [],
'cookie' => ['XSRF-TOKEN', (string) config('session.cookie', 'laravel_session'), 'gdpr_session'],
];
}
public function getConfigValues(): array public function getConfigValues(): array
{ {
return [ return [
@@ -498,7 +520,7 @@ class Plugin extends AbstractPlugin
// 잠금 항목 — 설정이 아니라 코드가 정한다. 스키마에 넣지 않으므로 저장 요청에 // 잠금 항목 — 설정이 아니라 코드가 정한다. 스키마에 넣지 않으므로 저장 요청에
// 섞여 와도 validated() 에서 배제되어 저장되지 않는다. 화면에는 읽기 전용으로 // 섞여 와도 validated() 에서 배제되어 저장되지 않는다. 화면에는 읽기 전용으로
// 표시하고, 판정은 언제나 '운영자 목록 ∪ 이 집합' 이다. // 표시하고, 판정은 언제나 '운영자 목록 ∪ 이 집합' 이다.
'necessary_storage_locked' => NecessaryAllowlist::locked(), 'necessary_storage_locked' => self::lockedNecessaryStorage(),
'cookie_categories' => json_encode([ 'cookie_categories' => json_encode([
[ [
@@ -32,19 +32,15 @@ class NecessaryAllowlist
/** /**
* 잠금 항목 — 운영자가 지울 수 없는 최소 집합. * 잠금 항목 — 운영자가 지울 수 없는 최소 집합.
* *
* 설정에 넣지 않는다. 설정에 넣으면 API 로 지울 수 있어 잠금이 아니게 된다. * 정의는 진입 파일(`Plugin::lockedNecessaryStorage()`)이 소유한다. `getConfigValues()` 가
* 세션 쿠키 이름은 `session.cookie` 가 정하는 런타임 값이라 상수로 둘 수 없다 — * 신규 설치 흐름에서 이 클래스의 오토로드 등록보다 먼저 호출되기 때문이다. 여기서는
* 이름을 하드코딩하면 `SESSION_COOKIE` 를 지정한 사이트에서 그 항목이 죽는다. * 그 정의에 위임만 한다.
* *
* @return array<string, array<int, string>> 스코프 => 항목 배열 * @return array<string, array<int, string>> 스코프 => 항목 배열
*/ */
public static function locked(): array public static function locked(): array
{ {
return [ return Plugin::lockedNecessaryStorage();
'localStorage' => ['auth_token'],
'sessionStorage' => [],
'cookie' => ['XSRF-TOKEN', (string) config('session.cookie', 'laravel_session'), 'gdpr_session'],
];
} }
/** /**
@@ -0,0 +1,61 @@
<?php
namespace Plugins\Sirsoft\Gdpr\Tests\Unit;
use Plugins\Sirsoft\Gdpr\Plugin;
use Plugins\Sirsoft\Gdpr\Support\NecessaryAllowlist;
use Plugins\Sirsoft\Gdpr\Tests\PluginTestCase;
/**
* 진입 파일(plugin.php)은 자기 src/ 클래스의 오토로드에 의존하지 않는다.
*
* 신규 설치 흐름에서는 plugin.php 가 로드되어 getConfigValues() 가 호출되는 시점에 이 플러그인의
* PSR-4 매핑이 아직 없다. 진입 파일이 src/ 클래스를 실제로 호출(정적 메서드·new·상수)하면
* "Class not found" 로 설치가 중단되며, 업그레이드 경로는 기설치본 매핑이 있어 재현되지 않는다.
* (7.0.10 신규 설치에서 NecessaryAllowlist::locked() 호출로 실제 발생)
*/
class EntryFileAutoloadIndependenceTest extends PluginTestCase
{
/**
* plugin.php 가 use 한 자기 네임스페이스 클래스는 ::class 참조로만 쓰인다.
*/
public function test_entry_file_does_not_invoke_own_src_classes(): void
{
$file = dirname(__DIR__, 2).'/plugin.php';
$src = file_get_contents($file);
preg_match_all('/^use\s+Plugins\\\\Sirsoft\\\\Gdpr\\\\([A-Za-z\\\\]+?)(?:\s+as\s+([A-Za-z]+))?;/m', $src, $uses, PREG_SET_ORDER);
$this->assertNotEmpty($uses, 'plugin.php 의 자기 네임스페이스 use 문을 찾지 못했습니다');
$violations = [];
foreach (explode("\n", $src) as $i => $line) {
$t = trim($line);
if ($t === '' || str_starts_with($t, '*') || str_starts_with($t, '/*') || str_starts_with($t, '//') || str_starts_with($t, 'use ')) {
continue;
}
foreach ($uses as $u) {
$short = ! empty($u[2]) ? $u[2] : substr($u[1], strrpos('\\'.$u[1], '\\'));
if (preg_match('/(?<![A-Za-z\\\\])'.preg_quote($short, '/').'::(?!class\b)|new\s+'.preg_quote($short, '/').'\b/', $line)) {
$violations[] = ($i + 1).': '.$t;
}
}
}
$this->assertSame([], $violations, "plugin.php 가 src/ 클래스를 직접 호출합니다 (설치 시 오토로드 미등록):\n".implode("\n", $violations));
}
/**
* 잠금 집합은 진입 파일이 소유하고 Support 클래스는 같은 값을 돌려준다.
*/
public function test_locked_set_is_owned_by_entry_file_and_mirrored_by_support(): void
{
$fromEntry = Plugin::lockedNecessaryStorage();
$this->assertSame($fromEntry, NecessaryAllowlist::locked());
$this->assertSame($fromEntry, (new Plugin)->getConfigValues()['necessary_storage_locked']);
$this->assertSame(['auth_token'], $fromEntry['localStorage']);
$this->assertContains('XSRF-TOKEN', $fromEntry['cookie']);
$this->assertContains('gdpr_session', $fromEntry['cookie']);
$this->assertContains((string) config('session.cookie'), $fromEntry['cookie']);
}
}
@@ -0,0 +1,56 @@
<?php
namespace Tests\Unit\Extension;
use App\Extension\ModuleManager;
use App\Extension\PluginManager;
use PHPUnit\Framework\TestCase;
/**
* 확장 설치 흐름은 진입 파일을 로드하기 전에 그 확장의 PSR-4 매핑을 등록한다.
*
* 설치 시점에는 autoload-extensions.php 가 아직 갱신되지 않았다. 진입 파일(plugin.php / module.php)의
* getConfigValues()/getSettingsSchema() 가 자기 src/ 클래스를 호출하면 그 클래스가 해석되지 않아
* "Class not found" 로 설치가 중단된다. 종전에는 시더 실행 직전에만 등록해, 그보다 앞선 검증·설정
* 초기화 단계가 사각이었다 (7.0.10 신규 설치에서 gdpr 플러그인으로 실제 발생. 업그레이드 경로는
* 기설치본의 매핑이 이미 있어 재현되지 않는다).
*/
class ExtensionInstallAutoloadOrderTest extends TestCase
{
/**
* @return array<string, array{class-string, string, string, string, string}>
*/
public static function installFlows(): array
{
return [
'plugin' => [PluginManager::class, 'installPlugin', 'copyFromPendingOrBundled($pluginName', "registerExtensionAutoloadPaths('plugins', \$pluginName)", 'reloadPlugin($pluginName)'],
'module' => [ModuleManager::class, 'installModule', 'copyFromPendingOrBundled($moduleName', "registerExtensionAutoloadPaths('modules', \$moduleName)", 'reloadModule($moduleName)'],
];
}
/**
* @dataProvider installFlows
*/
public function test_autoload_paths_are_registered_between_copy_and_entry_file_load(
string $class,
string $method,
string $copyCall,
string $registerCall,
string $reloadCall,
): void {
$ref = new \ReflectionMethod($class, $method);
$lines = file($ref->getFileName());
$body = implode('', array_slice($lines, $ref->getStartLine() - 1, $ref->getEndLine() - $ref->getStartLine() + 1));
$copyPos = strpos($body, $copyCall);
$registerPos = strpos($body, $registerCall);
$reloadPos = strpos($body, $reloadCall);
$this->assertNotFalse($copyPos, "{$class}::{$method} 에 복사 호출이 없습니다");
$this->assertNotFalse($registerPos, "{$class}::{$method} 가 진입 파일 로드 전에 PSR-4 매핑을 등록하지 않습니다");
$this->assertNotFalse($reloadPos, "{$class}::{$method} 에 재로드 호출이 없습니다");
$this->assertGreaterThan($copyPos, $registerPos, '오토로드 등록은 활성 디렉토리 복사 뒤에 와야 합니다 (그 전에는 composer.json 이 없다)');
$this->assertLessThan($reloadPos, $registerPos, '오토로드 등록은 진입 파일 로드(reload) 앞에 와야 합니다');
}
}