/
razxc
/
telescope.php
Обзор
Документация
Войти
/
razxc
/
telescope.php
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Telescope.php
1 262 строки
32 KB
Alexandr Haritonov
Initial commit. Telescope 6.0.0
29 мар 2026, 11:23
29 мар 2026, 11:23
02aa6e9
Код
Авторство
О чём код?
<?php /** * The tool for error alerting, error tracking and debugging * in stages of development and maintenance. */ namespace razxc\telescope { use ArrayObject; use ErrorException; use RuntimeException; if (PHP_VERSION_ID >= 80400) { // https://www.php.net/manual/en/errorfunc.constants.php#constant.e-strict define(__NAMESPACE__ . '\E_STRICT', 2048); } /** * @see https://www.php.net/manual/en/class.errorexception.php */ class TelescopeErrorException extends ErrorException { /** * @deprecated 7.2+ * @removed 8.0+ */ public $context = array(); } class Telescope { const VERSION = '6.0.0'; const REF_ERRORS = 'https://www.php.net/manual/en/errorfunc.constants.php#constant.%s'; const REF_EXCEPTIONS = 'https://www.php.net/manual/en/class.%s.php'; /** * Save report with environments and snapshots. * Does not make sense on fail. */ const MODE_LOG = 1; /** * Show errors and exceptions. Make sense in web environment only. * Failures are ALWAYS displayed in CLI. */ const MODE_DISPLAY = 2; /** * Save and send crash report by email to webmasters on fail, * in according to configuration */ const MODE_TRACING = 4; /** * @see http://php.net/manual/en/errorfunc.constants.php */ protected static $errorToDescription = array( E_ERROR => 'Fatal run-time errors. These indicate errors that can not be recovered from, such as a memory allocation problem. Execution of the script is halted.', E_WARNING => 'Run-time warnings (non-fatal errors). Execution of the script is not halted.', E_PARSE => 'Compile-time parse errors. Parse errors should only be generated by the parser.', E_NOTICE => 'Run-time notices. Indicate that the script encountered something that could indicate an error, but could also happen in the normal course of running a script.', E_CORE_ERROR => 'Fatal errors that occur during PHP`s initial startup. This is like an E_ERROR, except it is generated by the core of PHP.', E_CORE_WARNING => 'Warnings (non-fatal errors) that occur during PHP`s initial startup. This is like an E_WARNING, except it is generated by the core of PHP.', E_COMPILE_ERROR => 'Fatal compile-time errors. This is like an E_ERROR, except it is generated by the Zend Scripting Engine.', E_COMPILE_WARNING => 'Compile-time warnings (non-fatal errors). This is like an E_WARNING, except it is generated by the Zend Scripting Engine.', E_USER_ERROR => 'User-generated error message. This is like an E_ERROR, except it is generated in PHP code by using the PHP function trigger_error(). (deprecated as of PHP 8.4.0)', E_USER_WARNING => 'User-generated warning message. This is like an E_WARNING, except it is generated in PHP code by using the PHP function trigger_error().', E_USER_NOTICE => 'User-generated notice message. This is like an E_NOTICE, except it is generated in PHP code by using the PHP function trigger_error().', E_STRICT => 'Run-time suggestions emitted by PHP about the executed code to ensure forward compatibility. (deprecated as of PHP 8.4.0)', E_RECOVERABLE_ERROR => 'Legacy engine "exceptions" which correspond to catchable fatal error. Similar to Error but must be caught via a user defined error handler (see set_error_handler()). If not handled, this behaves like E_ERROR. (introduced in PHP 5.2)', E_DEPRECATED => 'Run-time deprecation notices. Enable this to receive warnings about code that will not work in future versions. (introduced in PHP 5.3)', E_USER_DEPRECATED => 'User-generated deprecation message. This is like an E_DEPRECATED, except it is generated in PHP code by using the PHP function trigger_error().', ); private static $aConfig; private static $mode; private static $numPHPClasses; private static $timeStart; private static $contentLength = -1; private static $queryShot = array(); private static $shots = array(); private static $shutdownCallback; private static $sReport; private static $failure; private static $failureTrackCode; /** * Initialization * * Examples: * <code> * // Suitable modes: * * // DEVELOPMENT * Telescope::init($pathToIni, Telescope::MODE_DISPLAY); * Telescope::init($pathToIni, Telescope::MODE_DISPLAY + Telescope::MODE_LOG); * * // PRODUCTION * Telescope::init($pathToIni, Telescope::MODE_TRACING); * Telescope::init($pathToIni, Telescope::MODE_TRACING + Telescope::MODE_LOG); * * // CLI PRODUCTION * Telescope::init( * $pathToIni, * Telescope::MODE_TRACING + * Telescope::MODE_DISPLAY + * Telescope::MODE_LOG * ); * </code> * * @param string $iniFilePath * @param int $mode */ public static function init($iniFilePath, $mode=self::MODE_TRACING) { if (PHP_VERSION_ID < 50300) { die('Telescope required PHP 5.3+'); } $allowedModes = array( self::MODE_LOG, self::MODE_DISPLAY, self::MODE_TRACING, self::MODE_LOG + self::MODE_DISPLAY + self::MODE_TRACING, self::MODE_LOG + self::MODE_DISPLAY, self::MODE_LOG + self::MODE_TRACING, self::MODE_DISPLAY + self::MODE_TRACING, ); if (!in_array($mode, $allowedModes, true)) { die('Telescope: invalid mode'); } // prevent PHP Fatal error date_default_timezone_set('UTC'); // constant need for separating PHP-defined and user-defined constants define(__NAMESPACE__ . '\SEPARATOR', 1); self::$timeStart = microtime(true); self::$mode = $mode; self::$aConfig = self::loadConfig($iniFilePath); self::$numPHPClasses = count(get_declared_classes()); error_reporting(self::getConfigVar('error_reporting')); // possible values for display_errors: // Off - Do not display any errors // stderr - Display errors to STDERR (only CGI/CLI binaries!) // On or stdout - Display errors to STDOUT (default) $isShow = self::hasMode(self::MODE_DISPLAY); ini_set('display_errors', $isShow ? 'stdout' : 'Off'); ini_set('display_startup_errors', $isShow ? 'stdout' : 'Off'); set_error_handler(array(__CLASS__, 'errorHandler')); set_exception_handler(array(__CLASS__, 'exceptionHandler')); register_shutdown_function(array(__CLASS__, 'shutdownHandler')); } /** * Set callback (only one) that will be called additionally on shutdown. * * @param callable $callback */ public static function onShutdown($callback) { self::$shutdownCallback = $callback; } /** * Handler used for logging uncatchable errors. */ public static function shutdownHandler() { // give possibility for do reporting for cases when memory exhausted self::increaseMemoryLimit(); self::$contentLength = (ob_get_level() === 0) ? -1 : ob_get_length(); // log uncatchable errors $aErr = null; $lastErr = error_get_last(); if ($lastErr !== null && (error_reporting() & $lastErr['type'])) { $aErr = $lastErr; } if ($aErr !== null) { $file = $aErr['file']; $line = $aErr['line']; $type = $aErr['type']; $msg = $aErr['message']; $e = new TelescopeErrorException($msg, $type, 0, $file, $line); if (PHP_VERSION_ID >= 80300) { // Improved behavior for non-cached DEPRECATION errors in syntax // on startup throw $e; } self::failureHappened($e); self::reportFailure(); } if (self::$shutdownCallback) { if ($aErr === null) { $aErr = array(); } call_user_func_array(self::$shutdownCallback, $aErr); } if (!self::$failure && self::hasMode(self::MODE_LOG)) { self::logEnv(); } } /** * Error handler. * * @param int $no * @param string $str * @param string $file * @param int $line * @param array $context * @throws TelescopeErrorException * @see http://www.php.net/manual/en/function.set-error-handler.php */ public static function errorHandler( $no, $str, $file, $line, array $context=array() ) { if (error_reporting() & $no) { $e = new TelescopeErrorException($str, $no, 0, $file, $line); $e->context = $context; throw $e; // call exceptionHandler($e) } } /** * Exception handler. * * @param \Throwable $e */ public static function exceptionHandler($e) { self::failureHappened($e); if (!headers_sent()) { $protocol = self::getServerVar('SERVER_PROTOCOL'); header($protocol . ' 500 Internal Server Error'); } if (self::hasMode(self::MODE_DISPLAY) || self::isCli()) { self::showFailure(); } else { self::redirectOnFailure(); } if (self::hasMode(self::MODE_TRACING)) { self::reportFailure(); } exit(1); } /** * @param \Throwable $e */ private static function failureHappened($e) { self::$failure = $e; self::$failureTrackCode = self::toTrackCode($e); } private static function redirectOnFailure() { if (self::getServerVar('HTTP_X_REQUESTED_WITH')) { return; // on ajax } $url = self::getConfigVar('redirect_on_error'); if (!$url) { return; } $url .= ((strpos($url, '?') === false) ? '?' : '&'); $url .= 'code=' . self::$failureTrackCode; if (!headers_sent()) { header('Location: ' . $url, true, 307); // Temporary Redirect } else { echo '<script type="text/javascript">'; echo "document.location.href=$url;"; echo '</script>'; } } /** * Logging and sending message to webmasters. */ private static function reportFailure() { $e = self::$failure; $trackCode = self::$failureTrackCode; $format = self::getConfigVar('report_title'); $sSbj = sprintf($format, $trackCode); // create bug report for webmasters $sBody = self::template('error.mail.tpl.php', array( 'e' => $e, 'httpMethod' => self::getServerVar('REQUEST_METHOD'), 'IS_EE' => ($e instanceof ErrorException), 'ERROR_TITLE' => self::toTitle($e), 'ERROR_REF' => self::toReferenceLink($e), 'ERROR_HINT' => self::toHint($e), 'ERROR_NO' => $e->getCode(), 'ERROR_FILE' => $e->getFile(), 'ERROR_LINE' => $e->getLine(), 'ERROR_MSG' => $e->getMessage(), 'BACKTRACE' => $e->getTrace(), 'inCLI' => self::isCli(), 'ARGV' => implode(' ', self::getServerVar('argv', array())), 'URL' => self::getRequestUrl(), 'ENV_REPORT' => self::getEnvBody(), )); // report will be saved not more than ... $lim = self::getConfigVar('error_log_limit'); $errorLogDir = self::getConfigVar('error_log_dir'); $aFiles = glob("$errorLogDir/telescope-$trackCode-*.error.log"); $n = count($aFiles); if ($n < $lim) { // save in log $filename = "$errorLogDir/telescope-$trackCode-" . ($n + 1) . '.error.log'; $message = html_entity_decode($sSbj . "\n" . strip_tags($sBody)); $h = fopen($filename, 'cb'); if (flock($h, LOCK_EX|LOCK_NB)) { // skip write on race condition fwrite($h, $message); flock($h, LOCK_UN); } fclose($h); } // report will be sent not more than ... $lim = self::getConfigVar('error_num_notify'); if ($n < $lim) { $toEmails = self::getConfigVar('report_emails'); self::sendMailBy($toEmails, $sSbj, $sBody); } } private static function showFailure() { $e = self::$failure; if (self::isCli()) { // CLI output $str = self::template('error.cli.tpl.php', array( 'IS_EE' => ($e instanceof ErrorException), 'ERROR_TITLE' => self::toTitle($e), 'ERROR_REF' => self::toReferenceLink($e), 'ERROR_NO' => $e->getCode(), 'ERROR_FILE' => $e->getFile(), 'ERROR_LINE' => $e->getLine(), 'ERROR_MSG' => $e->getMessage(), 'BACKTRACE' => $e->getTrace(), 'GENERATOR' => self::getEngineName(), 'trackCode' => self::$failureTrackCode, )); fwrite(STDERR, $str); } else { // HTTP output $sBefore = ''; while (ob_get_level() > 0) { $sBefore.= ob_get_clean(); } echo self::template('error.web.tpl.php', array( 'e' => $e, 'trackCode' => self::$failureTrackCode, 'IS_EE' => ($e instanceof ErrorException), 'ERROR_TITLE' => self::toTitle($e), 'ERROR_REF' => self::toReferenceLink($e), 'ERROR_HINT' => self::toHint($e), 'ERROR_NO' => $e->getCode(), 'ERROR_FILE' => $e->getFile(), 'ERROR_LINE' => $e->getLine(), 'ERROR_MSG' => $e->getMessage(), 'BACKTRACE' => $e->getTrace(), 'CONTENT_BEFORE' => $sBefore, 'GENERATOR' => self::getEngineName(), 'maxLevel' => self::getConfigVar('backtrace_max_depth'), 'ENV_REPORT' => self::getEnvBody(), )); } } /** * Returns tip for error. * * @param \Throwable $e * @return string */ private static function toHint($e) { if ($e instanceof ErrorException) { $a =& self::$errorToDescription; return empty($a[$e->getCode()]) ? '' : $a[$e->getCode()]; } return ''; } /** * Returns URL to documentation. * * @param \Throwable $e * @return string */ private static function toReferenceLink($e) { if ($e instanceof ErrorException) { $refTpl = self::REF_ERRORS; } else { $refTpl = self::REF_EXCEPTIONS; } $name = str_replace('_', '-', strtolower(self::toTitle($e))); return sprintf($refTpl, $name); } /** * Returns user friendly title for exceptions. * * @param \Throwable $e * @return string */ private static function toTitle($e) { if ($e instanceof ErrorException) { $title = self::typeToName($e->getCode()); } else { $title = get_class($e); } return $title; } /** * Returns name of PHP-constant by its value. * * @param int $type * @return string */ private static function typeToName($type) { $cl = get_defined_constants(true); $a = array_flip(array_slice($cl['Core'], 1, 15, true)); return isset($a[$type]) ? $a[$type] : '#n/a'; } /** * Returns unique code for error tracking. * * @param \Throwable $e * @return string */ private static function toTrackCode($e) { $hash = md5($e->getFile() . $e->getLine() . $e->getTraceAsString()); return substr($hash, 0, 8); /* $code = $e->getCode(); return substr($hash, 0, 8).($code ? ':'.$code : ''); */ } /** * Save environment report. */ private static function logEnv() { $logBasePath = self::getConfigVar('env_log_dir'); $logBasePath .= '/telescope-' . date('Ymd_His-'); $i = 0; $logFile = $logBasePath . $i . '.env.log'; while (file_exists($logFile)) { $logFile = $logBasePath . (++$i) . '.env.log'; } $sText = self::getEnvBody(); $sText = strip_tags($sText); $sText = html_entity_decode($sText); $h = fopen($logFile, 'cb'); if (flock($h, LOCK_EX|LOCK_NB)) { // skip write on race condition fwrite($h, $sText); flock($h, LOCK_UN); } fclose($h); self::removeOldEnvLogs(); } /** * Remove old report files. */ private static function removeOldEnvLogs() { $sPath = self::getConfigVar('env_log_dir'); $aFiles = glob("$sPath/*.env.log"); $oArray = new ArrayObject($aFiles); if ($oArray->count() === 0) { return; } // delete old log files $oArray->asort(); $oIterator = $oArray->getIterator(); $limit = $oIterator->count() - self::getConfigVar('env_log_limit'); for ($i = 0; $i < $limit; $i++) { $pathToFile = $oIterator->current(); unlink($pathToFile); $oIterator->next(); } } /** * Returns assoc array with settings from ini-file. * * @param string $cfgFile * @return array */ private static function loadConfig($cfgFile) { if (!is_readable($cfgFile)) { echo 'Telescope: Can not read ini file'.PHP_EOL; exit(1); } $aIni = parse_ini_file($cfgFile); // Evaluate ini-path // WARNING: Result of realpath() inside of shutdown functions is '/' (root) $logDir = $aIni['error_log_dir']; if (0 !== strpos($logDir, '/')) { $logDir = dirname($cfgFile).'/'.$logDir; $aIni['error_log_dir'] = realpath($logDir); } $logDir = $aIni['env_log_dir']; if (0 !== strpos($logDir, '/')) { $logDir = dirname($cfgFile).'/'.$logDir; $aIni['env_log_dir'] = realpath($logDir); } // check ini keys if (self::$mode ^ self::MODE_TRACING) { $aRequiredSettings = array( 'error_reporting', 'report_emails', 'report_title', 'redirect_on_error', 'error_log_dir', 'error_log_limit', 'error_num_notify', 'env_log_dir', 'env_log_limit', 'backtrace_max_depth', 'code_snippet_radius', 'syntax_highlight', 'pretty_sql', ); $aIniBase = $aIni; for ($i = 1; $i < 10; $i++) { unset($aIniBase["highlight_regex_$i"], $aIniBase["highlight_style_$i"]); } unset($aIniBase['background_color'], $aIniBase['color']); if ((count($aIniBase) !== count($aRequiredSettings)) || array_diff(array_keys($aIniBase), $aRequiredSettings)) { echo 'Telescope: Bad config data'; echo PHP_EOL; exit(2); } if (!is_writable($aIni['error_log_dir'])) { echo 'Telescope: Can not write to file. Checks "error_log_dir" option'; echo PHP_EOL; exit(3); } if (!is_writable($aIni['env_log_dir'])) { echo 'Telescope: Can not write to file. Checks "env_log_dir" option'; echo PHP_EOL; exit(3); } } $aDefaultSettings = array( 'report_title' => 'Telescope crash report #%s', 'error_log_limit' => 10, 'error_num_notify' => 1, 'env_log_limit' => 10, 'backtrace_max_depth' => 4, 'code_snippet_radius' => 7, 'background_color' => '#FFF', 'color' => 'blue', ); for ($i = 1; $i < 10; $i++) { $aDefaultSettings["highlight_regex_$i"] = ''; $aDefaultSettings["highlight_style_$i"] = ''; } foreach ($aDefaultSettings as $k => $v) { if (empty($aIni[$k])) { $aIni[$k] = $v; } } return $aIni; } /** * Returns value of config variable. * * @param string $varName * @return string */ private static function getConfigVar($varName) { return self::$aConfig[$varName]; } /** * Access to super global array. * * @param string $varName * @param string $defValue * @return string|array */ private static function getServerVar($varName, $defValue='') { return @$_SERVER[$varName] ?: $defValue; } /** * Checks if we work in Command Line Interface. * * @return bool */ private static function isCli() { return empty($_SERVER['REQUEST_URI']); } /** * Return self name. * * @return string */ public static function getEngineName() { return 'Telescope ' . self::VERSION; } /** * Returns internal mode value. * * @return int */ public static function getMode() { return self::$mode; } /** * Checks current mode. * * @param self::MODE_TRACING|self::MODE_DISPLAY|self::MODE_LOG $mode * @return bool */ public static function hasMode($mode) { return (self::$mode & $mode) > 0; } /** * Returns start moment as timestamp. * * @return int */ public static function getStartTime() { return self::$timeStart; } /** * Set info about executed SQL that uses in result report. * * Examples: * <code> * Telescope::shootQueries(array( * array( * 'SELECT * FROM some_table', // SQL * 0.023, // time of performance (in seconds) * '0', // index of connection as string * 'SomeClass->getMyData()' // bookmark * ), * array( * 'SELECT * FROM some_table2', * 0.018, * '1', * 'SomeClass::getAllData()' * ) * )); * </code> * * @param array $aSQL */ public static function shootQueries(array $aSQL) { self::$queryShot = $aSQL; } /** * Add info about executed SQL that uses in result report. * * Examples: * <code> * Telescope::shootQuery(array( * 'SELECT * FROM some_table', // SQL * 0.023, // time of performance (in seconds) * '0', // index of connection as string * 'SomeClass->getMyData()' // bookmark * )); * </code> * * @param array $aQueryInfo */ public static function shootQuery(array $aQueryInfo) { self::$queryShot[] = $aQueryInfo; } /** * Add info to report. * <var>$aPoint</var> should be used when method calls through * proxy-function * * Examples: * <code> * function telescope($var, $title='') { * if(!class_exists('\razxc\telescope\Telescope', false)) return; * $point = current(debug_backtrace()); * \razxc\telescope\Telescope::shoot($var, $title, $point); * } * </code> * * @param mixed $var * @param string $title * @param array|false|null $aPoint */ public static function shoot($var, $title=null, $aPoint=null) { if($title === null) { $title = 'Snapshot'; } $aPoint = $aPoint ?: current(debug_backtrace()); self::$shots[] = array($title, $var, $aPoint); } /** * Generates detailed report as full-formed HTML page for normal mode. * Make sense on shutdown. * Possible options: * - `html_title` * - `tpl_path` * * @param array $options * @return string * @see onShutdown() */ public static function buildDetailReport(array $options=array()) { $tplPath = @$options['tpl_path']; $reportTitle = @$options['html_title']; $vars = array( 'backgroundColor' => self::getConfigVar('background_color'), 'color' => self::getConfigVar('color'), 'DOC_TITLE' => $reportTitle ?: self::getEnvTitle(), 'ENV_REPORT' => self::getEnvBody(), 'GENERATOR' => self::getEngineName(), ); if ($tplPath) { return self::phpTemplate($tplPath, $vars); } return self::template('details-page.tpl.php', $vars); } /** * Returns report title depends on console or web server mode. * * @return string */ private static function getEnvTitle() { if (self::isCli()) { $title = self::getConsoleFilepath(); } else { $m = self::getServerVar('REQUEST_METHOD'); $title = $m . ' ' . urldecode(self::getRequestUrl()); } return $title; } /** * Returns full path to executed file. * * @return string */ private static function getConsoleFilepath() { return $_SERVER['SCRIPT_FILENAME']; } /** * Returns 'http' or 'https'. * * @return string */ private static function getServerProtocol() { return isset($_SERVER['HTTPS']) ? 'https' : 'http'; } /** * Returns current full web URL which was requested. * * @return string */ private static function getRequestUrl() { $sProtocol = self::getServerProtocol(); $sHost = self::getServerVar('SERVER_NAME'); $sPort = self::getServerVar('SERVER_PORT'); $sRequestUri = self::getServerVar('REQUEST_URI'); if ('80' === $sPort) { $sPort = ''; } if ('https' === $sProtocol) { $sPort = ''; } if ($sPort) { $sPort = ':' . $sPort; } return $sProtocol . '://' . $sHost . $sPort . $sRequestUri; } /** * Returns environment report. * * @return string */ private static function getEnvBody() { if(self::$sReport === null) { self::$sReport = self::buildEnvBody(); } return self::$sReport; } /** * Builds environment report (should be called only once). * * @return string */ private static function buildEnvBody() { $queryShot = array(); $queriesTime = 0; $indent = ''; foreach (self::$queryShot as $i => $query) { list($sSql, $queryTime, $conn, $sInfo, $point) = $query; if (self::getConfigVar('pretty_sql')) { $sSql = self::prettifySql($sSql); } $queriesTime += $queryTime; $shot = array(); $shot[] = "$indent-- #$i"; $shot[] = self::htmlEsc(str_replace("\n", "\n$indent", $sSql)); $shot[] = "$indent-- " . sprintf('%2.6f', $queryTime) . ' s.'; if ($conn) { $shot[] = "$indent-- connection: $conn"; } if ($sInfo) { $shot[] = "$indent-- " . $sInfo; } if ($point) { if (isset($point['file'])) { $shot[] = "$indent-- " . $point['file'] . ':' . $point['line']; } else { $shot[] = "$indent-- " . self::_call_name($point); } } $queryShot[] = $shot; } if (self::isCli()) { $location = self::getConsoleFilepath(); } else { $m = self::getServerVar('REQUEST_METHOD'); $location = $m . ' ' . urldecode(self::getRequestUrl()); } return self::template('details.tpl.php', array( 'origMethod' => self::getServerVar('REQUEST_METHOD'), 'origUrl' => self::getRequestUrl(), 'location4human' => $location, 'SERVER' => self::htmlEscapeRecursive($_SERVER), 'GET' => empty($_GET) ? null : self::htmlEscapeRecursive($_GET), 'POST' => empty($_POST) ? null : self::htmlEscapeRecursive($_POST), 'COOKIE' => empty($_COOKIE) ? null : self::htmlEscapeRecursive($_COOKIE), 'FILES' => empty($_FILES) ? null : self::htmlEscapeRecursive($_FILES), 'SESSION' => empty($_SESSION) ? null : self::htmlEscapeRecursive($_SESSION), 'memoryUsage' => function_exists('memory_get_usage') ? memory_get_usage(true) : null, 'shots' => self::htmlEscapeRecursive(self::$shots), 'queryShot' => $queryShot, 'queriesTime' => $queriesTime, 'aIncludedFiles' => self::getIncludedFiles(), 'contentLength' => self::$contentLength, 'performanceTime' => microtime(true) - self::$timeStart, 'HEADERS_REQUEST' => self::headersOnRequest(), 'HEADERS_RESPONSE' => self::headersOnResponse(), )); } public static function headersOnRequest() { $headers = array(); foreach ($_SERVER as $key => $value) { // RFC2616 (HTTP/1.1) defines header fields as case-insensitive entities. if (0 === stripos($key, 'http_')) { $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))))] = $value; } } return $headers; } public static function headersOnResponse() { return headers_list(); } /** * @return array */ private static function getIncludedFiles() { $fnMatch = function($filename, $re) { return $re ? preg_match($re, $filename) : false; }; $aIncludedFiles = get_included_files(); foreach ($aIncludedFiles as &$path) { for ($i = 1; $i < 10; $i++) { $regex = self::getConfigVar("highlight_regex_$i"); $style = self::getConfigVar("highlight_style_$i"); if ($style && $fnMatch($path, $regex)) { $path = '<span style="' . $style . '">' . $path . '</span>'; break; } } } return $aIncludedFiles; } /** * Prevents cases when allowed memory size exhausted and * error reporting failed. * * @param float $percent */ private static function increaseMemoryLimit($percent=.3) { $memoryLimitValue = ini_get('memory_limit'); if (!is_numeric($memoryLimitValue)) { $value = substr($memoryLimitValue, 0, -1); $unit = strtolower(substr($memoryLimitValue, -1)); switch ($unit) { case 'g': $value *= 1024; case 'm': $value *= 1024; case 'k': $value *= 1024; } $memoryLimitValue = $value; } ini_set('memory_limit', (int) ($memoryLimitValue * (1 + $percent))); } private static function htmlEsc($str) { return htmlspecialchars($str); } /** * Sanitize array data for HTML output. * * @param array $a * @return array */ public static function htmlEscapeRecursive(array $a) { array_walk_recursive($a, function(&$v) { if (is_object($v) || is_a($v, '__PHP_Incomplete_Class')) { return; } $v = self::htmlEsc($v); }); return $a; } private static function template($tplFileName, array $aVars=array()) { return self::phpTemplate(__DIR__."/tpls/$tplFileName", $aVars); } /** * Native template engine by <code>include()</code> * * @param string $__filename * @param array $__vars * @return string */ public static function phpTemplate($__filename, array $__vars=array()) { extract($__vars, EXTR_SKIP); ob_start(); include $__filename; return ob_get_clean(); } /** * The simplest logging. * On empty <var>$filename</var> store data in the buffer, * otherwise write data in file and reset buffer. * Returns <code>true</code> if data saved in file. * * Examples: * <code> * // Add prefix date and message * self::log($someVar, 'myLogFile'); * // Rewrite file message * self::log($someVar, 'myLogFile', "w"); * // Rewrite file message without prefix * self::log($someVar, 'myLogFile', "w", false); * // Rewrite file message with headline * self::log($someVar, 'myLogFile', "w", 'some header'); * </code> * * @param mixed $mixVar * @param string $fileName * @param string $fileMode * @param mixed $headLine * date in format "Y-m-d H:i:s (e), D" is default value * @return bool * @throws RuntimeException */ public static function log( $mixVar, $fileName = '', $fileMode = 'a', $headLine = true ) { static $log = ''; if (!$log && $headLine !== false) { $s = ($headLine === true) ? date('Y-m-d H:i:s (e), D') : $headLine; $log .= $s . PHP_EOL; } switch (gettype($mixVar)) { case 'array': $str = var_export($mixVar, 1); break; case 'object': $str = var_export(get_object_vars($mixVar), 1); break; case 'boolean': $str = $mixVar ? 'true' : 'false'; break; default: $str = $mixVar; } $log .= $str . PHP_EOL; if ($fileName) { $f = fopen($fileName, $fileMode . 'b'); if (!$f) { throw new RuntimeException('Telescope: Can not open log file'); } flock($f, LOCK_EX); fwrite($f, $log . PHP_EOL); flock($f, LOCK_UN); fclose($f); $log = ''; return true; // wrote } return false; // saved in memory } /** * Formats SQL string to human view. * * @param string $sql * @return string */ private static function prettifySql($sql) { $sql = preg_replace('~\s+(SELECT|FROM|WHERE|ORDER BY|GROUP BY|LIMIT|LEFT JOIN|INNER JOIN)\s+~', PHP_EOL . '$1 ', $sql); $sql = preg_replace('~\s+(AND|OR)\s+~', PHP_EOL . "\t" . '$1 ', $sql); return trim($sql); } /** * Template function. Returns code fragment. * * @param string $file * @param int $line * @return string[] */ private static function _fileExcerpt($file, $line) { if (!is_readable($file)) { return array(); } if (self::getConfigVar('syntax_highlight')) { $hlSource = highlight_file($file, true); if (PHP_VERSION_ID < 80300) { $content = substr($hlSource, 36, -15); } else { $content = substr($hlSource, 34, -13); } $codes = array('<br /></span>', '<br />'); $replace = array("</span>\n", "\n"); $content = str_replace($codes, $replace, $content); $allLines = explode("\n", $content); $allLines[0] = '<code><span style="color: #000000">' . $allLines[0]; $allLines[count($allLines)-1] .= '</span></code>'; } else { $allLines = file($file); } return self::getLinesInRadius($allLines, $line-1); } /** * @param array $allLines * @param int $line * @return string[] */ private static function getLinesInRadius(array $allLines, $line) { $radius = (int) self::getConfigVar('code_snippet_radius'); $resultLines = array(); $begin = max($line - $radius, 0); $end = min($line + $radius, count($allLines)-1); for ($i = $begin; $i <= $end; $i++) { $oneLine = $allLines[$i]; $resultLines[$i+1] = $oneLine; } return $resultLines; } /** * Template function * * @param mixed $a * @param int $level * @param string $sym * @return string */ private static function _print_r($a, $level=0, $sym="\t") { $str = ''; if (is_array($a)) { $indent = str_repeat($sym, $level); foreach ($a as $k => $v) { $BR1 = ''; $BR2 = ''; if (is_array($v)) { $BR1 = "array(\n\n"; $BR2 = $indent.")\n"; } $ss = self::_print_r($v, $level + 1); $str .= "\t[".$indent.$k. "] => $BR1$ss$BR2"; } return $str; } if (is_a($a, '__PHP_Incomplete_Class')) { $a = get_object_vars($a); // // alternative: $a = (array)$var; $cls = $a['__PHP_Incomplete_Class_Name']; return "__PHP_Incomplete_Class_Name:$cls,\n"; } return $a . ",\n"; } /** * Template function. Returns code call as string. * * @param array $aTrace * @return string */ private static function _call_name(array $aTrace) { if (isset($aTrace['class'])) { return $aTrace['class'] . $aTrace['type'] . $aTrace['function']; } return $aTrace['function']; } private static function sendMailBy($listTo, $subject, $message) { if (!$listTo) { return; } $a = explode(',', $listTo); foreach ($a as &$to) { $to = trim($to); self::sendMailTo($to, $subject, $message); } } /** * Send email: used <code>mail()</code> function. * * @param array|string $to * @param string $subject * @param string $message * @return bool */ private static function sendMailTo($to, $subject, $message) { if (is_array($to)) { $to = '=?UTF-8?B?' . base64_encode($to[0]) . '?= <' . $to[1] . '>'; } $isHtml = (false !== strpos($message, '</')); $contentType = 'text/plain'; if ($isHtml) { $contentType = 'text/html'; } $subject = '=?UTF-8?B?' . base64_encode($subject) . '?='; $mailHeaders = array( 'MIME-Version: 1.0', 'Content-type: ' . $contentType . '; charset=utf-8', 'Content-Transfer-Encoding: base64', 'Date: ' . date('r (T)'), 'X-Mailer: PHP ' . PHP_VERSION . '/' . self::getEngineName(), ); $header = implode($eol = "\r\n", $mailHeaders) . $eol; return mail($to, $subject, base64_encode($message), $header); } } } // namespace