Files
Gnuboard7/public/install/api/save-extensions.php
T
HeuJung 01d319e74b fix(core,installer): 프로세스 출력 인코딩으로 인한 JSON 빈 응답 차단
한국어 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 이 되던 것도 함께 막았다.
2026-08-29 15:13:14 +09:00

187 lines
5.6 KiB
PHP

<?php
/**
* G7 인스톨러 - 선택된 확장 기능 저장 API
*
* 사용자가 선택한 확장 기능 목록을 state.json에 저장합니다.
*
* @method POST
*
* @body {
* "admin_templates": ["sirsoft-admin_basic"],
* "user_templates": ["sirsoft-basic"],
* "modules": ["sirsoft-board", "sirsoft-ecommerce"],
* "plugins": ["sirsoft-verification"]
* }
*
* @response {
* "success": true,
* "message": "확장 기능 선택이 저장되었습니다."
* }
*/
// 기본 설정
header('Content-Type: application/json; charset=utf-8');
// 프로젝트 루트 경로
define('BASE_PATH', realpath(dirname(__DIR__, 3)) ?: dirname(__DIR__, 3));
// 세션 및 설정 포함
require_once dirname(__DIR__).'/includes/session.php';
require_once dirname(__DIR__).'/includes/config.php';
require_once dirname(__DIR__).'/includes/installer-state.php';
require_once dirname(__DIR__).'/includes/functions.php';
require_once __DIR__.'/_guard.php';
installer_guard_or_410();
// 다국어 로드
$currentLang = getCurrentLanguage();
$translations = loadTranslations($currentLang);
// POST 요청만 허용
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo installer_json_encode([
'success' => false,
'error' => lang('api_method_not_allowed'),
], JSON_UNESCAPED_UNICODE);
exit;
}
// 요청 본문 파싱
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo installer_json_encode([
'success' => false,
'error' => lang('api_invalid_request'),
], JSON_UNESCAPED_UNICODE);
exit;
}
// 필드 검증 및 기본값 설정
$adminTemplates = isset($input['admin_templates']) && is_array($input['admin_templates'])
? $input['admin_templates']
: [];
$userTemplates = isset($input['user_templates']) && is_array($input['user_templates'])
? $input['user_templates']
: [];
$modules = isset($input['modules']) && is_array($input['modules'])
? $input['modules']
: [];
$plugins = isset($input['plugins']) && is_array($input['plugins'])
? $input['plugins']
: [];
$languagePacks = isset($input['language_packs']) && is_array($input['language_packs'])
? $input['language_packs']
: [];
// 확장 이름 매핑 (identifier → {ko: '...', en: '...'})
$extensionNames = isset($input['extension_names']) && is_array($input['extension_names'])
? $input['extension_names']
: [];
// 관리자 템플릿은 최소 1개 필수
if (empty($adminTemplates)) {
http_response_code(400);
echo installer_json_encode([
'success' => false,
'error' => lang('error_admin_template_required'),
], JSON_UNESCAPED_UNICODE);
exit;
}
// 문자열 배열로 정리 (보안)
$adminTemplates = array_values(array_filter(array_map('strval', $adminTemplates)));
$userTemplates = array_values(array_filter(array_map('strval', $userTemplates)));
$modules = array_values(array_filter(array_map('strval', $modules)));
$plugins = array_values(array_filter(array_map('strval', $plugins)));
$languagePacks = array_values(array_filter(array_map('strval', $languagePacks)));
// 식별자 형식 검증: 셸 메타문자/경로 조합 차단 후 artisan 명령에 안전하게 보간되도록 한다.
// 실제 번들 식별자 예시: sirsoft-board, gnuboard7-hello_module, g7-module-sirsoft-board-ja
$identifierPattern = '/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/';
$identifierGroups = [
'admin_templates' => $adminTemplates,
'user_templates' => $userTemplates,
'modules' => $modules,
'plugins' => $plugins,
'language_packs' => $languagePacks,
];
foreach ($identifierGroups as $groupKey => $ids) {
foreach ($ids as $id) {
if (! preg_match($identifierPattern, $id)) {
http_response_code(400);
echo installer_json_encode([
'success' => false,
'error' => lang('error_invalid_extension_identifier', ['identifier' => $id, 'group' => $groupKey]),
], JSON_UNESCAPED_UNICODE);
exit;
}
}
}
try {
// 현재 상태 가져오기
$state = getInstallationState();
// selected_extensions 업데이트
$state['selected_extensions'] = [
'admin_templates' => $adminTemplates,
'user_templates' => $userTemplates,
'modules' => $modules,
'plugins' => $plugins,
'language_packs' => $languagePacks,
];
// 확장 이름 매핑 저장 (설치 시 표시용)
$state['extension_names'] = $extensionNames;
// Step 4 완료 표시
$state['step_status']['4'] = 'completed';
// 다음 스텝으로 이동
$state['current_step'] = 5;
// 세션도 동기화 (Step 5로 이동 허용)
$_SESSION['installer_current_step'] = 5;
// 상태 저장
$saved = saveInstallationState($state);
if (! $saved) {
throw new Exception(lang('state_save_failed'));
}
// 로그 기록
addLog(lang('log_extensions_selected', [
'admin' => count($adminTemplates),
'user' => count($userTemplates),
'modules' => count($modules),
'plugins' => count($plugins),
'language_packs' => count($languagePacks),
]));
// 성공 응답
echo installer_json_encode([
'success' => true,
'message' => lang('log_extensions_saved'),
'data' => [
'selected_extensions' => $state['selected_extensions'],
'next_step' => 5,
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo installer_json_encode([
'success' => false,
'error' => $e->getMessage(),
], JSON_UNESCAPED_UNICODE);
}