Files
Gnuboard7/public/install/includes/request-handler.php
T
HeuJung cbc91117d7 fix(installer): Read DB 미사용 시 Write DB만 참조하도록 수정 + GitHub 이슈 템플릿 신설
설치 시 use_read_db=false 인데도 db_read_host 잔존값으로 read 커넥션을
조립해 5단계 마이그레이션이 실패하던 문제를 수정한다. "read 사용 여부"
판정이 세 지점에서 달랐던 것이 근본 원인으로, use_read_db 플래그를 단일
SSoT 로 통일한다.

- installer-runtime.php: read 블록을 use_read_db 게이트로. 미사용 시 runtime
 에 read 키 미생성 → 하류가 write 로 자동 fallback. mergeRuntimeIntoEnv 도
 read 부재 시 DB_READ_* 를 빈 값으로 기록(write 복사 제거).
- config/database.php: read env 를 Elvis(?:) fallback 으로 복원. 중첩
 env(A, env(B)) 는 DB_READ_* 가 빈 문자열일 때 fallback 이 무력화되므로,
 빈 값까지 흡수하는 ?: 를 사용(tinker 실측 확인). 설치 이후 운영 중
 DB_READ_* 공란 시에도 SELECT 가 write DB 로 정확히 향하게 한다.
- functions.php / request-handler.php: read 미사용 시 .env DB_READ_* 빈 값
 기록 + state.json 잔존 read 필드 제거(2단계 방어).

기존 설치 환경은 .env 에 DB_READ_* 가 이미 채워져 Elvis fallback 미발동 →
동작 불변이라 업그레이드 스텝 불필요(실측 확인).

무관 사전 실패 테스트(beta.7 stale-ENV 복구)는 코어 version 이 beta.7 이
아니면 skip 하도록 가드 헬퍼 추가(프로덕션 한시 가드 전제를 테스트에 반영).

자유형식 제보에서 진단 축(버전/활성 확장/커스텀 자산/환경)이 누락되기 쉬워
G7 전용 이슈 템플릿을 추가한다. bug_report / feature_request / upgrade_issue
+ config.yml(blank 비활성, Discussions·보안신고 링크).
2026-07-06 17:15:29 +09:00

259 lines
8.3 KiB
PHP

<?php
/**
* 그누보드7 웹 인스톨러 요청 처리 핸들러
*
* 모든 POST 요청 및 비즈니스 로직을 처리합니다.
*/
/**
* 설치 흐름 검증 및 리다이렉트
*
* @param int $currentStep 현재 단계
* @param array $state 설치 상태
*/
function validateInstallationFlow(int $currentStep, array $state): void
{
// 설치 완료 시 홈으로 리다이렉트
if (isInstallationCompleted()) {
$translations = loadTranslations(getCurrentLanguage());
showInstallationCompletedAlert();
}
// 설치 진행 중 체크 (Step 5 제외 - Installation 화면)
if (isInstallationRunning() && $currentStep !== 5) {
showInstallationRunningAlert();
}
// 상태 파일의 last_completed_step 가져오기
$lastCompletedStep = getLastCompletedStep();
// 허용된 최대 단계 계산 (마지막 완료 단계 + 1)
$allowedMaxStep = $lastCompletedStep + 1;
// 현재 단계가 허용된 범위를 벗어나면 알림 후 올바른 단계로 리다이렉트
if ($currentStep > $allowedMaxStep) {
showInvalidStepAccessAlert($allowedMaxStep);
}
}
/**
* 잘못된 단계 접근 시 알림 표시
*
* @param int $correctStep 올바른 단계 번호
*/
function showInvalidStepAccessAlert(int $correctStep): void
{
global $translations;
// 번역이 로드되지 않은 경우 로드
if (! isset($translations)) {
$translations = loadTranslations(getCurrentLanguage());
}
// 세션에 올바른 단계 설정
$_SESSION['installer_current_step'] = $correctStep;
showAlertAndRedirect(
lang('error_invalid_step_access'),
lang('error_invalid_step_access_message'),
INSTALLER_BASE_URL.'/'
);
}
/**
* Step 0 POST 처리 (환영 화면)
*/
function handleStep0Post(): void
{
if (isset($_POST['language'])) {
handleLanguageChange($_POST['language']);
}
// 다음 버튼 클릭 시 (언어 선택 변경이 아닌 경우)
if (! isset($_POST['language_change_only'])) {
unset($_SESSION['license_agreed']);
updateStepStatus(0, 1);
redirectToStep(1);
}
redirectToStep(0);
}
/**
* Step 1 POST 처리 (라이선스 동의)
*
* @param string $currentLang 현재 언어
* @param string|null &$error 에러 메시지 (참조)
*/
function handleStep1Post(string $currentLang, ?string &$error = null): void
{
$translations = loadTranslations($currentLang);
if (! isset($_POST['agree']) || $_POST['agree'] !== '1') {
$error = lang('must_agree');
} else {
$_SESSION['license_agreed'] = true;
updateStepStatus(1, 2);
redirectToStep(2);
}
}
/**
* Step 2 POST 처리 (요구사항 검증)
*/
function handleStep2Post(): void
{
if (isset($_POST['proceed'])) {
updateStepStatus(2, 3);
redirectToStep(3);
}
}
/**
* Step 3 POST 처리 (데이터베이스 및 사이트 설정)
*
* @param string $currentLang 현재 언어
* @param array &$formData 폼 데이터 (참조)
* @param array &$errors 에러 배열 (참조)
*/
function handleStep3Post(string $currentLang, array &$formData, array &$errors): void
{
$translations = loadTranslations($currentLang);
$formData = array_merge($formData, $_POST);
$formData['use_read_db'] = isset($_POST['use_read_db']) ? true : false;
// Write DB 검증
if (empty($formData['db_write_host'])) {
$errors['db_write_host'] = lang('error_db_host_required');
}
if (empty($formData['db_write_database'])) {
$errors['db_write_database'] = lang('error_db_name_required');
}
// Read DB 검증 (사용하는 경우)
if (! empty($formData['use_read_db'])) {
if (empty($formData['db_read_host'])) {
$errors['db_read_host'] = lang('error_db_host_required');
}
if (empty($formData['db_read_database'])) {
$errors['db_read_database'] = lang('error_db_name_required');
}
}
// 관리자 정보 검증
if (empty($formData['admin_email']) || ! filter_var($formData['admin_email'], FILTER_VALIDATE_EMAIL)) {
$errors['admin_email'] = lang('error_admin_email_invalid');
}
if (! array_key_exists($formData['admin_language'] ?? '', SUPPORTED_LANGUAGES)) {
$errors['admin_language'] = lang('error_admin_language_invalid');
}
if (empty($formData['admin_password']) || strlen($formData['admin_password']) < 8) {
$errors['admin_password'] = lang('error_admin_password_min');
}
if ($formData['admin_password'] !== $formData['admin_password_confirm']) {
$errors['admin_password_confirm'] = lang('error_password_mismatch');
}
// PHP CLI / Composer 경로 처리
$phpBinary = trim($formData['php_binary'] ?? 'php');
$formData['php_binary'] = $phpBinary !== '' ? $phpBinary : 'php';
$formData['composer_binary'] = trim($formData['composer_binary'] ?? '');
// Vendor 설치 모드 처리 (auto|composer|bundled)
$vendorMode = trim($formData['vendor_mode'] ?? 'auto');
if (! in_array($vendorMode, ['auto', 'composer', 'bundled'], true)) {
$vendorMode = 'auto';
}
$formData['vendor_mode'] = $vendorMode;
// 코어 업데이트 _pending 경로 검증 (입력된 경우만)
$corePendingPath = trim($formData['core_update_pending_path'] ?? '');
if ($corePendingPath !== '') {
// 절대 경로 변환
$absolutePath = str_starts_with($corePendingPath, '/')
? $corePendingPath
: BASE_PATH.'/'.$corePendingPath;
if (file_exists($absolutePath) && ! is_dir($absolutePath)) {
$errors['core_update_pending_path'] = lang('error_core_pending_not_directory');
}
}
// DB 테스트 완료 확인
if (empty($errors) && ! isset($_SESSION['db_write_tested'])) {
$errors['db_test'] = lang('error_db_not_tested');
}
if (empty($errors) && ! empty($formData['use_read_db']) && ! isset($_SESSION['db_read_tested'])) {
$errors['db_test'] = lang('error_db_not_tested');
}
// 검증 통과 시 다음 단계로 이동
if (empty($errors)) {
$_SESSION['install_config'] = $formData;
// state.json에는 비밀번호를 저장하지 않음 (보안)
$safeFormData = $formData;
unset($safeFormData['db_write_password'], $safeFormData['db_read_password']);
// Read DB 미사용 시 read 필드 잔존값을 제거한다 (이슈 #63 2단계 방어).
// use_read_db=false 인데 db_read_host 등이 남아 있으면 하류(installer-runtime.php)
// 로 유입되어 잘못된 read 커넥션을 만들 수 있으므로 원천 차단한다.
if (empty($safeFormData['use_read_db'])) {
unset(
$safeFormData['db_read_host'],
$safeFormData['db_read_port'],
$safeFormData['db_read_database'],
$safeFormData['db_read_username']
);
}
updateStepStatus(3, 4, [
'config' => $safeFormData,
'installation_status' => 'ready',
]);
redirectToStep(4);
}
}
/**
* POST 요청 라우팅
*
* @param int $currentStep 현재 단계
* @param string $currentLang 현재 언어
* @param array &$formData 폼 데이터 (참조)
* @param array &$errors 에러 배열 (참조)
* @param string|null &$error 에러 메시지 (참조)
*/
function handlePostRequest(int $currentStep, string $currentLang, array &$formData = [], array &$errors = [], ?string &$error = null): void
{
// 단계 이동 처리 (go_to_step POST 파라미터)
if (isset($_POST['go_to_step'])) {
$step = (int) $_POST['go_to_step'];
if ($step >= 0 && $step <= 5) {
$_SESSION['installer_current_step'] = $step;
}
header('Location: '.INSTALLER_BASE_URL.'/');
exit;
}
// 구 방식 언어 전환 처리 (호환성 유지)
if (isset($_POST['change_language'])) {
$lang = $_POST['lang'] ?? 'ko';
handleLanguageChange($lang);
redirectToStep($currentStep);
}
// Step별 POST 처리
match ($currentStep) {
0 => handleStep0Post(),
1 => handleStep1Post($currentLang, $error),
2 => handleStep2Post(),
3 => handleStep3Post($currentLang, $formData, $errors),
default => null
};
}