update index
This commit is contained in:
parent
3a757163ae
commit
6d25117591
5
cookies_repro.txt
Normal file
5
cookies_repro.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
127.0.0.1 FALSE / FALSE 0 PHPSESSID f3c2q95r0m6iaptq2skotkpemo
|
||||
178
index.php
178
index.php
@ -82,6 +82,184 @@ if (!function_exists('db_table_exists')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('runtime_debug_can_render_details')) {
|
||||
function runtime_debug_can_render_details(): bool {
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('runtime_debug_infer_schema_hint')) {
|
||||
function runtime_debug_infer_schema_hint(Throwable $throwable): ?string {
|
||||
$message = $throwable->getMessage();
|
||||
|
||||
if (preg_match("/Table '[^']+\.([^']+)' doesn't exist/i", $message, $matches)) {
|
||||
return 'Missing table: ' . $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match("/Unknown column '([^']+)'/i", $message, $matches)) {
|
||||
return 'Missing column: ' . $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/Base table or view not found: [0-9]+ ([a-zA-Z0-9_]+)/i', $message, $matches)) {
|
||||
return 'Missing table or view: ' . $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('runtime_debug_log')) {
|
||||
function runtime_debug_log(Throwable $throwable): void {
|
||||
$parts = [
|
||||
date('Y-m-d H:i:s'),
|
||||
'[' . get_class($throwable) . ']',
|
||||
$throwable->getMessage(),
|
||||
'file=' . $throwable->getFile() . ':' . $throwable->getLine(),
|
||||
'page=' . ($_GET['page'] ?? 'dashboard'),
|
||||
'uri=' . ($_SERVER['REQUEST_URI'] ?? 'cli'),
|
||||
'user_id=' . (string)($_SESSION['user_id'] ?? 0),
|
||||
];
|
||||
|
||||
$hint = runtime_debug_infer_schema_hint($throwable);
|
||||
if ($hint !== null) {
|
||||
$parts[] = 'hint=' . $hint;
|
||||
}
|
||||
|
||||
if ($throwable instanceof PDOException && isset($throwable->errorInfo) && is_array($throwable->errorInfo)) {
|
||||
$parts[] = 'error_info=' . json_encode($throwable->errorInfo, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
$trace = explode("
|
||||
", $throwable->getTraceAsString());
|
||||
if ($trace !== []) {
|
||||
$parts[] = 'trace=' . implode(' | ', array_slice($trace, 0, 5));
|
||||
}
|
||||
|
||||
@file_put_contents(__DIR__ . '/runtime_debug.log', implode(' || ', $parts) . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('runtime_debug_render_exception')) {
|
||||
function runtime_debug_render_exception(Throwable $throwable): void {
|
||||
runtime_debug_log($throwable);
|
||||
$showDetails = runtime_debug_can_render_details();
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
|
||||
if (!headers_sent()) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('X-Robots-Tag: noindex, nofollow');
|
||||
}
|
||||
|
||||
$hint = runtime_debug_infer_schema_hint($throwable);
|
||||
$requestUri = (string)($_SERVER['REQUEST_URI'] ?? 'cli');
|
||||
$page = (string)($_GET['page'] ?? 'dashboard');
|
||||
$tracePreview = array_slice(explode("
|
||||
", $throwable->getTraceAsString()), 0, 8);
|
||||
$traceText = implode("
|
||||
", $tracePreview);
|
||||
$title = $showDetails ? 'Application Debug' : 'Application Error';
|
||||
$summary = $showDetails
|
||||
? 'The request failed. The details below should help identify the missing table or column.'
|
||||
: 'An unexpected error occurred while loading this page.';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><?= htmlspecialchars($title) ?></title>
|
||||
<style>
|
||||
body { margin: 0; font-family: Inter, Arial, sans-serif; background: #f6f8fb; color: #1f2937; }
|
||||
.wrap { max-width: 900px; margin: 48px auto; padding: 0 20px; }
|
||||
.card { background: #fff; border-radius: 18px; box-shadow: 0 18px 60px rgba(15, 23, 42, 0.08); padding: 28px; }
|
||||
h1 { margin: 0 0 10px; font-size: 28px; }
|
||||
p { color: #4b5563; line-height: 1.6; }
|
||||
.badge { display: inline-block; padding: 6px 10px; border-radius: 999px; background: #fee2e2; color: #991b1b; font-weight: 600; font-size: 13px; }
|
||||
.grid { display: grid; grid-template-columns: 180px 1fr; gap: 12px 16px; margin-top: 22px; }
|
||||
.label { font-weight: 700; color: #111827; }
|
||||
.value { word-break: break-word; }
|
||||
.hint { margin-top: 18px; padding: 14px 16px; border-radius: 14px; background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }
|
||||
pre { margin: 18px 0 0; padding: 16px; background: #0f172a; color: #e2e8f0; border-radius: 14px; overflow: auto; font-size: 12px; line-height: 1.5; }
|
||||
.actions { margin-top: 22px; display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.btn { display: inline-block; text-decoration: none; border-radius: 12px; padding: 10px 14px; font-weight: 600; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-secondary { background: #e5e7eb; color: #111827; }
|
||||
.note { margin-top: 20px; font-size: 14px; color: #6b7280; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="card">
|
||||
<span class="badge">HTTP 500</span>
|
||||
<h1><?= htmlspecialchars($title) ?></h1>
|
||||
<p><?= htmlspecialchars($summary) ?></p>
|
||||
|
||||
<?php if ($showDetails): ?>
|
||||
<div class="grid">
|
||||
<div class="label">Exception</div>
|
||||
<div class="value"><?= htmlspecialchars(get_class($throwable)) ?></div>
|
||||
<div class="label">Message</div>
|
||||
<div class="value"><?= htmlspecialchars($throwable->getMessage()) ?></div>
|
||||
<div class="label">File</div>
|
||||
<div class="value"><?= htmlspecialchars($throwable->getFile()) ?></div>
|
||||
<div class="label">Line</div>
|
||||
<div class="value"><?= (int)$throwable->getLine() ?></div>
|
||||
<div class="label">Page</div>
|
||||
<div class="value"><?= htmlspecialchars($page) ?></div>
|
||||
<div class="label">Request URI</div>
|
||||
<div class="value"><?= htmlspecialchars($requestUri) ?></div>
|
||||
</div>
|
||||
|
||||
<?php if ($hint !== null): ?>
|
||||
<div class="hint"><strong>Schema hint:</strong> <?= htmlspecialchars($hint) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($traceText !== ''): ?>
|
||||
<pre><?= htmlspecialchars($traceText) ?></pre>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" href="schema_debug.php">Open schema debug report</a>
|
||||
<a class="btn btn-secondary" href="index.php">Retry dashboard</a>
|
||||
</div>
|
||||
|
||||
<p class="note">A copy of this failure was written to <code>runtime_debug.log</code>.</p>
|
||||
<?php else: ?>
|
||||
<div class="actions">
|
||||
<a class="btn btn-secondary" href="index.php">Back to home</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('APP_RUNTIME_DEBUG_HANDLER_REGISTERED')) {
|
||||
define('APP_RUNTIME_DEBUG_HANDLER_REGISTERED', true);
|
||||
set_exception_handler(static function (Throwable $throwable): void {
|
||||
runtime_debug_render_exception($throwable);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Outlet Switch
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) {
|
||||
$target_id = (int)$_GET['id'];
|
||||
|
||||
@ -148,3 +148,5 @@
|
||||
2026-05-01 19:22:46 - POST: {"start_trial":"1"}
|
||||
2026-05-01 19:22:46 - POST: {"username":"admin","password":"admin","login":"1"}
|
||||
2026-05-01 19:32:47 - POST: {"username":"admin","password":"admin","login":"1"}
|
||||
2026-05-02 02:56:23 - POST: {"username":"admin","password":"admin","login":"1"}
|
||||
2026-05-02 03:00:39 - POST: {"username":"admin","password":"admin","login":"1"}
|
||||
|
||||
256
schema_debug.php
Normal file
256
schema_debug.php
Normal file
@ -0,0 +1,256 @@
|
||||
<?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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user