공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다. 브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도 남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데 자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기 하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발 대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다. 런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다. 자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그 실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML 에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다. 편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다. 두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의 custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에 의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다. 확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을 고치면 그 변경을 감지해 재게시까지 예약된다. FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접 넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로 나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린 스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과 분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠 화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를 함께 뒀다. 동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에 써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
149 lines
5.2 KiB
PHP
149 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit;
|
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase;
|
|
|
|
/**
|
|
* 동봉 자산 선언 ↔ 실제 파일 정합 테스트
|
|
*
|
|
* 편집기 본체를 CDN 이 아니라 플러그인이 직접 담게 되면서, "선언한 경로에 파일이
|
|
* 실제로 있는가" 가 새로운 실패 지점이 됐다. 이 정합이 깨지면 개발 환경에서는
|
|
* 아무 증상이 없고 **배포본에서만** 편집기가 뜨지 않는다 — 자산 404 는 예외를
|
|
* 남기지 않고 빈 화면으로만 나타나기 때문이다.
|
|
*
|
|
* 경로가 세 곳(레이아웃 확장 JSON · TS 상수 · 실제 디렉토리)에 나뉘어 있으므로
|
|
* 셋이 같은 버전을 가리키는지도 함께 본다.
|
|
*/
|
|
class VendoredAssetManifestTest extends PluginTestCase
|
|
{
|
|
/** 동봉 자산 루트 (플러그인 루트 기준) */
|
|
private const VENDOR_ROOT = 'dist/vendor/ckeditor5';
|
|
|
|
/**
|
|
* 플러그인 루트 경로를 돌려줍니다.
|
|
*
|
|
* @return string 플러그인 루트 절대 경로
|
|
*/
|
|
private function pluginPath(): string
|
|
{
|
|
return dirname(__DIR__, 2);
|
|
}
|
|
|
|
/**
|
|
* `ckeditorAssets.ts` 가 선언한 동봉 버전을 읽습니다.
|
|
*
|
|
* @return string 버전 문자열
|
|
*/
|
|
private function declaredVersion(): string
|
|
{
|
|
$source = file_get_contents($this->pluginPath().'/resources/js/handlers/ckeditorAssets.ts');
|
|
|
|
$this->assertIsString($source, 'ckeditorAssets.ts 를 읽을 수 없습니다.');
|
|
$this->assertSame(
|
|
1,
|
|
preg_match("/CKEDITOR5_VERSION\s*=\s*'([^']+)'/", $source, $matches),
|
|
'ckeditorAssets.ts 에서 CKEDITOR5_VERSION 을 찾을 수 없습니다.'
|
|
);
|
|
|
|
return $matches[1];
|
|
}
|
|
|
|
/**
|
|
* @scenario asset_class=vendored, outcome=loaded
|
|
*
|
|
* @effects vendored_asset_declared_path_exists_on_disk
|
|
*/
|
|
#[Test]
|
|
public function 동봉_디렉토리에_필수_파일이_모두_존재한다(): void
|
|
{
|
|
$version = $this->declaredVersion();
|
|
$base = $this->pluginPath().'/'.self::VENDOR_ROOT.'/'.$version;
|
|
|
|
foreach (['ckeditor5.umd.js', 'ckeditor5.css', 'LICENSE.md', 'translations/ko.umd.js'] as $relative) {
|
|
$this->assertFileExists($base.'/'.$relative, "동봉 자산이 없습니다: {$relative}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @effects vendored_asset_declared_path_exists_on_disk, runtime_asset_served_same_origin
|
|
*/
|
|
#[Test]
|
|
public function 레이아웃_확장이_가리키는_스크립트가_실제로_존재한다(): void
|
|
{
|
|
$extension = json_decode(
|
|
file_get_contents($this->pluginPath().'/resources/extensions/html-editor.json'),
|
|
true
|
|
);
|
|
|
|
$src = $extension['scripts'][0]['src'] ?? null;
|
|
|
|
$this->assertIsString($src, 'html-editor.json 에 스크립트 src 가 없습니다.');
|
|
$this->assertStringStartsWith(
|
|
'/api/plugins/assets/sirsoft-ckeditor5/',
|
|
$src,
|
|
'편집기 본체는 same-origin 자산 경로여야 합니다 (외부 CDN 금지).'
|
|
);
|
|
|
|
$relative = substr($src, strlen('/api/plugins/assets/sirsoft-ckeditor5/'));
|
|
|
|
$this->assertFileExists(
|
|
$this->pluginPath().'/'.$relative,
|
|
"레이아웃 확장이 가리키는 파일이 없습니다: {$relative}"
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function 선언한_버전과_레이아웃_확장_경로의_버전이_일치한다(): void
|
|
{
|
|
$version = $this->declaredVersion();
|
|
$extension = json_decode(
|
|
file_get_contents($this->pluginPath().'/resources/extensions/html-editor.json'),
|
|
true
|
|
);
|
|
|
|
$this->assertStringContainsString(
|
|
self::VENDOR_ROOT.'/'.$version.'/',
|
|
$extension['scripts'][0]['src'] ?? '',
|
|
'TS 상수의 버전과 레이아웃 확장 경로의 버전이 어긋났습니다.'
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function 자체_제공으로_전환했으므로_신뢰_외부_호스트_선언이_없다(): void
|
|
{
|
|
$manifest = json_decode(file_get_contents($this->pluginPath().'/plugin.json'), true);
|
|
|
|
$this->assertArrayNotHasKey(
|
|
'trusted_script_hosts',
|
|
$manifest,
|
|
'동봉으로 전환한 뒤에도 외부 호스트 선언이 남아 있으면, 그 호스트로 되돌아가는 회귀를 막지 못합니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @effects no_third_party_request_on_page_load
|
|
*/
|
|
#[Test]
|
|
public function 런타임_소스에_외부_cd_n_주소가_남아_있지_않다(): void
|
|
{
|
|
$sources = [
|
|
'resources/js/handlers/initEditor.ts',
|
|
'resources/js/index.ts',
|
|
'resources/extensions/html-editor.json',
|
|
];
|
|
|
|
foreach ($sources as $relative) {
|
|
$content = file_get_contents($this->pluginPath().'/'.$relative);
|
|
|
|
// 주석의 설명 문구는 대상이 아니다 — 실제 로드 대상이 되는 URL 형태만 본다
|
|
$this->assertDoesNotMatchRegularExpression(
|
|
'#https?://cdn\.ckeditor\.com#',
|
|
$content,
|
|
"{$relative} 에 외부 CDN 주소가 남아 있습니다."
|
|
);
|
|
}
|
|
}
|
|
}
|