MDErrorReporter/MDErrorReporter.php

254 lines
8.5 KiB
PHP

<?PHP
/**
* Debugging and error handling helper for MD.
*/
declare(strict_types = 1);
/**
* Debugging and error handling class.
*/
final class MDErrorReporter {
const MD_ERROR_UNKNOWN = 20;
const MD_ERROR_CRITICAL = 10;
const MD_ERROR_KNOWN = 0;
/** @var string */
private string $_context;
/** @var string */
private string $_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 MDgenericInvalidInputsException
|| $exception instanceof MDExpectedException
|| $exception instanceof MDMysqliExpectedError
|| $exception instanceof MDcouldNotSetPublic
) {
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 ($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|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 ($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 = MDMailerHelper::pgp_encrypt($to, $msg);
$mail = MDMailerHelper::setup_PHPMailer();
$mail->setFrom($this->_contact_mail_addr, $this->_contact_mail_addr);
$mail->addAddress($to);
$mail->addReplyTo(MD_CONF_EMAIL::SMTP_REPLY_TO_ERROR);
$mail->isHTML(false);
$mail->Subject = $subject;
$mail->Body = "$msg";
$mail->send();
}
/**
* 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
);
\error_log((string)$exception);
}
/**
* 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;
}
}