getMessage(); $error_file = is_null($exception) ? $error_file : $exception->getFile(); $error_line = is_null($exception) ? (int) $error_line : $exception->getLine(); // Create a log entry for viewing in staffcp self::logError('fatal', $error_file . '(' . $error_line . '): ' . $error_string); // If this is an API request, print the error in plaintext and dont render the whole error trace page if (self::shouldUsePlainText()) { if (!Debugging::canViewDetailedError()) { // If we can't view the full error (i.e. not authenticated), show a simple message die('Fatal error during request'); } die($error_string . ' in ' . $error_file . ' on line ' . $error_line . (!is_null($exception) ? PHP_EOL . $exception->getTraceAsString() : '')); } if (Debugging::canViewDetailedError()) { $frames = []; // Most recent frame is not included in getTrace(), so deal with it individually $frames[] = self::parseFrame($exception, $error_file, $error_line); $skip_frames = 0; // Loop all frames in the exception trace & get relevent information if ($exception != null) { $i = count($exception->getTrace()); foreach ($exception->getTrace() as $frame) { // Check if previous frame had same file and line number (ie: DB->query(...) reports same file and line twice in a row) if (end($frames)['file'] == $frame['file'] && end($frames)['line'] == $frame['line']) { $skip_frames++; continue; } // Skip frame if it is a closure // @phpstan-ignore-next-line (it does not know that $frame['function'] is valid) if (isset($frame['function']) && $frame['function'] === '{closure}') { $skip_frames++; continue; } $frames[] = self::parseFrame($exception, $frame['file'], $frame['line'], $i); $i--; } } $sql_frames = QueryRecorder::getInstance()->getSqlStack(); } if (defined('LANGUAGE')) { $language = new Language('core', LANGUAGE); } else { // NamelessMC not installed yet $language = new Language('core', 'en_UK'); } $path = (defined('CONFIG_PATH') ? CONFIG_PATH : '') . '/core/assets/'; $site_name = defined('SITE_NAME') ? Output::getClean(SITE_NAME) : 'NamelessMC'; $smarty = new Smarty(); $smarty->setCompileDir(ROOT_PATH . '/cache/templates_c'); $smarty->assign([ 'LANG' => defined('HTML_LANG') ? HTML_LANG : 'en', 'RTL' => defined('HTML_RTL') && HTML_RTL === true ? ' dir="rtl"' : '', 'TITLE' => $language->get('errors', 'fatal_error') . ' - ' . $site_name, 'SITE_NAME' => $site_name, 'FOMANTIC_JS' => $path . 'vendor/fomantic-ui/dist/semantic.min.js', 'FOMANTIC_CSS' => $path . 'vendor/fomantic-ui/dist/semantic.min.css', 'FONT_AWESOME' => $path . 'vendor/@fortawesome/fontawesome-free/css/all.min.css', 'JQUERY' => $path . 'vendor/jquery/dist/jquery.min.js', 'PRISM_CSS' => $path . 'plugins/prism/prism_light_atom.css', 'PRISM_JS' => $path . 'plugins/prism/prism.js', 'DETAILED_ERROR' => Debugging::canViewDetailedError(), 'LOGO' => $path . 'img/namelessmc_logo.png', 'FATAL_ERROR_TITLE' => $language->get('errors', 'fatal_error_title'), 'FATAL_ERROR_MESSAGE_ADMIN' => $language->get('errors', 'fatal_error_message_admin'), 'FATAL_ERROR_MESSAGE_USER' => $language->get('errors', 'fatal_error_message_user'), 'ERROR_TYPE' => is_null($exception) ? $language->get('general', 'error') : (new ReflectionClass($exception))->getName(), 'ERROR_STRING' => Output::getClean($error_string), 'ERROR_FILE' => $error_file, 'CANCEL' => $language->get('general', 'cancel'), 'CAN_GENERATE_DEBUG' => Debugging::canGenerateDebugLink(), 'DEBUG_LINK' => $language->get('admin', 'debug_link'), 'DEBUG_LINK_INFO' => $language->get('admin', 'debug_link_info'), 'DEBUG_LINK_URL' => URL::build('/queries/debug_link'), 'ERROR_SQL_STACK' => $sql_frames ?? [], 'CURRENT_URL' => HttpUtils::getProtocol() . '://' . HttpUtils::getHeader('Host') . $_SERVER['REQUEST_URI'], 'FRAMES' => $frames ?? [], 'SKIP_FRAMES' => $skip_frames ?? 0, 'BACK' => $language->get('general', 'back'), 'HOME' => $language->get('general', 'home'), 'HOME_URL' => URL::build('/'), 'GENERATE' => $language->get('general', 'generate'), 'GENERATE_DEBUG_LINK' => $language->get('general', 'generate_debug_link'), 'CANNOT_READ_FILE' => $language->get('general', 'cannot_read_file'), 'FRAME' => $language->get('general', 'frame'), 'SQL_QUERY' => $language->get('general', 'sql_query'), 'NAMELESSMC_SUPPORT' => $language->get('general', 'namelessmc_support'), 'NAMELESSMC_DOCS' => $language->get('general', 'namelessmc_documentation'), 'DEBUG_TOAST_CLICK' => $language->get('admin', 'debug_link_toast', [ 'linkStart' => '', 'linkEnd' => '', ]), 'DEBUG_CANNOT_GENERATE' => $language->get('general', 'debug_link_cannot_generate'), 'DEBUG_COPIED' => $language->get('general', 'debug_link_copied'), ]); $smarty->display(ROOT_PATH . '/core/includes/error.tpl'); die; } /** * For API requests, query requests and AJAX requests, return a plain text error message. * * @return bool Whether the error page should be in plain text rather than a user friendly HTML page. */ private static function shouldUsePlainText(): bool { $route = $_REQUEST['route'] ?? ''; return str_contains($route, '/api/v2/') || str_contains($route, '/queries/') || isset($_SERVER['HTTP_X_REQUESTED_WITH']); } /** * Write error to specific log file. * * @param string $type Which category/file to log this to. Must be: `warning`, `notice`, `other` or `fatal`. * @param string $contents The message to be saved. */ private static function logError(string $type, string $contents): void { $dir_exists = false; try { if (!is_dir(implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'cache', 'logs']))) { if (is_writable(ROOT_PATH . DIRECTORY_SEPARATOR . 'cache')) { mkdir(ROOT_PATH . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'logs'); $dir_exists = true; } } else { $dir_exists = true; } if ($dir_exists) { file_put_contents(implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'cache', 'logs', $type . '-log.log']), '[' . date('Y-m-d, H:i:s') . '] ' . $contents . PHP_EOL, FILE_APPEND); } } catch (Exception $ignored) { } } /** * Returns frame array from specified information. * Leaving number as null will use Exception trace count + 1 (for most recent frame). * * @param Throwable|null $exception Exception object caught and whose trace to count. If null, $number will be used for frame number. * @param string $frame_file Path to file which was referenced in this frame. * @param int $frame_line Line number of $frame_file which Exception was thrown at. * @param int|null $number Higher number = more recent frame. If null, will use $exception trace count + 1. * @return array This frame in an array form. */ public static function parseFrame(?Throwable $exception, string $frame_file, int $frame_line, ?int $number = null): array { $lines = file($frame_file); return [ 'number' => is_null($number) ? (is_null($exception) ? 1 : count($exception->getTrace()) + 1) : $number, 'file' => $frame_file, 'line' => $frame_line, 'start_line' => (is_array($lines) && count($lines) >= self::LINE_BUFFER && ($frame_line - self::LINE_BUFFER > 0)) ? ($frame_line - self::LINE_BUFFER) : 1, 'highlight_line' => (is_array($lines) && count($lines) >= self::LINE_BUFFER && $frame_line - self::LINE_BUFFER > 0) ? (self::LINE_BUFFER + 1) : $frame_line, 'code' => self::parseFile($lines, $frame_line), ]; } /** * Create purified and truncated string from a file for use with error source code preview. * * @param array|bool $lines Array of lines in this file. If false, will return nothing (means PHP cannot access file). * @param int $error_line Line to center output around. * @return string Truncated string from this file. */ private static function parseFile($lines, int $error_line): string { if ($lines == false || count($lines) < 1) { return ''; } $line_num = 1; $return = ''; foreach ($lines as $line) { if (($error_line - self::LINE_BUFFER) <= $line_num && $line_num <= ($error_line + self::LINE_BUFFER)) { $return .= Output::getClean($line); } $line_num++; } return $return; } /** * Called at end of every execution on page load. * If an error exists, and the type is fatal, pass execution to catchException(). */ public static function catchShutdownError(): void { $error = error_get_last(); if ($error === null) { return; } if ($error['type'] === E_ERROR) { self::catchException(null, $error['message'], $error['file'], $error['line']); } } /** * Log a custom error, uses `other` type. * Not used internally, only for modules to use. * * @param string $contents Error to write to file. */ public static function logCustomError(string $contents): void { self::logError('other', $contents); } /** * Write a message to the 'warning' log. * * @param string $contents Warning to write to file. */ public static function logWarning(string $contents): void { self::logError('warning', $contents); } }