비밀을 state.json(0664, 실패 시 무기한 잔존)에 기록하지 않고 runtime.php(0600, finalize 시 삭제)로 채널을 일원화한다. 정상 완료 경로에서는 finalize 가 state.json 을 삭제하지만, finalize 미호출 / unlink 실패 / 설치 중단 시에는 평문이 영구히 남았고, finalize 가드가 재호출을 410 차단해 자가 회복도 불가능했다. 삭제라는 단일 경로에 의존해 평문을 보호하던 구조 자체를 제거한다. SSE 워커는 세션에 접근할 수 없어 파일 경유가 불가피한데, 그 파일이 0664 의 state.json 이었던 것이 구조적 원인이다. 세션을 보유한 유일한 공통 시작점인 install-process.php 를 비밀 이송 지점으로 삼고, db_seed 가 소비한 즉시 제거한다. 레거시 / 실패 경로는 완료·중단·초기화·finalize 시점 redact 로 방어. 동반 수정: - 세션 config 재병합으로 무력화되던 기존 DB 비밀번호 마스킹 복구 - db_seed 후 putenv 미해제로 모든 exec 자식에 평문이 ENV 상속되던 누출 차단 - runtime read 비밀번호 폴백이 write 값으로 잘못 대체되던 문제 ( 인접) 이미 설치된 사이트는 7.0.4 업그레이드 스텝이 잔존 파일을 정리한다. 완료 증거가 있으면 삭제하고, 설치 진행 중일 수 있으면 비밀만 제거한다.
179 lines
6.4 KiB
PHP
179 lines
6.4 KiB
PHP
<?php
|
|
/**
|
|
* 그누보드7 웹 인스톨러 메인 라우터
|
|
*
|
|
* @author sirsoft
|
|
*/
|
|
|
|
require_once __DIR__.'/includes/config.php';
|
|
require_once __DIR__.'/includes/functions.php';
|
|
require_once __DIR__.'/includes/session.php';
|
|
require_once __DIR__.'/includes/installer-state.php';
|
|
require_once __DIR__.'/includes/request-handler.php';
|
|
require_once __DIR__.'/api/_guard.php';
|
|
installer_guard_or_410();
|
|
|
|
$currentLang = getCurrentLanguage();
|
|
|
|
// 세션 기반 단계 관리 (URL 파라미터 무시)
|
|
if (! isset($_SESSION['installer_current_step'])) {
|
|
// 세션이 없으면 state.json에서 현재 step 가져오기
|
|
$state = getInstallationState();
|
|
|
|
// 설치가 진행 중이거나 중단/실패 상태면 state의 current_step 사용
|
|
if (isset($state['installation_status']) &&
|
|
in_array($state['installation_status'], ['running', 'aborted', 'failed', 'pending'])) {
|
|
$_SESSION['installer_current_step'] = $state['current_step'] ?? 0;
|
|
} else {
|
|
// 그 외에는 0부터 시작
|
|
$_SESSION['installer_current_step'] = 0;
|
|
}
|
|
}
|
|
$currentStep = $_SESSION['installer_current_step'];
|
|
|
|
// URL 파라미터로 step 접근 시 알림 후 리다이렉트
|
|
if (isset($_GET['step'])) {
|
|
$urlStep = (int) $_GET['step'];
|
|
|
|
// 번역 로드
|
|
if (! isset($translations)) {
|
|
$translations = loadTranslations($currentLang);
|
|
}
|
|
|
|
// URL 파라미터의 step과 세션 step이 다른 경우
|
|
if ($urlStep !== $currentStep) {
|
|
showAlertAndRedirect(
|
|
lang('url_parameter_not_supported'),
|
|
lang('url_parameter_redirect_message', [
|
|
'requested' => $urlStep,
|
|
'current' => $currentStep,
|
|
]),
|
|
INSTALLER_BASE_URL.'/'
|
|
);
|
|
}
|
|
|
|
// 같은 경우에도 깔끔한 URL로 리다이렉트
|
|
header('Location: '.INSTALLER_BASE_URL.'/');
|
|
exit;
|
|
}
|
|
|
|
$state = getInstallationState();
|
|
$errors = [];
|
|
$formData = [];
|
|
$error = null;
|
|
|
|
// Step 3 기본값 설정
|
|
if ($currentStep === 3) {
|
|
$defaults = DEFAULT_INSTALL_CONFIG;
|
|
$defaults['app_url'] = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST'];
|
|
$defaults['admin_language'] = getCurrentLanguage();
|
|
// state.json에서 비밀번호 제외된 config 복원 (폼에 비밀번호를 사전 입력하지 않음).
|
|
// state 에는 이미 비밀이 기록되지 않지만(이슈 #465), 레거시 state.json 이 남아 있는
|
|
// 환경에서도 평문이 HTML 로 렌더되지 않도록 프리필 단계에서 다시 제거한다.
|
|
$savedConfig = $state['config'] ?? $defaults;
|
|
unset(
|
|
$savedConfig['db_write_password'],
|
|
$savedConfig['db_read_password'],
|
|
$savedConfig['admin_password'],
|
|
$savedConfig['admin_password_confirm']
|
|
);
|
|
$formData = array_merge($defaults, $savedConfig);
|
|
}
|
|
|
|
// 설치 흐름 검증
|
|
validateInstallationFlow($currentStep, $state);
|
|
|
|
// POST 요청 처리
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
handlePostRequest($currentStep, $currentLang, $formData, $errors, $error);
|
|
}
|
|
|
|
// 번역 로드
|
|
if (! isset($translations)) {
|
|
$translations = loadTranslations($currentLang);
|
|
}
|
|
|
|
// Step 범위 체크 (0: welcome ~ 6: complete)
|
|
if ($currentStep < 0 || $currentStep > 6) {
|
|
$currentStep = 0;
|
|
}
|
|
|
|
$stepFile = __DIR__.'/views/'.$currentStep.'-'.(STEP_FILE_MAP[$currentStep] ?? 'welcome').'.php';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="<?= $currentLang ?>">
|
|
<script>
|
|
// 다크모드 즉시 적용 - 렌더링 전 실행 (FOUC 깜빡임 방지)
|
|
(function(){
|
|
try {
|
|
const theme = localStorage.getItem('g7_color_scheme')
|
|
|| (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
} catch(e) {
|
|
// localStorage 접근 실패 시 기본값(light) 사용
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
}
|
|
})();
|
|
</script>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
<title><?= lang('welcome_title') ?> - <?= lang('brand_name') ?></title>
|
|
|
|
<!-- CSS -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
<link rel="stylesheet" href="<?= INSTALLER_BASE_URL ?>/assets/css/installer.css?v=<?= time() ?>">
|
|
</head>
|
|
<body>
|
|
<?php if ($currentStep > 0) { ?>
|
|
<!-- Installer Header Bar -->
|
|
<div class="installer-header-bar">
|
|
<div class="installer-header-content">
|
|
<div class="installer-header-left">
|
|
<span class="installer-logo"><?= htmlspecialchars(lang('brand_name')) ?></span>
|
|
</div>
|
|
<div class="installer-header-right">
|
|
<span class="installer-step-indicator">
|
|
[<?= $currentStep ?>/5] <?= lang(getStepName($currentStep)) ?>
|
|
</span>
|
|
<button
|
|
id="theme-toggle-btn"
|
|
class="theme-toggle"
|
|
type="button"
|
|
aria-label="<?= lang('toggle_theme') ?>"
|
|
title="<?= lang('toggle_theme') ?>"
|
|
>
|
|
<span id="theme-toggle-icon">🌙</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
<!-- Main Content -->
|
|
<?php
|
|
if (file_exists($stepFile)) {
|
|
require_once $stepFile;
|
|
} else {
|
|
showStepFileNotFoundError($currentStep);
|
|
}
|
|
?>
|
|
|
|
<!-- Installer Footer -->
|
|
<footer class="installer-footer">
|
|
<p>© <?= APP_RELEASE_YEAR ?> Gnuboard7. All rights reserved.</p>
|
|
</footer>
|
|
|
|
<!-- JavaScript -->
|
|
<script>
|
|
window.INSTALLER_BASE_URL = '<?= INSTALLER_BASE_URL ?>';
|
|
window.CURRENT_STEP = <?= $currentStep ?>;
|
|
window.INSTALLER_LANG = <?= json_encode($translations ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
|
window.INSTALLER_STATE_LOCALE = <?= json_encode($state['g7_locale'] ?? null) ?>;
|
|
</script>
|
|
<script src="<?= INSTALLER_BASE_URL ?>/assets/js/installation-monitor.js?v=<?= time() ?>"></script>
|
|
<script src="<?= INSTALLER_BASE_URL ?>/assets/js/installer.js?v=<?= time() ?>"></script>
|
|
</body>
|
|
</html>
|