Class it:
/**
* If display_errors is on or stdout is a tty, shows error in page or on stdout respectively
* If display_errors is off, mails (with rate limiting) diagnostics to .diffnotice addresses or file owner or SERVER_ADMIN
*
* Controlling error mail content
* @param $p either error title or assoc array of params, see below
* @param $p['title'] error title, one line. also accepted in $p[0] (with priority). false means suppress error
* @param $p['body'] error body: multiline string or any data type (will be dumped)
* @param $p['backtraceskip'] number of stack levels to drop
* @param $p['skipfiles'] files to skip in backtrace
* @param $p['omitdebuginfo'] value 1 omits long stack and var dumps, value 2 also minimal infos
*
* Controlling the sending of mails
* @param $p['to'] comma separated recipient list
* @param $p['timewindow'] in secs. "5-35" means for an mail to be sent, another err must occur 5 to 35 seconds after first one
* @param $p['id'] id of error, used with timewindow, defaults to file and line of caller
* @param $p['blockmailid'] block mail for $p['blockmail'] seconds with same id. Default: $p['to']
* @param $p['blockmail'] number of seconds to block mails after having sent a mail [3600]
* @param $p['okstate'] give current ok label and ok state, e.g. telresult=1 for working. see failcount
* @param $p['failcount'] give number of consecutive okstate failures needed for creating an error [2]
* @param $p['fatal'] exit after displaying error
* @param $p['graceperiod'] in seconds. DEPRECATED, see $p['timewindow']
* Use debug param 'verboseerrors' to include $p['body'] when interactive
*
* @return always null (so users can return it::error() in error cases)
*
* TIMEWINDOW 5-35 (x = error occurs, no mail; m = error occurs, mail sent)
* time ---|5 secs|30 secs |------|5 secs|30 secs
* |5 secs|30 secs
* x x x x m x m x
* BLOCKMAIL
* time ---|blockmail|--|blockmail|---->
* blockmailid m x x m x
*
* GRACPERIOD/TIMEWINDOW (DEPRECATED, graceperiod is first argument of 5-35, timewindow is difference between 5 and 35)
* time ---|graceperiod|timewindow|
* |graceperiod|timewindow|--.
* |graceperiod|timewindow|------>
* id x x x m x x x x
*/
static function error($p = array(), $extra = null)
{
$p = $origp = (array)$p;
if (error_reporting() == @error_reporting() || $p[0] === false || $p['title'] === false) # called with @ or suppressed
return $p['fatal'] ? self::_exit($p) : null;
$p['title'] = $p[0] ?: $p['title']; # handle 'it_error' => "oops" that was cast to array on the way
$p['title'] = rtrim(grapheme_substr($p['title'], 0, 2000) ?: substr($p['title'], 0, 2000));
$p += is_callable(self::$error_context) ? (self::$error_context)() : [];
$home = $GLOBALS['ULTRAHOME'];
# support for errlike() in tests
$GLOBALS['ULTRAERROR'] = $p['title'];
if ($GLOBALS['ULTRANOERRORS'])
return null;
if ($extra)
$p = ['title' => 'extraneous params passed to it::error', 'fatal' => $p['fatal']];
foreach (array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10), 1) as $level)
if ($level["class"] . $level["type"] . $level["function"] == "it::error")
return null; # prevent recursion
if ($_SERVER['REMOTE_ADDR'])
$url = ($_SERVER['HTTPS'] ? "https://" : "http://") . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; # HTTP OK
else
$url = $_SERVER['SCRIPT_NAME'] . " " . implode(" ", array_slice($GLOBALS['argv'], 1)) . " (pwd " . $_SERVER['PWD'] . ")";
$authors = it::replace(['\*' => ""], join(", ", it::grep('\*', (array)@it::cat("$home/.diffnotice")->lines, ['invert' => true])));
if (!$p['to'])
unset($p['to']); # allow defaults to kick in
$p += array(
'title' => "it::error",
'to' => $authors ? $authors : (get_current_user() ? get_current_user() : $_SERVER['SERVER_ADMIN']),
'graceperiod' => 60,
'timewindow' => 25*3600,
'blockmail' => $p['okstate'] ? 60 : 3600,
'omitdebuginfo' => false,
'failcount' => 2,
'id' => $p['timewindow'] ? "it_error_id_" . it_debug::backtrace(['skipfiles' => "it.class", 'levels' => 1]) : 0,
);
if (it::match('-', $p['timewindow']) && ($parts = explode("-", $p['timewindow'])))
list($p['graceperiod'], $p['timewindow']) = [$parts[0], $parts[1] - $parts[0]];
$p += array('blockmailid' => $GLOBALS['ULTRASITE'] . "." . md5($p['to']));
$traceline = it_debug::backtrace(['skiplevels' => $p['backtraceskip'] + ($p['trace'] ? 0 : 1), 'skipfiles' => $p['skipfiles'], 'trace' => $p['trace']]);
$mailid = sprintf("mail%04d", crc32($traceline ?: $p['title']) % 10000);
@mkdir("/tmp/alertdata");
@chmod("/tmp/alertdata", 0777);
if ($p['okstate'])
{
list($okfn, $okstatus) = explode("=", "$home/tmp/okstate_" . $p['okstate']);
$failcount = $okstatus ? 0 : (int)@file_get_contents($okfn) + 1; # NOPHPLINT
file_put_contents($okfn, "$failcount\n"); # NOPHPLINT
if ($failcount != $p['failcount'] && $failcount != $p['failcount'] * 30)
return $p['fatal'] && !$okstatus ? self::_exit($p) : null; # nothing to report
}
$toscreen = getenv('NOERRORMAILS') || ini_get('display_errors') || (defined("STDOUT") && posix_isatty(STDERR)) || EDC('astwin') || EDC('asdevel') || $p['toscreen'];
if ($toscreen && !it::is_live())
$GLOBALS['debug_noredir'] = 1;
if (!$toscreen) # this error can only be sent by mail: find out if we want to suppress it
{
if (!$p['id'])
$sendmail = true;
else
{
$errstampfn = "/tmp/alertdata/errstamp_" . urlencode($p['id']);
$errstamp = @filemtime($errstampfn);
$errstampage = time() - $errstamp;
$sendmail = $errstampage >= $p['graceperiod'] && $errstampage <= $p['graceperiod'] + $p['timewindow'];
if ($errstampage > $p['graceperiod'])
{
@unlink($errstampfn);
@touch($errstampfn);
}
}
if ($sendmail)
{
$lastsentfn = "/tmp/alertdata/it_error_mailsent_" . urlencode($p['blockmailid']);
clearstatcache();
$sendmail = time() - max(@filemtime($lastsentfn), it_cache::get($lastsentfn, ['distributed' => true])) > $p['blockmail'] && !it_cache::get('it_error_mute_' . $GLOBALS['ULTRASITE'], ['distributed' => true]);
if ($sendmail)
{
@unlink($lastsentfn);
@touch($lastsentfn);
if (it::is_live())
@it_cache::put($lastsentfn, time(), ['distributed' => true, 'ttl' => 7*86400, 'safety' => 0]);
}
}
}
if ($p['fatal'] && $_SERVER['REMOTE_ADDR'])
{
http_response_code(500);
echo "<!DOCTYPE html><html><head><title>Internal Server Error</title></head><body>Internal Server Error</body></html>\n";
}
if ($toscreen || $sendmail)
{
$p['body'] = is_string($p['body']) || !array_key_exists('body', $p) ? $p['body'] : it_debug::dump([$p['body']], ['html' => false, 'short' => true, 'color' => false]);
if (strlen($p['body']) > 500000 || it::match('[\x00-\x08\x0E-\x1F]', $p['body'], ['utf8' => false]) !== null)
{
@file_put_contents($datafn = "$home/tmp/error-" . substr(md5($p['body']), 0, 2), $p['body']); # NOPHPLINT
$p['body'] = " See " . getenv('HOSTNAME') . ":$datafn";
}
$service = it::match('[^.]*', $GLOBALS['ULTRASITE']);
$body =
($p['omitdebuginfo'] >= 2 ? "" : ($url && !$toscreen? "Title: {$p['title']}\nUrl: $url\n" : "") .
($traceline ? ($sendmail ? "" : " ") . "Trace: $traceline\n" : "")) .
(!$sendmail || $p['omitdebuginfo'] >= 2 ? "" : "Host: " . getenv('HOSTNAME') . " at " . date("Y-m-d H:i:s") . (($t = time() - $_SERVER['REQUEST_TIME']) ? " (invoked {$t}s before)" : "") . " id $mailid\n") . # no it::date() due to time- debug param
($sendmail && it::is_live() && $p['omitdebuginfo'] < 2 ? "Log: see $service/log/alertlog $mailid\n" : "") .
($sendmail ? $p['head'] : "") .
($p['id'] && !$toscreen ? "Filter: timewindow=" . $p['graceperiod'] . "-" . ($p['graceperiod'] + $p['timewindow']) . " (previous err: " . it::date('', $errstamp) . ")\n" : "") .
(!$origp['blockmail'] || $p['omitdebuginfo'] || $toscreen ? "" : "Block-resend: " . $origp['blockmail'] . " seconds\n") .
($p['body'] ? ($p['omitdebuginfo'] ? "" : "Body:\n") . trim($p['body'])."\n\n" : "");
if ($sendmail || EDC('verboseerrors')) # we're mailing: send maximum info
{
$p['title'] = it::replace(['alert:|server:|^: "' => "", '(pw|passw|password\d*|secret|api.?key)(=)[^&\s]*' => '$1$2*****'], "$service: " . $p['title']) . " (via " . getenv('HOSTNAME') . ")";
if (!$p['omitdebuginfo'])
{
$trace = $p['trace'] ?: array_slice(debug_backtrace(0), $p['backtraceskip']);
$mediumstack = it_debug::backtrace(['trace' => $trace, 'format' => "medium"]);
$longstack = substr(it_debug::backtrace(['trace' => $trace, 'format' => "long"]), 0, 200000); # use substr() because data might be binary
$reqbody = $_POST ? "" : it::file_get_contents("php://input");
$body .= ($traceline and $t = it::exec('grep -h {0} `ls 2>/dev/null {1}/log/alertlog-*|tail -3` /dev/null 2>/dev/null | grep ^2 | cut -d : -f 1-2 | sort | uniq -c | tail -10', $traceline, $home)) ? "Histogram: (last 10 affected minutes in 3 days)\n$t\n" : "";
$body .= $mediumstack ? "Stack:\n " . it::replace(["\n" => "\n "], $mediumstack) . "\n" : "";
$body .= $_GET ? "\$_GET: " . it::json_encode($_GET, ['pretty' => true]) . "\n" : "";
$body .= $_POST ? "\$_POST: " . it::json_encode($_POST, ['pretty' => true]) . "\n" : "";
$body .= $reqbody ? "\$reqbody: " . it::json_encode($reqbody, ['pretty' => true]) . "\n" : "";
$body .= $_COOKIE ? "\$_COOKIE: " . it::json_encode($_COOKIE, ['pretty' => true]) . "\n" : "";
$body .= $_SERVER['REMOTE_ADDR'] ? "" : "Pstree:\n" . it::exec("pstree -als {pid} | head -n -3", ['pid' => getmypid()]) . "\n";
$body .= $_SERVER ? "\$_SERVER: " . it::json_encode($_SERVER, ['pretty' => true]) . "\n" : "";
$body .= $_FILES ? "\$_FILES: " . it::json_encode($_FILES, ['pretty' => true]) . "\n" : "";
$body .= "Processes:\n" . it::exec('ps auxf | egrep -v "rotatelogs|getbanner|logaction|httpd|systemd|sd-pam"|egrep "^www|^cron"') . "\n";
$body .= $longstack ? "Full stack: " . "$longstack\n" : "";
$body = it::replace(['(pw|passw|password\d*|secret|api.?key)(\' => |\] => |=)[^&\s\']*' => '$1$2********'], $body, ['utf8' => false]);
$body = it::replace(['"(pw|passw|password\d*|secret|api.?key)": *"[^"]*"' => '"$1": "*******"'], $body, ['utf8' => false]);
}
$type = ($p['fatal'] ? (it::is_live() ? "FATAL: " : "Fatal: ") : "Error: ");
$debugparams = it::match('\.([^-.]+)', $_SERVER['HTTP_HOST'], ['all' => true]);
if (!it::is_live() || array_diff($debugparams, it::match('[-\w]+', $home, ['all' => true]), ["devel", "twin", gethostname()]))
$type = mb_strtolower($type);
$user = posix_getpwuid(posix_geteuid())['name'];
if ($sendmail)
it::mail([
'From' => "\"$user@" . gethostname() . " $mailid\" <$user>",
'To' => $p['to'],
'Reply-To' => $p['to'],
'Cc' => $GLOBALS['it_defaultconfig']['error_cc'],
'Subject' => $type . substr($p['title'], 0, 160),
'Body' => it::replace(['\x00' => "[NULLBYTE]"], $body, ['utf8' => false]),
'forcemail' => !it::is_devel(),
]);
else
echo $body;
$p['title'] = "Mail: " . $p['title'];
}
else if ($_SERVER['REMOTE_ADDR'] && EDC('edplain')) # toscreen mode: web
echo $p['title'] . "\n";
else if ($_SERVER['REMOTE_ADDR']) # toscreen mode: web
echo "<pre style='z-index:10000; position:relative; background:white'>" . htmlspecialchars($p['title'] . "\n" . rtrim($body), ENT_COMPAT, "iso-8859-1") . "</pre>"; # works with iso-8859-1 or utf-8, UTF8SAFE
else # toscreen mode: shell (outputs to stderr)
error_log(substr($p['title'], 0, 100000) . ($p['omitdebuginfo'] >= 2 ? "" : " in " . ($traceline ? $traceline : " {$p['file']}:{$p['line']}") . " Url: $url " . (EDC('verboseerrors') ? D($p['body']) : "")));
}
if ($_SERVER['REMOTE_ADDR']) # additional entry in log/error_log
error_log("it::error: " . $p['title'] . " Url: $url");
it::log("alertlog", $p['title'] . " ($mailid) in " . ($traceline ? $traceline : "{$p['file']}:{$p['line']}") . " Url: $url" . ($p['body'] ? "\n" . substr(D($p['body']), 0, 5000) . "\n" : ""));
return $p['fatal'] ? self::_exit($p) : null;
}