$item) { $normalized[is_string($key) ? installer_utf8_normalize($key) : $key] = installer_utf8_normalize_deep($item); } return $normalized; } } if (! function_exists('installer_utf8_invalid_paths')) { /** * invalid UTF-8 이 남아 있는 키 경로 목록을 반환합니다. * * @param mixed $value 검사할 값 * @return list 점 구분 키 경로 목록 */ function installer_utf8_invalid_paths(mixed $value): array { if (class_exists(ProcessOutputEncoding::class)) { return ProcessOutputEncoding::invalidPaths($value); } return []; } } if (! function_exists('installer_normalize_process_output')) { /** * 외부 프로세스·환경값을 정규화하고, 정규화가 실제로 필요했으면 설치 로그에 1회 남깁니다. * * 제보 사례(gnuboard/g7#62)의 가장 큰 어려움은 로그에 흔적이 0 이었다는 점이다. * 운영자가 "이 서버는 계정명이 UTF-8 이 아니다" 를 로그만으로 알 수 있어야 한다. * * @param string $value 원본 값 * @param string $source 값의 출처 라벨 (예: 'whoami') * @return string 유효한 UTF-8 값 */ function installer_normalize_process_output(string $value, string $source): string { if ($value === '' || mb_check_encoding($value, 'UTF-8')) { return $value; } $normalized = installer_utf8_normalize($value); // 같은 출처를 요청당 한 번만 기록한다 (getWebServerUser 는 한 응답에서 여러 번 호출된다). static $reported = []; if (! isset($reported[$source])) { $reported[$source] = true; $notice = '[env] '.$source.' output was not UTF-8 — normalized (os: '.PHP_OS_FAMILY. ', oem cp: '.(function_exists('sapi_windows_cp_get') ? (string) sapi_windows_cp_get('oem') : 'n/a').')'; error_log($notice); if (function_exists('addLog')) { addLog($notice); } } return $normalized; } } if (! function_exists('installer_json_encode')) { /** * 인스톨러 응답 JSON 을 생성합니다. **절대 false 나 빈 문자열을 반환하지 않습니다.** * * 1) 값 전체를 유효 UTF-8 로 정규화하고 * 2) `JSON_INVALID_UTF8_SUBSTITUTE` 를 덧붙여 인코딩하며 * 3) 그래도 실패하면 실패 사유와 문제 필드 경로를 로그에 남기고 * 파싱 가능한 오류 JSON 을 반환한다. * * @param mixed $data 직렬화할 값 * @param int $flags 추가 json_encode 플래그 * @param array $fallbackShape 3) 의 오류 JSON 에 함께 실을 키 (호출자 계약 보존용). * 폴링 응답처럼 소비자가 특정 키를 반드시 읽어야 하는 * 엔드포인트는 그 최소 형태를 여기로 넘긴다. * @return string 항상 파싱 가능한 JSON 문자열 */ function installer_json_encode(mixed $data, int $flags = JSON_UNESCAPED_UNICODE, array $fallbackShape = []): string { $normalized = installer_utf8_normalize_deep($data); // json-encode:allow — 이 함수가 인스톨러의 유일한 json_encode 자리다. $encoded = json_encode($normalized, $flags | JSON_INVALID_UTF8_SUBSTITUTE); if (is_string($encoded) && $encoded !== '') { return $encoded; } $reason = json_last_error_msg(); $paths = installer_utf8_invalid_paths($data); $message = '[installer-json] encode failed ('.$reason.')'. ($paths === [] ? '' : ' at: '.implode(', ', array_slice($paths, 0, 20))); error_log($message); if (function_exists('addLog')) { addLog($message); } // 호출자가 넘긴 최소 형태를 먼저 깔고 그 위에 오류 정보를 얹는다. // (소비자가 반드시 읽는 키 — 예: 폴링의 status — 가 폴백에서도 살아 있어야 한다.) // json-encode:allow — 폴백 자체는 정규화된 ASCII 라 실패할 수 없다. $fallback = json_encode(array_merge( installer_utf8_normalize_deep($fallbackShape), [ 'success' => false, 'error' => 'json_encode_failed', 'message' => $reason, 'invalid_paths' => array_slice($paths, 0, 20), ] ), JSON_INVALID_UTF8_SUBSTITUTE); return is_string($fallback) && $fallback !== '' ? $fallback : '{"success":false,"error":"json_encode_failed"}'; } }