한국어 Windows 에서 whoami 가 CP949 로 출력해 계정명에 한글이 있으면 invalid UTF-8 이 응답 배열에 실리고 json_encode 가 false 를 반환한다. echo false 는 빈 문자열이라 HTTP 200 + 빈 본문이 나가는데 예외도 로그도 남지 않아, 설치 마법사 2단계가 진행 불가 상태로 멈춘다. 2026-07 수정(7.0.2)은 로그 축 4곳만 막았다. 출처에서 정규화하고(1차), 응답 경계를 단일 헬퍼로 닫고(2차), 실패 시 문제 필드 경로를 로그에 남기도록(3차) 세 층으로 처리해 다른 경로가 열린 채 남지 않게 했다. 정규화는 복원 가능한 코드페이지 출력을 먼저 되살리므로, 종전 mb_scrub 단독 처리와 달리 한글이 U+FFFD 로 훼손되지 않는다. 같은 원인으로 한국어 Windows 에서 관리자 환경설정의 시스템 정보가 500 이 되던 것도 함께 막았다.
321 lines
14 KiB
PHP
321 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Step 5: 설치 진행
|
|
*
|
|
* Composer 의존성 설치, .env 파일 생성, Artisan 명령 실행, 관리자 계정 생성,
|
|
* 선택한 확장 기능(템플릿, 모듈, 플러그인) 설치를 수행합니다.
|
|
* 진행률 표시와 실시간 로그를 통해 사용자에게 피드백을 제공합니다.
|
|
*/
|
|
|
|
// 설정 정보 가져오기 (state.json에서 로드 - 다른 브라우저에서도 접근 가능)
|
|
$config = $state['config'] ?? $_SESSION['install_config'] ?? [];
|
|
?>
|
|
|
|
<!-- 필수 파일 생성 안내 섹션 (JS에서 제어, 기본 숨김) -->
|
|
<div id="env-setup-section" class="installer-container installer-container-wide hidden">
|
|
<div class="requirement-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title"><?= htmlspecialchars(lang('file_setup_title')) ?></h3>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<p><?= htmlspecialchars(lang('file_setup_description')) ?></p>
|
|
|
|
<!-- 누락 파일 목록 (JS에서 동적 표시) -->
|
|
<ul id="missing-files-list" class="missing-files"></ul>
|
|
|
|
<!-- 명령어 안내 (절대경로) -->
|
|
<p><?= htmlspecialchars(lang('file_setup_guide')) ?></p>
|
|
<div class="code-box">
|
|
<pre id="file-setup-command"></pre>
|
|
<button class="btn-copy" onclick="copySetupCommand()"><?= htmlspecialchars(lang('copy_command')) ?></button>
|
|
</div>
|
|
|
|
<!-- 상대경로 명령어 -->
|
|
<p class="fix-guide-hint" id="file-setup-relative-hint" style="display:none;"><?= htmlspecialchars(lang('or_relative_path')) ?></p>
|
|
<div class="code-box" id="file-setup-relative-box" style="display:none;">
|
|
<pre id="file-setup-command-relative"></pre>
|
|
<button class="btn-copy" onclick="copySetupCommandRelative()"><?= htmlspecialchars(lang('copy_command')) ?></button>
|
|
</div>
|
|
|
|
<!-- 확인 버튼 -->
|
|
<div class="env-check-actions">
|
|
<button id="env-recheck-btn" class="btn btn-primary" onclick="recheckFiles()"><?= htmlspecialchars(lang('file_check_button')) ?></button>
|
|
<span id="env-check-status" class="hidden"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="installer-container installer-container-wide">
|
|
<div class="installer-header-with-abort">
|
|
<h1 id="installer-title" class="installer-title"><?= htmlspecialchars(lang('installation_title')) ?></h1>
|
|
<button id="abort-installation-btn" class="btn-abort" onclick="abortInstallation()" style="display: none;">
|
|
<svg class="abort-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<?= htmlspecialchars(lang('abort_installation')) ?>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 완료 섹션 -->
|
|
<?php
|
|
$completionButtons = '<a href="../admin/login" class="btn btn-success">'.htmlspecialchars(lang('go_to_admin_login')).'</a>';
|
|
|
|
$coreVersion = getCoreAppVersion();
|
|
$completionExtra = '<p class="result-version">'
|
|
.htmlspecialchars(lang('installed_version_label')).' '
|
|
.'<strong>'.htmlspecialchars($coreVersion).'</strong>'
|
|
.'</p>';
|
|
|
|
// .env 권한 권장 강화 안내 — 0600 이 아니면 조건부 노출 (#371)
|
|
// finalize-env.php 는 chmod 시도를 수행하지 않으므로 인스톨러 안내 단계의 권한
|
|
// (예: 0664 + chgrp www-data) 이 그대로 유지됨. 운영자가 추가로 0600 적용을
|
|
// 원할 수 있도록 현재 권한이 0600 미만 (그룹/기타 비트가 있음) 이면 명령 예시 노출.
|
|
//
|
|
// OS 가드: chmod/chgrp 는 POSIX 권한 모델 (Linux/macOS/BSD) 에서 동작.
|
|
// Windows 만 명령이 무의미하므로 Windows 만 비노출.
|
|
$envPath = BASE_PATH.'/.env';
|
|
if (PHP_OS_FAMILY !== 'Windows' && is_file($envPath)) {
|
|
$envPerms = fileperms($envPath) & 0777;
|
|
if ($envPerms !== 0600) {
|
|
$envOwnerUid = @fileowner($envPath);
|
|
$envOwnerName = ($envOwnerUid !== false && function_exists('posix_getpwuid'))
|
|
? (posix_getpwuid($envOwnerUid)['name'] ?? (string) $envOwnerUid)
|
|
: 'owner';
|
|
$webGroupName = function_exists('posix_getegid') && function_exists('posix_getgrgid')
|
|
? (posix_getgrgid(posix_getegid())['name'] ?? 'www-data')
|
|
: 'www-data';
|
|
|
|
$hardeningCommandSameOwner = sprintf('chmod 600 %s', htmlspecialchars($envPath));
|
|
$hardeningCommandDifferentOwner = sprintf(
|
|
'sudo chgrp %s %s && sudo chmod 640 %s',
|
|
htmlspecialchars($webGroupName),
|
|
htmlspecialchars($envPath),
|
|
htmlspecialchars($envPath)
|
|
);
|
|
|
|
$completionExtra .= '<div class="env-hardening-hint">'
|
|
.'<p class="env-hardening-hint-title">'.htmlspecialchars(lang('env_hardening_hint_title')).'</p>'
|
|
.'<p class="env-hardening-hint-status">'
|
|
.sprintf(
|
|
htmlspecialchars(lang('env_hardening_hint_current_status')),
|
|
decoct($envPerms),
|
|
htmlspecialchars($envOwnerName)
|
|
)
|
|
.'</p>'
|
|
.'<p class="env-hardening-hint-option">'
|
|
.htmlspecialchars(lang('env_hardening_hint_option_strict'))
|
|
.'</p>'
|
|
.'<div class="code-box"><pre>'.$hardeningCommandSameOwner.'</pre></div>'
|
|
.'<p class="env-hardening-hint-option">'
|
|
.htmlspecialchars(lang('env_hardening_hint_option_group'))
|
|
.'</p>'
|
|
.'<div class="code-box"><pre>'.$hardeningCommandDifferentOwner.'</pre></div>'
|
|
.'<p class="env-hardening-hint-note">'.htmlspecialchars(lang('env_hardening_hint_note')).'</p>'
|
|
.'</div>';
|
|
}
|
|
}
|
|
|
|
echo renderInstallResultSection('completion', 'success', 'installation_completed', 'installation_complete_message', $completionButtons, $completionExtra);
|
|
?>
|
|
|
|
<!-- 중단 섹션 -->
|
|
<?php
|
|
$abortedButtons = '<button onclick="resumeInstallationFromAborted()" class="btn btn-primary">'.htmlspecialchars(lang('resume_continue')).'</button>
|
|
<button onclick="goToSettingsWithConfirm()" class="btn btn-secondary">'.htmlspecialchars(lang('back_to_settings')).'</button>';
|
|
echo renderInstallResultSection('aborted', 'warning', 'installation_aborted', 'installation_aborted_message', $abortedButtons);
|
|
?>
|
|
|
|
<!-- 실패 섹션 -->
|
|
<?php
|
|
$failureButtons = '<button onclick="retryInstallation()" class="btn btn-primary">'.htmlspecialchars(lang('retry_installation')).'</button>
|
|
<button onclick="goToSettingsWithConfirm()" class="btn btn-secondary">'.htmlspecialchars(lang('back_to_settings')).'</button>';
|
|
echo renderInstallResultSection('failure', 'error', 'installation_failed', '', $failureButtons);
|
|
?>
|
|
|
|
<!-- 설치 진행 방식 선택 (SSE / 폴링) -->
|
|
<div class="requirement-card installation-mode-card" id="installation-mode-card">
|
|
<div class="requirement-card-header">
|
|
<h3 class="requirement-card-title"><?= htmlspecialchars(lang('installation_mode_label')) ?></h3>
|
|
</div>
|
|
<div class="requirement-card-body">
|
|
<div class="installation-mode-options">
|
|
<label class="installation-mode-option">
|
|
<input type="radio" name="installation_mode" value="sse" checked>
|
|
<div class="installation-mode-text">
|
|
<strong><?= htmlspecialchars(lang('installation_mode_sse_title')) ?></strong>
|
|
<span class="installation-mode-desc"><?= htmlspecialchars(lang('installation_mode_sse_desc')) ?></span>
|
|
</div>
|
|
</label>
|
|
<label class="installation-mode-option">
|
|
<input type="radio" name="installation_mode" value="polling">
|
|
<div class="installation-mode-text">
|
|
<strong><?= htmlspecialchars(lang('installation_mode_polling_title')) ?></strong>
|
|
<span class="installation-mode-desc"><?= htmlspecialchars(lang('installation_mode_polling_desc')) ?></span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 설치 시작 버튼 (초기 상태, 설치 시작 후 숨김) -->
|
|
<div id="installation-start-section" class="installation-start-section" style="display: none;">
|
|
<button id="start-installation-btn" class="btn btn-primary btn-lg" type="button" onclick="onStartInstallationClick()">
|
|
<?= htmlspecialchars(lang('start_installation_button')) ?>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="alert alert-warning" id="install-warning" style="display: none;">
|
|
⚠ <?= htmlspecialchars(lang('do_not_close_page')) ?>
|
|
</div>
|
|
|
|
<!-- 설치 진행 상황 카드 -->
|
|
<div class="requirement-card" id="installation-progress-card" style="display: none;">
|
|
<div class="requirement-card-header" id="installation-card-header" onclick="toggleInstallationCard()">
|
|
<h3 class="requirement-card-title">
|
|
<?= htmlspecialchars(lang('progress_status')) ?>
|
|
<span id="install-toggle-icon" class="toggle-icon-inline hidden">▼</span>
|
|
</h3>
|
|
</div>
|
|
|
|
<div id="installation-card-body" class="requirement-card-body">
|
|
<!-- 전체 진행률 -->
|
|
<div class="progress-section">
|
|
<div class="progress-header">
|
|
<span class="progress-label"><?= htmlspecialchars(lang('overall_progress')) ?></span>
|
|
<span id="overall-percentage" class="progress-percentage">0%</span>
|
|
</div>
|
|
<div class="progress-bar-bg">
|
|
<div id="overall-progress-bar" class="progress-bar-fill" style="width: 0%"></div>
|
|
</div>
|
|
<p id="current-task" class="progress-status"><?= htmlspecialchars(lang('preparing')) ?></p>
|
|
</div>
|
|
|
|
<!-- 작업 목록 -->
|
|
<div id="task-list" class="task-list">
|
|
<!-- JavaScript로 동적 생성 -->
|
|
</div>
|
|
|
|
<!-- 로그 섹션 헤더 -->
|
|
<h4 class="log-section-title"><?= htmlspecialchars(lang('installation_log')) ?></h4>
|
|
|
|
<!-- 로그 출력 -->
|
|
<div class="install-log" id="install-log">
|
|
<div class="log-placeholder"><?= htmlspecialchars(lang('waiting_installation')) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<?php
|
|
// 선택된 확장 기능 가져오기
|
|
$selectedExtensions = $state['selected_extensions'] ?? [];
|
|
$adminTemplates = $selectedExtensions['admin_templates'] ?? [];
|
|
$userTemplates = $selectedExtensions['user_templates'] ?? [];
|
|
$modules = $selectedExtensions['modules'] ?? [];
|
|
$plugins = $selectedExtensions['plugins'] ?? [];
|
|
$languagePacks = $selectedExtensions['language_packs'] ?? [];
|
|
?>
|
|
|
|
<script>
|
|
// Step 5 전용 데이터 전달 (INSTALLER_BASE_URL과 INSTALLER_LANG는 footer.php에서 처리)
|
|
window.INSTALLER_CONFIG = <?= installer_json_encode($config) ?>;
|
|
window.INSTALLER_SELECTED_EXTENSIONS = <?= installer_json_encode($selectedExtensions) ?>;
|
|
window.INSTALLER_EXTENSION_NAMES = <?= installer_json_encode($state['extension_names'] ?? []) ?>;
|
|
|
|
// 작업 그룹 정의
|
|
window.INSTALLER_TASK_GROUPS = [
|
|
{
|
|
id: 'environment',
|
|
labelKey: 'task_group_environment',
|
|
tasks: [
|
|
{ id: 'composer_check' },
|
|
{ id: 'composer_install' },
|
|
{ id: 'env_update' },
|
|
{ id: 'key_generate' }
|
|
]
|
|
},
|
|
{
|
|
id: 'database',
|
|
labelKey: 'task_group_database',
|
|
tasks: [
|
|
{ id: 'db_migrate' },
|
|
{ id: 'db_seed' }
|
|
]
|
|
},
|
|
{
|
|
id: 'admin_templates',
|
|
labelKey: 'task_group_admin_templates',
|
|
tasks: <?= installer_json_encode(array_map(function ($tpl) {
|
|
return [
|
|
['id' => 'template_install', 'target' => $tpl],
|
|
['id' => 'template_activate', 'target' => $tpl],
|
|
];
|
|
}, $adminTemplates)) ?>
|
|
},
|
|
{
|
|
id: 'modules',
|
|
labelKey: 'task_group_modules',
|
|
tasks: <?= installer_json_encode(array_map(function ($mod) {
|
|
return [
|
|
['id' => 'module_install', 'target' => $mod],
|
|
['id' => 'module_activate', 'target' => $mod],
|
|
];
|
|
}, $modules)) ?>
|
|
},
|
|
{
|
|
id: 'plugins',
|
|
labelKey: 'task_group_plugins',
|
|
tasks: <?= installer_json_encode(array_map(function ($plg) {
|
|
return [
|
|
['id' => 'plugin_install', 'target' => $plg],
|
|
['id' => 'plugin_activate', 'target' => $plg],
|
|
];
|
|
}, $plugins)) ?>
|
|
},
|
|
{
|
|
id: 'user_templates',
|
|
labelKey: 'task_group_user_templates',
|
|
tasks: <?= installer_json_encode(array_map(function ($tpl) {
|
|
return [
|
|
['id' => 'user_template_install', 'target' => $tpl],
|
|
['id' => 'user_template_activate', 'target' => $tpl],
|
|
];
|
|
}, $userTemplates)) ?>
|
|
},
|
|
{
|
|
id: 'language_packs',
|
|
labelKey: 'task_group_language_packs',
|
|
tasks: <?= installer_json_encode(array_map(function ($pack) {
|
|
return ['id' => 'language_pack_install', 'target' => $pack];
|
|
}, $languagePacks)) ?>
|
|
},
|
|
{
|
|
id: 'finalize',
|
|
labelKey: 'task_group_finalize',
|
|
tasks: [
|
|
{ id: 'create_settings_json' },
|
|
{ id: 'cache_clear' },
|
|
{ id: 'complete_flag' }
|
|
]
|
|
}
|
|
];
|
|
|
|
// 평탄화된 작업 목록 생성 (하위 호환성 유지)
|
|
window.INSTALLER_TASKS = (function() {
|
|
const tasks = [];
|
|
INSTALLER_TASK_GROUPS.forEach(group => {
|
|
if (!group.tasks || group.tasks.length === 0) return;
|
|
group.tasks.forEach(taskOrArray => {
|
|
if (Array.isArray(taskOrArray)) {
|
|
taskOrArray.forEach(t => tasks.push({ ...t, group: group.id }));
|
|
} else {
|
|
tasks.push({ ...taskOrArray, group: group.id });
|
|
}
|
|
});
|
|
});
|
|
return tasks;
|
|
})();
|
|
</script>
|