Copyright (c) 2001-2019 BohwaZ All rights reserved. KD2FW is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Foobar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Foobar. If not, see . */ namespace KD2; /** * Simple error and exception handler * * When enabled (with ErrorManager::enable(ErrorManager::DEVELOPMENT)) it will * catch any error, warning or exception and display it along with useful debug * information. If enabled it will also log the errors to a file and/or send * every error by email. * * In production mode no details are given, but a unique reference to the log * or email is displayed. * * This is similar in a way to http://tracy.nette.org/ * * @author bohwaz */ class ErrorManager { /** * Prod/dev modes */ const PRODUCTION = 1; const DEVELOPMENT = 2; const CLI_DEVELOPMENT = 4; /** * Term colors */ const RED = '[1;41m'; const RED_FAINT = '[1m'; const YELLOW = '[33m'; /** * true = catch exceptions, false = do nothing * @var null */ static protected $enabled = null; /** * HTML template used for displaying production errors * @var string */ static protected $production_error_template = 'Internal server error

Server error

Sorry but the server encountered an internal error and was unable to complete your request. Please try again later.

The webmaster has been noticed and this will be fixed ASAP.

Error reference: {$ref}

← Go back to the homepage

'; /** * E-Mail address where to send errors * @var boolean */ static protected $email_errors = false; /** * Reporting URL */ static protected $report_url = null; /** * Reporting automatically? */ static protected $report_auto = true; /** * Custom context */ static protected $context = []; /** * Custom exception handlers * @var array */ static protected $custom_handlers = []; /** * Does the terminal support ANSI colors * @var boolean */ static protected $term_color = false; /** * Will be set to true when catching an exception to avoid double catching * with the shutdown function * @var boolean */ static protected $catching = false; /** * Used to store timers and memory consumption * @var array */ static protected $run_trace = []; /** * Handles PHP shutdown on fatal error to be able to catch the error * @return void */ static public function shutdownHandler() { // Stop here if disabled or if the script ended with an exception if (!self::$enabled || self::$catching) return false; $error = error_get_last(); if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], TRUE)) { // Don't exit at the end, as there might be other shutdown handlers // after this one self::exceptionHandler(new \ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']), false); } } /** * Internal error handler to throw them as exceptions * (private use) */ static public function errorHandler($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // Don't report this error (for example @unlink) return; } $types = [ E_ERROR => 'Fatal error', E_USER_ERROR => 'User error', E_RECOVERABLE_ERROR => 'Recoverable error', E_CORE_ERROR => 'Core error', E_COMPILE_ERROR => 'Compile error', E_PARSE => 'Parse error', E_WARNING => 'Warning', E_CORE_WARNING => 'Core warning', E_COMPILE_WARNING => 'Compile warning', E_USER_WARNING => 'User warning', E_NOTICE => 'Notice', E_USER_NOTICE => 'User notice', E_STRICT => 'Strict standards', E_DEPRECATED => 'Deprecated', E_USER_DEPRECATED => 'User deprecated', ]; $type = array_key_exists($severity, $types) ? $types[$severity] : 'Unknown error'; $message = $type . ': ' . $message; // Catch ASSERT_BAIL errors differently because throwing an exception // in this case results in an execution shutdown, and shutdown handler // isn't even called. See https://bugs.php.net/bug.php?id=53619 if (assert_options(ASSERT_ACTIVE) && assert_options(ASSERT_BAIL) && substr($message, 0, 18) == 'Warning: assert():') { $message .= ' (ASSERT_BAIL detected)'; self::exceptionHandler(new \ErrorException($message, 0, $severity, $file, $line)); return true; } throw new \ErrorException($message, 0, $severity, $file, $line); return true; } /** * Main exception handler * @param object $e Exception or Error (PHP 7) object * @param boolean $exit Exit the script at the end * @return void */ static public function exceptionHandler($e, $exit = true) { self::$catching = true; try { self::reportException($e, $exit); } catch (\Throwable $e2) { echo $e2; echo PHP_EOL . PHP_EOL . $e; exit(1); } catch (\Exception $e2) { echo $e2; echo PHP_EOL . PHP_EOL . $e; exit(1); } return true; } /** * Main exception handler * @param object $e Exception or Error (PHP 7) object * @param boolean $exit Exit the script at the end * @return void */ static public function reportException($e, $exit) { foreach (self::$custom_handlers as $class=>$callback) { if ($e instanceOf $class) { call_user_func($callback, $e); $e = false; break; } } if ($e === false) { if ($exit) { exit(1); } else { return; } } extract(self::buildExceptionReport($e, false)); unset($e); // Log exception to file if (ini_get('log_errors')) { error_log($log); } // Disable any output if it was buffering if (ob_get_level()) { ob_end_clean(); } $is_curl = 0 === strpos($_SERVER['HTTP_USER_AGENT'] ?? '', 'curl/'); $is_cli = PHP_SAPI == 'cli'; if (!$is_cli) { http_response_code(500); } if ($is_curl && !headers_sent()) { header('Content-Type: text/plain; charset=utf-8', true); } if (($is_cli || $is_curl) && (self::$enabled & self::DEVELOPMENT || self::$enabled & self::CLI_DEVELOPMENT)) { foreach ($report->errors as $e) { self::termPrint(sprintf(' /!\\ %s ', $e->type), self::RED); self::termPrint($e->message, self::RED_FAINT); if (isset($e->line)) { self::termPrint(sprintf('Line %d in %s', $e->line, $e->file), self:: YELLOW); } // Ignore the error stack belonging to ErrorManager foreach ($e->backtrace as $i=>$t) { $file = !empty($t->file) ? $t->file : '[internal function]'; $line = !empty($t->line) ? '(' . $t->line . ')' : ''; if (isset($t->args)) { $args = $t->args; foreach ($args as &$arg) { if (strlen($arg) > 20) { $arg = substr($arg, 0, 19) . '…'; } } unset($arg); self::termPrint(sprintf('#%d %s%s: %s(%s)', $i, $file, $line, $t->function, implode(', ', $args))); } else { self::termPrint(sprintf('#%d %s%s', $i, $file, $line)); } } } } else if (($is_cli || $is_curl) && self::$enabled & self::PRODUCTION) { self::termPrint(' /!\\ An internal server error occurred ', self::RED); self::termPrint(' Error reference was: ' . $report->context->id, self::YELLOW); } else if (self::$enabled & self::PRODUCTION) { @header_remove('Content-Disposition'); @header('Content-Type: text/html; charset=utf-8', true); self::htmlProduction($report); } else { if (!headers_sent()) { header_remove(); header('Content-Type: text/html; charset=UTF-8', true); header('HTTP/1.1 500 Internal Server Error', true); } echo $html_report; } // Log exception to email if (self::$email_errors) { self::sendEmail($title, $report, $log, $html_report); } // Send report to URL if (self::$report_auto && self::$report_url) { self::sendReport($report, self::$report_url); } if ($exit) { exit(1); } } static public function reportExceptionSilent(\Throwable $e): void { extract(self::buildExceptionReport($e)); // Log exception to file if (ini_get('log_errors')) { error_log($log); } if (self::$email_errors) { self::sendEmail($title, $report, $log, $html_report); } } static protected function sendEmail(string $title, \stdClass $report, string $log, string $html): void { // From: sender $from = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : basename($report->context->root_directory ?? __FILE__); $msgid = $report->context->id . '@' . $from; $boundary = sprintf('-----=%s', md5(uniqid(rand()))); $header = sprintf("MIME-Version: 1.0\r\nFrom: \"%s\" <%s>\r\nIn-Reply-To: <%s>\r\nMessage-Id: <%s>\r\n", $from, self::$email_errors, $msgid, $msgid); $header.= sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n", $boundary); $header.= "\r\n"; $msg = "This message contains multiple MIME parts.\r\n\r\n"; $msg.= sprintf("--%s\r\n", $boundary); $msg.= "Content-Type: text/plain; charset=\"utf-8\"\r\n"; $msg.= "Content-Transfer-Encoding: 8bit\r\n\r\n"; $msg.= $log . "\r\n\r\n"; $msg.= sprintf("--%s\r\n", $boundary); $msg.= "Content-Type: text/html; charset=\"utf-8\"\r\n"; $msg.= "Content-Transfer-Encoding: 8bit\r\n\r\n"; $msg.= $html . "\r\n\r\n"; $msg.= sprintf("--%s--", $boundary); mail(self::$email_errors, sprintf('Error #%s: %s', $report->context->id, $title), $msg, $header); } /** * Prints a line to STDERR, eventually using a color */ static public function termPrint($message, $color = null) { if (!defined('\STDERR')) { echo $message . PHP_EOL; return; } if ($color && self::$term_color) { $message = chr(27) . $color . $message . chr(27) . "[0m"; } fwrite(\STDERR, $message . PHP_EOL); } /** * Return file location without the document root */ static protected function getFileLocation($file) { if (!empty(self::$context['root_directory']) && ($pos = strpos($file, self::$context['root_directory'])) === 0) { return '...' . substr($file, strlen(self::$context['root_directory'])); } return $file; } static public function buildExceptionReport(\Throwable $e, bool $force_html = false): array { $report = self::makeReport($e); $log = sprintf('=========== Error ref. %s ===========', $report->context->id) . PHP_EOL . PHP_EOL . (string) $e . PHP_EOL . PHP_EOL . '' . PHP_EOL . json_encode($report, \JSON_PRETTY_PRINT) . PHP_EOL . '' . PHP_EOL; $html_report = null; if ($force_html || self::$enabled & self::DEVELOPMENT || self::$email_errors) { $html_report = self::htmlReport($report); } $title = $e->getMessage(); return compact('report', 'log', 'html_report', 'title'); } /** * Generates a report from an exception */ static public function makeReport($e) { $report = (object) [ 'errors' => [], ]; while ($e !== null) { $class = get_class($e); $error = (object) [ 'message' => $e->getMessage(), 'errorCode' => $e->getCode(), 'type' => in_array($class, ['ErrorException', 'Error']) ? 'PHP error' : $class, 'backtrace' => [ (object) [ 'file' => $e->getFile() ? self::getFileLocation($e->getFile()) : null, 'line' => $e->getLine(), 'code' => $e->getFile() && $e->getLine() ? self::getSource($e->getFile(), $e->getLine()) : null, ], ], ]; foreach ($e->getTrace() as $i=>$t) { // Ignore the error stack from ErrorManager if (isset($t['class']) && $t['class'] === __CLASS__ && ($t['function'] === 'shutdownHandler' || $t['function'] === 'errorHandler')) { continue; } $args = []; // Display call arguments if (!empty($t['args'])) { // Find arguments variables names via reflection try { if (isset($t['class'])) { $r = new \ReflectionMethod($t['class'], $t['function']); } else { $r = new \ReflectionFunction($t['function']); } $params = $r->getParameters(); } catch (\Exception $_ignore) { $params = []; } foreach ($t['args'] as $name => $value) { if (array_key_exists($name, $params)) { $name = '$' . $params[$name]->name; } if (is_string($value)) { $value = self::getFileLocation($value); } $args[$name] = self::dump($value); if (strlen($args[$name]) > 2000) { $args[$name] = substr($args[$name], 0, 1999) . '…'; } } } $trace = (object) [ // Add class name to function 'function' => isset($t['class']) ? $t['class'] . $t['type'] . $t['function'] : $t['function'], ]; if (isset($t['file'])) { $trace->file = self::getFileLocation($t['file']); } if (isset($t['line'])) { $trace->line = (int) $t['line']; } if (count($args)) { $trace->args = $args; } if (isset($trace->file) && isset($trace->line)) { $trace->code = self::getSource($t['file'], $t['line']); } $error->backtrace[] = $trace; } $report->errors[] = $error; $e = $e->getPrevious(); } unset($error, $e, $params, $t); $context = array_merge([ 'id' => base_convert(substr(sha1(json_encode($report->errors)), 0, 10), 16, 36), 'date' => date(DATE_ATOM), 'os' => PHP_OS, 'language' => 'PHP ' . PHP_VERSION, 'environment' => self::$enabled & self::DEVELOPMENT ? 'development' : 'production:' . self::$enabled, 'php_sapi' => PHP_SAPI, 'remote_ip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null, 'http_method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null, 'http_files' => self::dump($_FILES), 'http_post' => self::dump($_POST, true), 'duration' => isset(self::$context['request_started']) ? (microtime(true) - self::$context['request_started'])*1000 : null, 'memory_peak' => memory_get_peak_usage(true), 'memory_used' => memory_get_usage(true), ], self::$context); ksort($context); unset($context['request_started']); $report->context = (object) $context; if (!empty($_SERVER['HTTP_HOST']) && !empty($_SERVER['REQUEST_URI'])) { $proto = empty($_SERVER['HTTPS']) ? 'http' : 'https'; $report->context->url = $proto . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } return $report; } /** * Displays an exception as HTML debug page */ static public function htmlException(\stdClass $e): string { $out = sprintf('

%s

%s

', $e->type, nl2br(htmlspecialchars($e->message))); foreach ($e->backtrace as $i=>$t) { $out .= '
'; if (isset($t->file) && isset($t->line)) { $dir = dirname($t->file); $dir = $dir == '/' ? $dir : $dir . '/'; $out .= sprintf('

in %s%s:%d

', htmlspecialchars($dir), htmlspecialchars(basename($t->file)), $t->line); } if (isset($t->function)) { $out .= sprintf('

→ %s (%d arg.)

', htmlspecialchars($t->function), isset($t->args) ? count($t->args) : 0); // Display call arguments if (!empty($t->args)) { $out .= ''; foreach ($t->args as $name => $value) { $out .= sprintf('', htmlspecialchars($name), htmlspecialchars($value)); } $out .= '
%s
%s
'; } } // Display source code if (isset($t->code) && isset($t->line)) { $out .= self::htmlSource($t->code, $t->line); } $out .= '
'; } $out .= '
'; return $out; } static public function htmlSource(array $source, $line) { $out = ''; foreach ($source as $i => $code) { $html = '' . ($i) . '' . htmlspecialchars($code, ENT_QUOTES); if ($i == $line) { $html = '' . $html . ''; } $out .= $html . PHP_EOL; } return '
' . $out . '
'; } /** * Get source code * @param string $file File location * @param integer $line Line to highlight * @return array */ static public function getSource($file, $line) { $out = []; $start = max(0, $line - 5); if (!file_exists($file)) { return [$line => 'Source file not found']; } $file = new \SplFileObject($file); $file->seek($start); for ($i = $start + 1; $i < $start+10; $i++) { if ($file->eof()) { break; } $out[$i] = trim($file->current(), "\r\n"); $file->next(); } unset($file); return $out; } static public function htmlProduction(\stdClass $report) { if (!headers_sent()) { header_remove(); header('HTTP/1.1 500 Internal Server Error', true, 500); header('Content-Type: text/html; charset=UTF-8', true); } echo self::htmlTemplate(self::$production_error_template, $report); } static public function htmlTemplate($str, \stdClass $report) { $str = strtr($str, [ '{$ref}' => $report->context->id, '{$report_json}' => htmlspecialchars(base64_encode(json_encode($report)), ENT_QUOTES), '{$report_url}' => htmlspecialchars((string) self::$report_url), ]); $str = preg_replace_callback('!(.*?)!is', function ($match) { switch ($match[1]) { case 'sent': case 'email': return self::$email_errors || (self::$report_auto && self::$report_url) ? $match[2] : ''; case 'logged': case 'log': return ini_get('error_log') ? $match[2] : ''; case 'report': return (!self::$report_auto && self::$report_url) ? $match[2] : ''; } }, $str); return $str; } static public function htmlReport(\stdClass $report): string { $out = ''; // Display debug $out .= self::htmlTemplate(ini_get('error_prepend_string'), $report); foreach ($report->errors as $e) { $out .= self::htmlException($e); } $out .= '

Context

'; foreach ($report->context as $name => $value) { $out .= sprintf('', htmlspecialchars($name), htmlspecialchars($value ?? '')); } $out .= '
%s%s
'; $out .= self::htmlTemplate(ini_get('error_append_string'), $report); return $out; } /** * Enable error manager * @param integer $type Type of error management (ErrorManager::PRODUCTION or ErrorManager::DEVELOPMENT) * You can also use ErrorManager::PRODUCTION | ErrorManager::CLI_DEVELOPMENT to get error messages in CLI but still hide errors * on web front-end. * @return void */ static public function enable($type = self::DEVELOPMENT) { if (self::$enabled) return true; self::$context['request_started'] = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true); self::$enabled = $type; self::$term_color = function_exists('posix_isatty') && defined('\STDOUT') && @posix_isatty(\STDOUT); ini_set('display_errors', false); ini_set('log_errors', false); ini_set('html_errors', false); ini_set('zend.exception_ignore_args', false); // We want to get the args in exceptions (since PHP 7.4) error_reporting($type & self::DEVELOPMENT ? -1 : E_ALL & ~E_DEPRECATED & ~E_STRICT); if ($type & self::DEVELOPMENT && PHP_SAPI != 'cli') { self::setHtmlHeader('
 \__/
(xx)
//||\\\\
'); } register_shutdown_function([self::class, 'shutdownHandler']); set_exception_handler([__CLASS__, 'exceptionHandler']); set_error_handler([__CLASS__, 'errorHandler']); if ($type & self::DEVELOPMENT) { self::startTimer('_global'); } // Assign default context static $defaults = [ 'hostname' => 'SERVER_NAME', 'http_user_agent' => 'HTTP_USER_AGENT', 'http_referrer' => 'HTTP_REFERER', 'user_addr' => 'REMOTE_ADDR', 'server_addr' => 'SERVER_ADDR', 'root_directory' => 'DOCUMENT_ROOT', ]; foreach ($defaults as $a => $b) { if (isset($_SERVER[$b]) && !isset(self::$context[$a])) { self::$context[$a] = $_SERVER[$b]; } } } /** * Reset error management to PHP defaults * @return boolean */ static public function disable() { self::$enabled = false; ini_set('error_prepend_string', null); ini_set('error_append_string', null); ini_set('log_errors', false); ini_set('display_errors', false); ini_set('error_reporting', E_ALL & ~E_DEPRECATED & ~E_STRICT); restore_error_handler(); return restore_exception_handler(); } /** * Sets a microsecond timer to track time and memory usage * @param string $name Timer name */ static public function startTimer($name) { self::$run_trace[$name] = [microtime(true), memory_get_usage()]; } /** * Stops a timer and return time spent and memory used * @param string $name Timer name */ static public function stopTimer($name) { self::$run_trace[$name] = [ microtime(true) - self::$run_trace[$name][0], memory_get_usage() - self::$run_trace[$name][1], ]; return self::$run_trace[$name]; } /** * Sets a log file to record errors * @param string $file Error log file */ static public function setLogFile($file) { ini_set('log_errors', true); return ini_set('error_log', $file); } /** * Sets an email address that should receive the logs * Set to FALSE to disable email sending (default) * @param string $email Email address */ static public function setEmail($email) { self::$email_errors = $email; } /** * @deprecated */ static public function setExtraDebugEnv($env) { self::setContext($env); } /** * Set the report context * @param mixed $env Variable content, could be application version, or an array of information... */ static public function setContext(array $context) { self::$context = array_merge(self::$context, $context); } /** * Enable or disable reporting of errors to a remote URL * @param null|string $url Reporting URL * @param boolean $auto Automatic reporting? If not users will be able to report the error by clicking a button on the error page */ static public function setRemoteReporting($url, $auto) { self::$report_url = empty($url) ? null : $url; self::$report_auto = (bool) $auto; } /** * Set the HTML header used by the debug error page * @param string $html HTML header */ static public function setHtmlHeader($html) { ini_set('error_prepend_string', $html); } /** * Set the HTML footer used by the debug error page * @param string $html HTML footer */ static public function setHtmlFooter($html) { ini_set('error_append_string', $html); } /** * Set the content of the HTML template used to display an error in production * {$ref} will be replaced by the error reference if log or email is enabled * ... block will be removed if email reporting is disabled * ... block will be removed if log reporting is disabled * @param string $html HTML template */ static public function setProductionErrorTemplate($html) { self::$production_error_template = $html; } static public function setCustomExceptionHandler($class, Callable $callback) { self::$custom_handlers[$class] = $callback; } static public function debug(...$vars) { echo '
';
		foreach ($vars as $var) {
			echo self::dump($var);
			echo '
'; } echo '
'; } /** * Copy of var_dump but returns a string instead of a variable * @param mixed $var variable to dump * @param bool $hide_values Do not return values if set to TRUE * @param integer $level Indentation level (internal use) * @return string */ static public function dump($var, $hide_values = false, $level = 0) { if ($level > 20) { return '*RECURSION*'; } switch (gettype($var)) { case 'boolean': return 'bool(' . ($var ? 'true' : 'false') . ')'; case 'integer': return 'int(' . $var . ')'; case 'double': return 'float(' . $var . ')'; case 'string': return 'string(' . strlen($var) . ') "' . ($hide_values ? '***HIDDEN***' : $var) . '"'; case 'NULL': return 'NULL'; case 'resource': return 'resource(' . (int)$var . ') of type (' . get_resource_type($var) . ')'; case 'array': case 'object': if (is_object($var)) { $out = 'object(' . get_class($var) . ') (' . count((array) $var) . ') {' . PHP_EOL; } else { $out = 'array(' . count((array) $var) . ') {' . PHP_EOL; } $level++; if ($var instanceof \Traversable) { $var2 = []; // Iterate as long as we can while (@$var->valid()) { $var2[] = $var->current(); $var->next(); } $var = $var2; } foreach ($var as $key=>$value) { $out .= str_repeat(' ', $level * 2); $out .= is_string($key) ? '["' . $key . '"]' : '[' . $key . ']'; $out .= '=> ' . self::dump($value, $hide_values, $level) . PHP_EOL; } $out .= str_repeat(' ', --$level * 2) . '}'; return $out; default: return gettype($var); } } /** * Upload a report to a remote errbit-compatible API * @see https://airbrake.io/docs/api/#create-notice-v3 */ static public function sendReport(\stdClass $report, $url) { $data = json_encode($report); $headers = [ 'Content-Type: application/json', 'Content-Lenth: ' . strlen($data), ]; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 3, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_TIMEOUT => 10, CURLOPT_POSTFIELDS => $data, ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); } else { $opts = ['http' => [ 'method' => 'POST', 'header' => $headers, 'content' => $data, 'max_redirects' => 3, 'timeout' => 10, 'ignore_errors' => true, ]]; $body = file_get_contents($url, false, stream_context_create($opts)); $code = null; foreach ($http_response_header as $header) { $a = substr($header, 0, 7); if ($a == 'HTTP/1.') { $code = substr($header, 11, 3); } } unset($http_response_header); } return [ 'code' => (int) $code, 'body' => $body, 'data' => json_decode($body), ]; } /** * Returns list of reports from error log * * @param string|null $log_file Log file to use, if NULL then the log file set in error_log will be used * @param string|null $filter_id Only return errors matching with this ID */ static public function getReportsFromLog($log_file = null, $filter_id = null) { if (!$log_file) { $log_file = ini_get('error_log'); } if (!file_exists($log_file)) { return []; } $reports = []; $report = null; foreach (file($log_file) as $line) { $line = trim($line); if ($line == '') { $report = ''; } elseif ($line == '') { $report = json_decode($report); if (!is_null($report) && isset($report->context->id) && (!$filter_id || $filter_id == $report->context->id)) { $reports[] = $report; } $report = null; } elseif ($report !== null) { $report .= $line; } } unset($line, $report, $log_file); return $reports; } }