move_detect/server/deploy/preflight.php
유창욱 3f2823a764 refactor: rename cloud/ to server/ (cafe24 is web hosting, not cloud)
The PHP app runs on Cafe24 shared web hosting -- calling it 'cloud'
was misleading. Renames the directory, the edge client module, and
every doc/script/identifier that used the term.

- cloud/ -> server/ (incl. server/deploy/build_cafe24_package.sh paths)
- edge/cloudclient.py -> edge/serverclient.py, CloudClient -> ServerClient
- cloud/docs/cloud-deploy.md -> server/docs/server-deploy.md
- CLOUD_* env/config keys -> SERVER_*
- tests renamed to match

Tests: PHP 109 pass (4 skipped), pytest 167 pass. dist/ regenerated.
2026-07-20 11:30:52 +09:00

160 lines
8.1 KiB
PHP

<?php
// Uploaded as www/_preflight.php. Open https://<domain>/_preflight.php once, confirm every
// row is OK, then DELETE THIS FILE.
//
// It exists because three host-level facts are unknowable from the repo and every one of
// them fails badly on a stock PHP build:
// * no argon2 -> src/Security/Password.php calls password_hash(..., PASSWORD_ARGON2ID)
// unguarded, and AuthController::DUMMY_HASH runs verify() on EVERY login
// attempt including unknown usernames -> the login page fatals for
// everyone, instantly.
// * no gd -> ImageProcessor::reencode() returns ok=false and IngestController's
// `continue` swallows it: events save with no snapshot, API still 201s.
// * no pdo_mysql -> every request 500s at Database::connect(). (mysqli being present does
// NOT imply pdo_mysql.)
//
// Deliberately prints booleans only — no phpinfo(), no paths, no config values — so the
// window between upload and deletion leaks nothing useful.
declare(strict_types=1);
header('Content-Type: text/html; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
// Find the app root exactly the way public/index.php does — container first, then the bare
// directory, probing each level for any marker. Using ANY marker (not all three) is
// deliberate here and differs from index.php on purpose: this page exists to DIAGNOSE a
// half-finished upload, so it must still locate a root that is missing vendor/ or config.php
// and then report precisely which piece is absent.
const APP_PRIVATE_DIR = 'move_detect_app';
$root = null;
$hasAnyMarker = static fn (string $d): bool =>
is_file($d . '/config.php') || is_file($d . '/vendor/autoload.php') || is_file($d . '/src/Config.php');
for ($d = dirname(__DIR__), $i = 0; $i < 4; $i++) {
foreach ([$d . '/' . APP_PRIVATE_DIR, $d] as $cand) {
if ($hasAnyMarker($cand)) { $root = $cand; break 2; }
}
if (dirname($d) === $d) break;
$d = dirname($d);
}
// Nothing found anywhere IS the diagnosis: fall back to where the files SHOULD be, so every
// row below reports a truthful "missing" instead of crashing on null.
$root = $root ?? dirname(dirname(__DIR__)) . '/' . APP_PRIVATE_DIR;
$checks = [
'PHP >= 8.2' => [
PHP_VERSION_ID >= 80200,
'Cafe24 관리자에서 PHP 버전을 8.2 이상으로 올리세요. (주의: 카페24는 PHP 버전 변경 시 데이터와 DB가 초기화된다고 안내합니다. 반드시 업로드 전에 정하세요.)',
],
'pdo_mysql 확장' => [
in_array('mysql', PDO::getAvailableDrivers(), true),
'DB 연결이 전혀 안 됩니다. 카페24에 pdo_mysql 활성화를 요청하세요.',
],
'gd 확장' => [
function_exists('imagecreatefromstring') && function_exists('imagejpeg'),
'사진(스냅샷)이 조용히 사라집니다. 이벤트는 저장되는데 사진만 없어집니다. 카페24에 gd 활성화를 요청하세요.',
],
'Argon2id 암호 해시' => [
defined('PASSWORD_ARGON2ID') && in_array('argon2id', password_algos(), true),
'로그인 페이지가 즉시 죽습니다. 이게 가장 흔한 배포 실패 원인입니다. 카페24에 Argon2 포함 PHP 빌드를 요청하세요.',
],
'config.php 있음' => [
is_readable($root . '/config.php'),
'FTP 최상위(www 밖)에 config.php 를 올리세요.',
],
// Checks the parsed VALUES, not the file text. Grepping the raw file for 'CHANGEME'
// would also match the template's explanatory comments, which survive a correct edit —
// so a properly-filled config would report 실패 forever.
'config.php 를 수정했음' => (function () use ($root) {
$fix = 'config.php 안의 CHANGEME 를 실제 DB 이름/아이디/비밀번호로 바꾸세요.';
if (!is_readable($root . '/config.php')) {
return [false, $fix];
}
$cfg = @require $root . '/config.php';
if (!is_array($cfg)) {
return [false, 'config.php 가 PHP 배열을 return 하지 않습니다. 파일이 깨졌는지 확인하세요.'];
}
foreach (['db_dsn', 'db_user', 'db_pass'] as $k) {
if (str_contains((string)($cfg[$k] ?? ''), 'CHANGEME')) {
return [false, $fix];
}
}
return [true, ''];
})(),
'storage/snapshots 쓰기 가능' => [
is_dir($root . '/storage/snapshots') && is_writable($root . '/storage/snapshots'),
'사진이 조용히 사라집니다. 폴더를 만들고 권한 750 으로 주세요.',
],
'src/ 있음' => [
is_readable($root . '/src/Config.php'),
'src/ 폴더를 FTP 최상위(www 밖)에 올리세요.',
],
'vendor/ 오토로더 있음' => [
is_readable($root . '/vendor/autoload.php'),
'vendor/ 폴더를 FTP 최상위(www 밖)에 올리세요. 없으면 모든 페이지가 죽습니다.',
],
// These probe THIS directory (the one the web serves), not $root. If the secrets turned
// up here, they are downloadable.
'src/ 가 웹에 노출되지 않음' => [
!is_readable(__DIR__ . '/src/Config.php'),
'src/ 가 웹에서 접근 가능한 폴더 안에 있습니다. FTP 최상위(www 밖)로 옮기세요.',
],
'config.php 가 웹에 노출되지 않음' => [
!is_readable(__DIR__ . '/config.php'),
'config.php 가 웹에서 접근 가능한 폴더 안에 있습니다. DB 비밀번호가 노출됩니다. FTP 최상위(www 밖)로 옮기세요.',
],
// Reports the LIVE mode rather than trusting the build. The package's chmod 600 cannot
// survive a zip round-trip or an FTP upload (neither carries unix modes), so the only
// honest place to check this is here, on the host. 0077 — NOT 0o077, which is PHP 8.1+
// syntax and would make this file fail to parse on the very old build we want to warn about.
'config.php 권한이 600' => [
is_readable($root . '/config.php')
&& (fileperms($root . '/config.php') & 0077) === 0,
'FTP 클라이언트에서 config.php 권한을 600 으로 바꾸세요. DB 비밀번호가 들어있습니다.',
],
'HTTPS 로 접속됨' => [
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['SERVER_PORT'] ?? '') === '443'
|| (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'),
'세션 쿠키가 cookie_secure=true 라서 HTTP 로는 로그인이 안 됩니다. 카페24에서 무료 SSL 을 켜세요.',
],
];
$failed = array_keys(array_filter($checks, fn($c) => !$c[0]));
?>
<!doctype html>
<meta charset="utf-8">
<title>move-detect 설치 점검</title>
<style>
body { font: 15px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif; max-width: 46rem; margin: 2rem auto; padding: 0 1rem; }
table { border-collapse: collapse; width: 100%; }
td { border-bottom: 1px solid #e5e5e5; padding: .5rem .4rem; vertical-align: top; }
td:first-child { white-space: nowrap; }
.ok { color: #0a7c2f; font-weight: 600; }
.no { color: #c02626; font-weight: 600; }
.fix { color: #666; font-size: 13px; }
.banner { padding: .75rem 1rem; border-radius: 6px; margin: 1rem 0; }
.good { background: #e8f6ec; }
.bad { background: #fdeaea; }
.warn { background: #fff6d8; border: 1px solid #e8cf7a; }
</style>
<h1>move-detect 설치 점검</h1>
<?php if ($failed): ?>
<p class="banner bad"><strong><?= count($failed) ?>개 항목 실패.</strong> 아래 빨간 줄의 안내를 먼저 해결하세요.</p>
<?php else: ?>
<p class="banner good"><strong>전부 통과.</strong> <a href="/login">/login</a> 으로 접속해 관리자 로그인을 확인하세요.</p>
<?php endif; ?>
<table>
<?php foreach ($checks as $name => [$ok, $fix]): ?>
<tr>
<td><?= htmlspecialchars($name, ENT_QUOTES) ?></td>
<td class="<?= $ok ? 'ok' : 'no' ?>"><?= $ok ? 'OK' : '실패' ?></td>
<td class="fix"><?= $ok ? '' : htmlspecialchars($fix, ENT_QUOTES) ?></td>
</tr>
<?php endforeach; ?>
</table>
<p class="banner warn">
<strong>확인이 끝나면 이 파일(<code>www/_preflight.php</code>)을 반드시 지우세요.</strong>
진단용이라 계속 열어둘 이유가 없습니다.
</p>