38471-vm/schema_debug.php
2026-05-02 03:04:42 +00:00

257 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
if (PHP_SAPI !== 'cli') {
header('Content-Type: text/plain; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
}
@set_time_limit(0);
function schemaDebugOutput(string $message = ''): void
{
echo $message . PHP_EOL;
}
function configureSchemaDebugSession(): void
{
if (PHP_SAPI === 'cli') {
return;
}
$sessionsDir = __DIR__ . '/sessions';
if (!is_dir($sessionsDir)) {
@mkdir($sessionsDir, 0777, true);
}
if (is_writable($sessionsDir)) {
session_save_path($sessionsDir);
}
if (
(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'None',
]);
}
}
function canViewSchemaDebug(): bool
{
if (PHP_SAPI === 'cli') {
return true;
}
configureSchemaDebugSession();
if (session_status() === PHP_SESSION_NONE) {
@session_start();
}
$roleName = (string) ($_SESSION['user_role_name'] ?? '');
if (strcasecmp($roleName, 'Administrator') === 0 || (int) ($_SESSION['user_id'] ?? 0) === 1) {
return true;
}
$remoteAddress = $_SERVER['REMOTE_ADDR'] ?? '';
return in_array($remoteAddress, ['127.0.0.1', '::1'], true);
}
function getSchemaSourceFile(): string
{
$complete = __DIR__ . '/complete_schema.sql';
if (is_file($complete)) {
return $complete;
}
return __DIR__ . '/db/schema.sql';
}
function parseExpectedSchema(string $filePath): array
{
if (!is_file($filePath)) {
throw new RuntimeException('Schema file not found: ' . basename($filePath));
}
$sql = file_get_contents($filePath);
if ($sql === false) {
throw new RuntimeException('Unable to read schema file: ' . basename($filePath));
}
$lines = preg_split('/\R/', $sql) ?: [];
$schema = [];
$currentTable = null;
foreach ($lines as $line) {
if (preg_match('/^\s*CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i', $line, $matches)) {
$currentTable = strtolower((string) $matches[1]);
$schema[$currentTable] = [];
continue;
}
if ($currentTable === null) {
continue;
}
if (preg_match('/^\s*\)\s*(ENGINE|DEFAULT|COMMENT|CHARSET|COLLATE|ROW_FORMAT|AUTO_INCREMENT|PARTITION|;)/i', $line)) {
$currentTable = null;
continue;
}
if (preg_match('/^\s*`([^`]+)`\s+/u', $line, $matches)) {
$schema[$currentTable][] = (string) $matches[1];
}
}
foreach ($schema as $tableName => $columns) {
$columns = array_values(array_unique($columns));
natcasesort($columns);
$schema[$tableName] = array_values($columns);
}
ksort($schema, SORT_NATURAL | SORT_FLAG_CASE);
return $schema;
}
function fetchActualSchema(PDO $pdo): array
{
$stmt = $pdo->query(
"SELECT TABLE_NAME, COLUMN_NAME\n"
. "FROM information_schema.COLUMNS\n"
. "WHERE TABLE_SCHEMA = DATABASE()\n"
. "ORDER BY TABLE_NAME ASC, ORDINAL_POSITION ASC"
);
$rows = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
$schema = [];
foreach ($rows as $row) {
$tableName = strtolower((string) ($row['TABLE_NAME'] ?? ''));
$columnName = (string) ($row['COLUMN_NAME'] ?? '');
if ($tableName === '' || $columnName === '') {
continue;
}
$schema[$tableName] ??= [];
$schema[$tableName][] = $columnName;
}
foreach ($schema as $tableName => $columns) {
$columns = array_values(array_unique($columns));
natcasesort($columns);
$schema[$tableName] = array_values($columns);
}
ksort($schema, SORT_NATURAL | SORT_FLAG_CASE);
return $schema;
}
function tailRuntimeDebugLog(int $maxLines = 10): array
{
$logFile = __DIR__ . '/runtime_debug.log';
if (!is_file($logFile)) {
return [];
}
$lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($lines) || $lines === []) {
return [];
}
return array_slice($lines, -$maxLines);
}
if (!canViewSchemaDebug()) {
http_response_code(403);
schemaDebugOutput('Forbidden');
exit;
}
try {
$pdo = db();
$schemaFile = getSchemaSourceFile();
$expectedSchema = parseExpectedSchema($schemaFile);
$actualSchema = fetchActualSchema($pdo);
$expectedTables = array_keys($expectedSchema);
$actualTables = array_keys($actualSchema);
$missingTables = array_values(array_diff($expectedTables, $actualTables));
$unexpectedTables = array_values(array_diff($actualTables, $expectedTables));
natcasesort($missingTables);
natcasesort($unexpectedTables);
$missingTables = array_values($missingTables);
$unexpectedTables = array_values($unexpectedTables);
$tablesWithMissingColumns = [];
foreach ($expectedSchema as $tableName => $expectedColumns) {
if (!isset($actualSchema[$tableName])) {
continue;
}
$missingColumns = array_values(array_diff($expectedColumns, $actualSchema[$tableName]));
if ($missingColumns !== []) {
natcasesort($missingColumns);
$tablesWithMissingColumns[$tableName] = array_values($missingColumns);
}
}
schemaDebugOutput('Schema Debug Report');
schemaDebugOutput('===================');
schemaDebugOutput('Database: ' . DB_NAME);
schemaDebugOutput('Schema source: ' . basename($schemaFile));
schemaDebugOutput('Expected tables: ' . count($expectedTables));
schemaDebugOutput('Actual tables: ' . count($actualTables));
schemaDebugOutput('Missing tables: ' . count($missingTables));
schemaDebugOutput('Tables with missing columns: ' . count($tablesWithMissingColumns));
schemaDebugOutput('Unexpected tables: ' . count($unexpectedTables));
if ($missingTables === [] && $tablesWithMissingColumns === []) {
schemaDebugOutput('');
schemaDebugOutput('OK: current database matches the schema snapshot for table and column presence.');
}
if ($missingTables !== []) {
schemaDebugOutput('');
schemaDebugOutput('Missing tables:');
foreach ($missingTables as $tableName) {
schemaDebugOutput('- ' . $tableName);
}
}
if ($tablesWithMissingColumns !== []) {
schemaDebugOutput('');
schemaDebugOutput('Missing columns by table:');
foreach ($tablesWithMissingColumns as $tableName => $columns) {
schemaDebugOutput('- ' . $tableName . ': ' . implode(', ', $columns));
}
}
if ($unexpectedTables !== []) {
schemaDebugOutput('');
schemaDebugOutput('Unexpected tables (present in DB but not in schema snapshot):');
foreach ($unexpectedTables as $tableName) {
schemaDebugOutput('- ' . $tableName);
}
}
$recentErrors = tailRuntimeDebugLog();
if ($recentErrors !== []) {
schemaDebugOutput('');
schemaDebugOutput('Recent runtime_debug.log entries:');
foreach ($recentErrors as $line) {
schemaDebugOutput('- ' . $line);
}
}
} catch (Throwable $throwable) {
http_response_code(500);
schemaDebugOutput('Schema debug failed: ' . $throwable->getMessage());
exit(1);
}