150 lines
5.8 KiB
PHP
150 lines
5.8 KiB
PHP
<?php
|
|
namespace App\Exceptions;
|
|
|
|
use App\Helpers\Response;
|
|
use PDOException;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Class Handler
|
|
* Enterprise Global Exception & Error Handler (OWASP Information Leakage Guard)
|
|
*
|
|
* @package App\Exceptions
|
|
*/
|
|
class Handler
|
|
{
|
|
/**
|
|
* Register Global Exception and Error Handlers
|
|
*/
|
|
public static function register(): void
|
|
{
|
|
set_exception_handler([self::class, 'handleException']);
|
|
set_error_handler([self::class, 'handleError']);
|
|
register_shutdown_function([self::class, 'handleShutdown']);
|
|
}
|
|
|
|
/**
|
|
* Handle uncaught exceptions
|
|
*/
|
|
public static function handleException(Throwable $e): void
|
|
{
|
|
$status = 500;
|
|
$message = "Internal Server Error";
|
|
$details = [];
|
|
|
|
if ($e instanceof PDOException) {
|
|
$status = 500;
|
|
$message = "Database Query Error";
|
|
// ในโหมด Debug จะแสดง Error จริง ถ้า Production ซ่อนรายละเอียดป้องกัน SQLi enumeration
|
|
$details = getenv('APP_DEBUG') === 'true' ? ['pdo_error' => $e->getMessage()] : ['info' => 'Please check database logs'];
|
|
error_log("PDOException: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
|
|
} elseif (is_int($e->getCode()) && $e->getCode() >= 400 && $e->getCode() <= 599) {
|
|
$status = $e->getCode();
|
|
$message = $e->getMessage();
|
|
} else {
|
|
$message = $e->getMessage() ?: 'Unexpected Error Occurred';
|
|
error_log("Exception: " . $message . " in " . $e->getFile() . ":" . $e->getLine());
|
|
}
|
|
|
|
if (getenv('APP_DEBUG') === 'true') {
|
|
$details['file'] = $e->getFile();
|
|
$details['line'] = $e->getLine();
|
|
$details['trace'] = explode("\n", $e->getTraceAsString());
|
|
}
|
|
|
|
if (self::isApiRequest()) {
|
|
Response::error($message, $status, $details);
|
|
} else {
|
|
self::renderHtmlError($status, $message, $details, $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle PHP Warnings/Notices as Exceptions
|
|
*
|
|
* @param int $level
|
|
* @param string $message
|
|
* @param string $file
|
|
* @param int $line
|
|
* @return bool
|
|
*/
|
|
public static function handleError(int $level, string $message, string $file, int $line): bool
|
|
{
|
|
if (!(error_reporting() & $level)) {
|
|
return false;
|
|
}
|
|
error_log("PHP Error [{$level}]: {$message} in {$file}:{$line}");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle Fatal Errors on shutdown
|
|
*/
|
|
public static function handleShutdown(): void
|
|
{
|
|
$error = error_get_last();
|
|
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
|
error_log("Fatal Shutdown Error: {$error['message']} in {$error['file']}:{$error['line']}");
|
|
if (self::isApiRequest()) {
|
|
Response::error("Fatal Server Error: {$error['message']}", 500);
|
|
} else {
|
|
self::renderHtmlError(500, "Fatal PHP Error: " . $error['message'], ['file' => $error['file'], 'line' => $error['line']]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function isApiRequest(): bool
|
|
{
|
|
$uri = (string)($_SERVER['REQUEST_URI'] ?? '');
|
|
$accept = (string)($_SERVER['HTTP_ACCEPT'] ?? '');
|
|
return (strpos($uri, '/api/') !== false) ||
|
|
(strpos($uri, 'api=') !== false) ||
|
|
(strpos($uri, 'endpoint=') !== false) ||
|
|
(strpos($accept, 'application/json') !== false) ||
|
|
isset($_GET['api']) || isset($_GET['endpoint']);
|
|
}
|
|
|
|
/**
|
|
* Render HTML Error Page
|
|
*
|
|
* @param int $status
|
|
* @param string $message
|
|
* @param array<string, mixed> $details
|
|
* @param Throwable|null $e
|
|
*/
|
|
private static function renderHtmlError(int $status, string $message, array $details = [], ?Throwable $e = null): void
|
|
{
|
|
http_response_code($status);
|
|
$debugHtml = '';
|
|
if ((getenv('APP_DEBUG') === 'true' || getenv('APP_ENV') === 'local') && ($details || $e)) {
|
|
$errText = $e ? htmlspecialchars($e->getMessage()) : htmlspecialchars(json_encode($details, JSON_UNESCAPED_UNICODE));
|
|
$errFile = $e ? htmlspecialchars($e->getFile() . ':' . $e->getLine()) : '';
|
|
$debugHtml = "<div class='mt-4 p-4 bg-slate-950/80 rounded-xl text-left border border-rose-500/30 overflow-auto max-h-60 text-xs font-mono text-rose-300'>
|
|
<p class='font-bold underline mb-1'>Debug Information (APP_DEBUG=true):</p>
|
|
<p class='mb-2'><strong>Message:</strong> {$errText}</p>
|
|
" . ($errFile ? "<p class='mb-2'><strong>Location:</strong> {$errFile}</p>" : "") . "
|
|
</div>";
|
|
}
|
|
echo "<!DOCTYPE html>
|
|
<html lang='th'>
|
|
<head>
|
|
<meta charset='UTF-8'>
|
|
<title>Error {$status} - TTMQMS</title>
|
|
<script src='https://cdn.tailwindcss.com'></script>
|
|
</head>
|
|
<body class='bg-slate-900 text-slate-100 flex items-center justify-center min-h-screen font-sans p-4'>
|
|
<div class='bg-slate-800 border border-slate-700 rounded-2xl p-8 max-w-xl w-full text-center shadow-2xl'>
|
|
<div class='text-6xl mb-4'>⚠️</div>
|
|
<h1 class='text-4xl font-bold text-rose-500 mb-2'>{$status}</h1>
|
|
<p class='text-lg text-slate-300 mb-4'>{$message}</p>
|
|
{$debugHtml}
|
|
<div class='mt-6'>
|
|
<a href='" . (defined('BASE_URL') ? BASE_URL : '') . "/' class='bg-emerald-600 hover:bg-emerald-500 text-white font-medium px-6 py-2.5 rounded-xl transition inline-block'>กลับสู่หน้าหลัก / รีเฟรช</a>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>";
|
|
exit();
|
|
}
|
|
}
|