<?PHP /** * Debugging and error handling helper for MD. */ declare(strict_types = 1); /** * Debugging and error handling class. */ class MDErrorReporter { const MD_ERROR_UNKNOWN = 20; const MD_ERROR_CRITICAL = 10; const MD_ERROR_KNOWN = 0; private $_context; private $_contact_mail_addr; /** * Categorizes an error to better decide how to act on it. * * @param Throwable $exception Exception to be categorized. * * @return integer */ final public static function categorizeError(Throwable $exception):int { if ($exception instanceof MDpageParameterMissingException || $exception instanceof MDpageParameterNotNumericException || $exception instanceof MDpageParameterNotFromListException || $exception instanceof MDmainEntityNotExistentException || $exception instanceof MDmainEntityNotPublicException || $exception instanceof MDwriteAccessDeniedException || $exception instanceof MDNoUpdateVarSetException || $exception instanceof MDDBConnectionImpossible || $exception instanceof MDPageNotInAggregatedException || $exception instanceof MDWrongFileType || $exception instanceof MDTooManyFilesUploadException || $exception instanceof MDMysqliExpectedError || $exception instanceof MDMysqliTimeout || $exception instanceof MDMysqliInvalidInputEncoding ) { return self::MD_ERROR_KNOWN; } return self::MD_ERROR_UNKNOWN; } /** * Function human_filesize translates byte-level filesizes to human readable ones. * Thanks to Jeffrey Sambells http://jeffreysambells.com/2012/10/25/human-readable-filesize-php * * @param string $bytes A file size, e.g. returned from filesize(). * @param integer $decimals Number of decimal digits to allow. * * @return string */ final public function human_filesize(string $bytes, int $decimals = 2):string { $size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB']; $factor = floor((strlen($bytes) - 1) / 3); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $size[$factor]; } /** * Function for formatting a section in an error mail. * * @param string $headline Headline of the section. * @param array<mixed> $toFormat Associative array containing information to * be formatted. * @param integer $level Indentation level. Defaults to 0. * * @return string */ final private function _formulateDebugMailSection(string $headline, array $toFormat, int $level = 0):string { $msg = ""; if (!empty($headline)) { if ($level === 0) { $msg .= PHP_EOL . "## " . $headline; $msg .= PHP_EOL . "----------------------" . PHP_EOL . PHP_EOL; } else $msg .= "$headline" . PHP_EOL; } $longestKey = 0; foreach ($toFormat as $key => $value) { if (strlen((string)$key) + (4 * $level) > $longestKey) $longestKey = strlen((string)$key) + (4 * $level); } foreach ($toFormat as $key => $value) { if (is_array($value)) { $msg .= $this->_formulateDebugMailSection((string)$key, $value, $level + 1); } else if (is_object($value)) { $msg .= sprintf("%-{$longestKey}s :: %s", $key, var_export($value, true)) . PHP_EOL; } else { $msg .= sprintf("%-{$longestKey}s :: %s", $key, (string)$value) . PHP_EOL; } } $msg .= PHP_EOL; return $msg; } /** * Gets additional debugging information, e.g. RAM usage. * * @return array<string> */ final public function getAdditionalDebuggingInfo():array { $output = [ "Current memory usage" => $this->human_filesize((string)memory_get_usage()), "Peak memory usage" => $this->human_filesize((string)memory_get_peak_usage()), "Included / required files" => get_included_files(), # "Allowed memory usage" => $this->human_filesize((string)ini_get("memory_limit")), ]; return $output; } /** * This function sends a mail to the specified address, containing relevant debug information. * . * * @param string $to Recipient mail address. * @param string $subject Optional subject of the notification. Defaults to "". * @param string $description Optional text of the notification. Defaults to : "". * @param array<mixed> $contextInfo Context information that will be displayed in a * key-value format at the start of the mail. * * @return void */ final public function sendDebugMail(string $to, string $subject = "", string $description = "", array $contextInfo = []):void { $subject = "[{$this->_context}]{$subject}[Controlled error notification]"; $msg = " # Automated error message ========================= "; if (!empty($contextInfo)) { $msg .= $this->_formulateDebugMailSection("Context information", $contextInfo); } if ($description != "") { $msg .= "## Description" . PHP_EOL; $msg .= "--------------" . PHP_EOL . PHP_EOL; $msg .= "$description" . PHP_EOL; } $msg .= $this->_formulateDebugMailSection("Debug backtrace", debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)); if (PHP_SAPI === 'cli') { echo $msg; exit; } // Append the contents of $_SERVER if (!empty($_SERVER)) { $msg .= $this->_formulateDebugMailSection("The current content of \$_SERVER", $_SERVER); } // Append the contents of $_SERVER if (!empty($_GET)) { $msg .= $this->_formulateDebugMailSection("The current content of \$_GET", $_GET); } // Append the contents of $_SERVER if (!empty($_POST)) { $msg .= $this->_formulateDebugMailSection("The current content of \$_POST", $_POST); } // Append the contents of $_SESSION if (session_status() == PHP_SESSION_ACTIVE) { $msg .= $this->_formulateDebugMailSection("The current content of \$_SESSION", $_SESSION); } else $msg .= "No active session"; $msg .= $this->_formulateDebugMailSection("Additional debugging information", $this->getAdditionalDebuggingInfo()); $msg .= PHP_EOL . PHP_EOL; $msg = shell_exec("echo " . escapeshellarg($msg) . " | gpg2 --always-trust --recipient " . escapeshellarg($to) . " --encrypt --armor --local-user 35CA0E31F6F44FB5 --sign"); // Set header $header = [ 'From' => $this->_contact_mail_addr, 'X-Mailer' => 'PHP/' . phpversion() ]; // Send email mail("$to", "$subject", "$msg", $header); } /** * Wrapper around MDErrorReporter::sendDebugMail that can handle a throwable. * * @param Throwable $exception Exception to report on. * @param string $recipient Recipient mail address. * @param string $addDesc Additional descriptive information. * * @return void */ final public function sendErrorReport(Throwable $exception, string $recipient, string $addDesc = ""):void { $subject = get_class($exception) . ": " . $exception->getCode(); $description = $addDesc; $additionalContextInfo = [ "Message" => $exception->getMessage(), "File" => $exception->getFile(), "Line" => $exception->getLine() ]; $this->sendDebugMail($recipient, $subject, $description, $additionalContextInfo ); } /** * Constructor. * * @param string $toolName Name of the current tool. * @param string $contact_mail_addr Mail address for mail's from header. * * @return void */ public function __construct(string $toolName = "museum-digital (unspecified)", string $contact_mail_addr = "bugs-md@museum-digital.de") { $this->_context = $toolName; $this->_contact_mail_addr = $contact_mail_addr; } }