✏️ 正在编辑: xray-profiler-log.php
路径:
/opt/alt/php-xray/php/profiler/classes/xray-profiler-log.php
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
<?php /** * Copyright (с) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2022 All Rights Reserved * * Licensed under CLOUD LINUX LICENSE AGREEMENT * https://www.cloudlinux.com/legal/ */ namespace XrayProfiler; if (!class_exists('\XrayProfiler\Log')) { class Log { /** * @var string */ private $website = ''; /** * @var string */ private $user = ''; /** * @var string */ private $request_uri = ''; /** * @var int */ private $http_code = 0; /** * @var array<string> * * PHP Core Exceptions */ public $core_exceptions = array( E_ERROR => 'E_ERROR', //1 E_WARNING => 'E_WARNING', //2 E_PARSE => 'E_PARSE', //4 E_NOTICE => 'E_NOTICE', //8 E_CORE_ERROR => 'E_CORE_ERROR', //16 E_CORE_WARNING => 'E_CORE_WARNING', //32 E_COMPILE_ERROR => 'E_COMPILE_ERROR', //64 E_COMPILE_WARNING => 'E_COMPILE_WARNING', //128 E_USER_ERROR => 'E_USER_ERROR', //256 E_USER_WARNING => 'E_USER_WARNING', //512 E_USER_NOTICE => 'E_USER_NOTICE', //1024 E_STRICT => 'E_STRICT', //2048 E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', //4096 E_DEPRECATED => 'E_DEPRECATED', //8192 E_USER_DEPRECATED => 'E_USER_DEPRECATED', //16384 E_ALL => 'E_ALL', //32767 ); /** * @var array<array<int|string|null>> */ private $data = array(); /** * @var self|null */ private static $instance = null; private function __construct() { } private function __clone() { } /** * @return string */ protected function wpHomeConstant() { if (defined('WP_HOME') && WP_HOME) { return (string)WP_HOME; } return ''; } /** * @return string */ protected function wpHomeOption() { if (function_exists('get_option')) { $home = get_option('home'); if (is_string($home)) { return $home; } } return ''; } /** * @return string */ public function website() { if (! empty($this->website)) { return $this->website; } $wp_home_constant = $this->wpHomeConstant(); $wp_home_option = $this->wpHomeOption(); if (! empty($wp_home_constant)) { $this->website = $wp_home_constant; } elseif (! empty($wp_home_option)) { $this->website = $wp_home_option; } elseif (is_array($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) { $this->website = (string)$_SERVER['SERVER_NAME']; } return $this->website; } /** * @return string */ public function user() { if (!empty($this->user)) { return $this->user; } $parse = parse_url($this->website()); if (is_array($parse) and array_key_exists('host', $parse)) { $this->user = $parse['host']; } return $this->user; } /** * @return string */ public function requestUri() { if (! empty($this->request_uri)) { return $this->request_uri; } if (is_array($_SERVER) && array_key_exists('REQUEST_URI', $_SERVER)) { /* keep path only; drop query/fragment - they may carry secrets: tokens, signed-URL params, emails, API keys */ $uri = (string)$_SERVER['REQUEST_URI']; $uri = explode('?', $uri, 2)[0]; $this->request_uri = explode('#', $uri, 2)[0]; } return $this->request_uri; } /** * @return int */ public function httpCode() { if (! empty($this->http_code)) { return $this->http_code; } if (function_exists('http_response_code')) { $this->http_code = (int)http_response_code(); } return $this->http_code; } /** * @return self */ public static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); self::$instance->clean(); } return self::$instance; } /** * @return array */ public function getData() { return $this->data; } /** * @param int $errno * @param string $errstr * @param ?string $errfile * @param ?int $errline * * @return void */ public function setData($errno, $errstr, $errfile = null, $errline = null) { $this->data[] = array( 'message' => $errstr, 'type' => isset($this->core_exceptions[$errno]) ? $this->core_exceptions[$errno] : 'Undefined: ' . $errno, 'filename' => $errfile, 'lineno' => $errline, ); } /** * @return string|bool */ public function sendData() { $data = $this->prepareSentryData($this->data); if (empty($data)) { return false; } $sentry_response = $this->sendSentryData($data); if (!$sentry_response) { return false; } $this->clean(); return $sentry_response; } /** * @param array $data * * @return string|bool */ private function prepareSentryData($data) { if (empty($data)) { return false; } $formatted = array(); foreach ($data as $row) { $formatted[] = array( "stacktrace" => array( "frames" => array( array( // minimize: drop server path prefix so the OS username is not leaked "filename" => $this->minimizePath($row["filename"]), "lineno" => $row["lineno"], ) ), "frames_omitted" => null, ), "type" => $row['type'], // minimize: scrub embedded absolute paths and cap length "value" => $this->minimizeMessage($row['message']), ); } $xray_version = defined('XRAY_RELEASE') ? XRAY_RELEASE : null; $sentry = array( "release" => $this->release($xray_version), "tags" => array( "php_version" => phpversion(), "xray_version" => $xray_version, ), "extra" => array( 'website' => $this->website(), 'request_uri' => $this->requestUri(), 'http_code' => $this->httpCode(), ), "user" => array( "username" => $this->user(), ), "sentry.interfaces.Exception" => array( "exc_omitted" => null, "values" => $formatted, ), ); // Defence-in-depth against json_encode() returning false (which // would make sendData()'s empty() guard silently drop the whole // Sentry POST). On PHP 7.2+ this flag substitutes U+FFFD for any // malformed UTF-8 in the payload. On older runtimes the flag is // undefined so flags=0; there truncateUtf8() guarantees OUR // truncation never introduces malformed UTF-8, so the patch does no // harm. Note: a RAW error message already malformed before // truncation can still fail json_encode on PHP <7.2 — that is the // exact pre-patch behavior, pre-existing and out of scope here (not // a regression introduced by this fix). $flags = defined('JSON_INVALID_UTF8_SUBSTITUTE') ? JSON_INVALID_UTF8_SUBSTITUTE : 0; return json_encode($sentry, $flags); } /** * Reduce the server path used for the frame.filename field to the file * basename, and redact any leaf that is not a recognised source/asset * file to the literal token '<path>'. In practice frame.filename is * PHP's errfile, which is always the executing source/template script, * so its basename ends in a known code/template/asset extension (.php, * .phtml, .tpl, .twig, .js, .css, ...) and is kept (e.g. "index.php", * "loader.php", "style.css"). Any other leaf is redacted: a bare home * or vhost directory leaf is the account name — possibly a DOTTED OS * username (/home/john.doe) or a domain-named Plesk vhost * (/var/www/vhosts/example.com) — and does NOT end in a source * extension. Matching on a source/asset extension allowlist (rather * than the earlier "any dot means it's a file" test) closes the dotted * directory/username/domain leak that the dot heuristic missed, while * still keeping legitimate dotted source files. The OS username / * filesystem layout therefore never egresses regardless of layout. * * @param string|null $path * * @return string|null */ private function minimizePath($path) { if (!is_string($path) || $path === '') { return $path; } $normalized = trim(str_replace('\\', '/', $path), '/'); if ($normalized === '') { return '<path>'; } $segments = explode('/', $normalized); $basename = end($segments); // Keep the basename only when it is plausibly a real source/asset // file (an errfile always is); otherwise it is a bare // directory/account leaf (dotless OR dotted username/domain) and // must be redacted. $source_ext_pattern = '/\.(php\d?|phtml|phps|inc|module|install' . '|engine|theme|profile|tpl|twig|html?|js|mjs|cjs|jsx|tsx?' . '|css|s[ac]ss|less|xml|ya?ml|json)$/i'; if ( $basename === '' || !preg_match($source_ext_pattern, $basename) ) { return '<path>'; } return $basename; } /** * Minimize a raw PHP error message before it enters the payload: redact * any embedded absolute filesystem path to the literal token '<path>' * and cap the overall length. PHP error strings routinely embed server * paths, query fragments and runtime values that need not be disclosed * off-host. A constant '<path>' replacement (rather than retaining the * basename) ensures a message can never leak a username/domain/layout * regardless of where the path terminates — e.g. an open_basedir or * disk-quota error names a directory whose leaf segment is the account * name or vhost domain. * * @param string|null $message * * @return string|null */ private function minimizeMessage($message) { if (!is_string($message) || $message === '') { return $message; } // Bound the input BEFORE scrubbing: the path-scrub regex must never // run on a huge subject. A PHP error string can be tens of KB (E_ALL, // big traces, var_dumps); on the default pcre.jit=1 the path regex on // such a subject can trip the PCRE JIT stack limit, making // preg_replace return NULL. Capping first (UTF-8-safe) keeps the // regex subject tiny (<= ~1027 bytes) so that never happens; we // remember whether we cut, then scrub the small result and re-append // the '...' suffix afterwards. $max_length = 1024; $truncated = false; if (strlen($message) > $max_length) { $message = $this->truncateUtf8($message, $max_length); $truncated = true; } // Redact absolute *nix paths to the literal token '<path>'. Two // alternatives: // 1. multi-segment (/a/b/c) matched anywhere, so a path embedded // mid-token (e.g. "in/home/bob/x") is still redacted; // 2. single-segment (/home, /var) matched ONLY at a token boundary // (start, whitespace or a delimiter -- the negative lookbehind // rejects a preceding word char or '/'). The boundary guard is // what keeps benign mid-word slashes like "and/or" or // "read/write" intact, so we close the bare-directory leak // (/home, /var in open_basedir / quota errors) without // over-redacting ordinary prose. $scrubbed = preg_replace( '#/(?:[^\s/:"\']+/)+[^\s/:"\']+|(?<![\w/])/[^\s/:"\']+#', '<path>', $message ); if (!is_string($scrubbed)) { // FAIL CLOSED: on any PCRE engine error never emit the raw // message (it could carry an unredacted absolute path); redact // the whole value instead. return '<redacted>'; } if ($truncated) { $scrubbed .= '...'; } return $scrubbed; } /** * Truncate a string to at most $max BYTES without splitting a UTF-8 * multibyte sequence. The output is always <= $max bytes and never ends * with a partial codepoint, so it is guaranteed valid UTF-8 (assuming * the input prefix was). Pure PHP with no mbstring/iconv dependency, so * it behaves identically on every supported runtime (PHP 5.4+), unlike * mb_strcut which silently degrades to a byte-wise substr when mbstring * is not loaded. * * @param string $s * @param int $max * * @return string */ private function truncateUtf8($s, $max) { if (!is_string($s) || strlen($s) <= $max) { return $s; } $s = substr($s, 0, $max); $len = strlen($s); // Count trailing UTF-8 continuation bytes (10xxxxxx). $cont = 0; while ($cont < $len && (ord($s[$len - 1 - $cont]) & 0xC0) === 0x80) { $cont++; } if ($cont < $len) { $lead = ord($s[$len - 1 - $cont]); $expected = 1; if ($lead >= 0xF0) { $expected = 4; } elseif ($lead >= 0xE0) { $expected = 3; } elseif ($lead >= 0xC0) { $expected = 2; } // If the final multibyte sequence is incomplete, drop it whole. if (($cont + 1) < $expected) { $len = $len - 1 - $cont; } } return substr($s, 0, $len); } /** * @param string|bool $data * * @return string|bool */ private function sendSentryData($data) { if (!function_exists('curl_init') || empty($data)) { return false; } $ch = curl_init(); $sentry_key = '8e6821f19d214977ace88586bc8569fd'; $url = 'https://cl.sentry.cloudlinux.com/api/23/store/'; curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json', 'X-Sentry-Auth: Sentry sentry_version=7,sentry_timestamp=' . time() . ',sentry_client=php-curl/1.0,sentry_key=' . $sentry_key, )); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_TIMEOUT, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); return $response; } /** * @return void */ public function clean() { $this->data = array(); $this->website = ''; $this->user = ''; $this->request_uri = ''; $this->http_code = 0; } /** * Builds the Sentry release string based on X-Ray version. * * @param string|null $xray_version X-Ray version from XRAY_RELEASE constant. * * @return string Sentry release string. * * @since 0.5-23 */ public function release($xray_version) { $version = 'dev'; if (! empty($xray_version)) { preg_match('/\d+\.\d+(\.\d+)?-\d+/', $xray_version, $matches); if (! empty($matches)) { $version = $matches[0]; } } return 'xray-php-profiler@' . $version; } } }
💾 保存文件
← 返回文件管理器