mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-14 10:37:30 -07:00
refactor(database): enhance error logging and various fixes (#1978)
* refactor(database): enhance error logging and handling in Database and DatabaseDebugger classes - Updated error handling in Database class to provide more meaningful error messages, including detailed PDO error information. - Enhanced log_error method in DatabaseDebugger to accept exceptions, allowing for more reliable error logging with comprehensive context. - Improved user-facing error messages while maintaining detailed logging for administrators and developers. - Added checks for connection status and query context in error logs to aid in debugging. * refactor(database): streamline error handling and improve code clarity in Database class - Removed unused imports and global variable references to enhance code readability. - Simplified error handling by refining conditions for empty input arrays. - Defined variables early in the error logging method to ensure consistent access throughout. - Improved comments for better understanding of the error handling logic. * fix(database): refine development mode condition in Database class - Removed unnecessary function check from the development mode condition to streamline the logic. * fix(redirect): add no-cache headers to prevent browser caching of redirects - Implemented `send_no_cache_headers()` in the redirect function to ensure that browsers do not cache redirect responses, adhering to best practices for HTTP redirects. * refactor(database): enhance error handling and logging in Database and Whoops classes - Improved error handling in the Database class to log detailed information for duplicate column errors and automatically retry problematic queries using PDO. - Introduced a new DatabaseErrorHandler to enhance Whoops error reporting with comprehensive database context and recent query history. - Updated EnhancedPrettyPageHandler to include dynamic database information in error outputs, improving debugging capabilities. - Added legacy query tracking in DatabaseDebugger to identify and log queries needing compatibility fixes, ensuring better maintenance of legacy code. * refactor(posting): rollback changes in SQL queries up to legacy format
This commit is contained in:
parent
7e723d6ad8
commit
7aed6bc7d8
8 changed files with 1150 additions and 45 deletions
|
@ -26,9 +26,9 @@ $posts_without_attach = $topics_without_attach = [];
|
|||
|
||||
DB()->query("
|
||||
CREATE TEMPORARY TABLE $tmp_attach_tbl (
|
||||
physical_filename VARCHAR(255) NOT NULL default '',
|
||||
physical_filename VARCHAR(255) NOT NULL default '' COLLATE utf8mb4_unicode_ci,
|
||||
KEY physical_filename (physical_filename(20))
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||
");
|
||||
DB()->add_shutdown_query("DROP TEMPORARY TABLE IF EXISTS $tmp_attach_tbl");
|
||||
|
||||
|
|
|
@ -1437,6 +1437,9 @@ function redirect($url)
|
|||
|
||||
$redirect_url = $server_protocol . $server_name . $server_port . $script_name . preg_replace('#^\/?(.*?)\/?$#', '/\1', $url);
|
||||
|
||||
// Send no-cache headers to prevent browsers from caching redirects
|
||||
send_no_cache_headers();
|
||||
|
||||
// Behave as per HTTP/1.1 spec for others
|
||||
header('Location: ' . $redirect_url, response_code: 301);
|
||||
exit;
|
||||
|
|
|
@ -10,16 +10,10 @@
|
|||
namespace TorrentPier\Database;
|
||||
|
||||
use Nette\Database\Connection;
|
||||
use Nette\Database\Conventions\DiscoveredConventions;
|
||||
use Nette\Database\Explorer;
|
||||
use Nette\Database\ResultSet;
|
||||
use Nette\Database\Row;
|
||||
use Nette\Database\Table\Selection;
|
||||
use Nette\Database\Structure;
|
||||
use Nette\Database\Conventions\DiscoveredConventions;
|
||||
use TorrentPier\Database\DebugSelection;
|
||||
use TorrentPier\Database\DatabaseDebugger;
|
||||
use TorrentPier\Dev;
|
||||
use TorrentPier\Legacy\SqlDb;
|
||||
|
||||
/**
|
||||
* Modern Database class using Nette Database with backward compatibility
|
||||
|
@ -55,6 +49,7 @@ class Database
|
|||
public float $sql_timetotal = 0;
|
||||
public float $cur_query_time = 0;
|
||||
public ?string $cur_query = null;
|
||||
public ?string $last_query = null; // Store last executed query for error reporting
|
||||
|
||||
public array $shutdown = [];
|
||||
public array $DBS = [];
|
||||
|
@ -64,8 +59,6 @@ class Database
|
|||
*/
|
||||
private function __construct(array $cfg_values, string $server_name = 'db')
|
||||
{
|
||||
global $DBS;
|
||||
|
||||
$this->cfg = array_combine($this->cfg_keys, $cfg_values);
|
||||
$this->db_server = $server_name;
|
||||
|
||||
|
@ -188,12 +181,13 @@ class Database
|
|||
// Initialize affected rows to 0 (most queries don't affect rows)
|
||||
$this->last_affected_rows = 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->debugger->log_error();
|
||||
$this->debugger->log_error($e);
|
||||
$this->result = null;
|
||||
$this->last_affected_rows = 0;
|
||||
}
|
||||
|
||||
$this->debugger->debug('stop');
|
||||
$this->last_query = $this->cur_query; // Preserve for error reporting
|
||||
$this->cur_query = null;
|
||||
|
||||
if ($this->inited) {
|
||||
|
@ -247,20 +241,69 @@ class Database
|
|||
return false;
|
||||
}
|
||||
|
||||
$row = $result->fetch();
|
||||
if (!$row) {
|
||||
return false;
|
||||
try {
|
||||
$row = $result->fetch();
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert Row to array for backward compatibility
|
||||
// Nette Database Row extends ArrayHash, so we can cast it to array
|
||||
$rowArray = (array)$row;
|
||||
|
||||
if ($field_name) {
|
||||
return $rowArray[$field_name] ?? false;
|
||||
}
|
||||
|
||||
return $rowArray;
|
||||
} catch (\Exception $e) {
|
||||
// Check if this is a duplicate column error
|
||||
if (str_contains($e->getMessage(), 'Found duplicate columns')) {
|
||||
// Log this as a problematic query that needs fixing
|
||||
$this->debugger->logLegacyQuery($this->last_query ?? $this->cur_query ?? 'Unknown query', $e->getMessage());
|
||||
|
||||
// Automatically retry by re-executing the query with direct PDO
|
||||
// This bypasses Nette's duplicate column check completely
|
||||
try {
|
||||
// Extract the clean SQL query
|
||||
$cleanQuery = $this->last_query ?? $this->cur_query ?? '';
|
||||
// Remove Nette's debug comment
|
||||
$cleanQuery = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $cleanQuery);
|
||||
|
||||
if (!$cleanQuery) {
|
||||
throw new \RuntimeException('Could not extract clean query for PDO retry');
|
||||
}
|
||||
|
||||
// Execute directly with PDO to bypass Nette's column checking
|
||||
$stmt = $this->connection->getPdo()->prepare($cleanQuery);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
// PDO::FETCH_ASSOC automatically handles duplicate columns by keeping the last occurrence
|
||||
// which matches MySQL's behavior for SELECT t.*, f.* queries
|
||||
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($field_name) {
|
||||
return $row[$field_name] ?? false;
|
||||
}
|
||||
|
||||
return $row;
|
||||
} catch (\Exception $retryException) {
|
||||
// If PDO retry also fails, log and re-throw
|
||||
$this->debugger->log_error($retryException);
|
||||
throw $retryException;
|
||||
}
|
||||
}
|
||||
|
||||
// Log the error including the query that caused it
|
||||
$this->debugger->log_error($e);
|
||||
|
||||
// Re-throw the exception so it can be handled by Whoops
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Convert Row to array for backward compatibility
|
||||
// Nette Database Row extends ArrayHash, so we can cast it to array
|
||||
$rowArray = (array)$row;
|
||||
|
||||
if ($field_name) {
|
||||
return $rowArray[$field_name] ?? false;
|
||||
}
|
||||
|
||||
return $rowArray;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -280,7 +323,22 @@ class Database
|
|||
$this->trigger_error();
|
||||
}
|
||||
|
||||
return $this->sql_fetchrow($result, $field_name);
|
||||
try {
|
||||
return $this->sql_fetchrow($result, $field_name);
|
||||
} catch (\Exception $e) {
|
||||
// Enhance the exception with query information
|
||||
$enhancedException = new \RuntimeException(
|
||||
"Database error during fetch_row: " . $e->getMessage() .
|
||||
"\nProblematic Query: " . ($this->cur_query ?: $this->last_query ?: 'Unknown'),
|
||||
$e->getCode(),
|
||||
$e
|
||||
);
|
||||
|
||||
// Log the enhanced error
|
||||
$this->debugger->log_error($enhancedException);
|
||||
|
||||
throw $enhancedException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,11 +351,48 @@ class Database
|
|||
}
|
||||
|
||||
$rowset = [];
|
||||
while ($row = $result->fetch()) {
|
||||
// Convert Row to array for backward compatibility
|
||||
// Nette Database Row extends ArrayHash, so we can cast it to array
|
||||
$rowArray = (array)$row;
|
||||
$rowset[] = $field_name ? ($rowArray[$field_name] ?? null) : $rowArray;
|
||||
|
||||
try {
|
||||
while ($row = $result->fetch()) {
|
||||
// Convert Row to array for backward compatibility
|
||||
// Nette Database Row extends ArrayHash, so we can cast it to array
|
||||
$rowArray = (array)$row;
|
||||
$rowset[] = $field_name ? ($rowArray[$field_name] ?? null) : $rowArray;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Check if this is a duplicate column error
|
||||
if (str_contains($e->getMessage(), 'Found duplicate columns')) {
|
||||
// Log this as a problematic query that needs fixing
|
||||
$this->debugger->logLegacyQuery($this->last_query ?? $this->cur_query ?? 'Unknown query', $e->getMessage());
|
||||
|
||||
// Automatically retry by re-executing the query with direct PDO
|
||||
try {
|
||||
// Extract the clean SQL query
|
||||
$cleanQuery = $this->last_query ?? $this->cur_query ?? '';
|
||||
// Remove Nette's debug comment
|
||||
$cleanQuery = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $cleanQuery);
|
||||
|
||||
if (!$cleanQuery) {
|
||||
throw new \RuntimeException('Could not extract clean query for PDO retry');
|
||||
}
|
||||
|
||||
// Execute directly with PDO to bypass Nette's column checking
|
||||
$stmt = $this->connection->getPdo()->prepare($cleanQuery);
|
||||
$stmt->execute();
|
||||
|
||||
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$rowset[] = $field_name ? ($row[$field_name] ?? null) : $row;
|
||||
}
|
||||
} catch (\Exception $retryException) {
|
||||
// If PDO retry also fails, log and re-throw
|
||||
$this->debugger->log_error($retryException);
|
||||
throw $retryException;
|
||||
}
|
||||
} else {
|
||||
// For other exceptions, just re-throw
|
||||
$this->debugger->log_error($e);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $rowset;
|
||||
|
@ -423,8 +518,8 @@ class Database
|
|||
$dont_escape = $data_already_escaped;
|
||||
$check_type = $check_data_type_in_escape;
|
||||
|
||||
if (empty($input_ary) || !is_array($input_ary)) {
|
||||
$this->trigger_error(__FUNCTION__ . ' - wrong params: $input_ary');
|
||||
if (empty($input_ary)) {
|
||||
$this->trigger_error(__FUNCTION__ . ' - wrong params: $input_ary is empty');
|
||||
}
|
||||
|
||||
if ($query_type == 'INSERT') {
|
||||
|
@ -549,9 +644,28 @@ class Database
|
|||
if ($this->connection) {
|
||||
try {
|
||||
$pdo = $this->connection->getPdo();
|
||||
$errorCode = $pdo->errorCode();
|
||||
$errorInfo = $pdo->errorInfo();
|
||||
|
||||
// Filter out "no error" states - PDO returns '00000' when there's no error
|
||||
if (!$errorCode || $errorCode === '00000') {
|
||||
return ['code' => '', 'message' => ''];
|
||||
}
|
||||
|
||||
// Build meaningful error message from errorInfo array
|
||||
// errorInfo format: [SQLSTATE, driver-specific error code, driver-specific error message]
|
||||
$message = '';
|
||||
if (isset($errorInfo[2]) && $errorInfo[2]) {
|
||||
$message = $errorInfo[2]; // Driver-specific error message is most informative
|
||||
} elseif (isset($errorInfo[1]) && $errorInfo[1]) {
|
||||
$message = "Error code: " . $errorInfo[1];
|
||||
} else {
|
||||
$message = "SQLSTATE: " . $errorCode;
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $pdo->errorCode(),
|
||||
'message' => implode(': ', $pdo->errorInfo())
|
||||
'code' => $errorCode,
|
||||
'message' => $message
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return ['code' => $e->getCode(), 'message' => $e->getMessage()];
|
||||
|
@ -753,7 +867,91 @@ class Database
|
|||
public function trigger_error(string $msg = 'Database Error'): void
|
||||
{
|
||||
$error = $this->sql_error();
|
||||
$error_msg = "$msg: " . $error['message'];
|
||||
|
||||
// Define these variables early so they're available throughout the method
|
||||
$is_admin = defined('IS_ADMIN') && IS_ADMIN;
|
||||
$is_dev_mode = (defined('APP_ENV') && APP_ENV === 'local') || (defined('DBG_USER') && DBG_USER);
|
||||
|
||||
// Build a meaningful error message
|
||||
if (!empty($error['message'])) {
|
||||
$error_msg = "$msg: " . $error['message'];
|
||||
if (!empty($error['code'])) {
|
||||
$error_msg = "$msg ({$error['code']}): " . $error['message'];
|
||||
}
|
||||
} else {
|
||||
// Base error message for all users
|
||||
$error_msg = "$msg: Database operation failed";
|
||||
|
||||
// Only add detailed debugging information for administrators or in development mode
|
||||
if ($is_admin || $is_dev_mode) {
|
||||
// Gather detailed debugging information - ONLY for admins/developers
|
||||
$debug_info = [];
|
||||
|
||||
// Connection status
|
||||
if ($this->connection) {
|
||||
$debug_info[] = "Connection: Active";
|
||||
try {
|
||||
$pdo = $this->connection->getPdo();
|
||||
if ($pdo) {
|
||||
$debug_info[] = "PDO: Available";
|
||||
$errorInfo = $pdo->errorInfo();
|
||||
if ($errorInfo && count($errorInfo) >= 3) {
|
||||
$debug_info[] = "PDO ErrorInfo: " . json_encode($errorInfo);
|
||||
}
|
||||
$debug_info[] = "PDO ErrorCode: " . $pdo->errorCode();
|
||||
} else {
|
||||
$debug_info[] = "PDO: Null";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$debug_info[] = "PDO Check Failed: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$debug_info[] = "Connection: None";
|
||||
}
|
||||
|
||||
// Query information
|
||||
if ($this->cur_query) {
|
||||
$debug_info[] = "Last Query: " . substr($this->cur_query, 0, 200) . (strlen($this->cur_query) > 200 ? '...' : '');
|
||||
} else {
|
||||
$debug_info[] = "Last Query: None";
|
||||
}
|
||||
|
||||
// Database information
|
||||
$debug_info[] = "Database: " . ($this->selected_db ?: 'None');
|
||||
$debug_info[] = "Server: " . $this->db_server;
|
||||
|
||||
// Recent queries from debug log (if available)
|
||||
if (isset($this->debugger->dbg) && !empty($this->debugger->dbg)) {
|
||||
$recent_queries = array_slice($this->debugger->dbg, -3); // Last 3 queries
|
||||
$debug_info[] = "Recent Queries Count: " . count($recent_queries);
|
||||
foreach ($recent_queries as $i => $query_info) {
|
||||
$debug_info[] = "Query " . ($i + 1) . ": " . substr($query_info['sql'] ?? 'Unknown', 0, 100) . (strlen($query_info['sql'] ?? '') > 100 ? '...' : '');
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug_info) {
|
||||
$error_msg .= " [DEBUG: " . implode("; ", $debug_info) . "]";
|
||||
}
|
||||
|
||||
// Log this for investigation
|
||||
if (function_exists('bb_log')) {
|
||||
bb_log("Unknown Database Error Debug:\n" . implode("\n", $debug_info), 'unknown_db_errors');
|
||||
}
|
||||
} else {
|
||||
// For regular users: generic message only + contact admin hint
|
||||
$error_msg = "$msg: A database error occurred. Please contact the administrator if this problem persists.";
|
||||
|
||||
// Still log basic information for debugging
|
||||
if (function_exists('bb_log')) {
|
||||
bb_log("Database Error (User-facing): $error_msg\nRequest: " . ($_SERVER['REQUEST_URI'] ?? 'CLI'), 'user_db_errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add query context for debugging (but only for admins/developers)
|
||||
if ($this->cur_query && ($is_admin || $is_dev_mode)) {
|
||||
$error_msg .= "\nQuery: " . $this->cur_query;
|
||||
}
|
||||
|
||||
if (function_exists('bb_die')) {
|
||||
bb_die($error_msg);
|
||||
|
@ -797,9 +995,9 @@ class Database
|
|||
/**
|
||||
* Log error (delegated to debugger)
|
||||
*/
|
||||
public function log_error(): void
|
||||
public function log_error(?\Exception $exception = null): void
|
||||
{
|
||||
$this->debugger->log_error();
|
||||
$this->debugger->log_error($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,6 +29,7 @@ class DatabaseDebugger
|
|||
// Debug storage
|
||||
public array $dbg = [];
|
||||
public int $dbg_id = 0;
|
||||
public array $legacy_queries = []; // Track queries that needed legacy compatibility fixes
|
||||
|
||||
// Explain functionality
|
||||
public string $explain_hold = '';
|
||||
|
@ -278,11 +279,131 @@ class DatabaseDebugger
|
|||
|
||||
/**
|
||||
* Log error
|
||||
*
|
||||
* NOTE: This method logs detailed information to FILES only (error_log, bb_log).
|
||||
* Log files are not accessible to regular users, so detailed information is safe here.
|
||||
* User-facing error display is handled separately with proper security checks.
|
||||
*/
|
||||
public function log_error(): void
|
||||
public function log_error(?\Exception $exception = null): void
|
||||
{
|
||||
$error = $this->db->sql_error();
|
||||
error_log("Database Error: " . $error['message'] . " Query: " . $this->db->cur_query);
|
||||
$error_details = [];
|
||||
$error_msg = '';
|
||||
|
||||
if ($exception) {
|
||||
// Use the actual exception information which is more reliable
|
||||
$error_msg = "Database Error: " . $exception->getMessage();
|
||||
$error_code = $exception->getCode();
|
||||
if ($error_code) {
|
||||
$error_msg = "Database Error ({$error_code}): " . $exception->getMessage();
|
||||
}
|
||||
|
||||
// Collect detailed error information
|
||||
$error_details[] = "Exception: " . get_class($exception);
|
||||
$error_details[] = "Message: " . $exception->getMessage();
|
||||
$error_details[] = "Code: " . $exception->getCode();
|
||||
$error_details[] = "File: " . $exception->getFile() . ":" . $exception->getLine();
|
||||
|
||||
// Add PDO-specific details if it's a PDO exception
|
||||
if ($exception instanceof \PDOException) {
|
||||
$error_details[] = "PDO Error Info: " . json_encode($exception->errorInfo ?? []);
|
||||
}
|
||||
} else {
|
||||
// Fallback to PDO error state (legacy behavior)
|
||||
$error = $this->db->sql_error();
|
||||
|
||||
// Only log if there's an actual error (not 00000 which means "no error")
|
||||
if (!$error['code'] || $error['code'] === '00000' || !$error['message']) {
|
||||
return; // Don't log empty or "no error" states
|
||||
}
|
||||
|
||||
$error_msg = "Database Error ({$error['code']}): " . $error['message'];
|
||||
$error_details[] = "PDO Error Code: " . $error['code'];
|
||||
$error_details[] = "PDO Error Message: " . $error['message'];
|
||||
}
|
||||
|
||||
// Add comprehensive context for debugging
|
||||
$error_details[] = "Query: " . ($this->db->cur_query ?: 'None');
|
||||
$error_details[] = "Source: " . $this->debug_find_source();
|
||||
$error_details[] = "Database: " . ($this->db->selected_db ?: 'None');
|
||||
$error_details[] = "Server: " . $this->db->db_server;
|
||||
$error_details[] = "Timestamp: " . date('Y-m-d H:i:s');
|
||||
$error_details[] = "Request URI: " . ($_SERVER['REQUEST_URI'] ?? 'CLI');
|
||||
$error_details[] = "User IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'Unknown');
|
||||
|
||||
// Check connection status
|
||||
try {
|
||||
if ($this->db->connection) {
|
||||
$error_details[] = "Connection Status: Active";
|
||||
$pdo = $this->db->connection->getPdo();
|
||||
$error_details[] = "PDO Connection: " . ($pdo ? 'Available' : 'Null');
|
||||
if ($pdo) {
|
||||
$errorInfo = $pdo->errorInfo();
|
||||
$error_details[] = "Current PDO Error Info: " . json_encode($errorInfo);
|
||||
}
|
||||
} else {
|
||||
$error_details[] = "Connection Status: No connection";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$error_details[] = "Connection Check Failed: " . $e->getMessage();
|
||||
}
|
||||
|
||||
// Build comprehensive log message
|
||||
$log_message = $error_msg . "\n" . implode("\n", $error_details);
|
||||
|
||||
// Log to both error_log and TorrentPier's logging system
|
||||
error_log($error_msg);
|
||||
|
||||
// Use TorrentPier's bb_log for better file management and organization
|
||||
if (function_exists('bb_log')) {
|
||||
bb_log($log_message, 'database_errors');
|
||||
}
|
||||
|
||||
// Also log to PHP error log for immediate access
|
||||
error_log("DETAILED: " . $log_message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log legacy query that needed automatic compatibility fix
|
||||
*/
|
||||
public function logLegacyQuery(string $query, string $error): void
|
||||
{
|
||||
$legacy_entry = [
|
||||
'query' => $query,
|
||||
'error' => $error,
|
||||
'source' => $this->debug_find_source(),
|
||||
'file' => $this->debug_find_source('file'),
|
||||
'line' => $this->debug_find_source('line'),
|
||||
'time' => microtime(true)
|
||||
];
|
||||
|
||||
$this->legacy_queries[] = $legacy_entry;
|
||||
|
||||
// Mark the CURRENT debug entry as legacy instead of creating a new one
|
||||
if ($this->dbg_enabled && !empty($this->dbg)) {
|
||||
// Find the most recent debug entry (the one that just executed and failed)
|
||||
$current_id = $this->dbg_id - 1;
|
||||
|
||||
if (isset($this->dbg[$current_id])) {
|
||||
// Mark the existing entry as legacy
|
||||
$this->dbg[$current_id]['is_legacy_query'] = true;
|
||||
|
||||
// Update the info to show it was automatically fixed
|
||||
$original_info = $this->dbg[$current_id]['info'] ?? '';
|
||||
$original_info_plain = $this->dbg[$current_id]['info_plain'] ?? $original_info;
|
||||
|
||||
$this->dbg[$current_id]['info'] = 'LEGACY COMPATIBILITY FIX APPLIED - ' . $original_info;
|
||||
$this->dbg[$current_id]['info_plain'] = 'LEGACY COMPATIBILITY FIX APPLIED - ' . $original_info_plain;
|
||||
}
|
||||
}
|
||||
|
||||
// Log to file for permanent record
|
||||
$msg = 'LEGACY QUERY DETECTED - NEEDS FIXING' . LOG_LF;
|
||||
$msg .= 'Query: ' . $query . LOG_LF;
|
||||
$msg .= 'Error: ' . $error . LOG_LF;
|
||||
$msg .= 'Source: ' . $legacy_entry['source'] . LOG_LF;
|
||||
$msg .= 'Time: ' . date('Y-m-d H:i:s', (int)$legacy_entry['time']) . LOG_LF;
|
||||
|
||||
bb_log($msg, 'legacy_queries', false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
38
src/Dev.php
38
src/Dev.php
|
@ -129,9 +129,9 @@ class Dev
|
|||
private function getWhoopsOnPage(): void
|
||||
{
|
||||
/**
|
||||
* Show errors on page
|
||||
* Show errors on page with enhanced database information
|
||||
*/
|
||||
$prettyPageHandler = new PrettyPageHandler();
|
||||
$prettyPageHandler = new \TorrentPier\Whoops\EnhancedPrettyPageHandler();
|
||||
foreach (config()->get('whoops.blacklist', []) as $key => $secrets) {
|
||||
foreach ($secrets as $secret) {
|
||||
$prettyPageHandler->blacklist($key, $secret);
|
||||
|
@ -195,9 +195,32 @@ class Dev
|
|||
public function getSqlLogInstance(): string
|
||||
{
|
||||
$log = '';
|
||||
$totalLegacyQueries = 0;
|
||||
|
||||
// Check for legacy queries across all database instances
|
||||
$server_names = \TorrentPier\Database\DatabaseFactory::getServerNames();
|
||||
foreach ($server_names as $srv_name) {
|
||||
try {
|
||||
$db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
|
||||
if (!empty($db_obj->debugger->legacy_queries)) {
|
||||
$totalLegacyQueries += count($db_obj->debugger->legacy_queries);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Skip if server not available
|
||||
}
|
||||
}
|
||||
|
||||
// Add warning banner if legacy queries were detected
|
||||
if ($totalLegacyQueries > 0) {
|
||||
$log .= '<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 10px; margin-bottom: 10px; border-radius: 4px;">'
|
||||
. '<strong>⚠️ Legacy Query Warning:</strong> '
|
||||
. $totalLegacyQueries . ' quer' . ($totalLegacyQueries > 1 ? 'ies' : 'y') . ' with duplicate columns detected and automatically fixed. '
|
||||
. 'These queries should be updated to explicitly select columns. '
|
||||
. 'Check the legacy_queries.log file for details.'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
// Get debug information from new database system
|
||||
$server_names = \TorrentPier\Database\DatabaseFactory::getServerNames();
|
||||
foreach ($server_names as $srv_name) {
|
||||
try {
|
||||
$db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
|
||||
|
@ -262,7 +285,14 @@ class Dev
|
|||
$info_plain = !empty($dbg['info_plain']) ? $dbg['info_plain'] . ' [' . $dbg['src'] . ']' : $dbg['src'];
|
||||
$info = !empty($dbg['info']) ? $dbg['info'] . ' [' . $dbg['src'] . ']' : $dbg['src'];
|
||||
|
||||
$log .= '<div onclick="$(this).toggleClass(\'sqlHighlight\');" class="sqlLogRow" title="' . htmlspecialchars($info_plain) . '">'
|
||||
// Check if this is a legacy query that needed compatibility fix
|
||||
$isLegacyQuery = !empty($dbg['is_legacy_query']);
|
||||
$rowClass = $isLegacyQuery ? 'sqlLogRow sqlLegacyRow' : 'sqlLogRow';
|
||||
$rowStyle = $isLegacyQuery ? ' style="background-color: #ffe6e6; border-left: 4px solid #dc3545; color: #721c24;"' : '';
|
||||
$legacyWarning = $isLegacyQuery ? '<span style="color: #dc3545; font-weight: bold; margin-right: 8px;">[LEGACY]</span>' : '';
|
||||
|
||||
$log .= '<div onclick="$(this).toggleClass(\'sqlHighlight\');" class="' . $rowClass . '" title="' . htmlspecialchars($info_plain) . '"' . $rowStyle . '>'
|
||||
. $legacyWarning
|
||||
. '<span style="letter-spacing: -1px;">' . $time . ' </span>'
|
||||
. '<span class="copyElement" data-clipboard-target="#' . $id . '" title="Copy to clipboard" style="color: rgb(128,128,128); letter-spacing: -1px;">' . $perc . '</span> '
|
||||
. '<span style="letter-spacing: 0;" id="' . $id . '">' . $sql . '</span>'
|
||||
|
|
353
src/Whoops/DatabaseErrorHandler.php
Normal file
353
src/Whoops/DatabaseErrorHandler.php
Normal file
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
/**
|
||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||
*/
|
||||
|
||||
namespace TorrentPier\Whoops;
|
||||
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Handler\HandlerInterface;
|
||||
|
||||
/**
|
||||
* Database Error Handler for Whoops
|
||||
*
|
||||
* Enhances error reporting by adding database query information,
|
||||
* recent SQL activity, and database error details to the error output.
|
||||
*/
|
||||
class DatabaseErrorHandler extends Handler implements HandlerInterface
|
||||
{
|
||||
private bool $addToOutput = true;
|
||||
private bool $includeQueryHistory = true;
|
||||
private int $maxQueryHistory = 5;
|
||||
|
||||
/**
|
||||
* Handle the exception and add database information
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!$this->addToOutput) {
|
||||
return Handler::DONE;
|
||||
}
|
||||
|
||||
$inspector = $this->getInspector();
|
||||
$exception = $inspector->getException();
|
||||
|
||||
// Add database information to the exception frames
|
||||
$this->addDatabaseContextToFrames($inspector);
|
||||
|
||||
// Add global database state information
|
||||
$this->addGlobalDatabaseInfo($exception);
|
||||
|
||||
return Handler::DONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to add database info to output
|
||||
*/
|
||||
public function setAddToOutput(bool $add): self
|
||||
{
|
||||
$this->addToOutput = $add;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to include query history
|
||||
*/
|
||||
public function setIncludeQueryHistory(bool $include): self
|
||||
{
|
||||
$this->includeQueryHistory = $include;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum number of queries to show in history
|
||||
*/
|
||||
public function setMaxQueryHistory(int $max): self
|
||||
{
|
||||
$this->maxQueryHistory = max(1, $max);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add database context information to exception frames
|
||||
*/
|
||||
private function addDatabaseContextToFrames($inspector): void
|
||||
{
|
||||
$frames = $inspector->getFrames();
|
||||
|
||||
foreach ($frames as $frame) {
|
||||
$frameData = [];
|
||||
|
||||
// Check if this frame involves database operations
|
||||
$fileName = $frame->getFile();
|
||||
$className = $frame->getClass();
|
||||
$functionName = $frame->getFunction();
|
||||
|
||||
// Detect database-related frames
|
||||
$isDatabaseFrame = $this->isDatabaseRelatedFrame($fileName, $className, $functionName);
|
||||
|
||||
if ($isDatabaseFrame) {
|
||||
$frameData['database_context'] = $this->getCurrentDatabaseContext();
|
||||
|
||||
// Add frame-specific database info
|
||||
$frame->addComment('Database Context', 'This frame involves database operations');
|
||||
|
||||
foreach ($frameData['database_context'] as $key => $value) {
|
||||
if (is_string($value) || is_numeric($value)) {
|
||||
$frame->addComment("DB: $key", $value);
|
||||
} elseif (is_array($value) && !empty($value)) {
|
||||
$frame->addComment("DB: $key", json_encode($value, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add global database information to the exception
|
||||
*/
|
||||
private function addGlobalDatabaseInfo($exception): void
|
||||
{
|
||||
try {
|
||||
$databaseInfo = $this->collectDatabaseInformation();
|
||||
|
||||
// Use reflection to add custom data to the exception
|
||||
// This will appear in the Whoops error page
|
||||
if (method_exists($exception, 'setAdditionalInfo')) {
|
||||
$exception->setAdditionalInfo('Database Information', $databaseInfo);
|
||||
} else {
|
||||
// Fallback: store in a property that Whoops can access
|
||||
if (!isset($exception->databaseInfo)) {
|
||||
$exception->databaseInfo = $databaseInfo;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Don't let database info collection break error handling
|
||||
if (method_exists($exception, 'setAdditionalInfo')) {
|
||||
$exception->setAdditionalInfo('Database Info Error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a frame is related to database operations
|
||||
*/
|
||||
private function isDatabaseRelatedFrame(?string $fileName, ?string $className, ?string $functionName): bool
|
||||
{
|
||||
if (!$fileName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file paths
|
||||
$databaseFiles = [
|
||||
'/Database/',
|
||||
'/database/',
|
||||
'Database.php',
|
||||
'DatabaseDebugger.php',
|
||||
'DebugSelection.php',
|
||||
];
|
||||
|
||||
foreach ($databaseFiles as $dbFile) {
|
||||
if (str_contains($fileName, $dbFile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check class names
|
||||
$databaseClasses = [
|
||||
'Database',
|
||||
'DatabaseDebugger',
|
||||
'DebugSelection',
|
||||
'DB',
|
||||
'Nette\Database',
|
||||
];
|
||||
|
||||
if ($className) {
|
||||
foreach ($databaseClasses as $dbClass) {
|
||||
if (str_contains($className, $dbClass)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check function names
|
||||
$databaseFunctions = [
|
||||
'sql_query',
|
||||
'fetch_row',
|
||||
'fetch_rowset',
|
||||
'sql_fetchrow',
|
||||
'query',
|
||||
'execute',
|
||||
];
|
||||
|
||||
if ($functionName) {
|
||||
foreach ($databaseFunctions as $dbFunc) {
|
||||
if (str_contains($functionName, $dbFunc)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current database context
|
||||
*/
|
||||
private function getCurrentDatabaseContext(): array
|
||||
{
|
||||
$context = [];
|
||||
|
||||
try {
|
||||
// Get main database instance
|
||||
if (function_exists('DB')) {
|
||||
$db = DB();
|
||||
|
||||
$context['current_query'] = $db->cur_query ?? 'None';
|
||||
$context['database_server'] = $db->db_server ?? 'Unknown';
|
||||
$context['selected_database'] = $db->selected_db ?? 'Unknown';
|
||||
|
||||
// Connection status
|
||||
$context['connection_status'] = $db->connection ? 'Active' : 'No connection';
|
||||
|
||||
// Query stats
|
||||
$context['total_queries'] = $db->num_queries ?? 0;
|
||||
$context['total_time'] = isset($db->sql_timetotal) ? sprintf('%.3f sec', $db->sql_timetotal) : 'Unknown';
|
||||
|
||||
// Recent error information
|
||||
$sqlError = $db->sql_error();
|
||||
if (!empty($sqlError['message'])) {
|
||||
$context['last_error'] = $sqlError;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$context['error'] = 'Could not retrieve database context: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect comprehensive database information
|
||||
*/
|
||||
private function collectDatabaseInformation(): array
|
||||
{
|
||||
$info = [
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'CLI',
|
||||
'user_ip' => $_SERVER['REMOTE_ADDR'] ?? 'Unknown',
|
||||
];
|
||||
|
||||
try {
|
||||
// Get information from all database servers
|
||||
if (class_exists('\TorrentPier\Database\DatabaseFactory')) {
|
||||
$serverNames = \TorrentPier\Database\DatabaseFactory::getServerNames();
|
||||
|
||||
foreach ($serverNames as $serverName) {
|
||||
try {
|
||||
$db = \TorrentPier\Database\DatabaseFactory::getInstance($serverName);
|
||||
|
||||
$serverInfo = [
|
||||
'server_name' => $serverName,
|
||||
'engine' => $db->engine ?? 'Unknown',
|
||||
'host' => $db->db_server ?? 'Unknown',
|
||||
'database' => $db->selected_db ?? 'Unknown',
|
||||
'connection_status' => $db->connection ? 'Connected' : 'Disconnected',
|
||||
'total_queries' => $db->num_queries ?? 0,
|
||||
'total_time' => isset($db->sql_timetotal) ? sprintf('%.3f sec', $db->sql_timetotal) : 'Unknown',
|
||||
];
|
||||
|
||||
// Current query
|
||||
if (!empty($db->cur_query)) {
|
||||
$serverInfo['current_query'] = $this->formatQueryForDisplay($db->cur_query);
|
||||
}
|
||||
|
||||
// Last error
|
||||
$sqlError = $db->sql_error();
|
||||
if (!empty($sqlError['message'])) {
|
||||
$serverInfo['last_error'] = $sqlError;
|
||||
}
|
||||
|
||||
// Recent query history (if available and enabled)
|
||||
if ($this->includeQueryHistory && !empty($db->dbg)) {
|
||||
$recentQueries = array_slice($db->dbg, -$this->maxQueryHistory);
|
||||
$serverInfo['recent_queries'] = [];
|
||||
|
||||
foreach ($recentQueries as $query) {
|
||||
$serverInfo['recent_queries'][] = [
|
||||
'sql' => $this->formatQueryForDisplay($query['sql'] ?? 'Unknown'),
|
||||
'time' => isset($query['time']) ? sprintf('%.3f sec', $query['time']) : 'Unknown',
|
||||
'source' => $query['src'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$info['databases'][$serverName] = $serverInfo;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$info['databases'][$serverName] = [
|
||||
'error' => 'Could not retrieve info: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy single database support
|
||||
if (function_exists('DB') && empty($info['databases'])) {
|
||||
$db = DB();
|
||||
|
||||
$info['legacy_database'] = [
|
||||
'engine' => $db->engine ?? 'Unknown',
|
||||
'host' => $db->db_server ?? 'Unknown',
|
||||
'database' => $db->selected_db ?? 'Unknown',
|
||||
'connection_status' => $db->connection ? 'Connected' : 'Disconnected',
|
||||
'total_queries' => $db->num_queries ?? 0,
|
||||
'total_time' => isset($db->sql_timetotal) ? sprintf('%.3f sec', $db->sql_timetotal) : 'Unknown',
|
||||
];
|
||||
|
||||
if (!empty($db->cur_query)) {
|
||||
$info['legacy_database']['current_query'] = $this->formatQueryForDisplay($db->cur_query);
|
||||
}
|
||||
|
||||
$sqlError = $db->sql_error();
|
||||
if (!empty($sqlError['message'])) {
|
||||
$info['legacy_database']['last_error'] = $sqlError;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$info['collection_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format SQL query for readable display
|
||||
*/
|
||||
private function formatQueryForDisplay(string $query, int $maxLength = 500): string
|
||||
{
|
||||
// Remove comments at the start (debug info)
|
||||
$query = preg_replace('#^/\*.*?\*/#', '', $query);
|
||||
$query = trim($query);
|
||||
|
||||
// Truncate if too long
|
||||
if (strlen($query) > $maxLength) {
|
||||
$query = substr($query, 0, $maxLength) . '... [truncated]';
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority - run after the main PrettyPageHandler
|
||||
*/
|
||||
public function contentType(): ?string
|
||||
{
|
||||
return 'text/html';
|
||||
}
|
||||
}
|
269
src/Whoops/EnhancedPrettyPageHandler.php
Normal file
269
src/Whoops/EnhancedPrettyPageHandler.php
Normal file
|
@ -0,0 +1,269 @@
|
|||
<?php
|
||||
/**
|
||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||
*/
|
||||
|
||||
namespace TorrentPier\Whoops;
|
||||
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
|
||||
/**
|
||||
* Enhanced PrettyPageHandler for TorrentPier
|
||||
*
|
||||
* Extends Whoops' default handler to include database query information
|
||||
* and other TorrentPier-specific debugging details in the error output.
|
||||
*/
|
||||
class EnhancedPrettyPageHandler extends PrettyPageHandler
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Add TorrentPier-specific database information
|
||||
// Note: We add these during handle() to ensure they're fresh and available
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive database information
|
||||
*/
|
||||
private function getDatabaseInformation(): array
|
||||
{
|
||||
$info = [];
|
||||
|
||||
try {
|
||||
// Get main database instance information
|
||||
if (function_exists('DB')) {
|
||||
$db = DB();
|
||||
|
||||
$info['Connection Status'] = $db->connection ? 'Connected' : 'Disconnected';
|
||||
$info['Database Server'] = $db->db_server ?? 'Unknown';
|
||||
$info['Selected Database'] = $db->selected_db ?? 'Unknown';
|
||||
$info['Database Engine'] = $db->engine ?? 'Unknown';
|
||||
$info['Total Queries'] = $db->num_queries ?? 0;
|
||||
|
||||
if (isset($db->sql_timetotal)) {
|
||||
$info['Total Query Time'] = sprintf('%.3f seconds', $db->sql_timetotal);
|
||||
}
|
||||
|
||||
// Current/Last executed query
|
||||
if (!empty($db->cur_query)) {
|
||||
$info['Current Query'] = $this->formatSqlQuery($db->cur_query);
|
||||
}
|
||||
|
||||
// Database error information
|
||||
$sqlError = $db->sql_error();
|
||||
if (!empty($sqlError['message'])) {
|
||||
$info['Last Database Error'] = [
|
||||
'Code' => $sqlError['code'] ?? 'Unknown',
|
||||
'Message' => $sqlError['message'],
|
||||
];
|
||||
}
|
||||
|
||||
// Connection details if available
|
||||
if ($db->connection) {
|
||||
try {
|
||||
$pdo = $db->connection->getPdo();
|
||||
if ($pdo) {
|
||||
$info['PDO Driver'] = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) ?? 'Unknown';
|
||||
$info['Server Version'] = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION) ?? 'Unknown';
|
||||
|
||||
// Current PDO error state
|
||||
$errorCode = $pdo->errorCode();
|
||||
if ($errorCode && $errorCode !== '00000') {
|
||||
$errorInfo = $pdo->errorInfo();
|
||||
$info['PDO Error State'] = [
|
||||
'Code' => $errorCode,
|
||||
'Info' => $errorInfo[2] ?? 'Unknown'
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$info['PDO Error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get information from all database servers (new system)
|
||||
if (class_exists('\TorrentPier\Database\DatabaseFactory')) {
|
||||
try {
|
||||
$serverNames = \TorrentPier\Database\DatabaseFactory::getServerNames();
|
||||
|
||||
if (count($serverNames) > 1) {
|
||||
foreach ($serverNames as $serverName) {
|
||||
try {
|
||||
$db = \TorrentPier\Database\DatabaseFactory::getInstance($serverName);
|
||||
$info["Server: $serverName"] = [
|
||||
'Host' => $db->db_server ?? 'Unknown',
|
||||
'Database' => $db->selected_db ?? 'Unknown',
|
||||
'Queries' => $db->num_queries ?? 0,
|
||||
'Connected' => $db->connection ? 'Yes' : 'No',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$info["Server: $serverName"] = ['Error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$info['Multi-Server Error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$info['Collection Error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent SQL queries from debug log
|
||||
*/
|
||||
private function getRecentSqlQueries(): array
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
try {
|
||||
if (function_exists('DB')) {
|
||||
$db = DB();
|
||||
|
||||
// Check if debug information is available
|
||||
if (!empty($db->dbg) && is_array($db->dbg)) {
|
||||
// Get last 5 queries
|
||||
$recentQueries = array_slice($db->dbg, -5);
|
||||
|
||||
foreach ($recentQueries as $index => $queryInfo) {
|
||||
$queryNum = $index + 1;
|
||||
$queries["Query #$queryNum"] = [
|
||||
'SQL' => $this->formatSqlQuery($queryInfo['sql'] ?? 'Unknown'),
|
||||
'Time' => isset($queryInfo['time']) ? sprintf('%.3f sec', $queryInfo['time']) : 'Unknown',
|
||||
'Source' => $queryInfo['src'] ?? 'Unknown',
|
||||
'Info' => $queryInfo['info'] ?? '',
|
||||
];
|
||||
|
||||
// Add memory info if available
|
||||
if (isset($queryInfo['mem_before'], $queryInfo['mem_after'])) {
|
||||
$memUsed = $queryInfo['mem_after'] - $queryInfo['mem_before'];
|
||||
$queries["Query #$queryNum"]['Memory'] = sprintf('%+d bytes', $memUsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($queries)) {
|
||||
$queries['Info'] = 'No query debug information available. Enable debug mode to see recent queries.';
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$queries['Error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TorrentPier environment information
|
||||
*/
|
||||
private function getTorrentPierEnvironment(): array
|
||||
{
|
||||
$env = [];
|
||||
|
||||
try {
|
||||
// Basic environment
|
||||
$env['Application Environment'] = defined('APP_ENV') ? APP_ENV : 'Unknown';
|
||||
$env['Debug Mode'] = defined('DBG_USER') && DBG_USER ? 'Enabled' : 'Disabled';
|
||||
$env['SQL Debug'] = defined('SQL_DEBUG') && SQL_DEBUG ? 'Enabled' : 'Disabled';
|
||||
|
||||
// Configuration status
|
||||
if (function_exists('config')) {
|
||||
$config = config();
|
||||
$env['Config Loaded'] = 'Yes';
|
||||
$env['TorrentPier Version'] = $config->get('tp_version', 'Unknown');
|
||||
$env['Board Title'] = $config->get('sitename', 'Unknown');
|
||||
} else {
|
||||
$env['Config Loaded'] = 'No';
|
||||
}
|
||||
|
||||
// Cache system
|
||||
if (function_exists('CACHE')) {
|
||||
$env['Cache System'] = 'Available';
|
||||
}
|
||||
|
||||
// Language system
|
||||
if (function_exists('lang')) {
|
||||
$env['Language System'] = 'Available';
|
||||
if (isset(lang()->getCurrentLanguage)) {
|
||||
$env['Current Language'] = lang()->getCurrentLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
// Memory and timing
|
||||
if (defined('TIMESTART')) {
|
||||
$env['Execution Time'] = sprintf('%.3f sec', microtime(true) - TIMESTART);
|
||||
}
|
||||
|
||||
if (function_exists('sys')) {
|
||||
// Use plain text formatting for memory values (no HTML entities)
|
||||
$env['Peak Memory'] = str_replace(' ', ' ', humn_size(sys('mem_peak')));
|
||||
$env['Current Memory'] = str_replace(' ', ' ', humn_size(sys('mem')));
|
||||
}
|
||||
|
||||
// Request information
|
||||
$env['Request Method'] = $_SERVER['REQUEST_METHOD'] ?? 'Unknown';
|
||||
$env['Request URI'] = $_SERVER['REQUEST_URI'] ?? 'CLI';
|
||||
$env['User Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
|
||||
$env['Remote IP'] = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$env['Error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format SQL query for display
|
||||
*/
|
||||
private function formatSqlQuery(string $query): string
|
||||
{
|
||||
// Remove debug comments
|
||||
$query = preg_replace('#^/\*.*?\*/#', '', $query);
|
||||
$query = trim($query);
|
||||
|
||||
// Truncate very long queries but keep them readable
|
||||
if (strlen($query) > 1000) {
|
||||
return substr($query, 0, 1000) . "\n... [Query truncated - " . (strlen($query) - 1000) . " more characters]";
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parent method to add database info and custom styling
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Add TorrentPier-specific database information dynamically
|
||||
try {
|
||||
$this->addDataTable('Database Information', $this->getDatabaseInformation());
|
||||
} catch (\Exception $e) {
|
||||
$this->addDataTable('Database Information', ['Error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->addDataTable('Recent SQL Queries', $this->getRecentSqlQueries());
|
||||
} catch (\Exception $e) {
|
||||
$this->addDataTable('Recent SQL Queries', ['Error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->addDataTable('TorrentPier Environment', $this->getTorrentPierEnvironment());
|
||||
} catch (\Exception $e) {
|
||||
$this->addDataTable('TorrentPier Environment', ['Error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return parent::handle();
|
||||
}
|
||||
}
|
131
src/Whoops/README.md
Normal file
131
src/Whoops/README.md
Normal file
|
@ -0,0 +1,131 @@
|
|||
# TorrentPier Whoops Enhanced Error Reporting
|
||||
|
||||
This directory contains enhanced Whoops error handlers specifically designed for TorrentPier to provide better debugging information when database errors occur.
|
||||
|
||||
## Features
|
||||
|
||||
### Enhanced Database Error Reporting
|
||||
|
||||
The enhanced Whoops handlers provide comprehensive database information when errors occur:
|
||||
|
||||
1. **Current SQL Query** - Shows the exact query that caused the error
|
||||
2. **Recent Query History** - Displays the last 5 SQL queries executed
|
||||
3. **Database Connection Status** - Connection state, server info, database name
|
||||
4. **Error Context** - PDO error codes, exception details, source location
|
||||
5. **TorrentPier Environment** - Debug mode status, system information
|
||||
|
||||
### Components
|
||||
|
||||
#### EnhancedPrettyPageHandler
|
||||
|
||||
Extends Whoops' default `PrettyPageHandler` to include:
|
||||
- **Database Information** table with connection details and current query
|
||||
- **Recent SQL Queries** table showing query history with timing
|
||||
- **TorrentPier Environment** table with system status
|
||||
|
||||
#### DatabaseErrorHandler
|
||||
|
||||
Specialized handler that:
|
||||
- Adds database context to exception stack frames
|
||||
- Identifies database-related code in the call stack
|
||||
- Collects comprehensive database state information
|
||||
- Formats SQL queries for readable display
|
||||
|
||||
## Usage
|
||||
|
||||
The enhanced handlers are automatically activated when `DBG_USER` is enabled in TorrentPier configuration.
|
||||
|
||||
### Automatic Integration
|
||||
|
||||
```php
|
||||
// In src/Dev.php - automatically configured
|
||||
$prettyPageHandler = new \TorrentPier\Whoops\EnhancedPrettyPageHandler();
|
||||
```
|
||||
|
||||
### Database Error Logging
|
||||
|
||||
Database errors are now automatically logged even when they occur in Nette Database layer:
|
||||
|
||||
```php
|
||||
// Enhanced error handling in Database.php
|
||||
try {
|
||||
$row = $result->fetch();
|
||||
} catch (\Exception $e) {
|
||||
// Log the error including the query that caused it
|
||||
$this->debugger->log_error($e);
|
||||
throw $e; // Re-throw for Whoops display
|
||||
}
|
||||
```
|
||||
|
||||
## Error Information Displayed
|
||||
|
||||
When a database error occurs, Whoops will now show:
|
||||
|
||||
### Database Information
|
||||
- Connection Status: Connected/Disconnected
|
||||
- Database Server: Host and port information
|
||||
- Selected Database: Current database name
|
||||
- Database Engine: MySQL/PostgreSQL/etc.
|
||||
- Total Queries: Number of queries executed
|
||||
- Total Query Time: Cumulative execution time
|
||||
- Current Query: The SQL that caused the error
|
||||
- Last Database Error: Error code and message
|
||||
- PDO Driver: Database driver information
|
||||
- Server Version: Database server version
|
||||
|
||||
### Recent SQL Queries
|
||||
- **Query #1-5**: Last 5 queries executed
|
||||
- SQL: Formatted query text
|
||||
- Time: Execution time in seconds
|
||||
- Source: File and line where query originated
|
||||
- Info: Additional query information
|
||||
- Memory: Memory usage if available
|
||||
|
||||
### TorrentPier Environment
|
||||
- Application Environment: local/production/etc.
|
||||
- Debug Mode: Enabled/Disabled
|
||||
- SQL Debug: Enabled/Disabled
|
||||
- TorrentPier Version: Current version
|
||||
- Config Loaded: Configuration status
|
||||
- Cache System: Availability status
|
||||
- Language System: Status and encoding
|
||||
- Template System: Twig-based availability
|
||||
- Execution Time: Request processing time
|
||||
- Peak Memory: Maximum memory used
|
||||
- Current Memory: Current memory usage
|
||||
- Request Method: GET/POST/etc.
|
||||
- Request URI: Current page
|
||||
- User Agent: Browser information
|
||||
- Remote IP: Client IP address
|
||||
|
||||
## Configuration
|
||||
|
||||
The enhanced handlers respect TorrentPier's debug configuration:
|
||||
|
||||
- `DBG_USER`: Must be enabled to show enhanced error pages
|
||||
- `SQL_DEBUG`: Enables SQL query logging and timing
|
||||
- `APP_ENV`: Determines environment-specific features
|
||||
|
||||
## Logging
|
||||
|
||||
Database errors are now logged in multiple locations:
|
||||
|
||||
1. **PHP Error Log**: Basic error message
|
||||
2. **TorrentPier bb_log**: Detailed error with context (`database_errors.log`)
|
||||
3. **Whoops Log**: Complete error details (`php_whoops.log`)
|
||||
|
||||
## Security
|
||||
|
||||
The enhanced handlers maintain security by:
|
||||
- Only showing detailed information when `DBG_USER` is enabled
|
||||
- Using Whoops' blacklist for sensitive data
|
||||
- Logging detailed information to files (not user-accessible)
|
||||
- Providing generic error messages to non-debug users
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
All enhancements are:
|
||||
- **100% backward compatible** with existing TorrentPier code
|
||||
- **Non-breaking** - existing error handling continues to work
|
||||
- **Optional** - only activated in debug mode
|
||||
- **Safe** - no security implications for production use
|
Loading…
Add table
Add a link
Reference in a new issue