feat(core,admin-template): 자산 URL 방식 감지 지점 보강 + 테스트 격리 결함 수정
계획서 §검증 후반부를 다시 대조해 미구현 3건을 확인하고 모두 구현했다. 앞선 커밋에서 "미구현 1건" 으로 보고한 것은 §12 불변식 표만 파일로 대조하고 §검증 섹션의 표 두 개는 확인하지 않은 결과였다. - 설치 2단계 요구사항 화면에 자산 URL 방식 항목 추가. 판정은 서버가 아니라 브라우저가 한다 — 서버에서 자기 APP_URL 로 요청하면 loopback 이 vhost·프록시 체인을 우회해 실제 방문자와 다른 답을 낸다. 2단계와 3단계가 같은 판정 함수를 쓰도록 추출했다 - 대시보드가 스스로 프로브를 던져 **저장된** 방식과 대조하고 어긋나면 안내한다. 런타임 값이 아니라 저장값을 보는 이유는, 자가 복구가 이미 전환해 놓았을 때가 바로 알려야 할 상황이기 때문이다 — 봇은 자바스크립트를 실행하지 않아 자가 복구가 닿지 않는다. 알리기만 하고 저장하지 않으므로 클라이언트가 서버 설정을 뒤집지 않는다 - 레이아웃 렌더링 테스트를 두 모드로 추가. 핵심 단언은 두 모드의 DOM 이 완전히 동일하다는 것이다 — 모드가 화면 구조로 새어나가면 확장자 없는 환경에서만 깨지고 그 환경은 개발 중 거의 재현되지 않는다 함께 고친 테스트 격리 결함 4건 (이번 기능과 무관하나 같은 세션에서 발견): - 인스톨러 공유 클래스 로드가 오토로드를 막고 있어, 앱 루트를 임시 디렉토리로 바꾸는 테스트에서 없는 파일을 읽으려다 죽었다 (8건 fatal) - 테스트용 번역 스텁이 실제 구현과의 정의 경쟁에서 이기면 주입된 번역을 무시했다. 클래스 단독 실행은 통과하고 스위트 실행만 실패하던 원인 - 모듈 활성 목록은 "디스크 로드분 ∩ DB 활성분" 인데 부팅 시 테이블이 비어 디스크 스캔이 일어나지 않아, 파일 캐시 잔재가 있을 때만 통과하던 순서 의존 테스트 - 라우트 프로바이더 가드 테스트가 Git 미추적 파일(.env) 존재에 의존해 신규 클론·CI 에서 실패했다. 임시 앱 루트로 격리 또한 일부 테스트가 실제 개발 환경 파일을 삭제·재생성하고 자신의 정리 단계에서 되돌리는데, 그 테스트가 죽으면 되돌아오지 않아 복구 불가능한 유실이 발생한다. 실제로 이번 세션에서 겪었으므로 테스트 부트스트랩에 프로세스 수준 복원 안전망을 추가했다. 검증: 백엔드 344건 / 템플릿 2799건 / 레이아웃 7건 / 브라우저 9건 green, audit 위반 0.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Support\OpcacheStatus;
|
||||
use App\Support\PrivilegedDatabaseAccounts;
|
||||
|
||||
/**
|
||||
* G7 인스톨러 - 검증 통합 API
|
||||
*
|
||||
@@ -70,6 +73,7 @@ class ValidationApi
|
||||
'required_files' => $this->checkRequiredFiles(),
|
||||
'https' => $this->checkHttps(),
|
||||
'opcache' => $this->checkOpcache(),
|
||||
'asset_url_mode' => $this->checkAssetUrlMode(),
|
||||
];
|
||||
|
||||
// 모든 필수 요구사항 통과 여부
|
||||
@@ -141,7 +145,7 @@ class ValidationApi
|
||||
// DB 최고권한 계정 차단 (Write/Read 공통).
|
||||
// 최고권한 계정 자격증명이 유출되면 데이터베이스 전체가 위험해지므로
|
||||
// 연결 시도 전에 막는다. 판정은 App\Support\PrivilegedDatabaseAccounts 가 SSoT.
|
||||
if (\App\Support\PrivilegedDatabaseAccounts::isBlocked($username)) {
|
||||
if (PrivilegedDatabaseAccounts::isBlocked($username)) {
|
||||
throw new Exception(
|
||||
lang('error_db_username_privileged', ['username' => $username])
|
||||
);
|
||||
@@ -190,7 +194,7 @@ class ValidationApi
|
||||
// 기존 테이블 감지 (Write DB만 수행)
|
||||
// 사용자가 입력한 db_prefix를 g7 시그니처에 적용하여 정확히 비교
|
||||
$existingTables = null;
|
||||
if (!$isReadDb) {
|
||||
if (! $isReadDb) {
|
||||
$detectPrefix = ($input['db_prefix'] ?? '') !== '' ? (string) $input['db_prefix'] : 'g7_';
|
||||
$existingTables = checkExistingTables($pdo, $database, $detectPrefix);
|
||||
}
|
||||
@@ -526,7 +530,7 @@ class ValidationApi
|
||||
*/
|
||||
private function checkOpcache(): array
|
||||
{
|
||||
$status = \App\Support\OpcacheStatus::probe();
|
||||
$status = OpcacheStatus::probe();
|
||||
|
||||
if ($status['enabled'] === null) {
|
||||
$message = lang('opcache_unknown');
|
||||
@@ -544,6 +548,27 @@ class ValidationApi
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 URL 방식 항목 정의 (권장 사항 — 설치를 차단하지 않음)
|
||||
*
|
||||
* 판정 자체는 **서버가 할 수 없다.** 서버에서 자기 APP_URL 로 curl 하면 loopback 이
|
||||
* nginx vhost·SSL·프록시 체인을 우회하거나 다른 vhost 를 타서, 실제 방문자가 겪는
|
||||
* 것과 다른 답을 낸다. 그래서 여기서는 프로브 경로만 내려주고 실제 판정은
|
||||
* 브라우저가 수행한다 (`installer.js::probeAssetUrlMode`).
|
||||
*
|
||||
* `detected` 가 항상 null 인 것은 미구현이 아니라 위 이유에 따른 설계다.
|
||||
* 프론트가 이 값을 받아 카드를 "확인 중" 상태로 먼저 그리고, 프로브가 끝나면 갱신한다.
|
||||
*/
|
||||
private function checkAssetUrlMode(): array
|
||||
{
|
||||
return [
|
||||
'required' => false, // 어느 방식이든 정상 — 통과/실패 게이트가 아니다
|
||||
'detected' => null, // 브라우저 프로브가 채운다 (서버 판정 불가)
|
||||
'probe_extension' => '/api/system/asset-probe.js',
|
||||
'probe_extensionless' => '/api/system/asset-probe',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 필수 PHP 함수(exec, proc_open) 비활성화 여부 검증
|
||||
*/
|
||||
@@ -601,7 +626,7 @@ class ValidationApi
|
||||
$returnCode = -1;
|
||||
exec('php --version 2>&1', $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0 && !empty($output)) {
|
||||
if ($returnCode === 0 && ! empty($output)) {
|
||||
$outputStr = implode("\n", $output);
|
||||
if (preg_match('/PHP\s+(\d+\.\d+\.\d+)/', $outputStr, $matches)) {
|
||||
$cliVersion = $matches[1];
|
||||
@@ -649,7 +674,7 @@ class ValidationApi
|
||||
$minVersion = MIN_PHP_VERSION;
|
||||
|
||||
// PHP_BINARY 상수
|
||||
if (defined('PHP_BINARY') && PHP_BINARY !== '' && !in_array(PHP_BINARY, $checkedPaths, true)) {
|
||||
if (defined('PHP_BINARY') && PHP_BINARY !== '' && ! in_array(PHP_BINARY, $checkedPaths, true)) {
|
||||
$checkedPaths[] = PHP_BINARY;
|
||||
$result = $this->validatePhpPath(PHP_BINARY);
|
||||
if ($result['valid']) {
|
||||
@@ -663,7 +688,7 @@ class ValidationApi
|
||||
continue;
|
||||
}
|
||||
$checkedPaths[] = $path;
|
||||
if (!file_exists($path)) {
|
||||
if (! file_exists($path)) {
|
||||
continue;
|
||||
}
|
||||
$result = $this->validatePhpPath($path);
|
||||
@@ -674,7 +699,7 @@ class ValidationApi
|
||||
|
||||
// 시스템 PATH의 'php'
|
||||
$defaultPhpAvailable = false;
|
||||
if (!in_array('php', $checkedPaths, true)) {
|
||||
if (! in_array('php', $checkedPaths, true)) {
|
||||
$result = $this->validatePhpPath('php');
|
||||
if ($result['valid']) {
|
||||
$found[] = ['path' => 'php', 'version' => $result['version']];
|
||||
@@ -691,11 +716,11 @@ class ValidationApi
|
||||
}
|
||||
|
||||
// Composer 자동 감지 (감지된 PHP 중 첫 번째로 검증)
|
||||
$phpForComposer = !empty($found) ? $found[0]['path'] : 'php';
|
||||
$phpForComposer = ! empty($found) ? $found[0]['path'] : 'php';
|
||||
$composerResult = $this->detectComposerBinary($phpForComposer);
|
||||
|
||||
echo json_encode([
|
||||
'success' => !empty($found),
|
||||
'success' => ! empty($found),
|
||||
'binaries' => $found,
|
||||
'default_php_available' => $defaultPhpAvailable,
|
||||
'composer' => $composerResult,
|
||||
@@ -706,7 +731,7 @@ class ValidationApi
|
||||
/**
|
||||
* Composer 바이너리 자동 감지
|
||||
*
|
||||
* @param string $phpPath .phar 실행 시 사용할 PHP 경로
|
||||
* @param string $phpPath .phar 실행 시 사용할 PHP 경로
|
||||
* @return array{found: bool, path: string|null, version: string|null}
|
||||
*/
|
||||
private function detectComposerBinary(string $phpPath = 'php'): array
|
||||
@@ -718,7 +743,7 @@ class ValidationApi
|
||||
}
|
||||
|
||||
// 2. 프로젝트 루트의 composer.phar
|
||||
$pharPath = realpath(__DIR__ . '/../../..') . '/composer.phar';
|
||||
$pharPath = realpath(__DIR__.'/../../..').'/composer.phar';
|
||||
if (file_exists($pharPath)) {
|
||||
$result = $this->validateComposerPath($pharPath, $phpPath);
|
||||
if ($result['valid']) {
|
||||
@@ -727,7 +752,7 @@ class ValidationApi
|
||||
}
|
||||
|
||||
// 3. 현재 디렉토리(public/install/api)의 composer.phar
|
||||
$localPhar = realpath(__DIR__) . '/composer.phar';
|
||||
$localPhar = realpath(__DIR__).'/composer.phar';
|
||||
if (file_exists($localPhar)) {
|
||||
$result = $this->validateComposerPath($localPhar, $phpPath);
|
||||
if ($result['valid']) {
|
||||
@@ -792,6 +817,7 @@ class ValidationApi
|
||||
$path = $_GET['path'] ?? '';
|
||||
if (empty($path)) {
|
||||
echo json_encode(['success' => false, 'message' => lang('error_path_required')], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -804,22 +830,24 @@ class ValidationApi
|
||||
'success' => false,
|
||||
'message' => lang('error_core_pending_path_invalid'),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$candidatePath = (str_starts_with($path, '/') || preg_match('#^[A-Za-z]:[/\\\\]#', $path) === 1)
|
||||
? $path
|
||||
: BASE_PATH . DIRECTORY_SEPARATOR . $path;
|
||||
: BASE_PATH.DIRECTORY_SEPARATOR.$path;
|
||||
|
||||
$resolved = @realpath($candidatePath);
|
||||
|
||||
if ($resolved === false || !is_dir($resolved)) {
|
||||
if ($resolved === false || ! is_dir($resolved)) {
|
||||
// 존재 여부/타입 차이를 응답으로 분기하지 않고 단일 메시지로 통일하여
|
||||
// 임의 경로 enumeration 신호 차단.
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => lang('error_core_pending_path_invalid'),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -850,7 +878,7 @@ class ValidationApi
|
||||
return false;
|
||||
}
|
||||
|
||||
return !preg_match('/[\s;`$|<>"\'&\x00-\x1F]/', $path);
|
||||
return ! preg_match('/[\s;`$|<>"\'&\x00-\x1F]/', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -860,16 +888,16 @@ class ValidationApi
|
||||
* `composer` 를 특정 PHP 로 실행하려는 운영 의도를 지원한다.
|
||||
* 두 토큰 모두 isInstallerSafePathArg 통과해야 정상 입력으로 인정.
|
||||
*
|
||||
* @return array{php: string, composer: string}|null 분해 실패 시 null
|
||||
* @return array{php: string, composer: string}|null 분해 실패 시 null
|
||||
*/
|
||||
private function splitPhpComposerTokens(string $path): ?array
|
||||
{
|
||||
if (!str_contains($path, ' ')) {
|
||||
if (! str_contains($path, ' ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokens = preg_split('/\s+/', trim($path), 2);
|
||||
if (!is_array($tokens) || count($tokens) !== 2) {
|
||||
if (! is_array($tokens) || count($tokens) !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -884,7 +912,7 @@ class ValidationApi
|
||||
/**
|
||||
* PHP 바이너리 경로 유효성 검증 헬퍼
|
||||
*
|
||||
* @param string $path PHP 바이너리 경로
|
||||
* @param string $path PHP 바이너리 경로
|
||||
* @return array{valid: bool, version: string|null, message: string}
|
||||
*/
|
||||
private function validatePhpPath(string $path): array
|
||||
@@ -896,11 +924,11 @@ class ValidationApi
|
||||
// 'php' 기본값이 아니면 셸 메타문자 차단. 파일 존재/실행 가능 검사는
|
||||
// open_basedir 같은 PHP 런타임 제약 환경의 false negative 를 피하기 위해
|
||||
// 생략하고, exec 결과로 최종 판정한다.
|
||||
if ($path !== 'php' && !$this->isInstallerSafePathArg($path)) {
|
||||
if ($path !== 'php' && ! $this->isInstallerSafePathArg($path)) {
|
||||
return ['valid' => false, 'version' => null, 'message' => lang('error_php_exec_failed', ['path' => $path])];
|
||||
}
|
||||
|
||||
$command = escapeshellarg($path) . ' --version 2>&1';
|
||||
$command = escapeshellarg($path).' --version 2>&1';
|
||||
$output = [];
|
||||
$returnCode = -1;
|
||||
exec($command, $output, $returnCode);
|
||||
@@ -919,6 +947,7 @@ class ValidationApi
|
||||
'message' => lang('success_php_binary_version', ['path' => $path, 'version' => $version]),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => false,
|
||||
'version' => $version,
|
||||
@@ -932,8 +961,8 @@ class ValidationApi
|
||||
/**
|
||||
* Composer 바이너리 경로 유효성 검증 헬퍼
|
||||
*
|
||||
* @param string $composerPath Composer 바이너리 경로 (빈 문자열이면 시스템 'composer')
|
||||
* @param string $phpPath PHP 바이너리 경로 (.phar 실행 시 사용)
|
||||
* @param string $composerPath Composer 바이너리 경로 (빈 문자열이면 시스템 'composer')
|
||||
* @param string $phpPath PHP 바이너리 경로 (.phar 실행 시 사용)
|
||||
* @return array{valid: bool, version: string|null, message: string}
|
||||
*/
|
||||
private function validateComposerPath(string $composerPath, string $phpPath = 'php'): array
|
||||
@@ -947,8 +976,8 @@ class ValidationApi
|
||||
if ($effectivePath !== 'composer' && str_contains($effectivePath, ' ')) {
|
||||
$tokens = $this->splitPhpComposerTokens($effectivePath);
|
||||
if ($tokens === null
|
||||
|| !$this->isInstallerSafePathArg($tokens['php'])
|
||||
|| !$this->isInstallerSafePathArg($tokens['composer'])
|
||||
|| ! $this->isInstallerSafePathArg($tokens['php'])
|
||||
|| ! $this->isInstallerSafePathArg($tokens['composer'])
|
||||
) {
|
||||
return [
|
||||
'valid' => false,
|
||||
@@ -957,7 +986,7 @@ class ValidationApi
|
||||
];
|
||||
}
|
||||
|
||||
$command = escapeshellarg($tokens['php']) . ' ' . escapeshellarg($tokens['composer']) . ' --version 2>&1';
|
||||
$command = escapeshellarg($tokens['php']).' '.escapeshellarg($tokens['composer']).' --version 2>&1';
|
||||
|
||||
$output = [];
|
||||
$returnCode = -1;
|
||||
@@ -971,6 +1000,7 @@ class ValidationApi
|
||||
$outputStr = implode("\n", $output);
|
||||
if (preg_match('/Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)/', $outputStr, $matches)) {
|
||||
$version = $matches[1];
|
||||
|
||||
return [
|
||||
'valid' => true,
|
||||
'version' => $version,
|
||||
@@ -983,7 +1013,7 @@ class ValidationApi
|
||||
|
||||
// 단일 토큰 — 시스템 기본('composer') 가 아니면 셸 메타문자 차단.
|
||||
// 파일 존재/실행 가능 검사는 open_basedir 환경의 false negative 회피를 위해 생략.
|
||||
if ($effectivePath !== 'composer' && !$this->isInstallerSafePathArg($effectivePath)) {
|
||||
if ($effectivePath !== 'composer' && ! $this->isInstallerSafePathArg($effectivePath)) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'version' => null,
|
||||
@@ -994,16 +1024,16 @@ class ValidationApi
|
||||
// .phar 파일이면 PHP 바이너리와 결합
|
||||
if (str_ends_with($effectivePath, '.phar')) {
|
||||
// phpPath 도 동일한 가드 — 'php' 기본값이 아니면 메타문자 없는 단일 토큰이어야 함
|
||||
if ($phpPath !== 'php' && !$this->isInstallerSafePathArg($phpPath)) {
|
||||
if ($phpPath !== 'php' && ! $this->isInstallerSafePathArg($phpPath)) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'version' => null,
|
||||
'message' => lang('error_php_exec_failed', ['path' => $phpPath]),
|
||||
];
|
||||
}
|
||||
$command = escapeshellarg($phpPath) . ' ' . escapeshellarg($effectivePath) . ' --version 2>&1';
|
||||
$command = escapeshellarg($phpPath).' '.escapeshellarg($effectivePath).' --version 2>&1';
|
||||
} else {
|
||||
$command = escapeshellarg($effectivePath) . ' --version 2>&1';
|
||||
$command = escapeshellarg($effectivePath).' --version 2>&1';
|
||||
}
|
||||
|
||||
$output = [];
|
||||
@@ -1020,6 +1050,7 @@ class ValidationApi
|
||||
$outputStr = implode("\n", $output);
|
||||
if (preg_match('/Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)/', $outputStr, $matches)) {
|
||||
$version = $matches[1];
|
||||
|
||||
return [
|
||||
'valid' => true,
|
||||
'version' => $version,
|
||||
|
||||
@@ -142,6 +142,10 @@
|
||||
document.getElementById('requirements-result').innerHTML = resultHtml;
|
||||
document.getElementById('navigation-buttons').classList.remove('hidden');
|
||||
|
||||
// 자산 URL 방식은 브라우저만 판정할 수 있어 카드 삽입 후 비동기로 채운다.
|
||||
// await 하지 않는다 — 프로브가 늦거나 실패해도 요구사항 화면을 막지 않는다.
|
||||
refreshAssetUrlModeCard();
|
||||
|
||||
// 버튼 표시 제어
|
||||
const recheckBtn = document.getElementById('recheck-btn');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
@@ -468,6 +472,20 @@
|
||||
);
|
||||
}
|
||||
|
||||
// 10. 자산 URL 방식 카드 (안내 항목, 필수 아님)
|
||||
// 서버는 판정할 수 없으므로(loopback 이 vhost·프록시를 우회) "확인 중" 으로 먼저
|
||||
// 그리고 refreshAssetUrlModeCard() 가 브라우저 프로브 결과로 갱신한다.
|
||||
// failedRequirements.push() 를 하지 않는다 — 어느 방식이든 정상 동작이다.
|
||||
if (data.asset_url_mode) {
|
||||
html += `<div id="asset-url-mode-card">${renderSingleItemCard(
|
||||
lang('asset_url_mode'),
|
||||
'status-warning',
|
||||
lang('asset_url_mode_checking'),
|
||||
'',
|
||||
false
|
||||
)}</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// 필수 조건 미충족 시 에러 박스 표시
|
||||
@@ -1555,7 +1573,11 @@
|
||||
* 실시간 검증 초기화
|
||||
*/
|
||||
/**
|
||||
* 자산 URL 방식을 브라우저에서 판정해 hidden 필드에 채운다 (이슈 #486 §6·§7).
|
||||
* 자산 URL 방식을 브라우저에서 판정한다 (이슈 #486 §6·§7).
|
||||
*
|
||||
* Step 2 요구사항 카드와 Step 3 설정 폼이 **같은 함수를 쓴다.** 판정 로직을 두 벌
|
||||
* 두면 한쪽만 고쳐져 화면마다 다른 답을 내놓는다 (이 이슈에서 이미 URL 생성부가
|
||||
* 그렇게 어긋났다).
|
||||
*
|
||||
* 정적 최적화 블록(`location ~* \.(js|css|json)$`)은 정규식 location 이라 프리픽스
|
||||
* location 보다 먼저 매칭되고, 그 안에 PHP 핸들러가 없으면 확장자 붙은 동적 응답이
|
||||
@@ -1572,19 +1594,16 @@
|
||||
* "404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를 반환하는 설정에서
|
||||
* 영원히 오판한다.
|
||||
*/
|
||||
async function detectAssetUrlMode() {
|
||||
const field = document.getElementById('asset_url_mode');
|
||||
if (!field) return;
|
||||
|
||||
async function probeAssetUrlMode(base) {
|
||||
const TOKEN = 'G7_ASSET_PROBE_OK';
|
||||
const base = (document.querySelector('[name="app_url"]')?.value || '').replace(/\/+$/, '');
|
||||
const root = (base || '').replace(/\/+$/, '');
|
||||
|
||||
/**
|
||||
* 프로브 1건을 던져 매직 토큰 포함 여부를 반환한다.
|
||||
*/
|
||||
const probe = async (path) => {
|
||||
try {
|
||||
const res = await fetch(`${base}${path}`, { cache: 'no-store', credentials: 'omit' });
|
||||
const res = await fetch(`${root}${path}`, { cache: 'no-store', credentials: 'omit' });
|
||||
if (!res.ok) return false;
|
||||
|
||||
// Content-Type 도 함께 본다 (L6). 토큰만 검사해도 200+HTML 오판은
|
||||
@@ -1603,13 +1622,60 @@
|
||||
probe('/api/system/asset-probe'),
|
||||
]);
|
||||
|
||||
let detected = '';
|
||||
if (withExt && withoutExt) {
|
||||
detected = 'extension';
|
||||
} else if (!withExt && withoutExt) {
|
||||
detected = 'extensionless';
|
||||
return 'extension';
|
||||
}
|
||||
// 둘 다 실패 = 모드 문제가 아니다(PHP/라우팅 장애). 빈 값으로 두어 기본값을 따르게 한다.
|
||||
if (!withExt && withoutExt) {
|
||||
return 'extensionless';
|
||||
}
|
||||
|
||||
// 둘 다 실패 = 모드 문제가 아니다(PHP/라우팅 장애). 빈 문자열로 판정 보류.
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel 앱 루트 경로를 반환한다 (`INSTALLER_BASE_URL` 은 항상 `/install` 로 끝난다).
|
||||
*
|
||||
* Step 2 에는 `app_url` 입력이 아직 없으므로 현재 origin 기준으로 조립한다.
|
||||
*/
|
||||
function getAppRootUrl() {
|
||||
const base = (window.INSTALLER_BASE_URL || '').replace(/\/install$/, '');
|
||||
|
||||
return `${window.location.origin}${base}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2 요구사항 카드의 자산 URL 방식 항목을 프로브 결과로 갱신한다 (§7).
|
||||
*
|
||||
* 서버는 이 값을 판정할 수 없어(loopback 이 vhost·프록시를 우회) 카드가 먼저
|
||||
* "확인 중" 으로 그려진 뒤 여기서 채워진다. 통과/실패 게이트가 아니라 안내 항목이라
|
||||
* `failedRequirements` 에 넣지 않는다 — 어느 방식이든 정상 동작이다.
|
||||
*/
|
||||
async function refreshAssetUrlModeCard() {
|
||||
const slot = document.getElementById('asset-url-mode-card');
|
||||
if (!slot) return;
|
||||
|
||||
const detected = await probeAssetUrlMode(getAppRootUrl());
|
||||
|
||||
// 확장자 미사용은 "서버가 확장자 주소를 가로채고 있다" 는 신호이므로 눈에 띄게 둔다.
|
||||
// 판정 불가도 마찬가지 — 설치 후 백지 화면의 예고일 수 있다.
|
||||
const statusClass = detected === 'extension' ? 'status-pass' : 'status-warning';
|
||||
const label = detected === 'extension'
|
||||
? lang('asset_url_mode_extension')
|
||||
: (detected === 'extensionless' ? lang('asset_url_mode_extensionless') : lang('asset_url_mode_unknown'));
|
||||
|
||||
// 카드 내부를 부분 수정하지 않고 통째로 다시 그린다 — 아이콘·수식자 클래스가
|
||||
// renderSingleItemCard 안에서 statusClass 로 함께 결정되므로, 밖에서 일부만
|
||||
// 건드리면 아이콘과 카드 색이 어긋난다.
|
||||
slot.innerHTML = renderSingleItemCard(lang('asset_url_mode'), statusClass, label, '', false);
|
||||
}
|
||||
|
||||
async function detectAssetUrlMode() {
|
||||
const field = document.getElementById('asset_url_mode');
|
||||
if (!field) return;
|
||||
|
||||
const base = (document.querySelector('[name="app_url"]')?.value || '').replace(/\/+$/, '');
|
||||
const detected = await probeAssetUrlMode(base);
|
||||
|
||||
field.value = detected;
|
||||
|
||||
|
||||
@@ -218,9 +218,14 @@ if (!defined('DELETE_INSTALLER_AFTER_COMPLETE')) {
|
||||
// 두 클래스는 프레임워크 의존성이 0 이라 이 방식이 성립하며, 인스톨러와 코어가
|
||||
// 서로 다른 판정을 내리는 것을 구조적으로 차단한다.
|
||||
// class_exists 가드: Laravel 부트가 선행된 테스트 환경에서 중복 로드 방지.
|
||||
if (!class_exists('App\\Support\\PrivilegedDatabaseAccounts', false)) {
|
||||
//
|
||||
// 오토로드를 **막지 않는다**(2번째 인자 생략). `false` 를 주면 오토로드 가능한
|
||||
// 클래스를 못 본 채 require 로 내려가는데, BASE_PATH 를 임시 디렉토리로 바꿔 두는
|
||||
// 인스톨러 단위 테스트에서는 그 경로에 app/Support 가 없어 fatal 이 된다.
|
||||
// 순수 인스톨러 실행 시에는 등록된 오토로더가 없어 false 를 반환하므로 require 가 그대로 돈다.
|
||||
if (!class_exists('App\\Support\\PrivilegedDatabaseAccounts')) {
|
||||
require_once BASE_PATH . '/app/Support/PrivilegedDatabaseAccounts.php';
|
||||
}
|
||||
if (!class_exists('App\\Support\\OpcacheStatus', false)) {
|
||||
if (!class_exists('App\\Support\\OpcacheStatus')) {
|
||||
require_once BASE_PATH . '/app/Support/OpcacheStatus.php';
|
||||
}
|
||||
@@ -841,6 +841,8 @@ Firewalls or proxies may be blocking long-lived HTTP connections.',
|
||||
'asset_url_mode_detected_extension' => 'Detected: file extensions work fine on this server.',
|
||||
'asset_url_mode_detected_extensionless' => 'Detected: the server intercepts extension URLs, so no extensions was selected.',
|
||||
'asset_url_mode_detected_unavailable' => 'Could not detect. If pages fail to load after installation, try switching to no extensions.',
|
||||
'asset_url_mode_checking' => 'Checking…',
|
||||
'asset_url_mode_unknown' => 'Unknown',
|
||||
'core_update_github_url' => 'GitHub Repository URL',
|
||||
'core_update_github_url_help' => 'GitHub repository URL to check for core updates.',
|
||||
'core_update_github_token' => 'GitHub Access Token',
|
||||
|
||||
@@ -841,6 +841,8 @@ ini_set(\'zlib.output_compression\', \'off\');
|
||||
'asset_url_mode_detected_extension' => '감지 결과: 확장자를 사용할 수 있는 환경입니다.',
|
||||
'asset_url_mode_detected_extensionless' => '감지 결과: 서버가 확장자 주소를 가로채고 있어 확장자 미사용을 선택했습니다.',
|
||||
'asset_url_mode_detected_unavailable' => '감지하지 못했습니다. 설치 후 화면이 뜨지 않으면 확장자 미사용으로 바꿔 보세요.',
|
||||
'asset_url_mode_checking' => '확인 중…',
|
||||
'asset_url_mode_unknown' => '확인 불가',
|
||||
'core_update_github_url' => 'GitHub 저장소 URL',
|
||||
'core_update_github_url_help' => '코어 업데이트를 확인할 GitHub 저장소 URL입니다.',
|
||||
'core_update_github_token' => 'GitHub 액세스 토큰',
|
||||
|
||||
Reference in New Issue
Block a user