* @copyright 2016-2017 GFI Informatique, 2016-2017 TNT * @license https://opensource.org/licenses/MIT MIT License */ require_once _PS_MODULE_DIR_.'tntofficiel/tntofficiel.php'; class TNTOfficiel_Debug { /** * @var bool Is debugging enabled ? */ private static $boolEnabled = false; /** * @var array List of allowed client IP. No client IP means all allowed. */ private static $arrRemoteIPAddressAllowed = array(); /** * @var bool Backtrace auto added. */ private static $boolBackTraceAuto = true; /** * @var int Backtrace maximum items. */ private static $intBackTraceMaxDeep = 1; //64; /** * @var bool Backtrace arguments detail at maximum. */ private static $boolBackTraceArgsDetail = false; /** * @var array */ private static $arrPHPErrorExclude = array( E_WARNING => array( '/^filemtime\(\): stat failed for/ui' ), //E_NOTICE => array() ); /** * @var array PHP Errors constant name list. */ private static $arrPHPErrorNames = array( 'E_ERROR', 'E_RECOVERABLE_ERROR', 'E_WARNING', 'E_PARSE', 'E_NOTICE', 'E_STRICT', 'E_DEPRECATED', // PHP 5.3+ 'E_CORE_ERROR', 'E_CORE_WARNING', 'E_COMPILE_ERROR', 'E_COMPILE_WARNING', 'E_USER_ERROR', 'E_USER_WARNING', 'E_USER_NOTICE', 'E_USER_DEPRECATED' // PHP 5.3+ ); /** * @var array PHP Errors map */ private static $arrPHPErrorMap = null; /** * @var array JSON Errors constant name list. PHP 5.3.0+. */ private static $arrJSONErrorNames = array( 'JSON_ERROR_NONE', 'JSON_ERROR_DEPTH', 'JSON_ERROR_STATE_MISMATCH', 'JSON_ERROR_CTRL_CHAR', 'JSON_ERROR_SYNTAX', 'JSON_ERROR_UTF8', // PHP 5.3.3+ 'JSON_ERROR_RECURSION', // PHP 5.5.0+ 'JSON_ERROR_INF_OR_NAN', // PHP 5.5.0+ 'JSON_ERROR_UNSUPPORTED_TYPE', // PHP 5.5.0+ ); /** * @var array JSON Errors map. PHP 5.3.0+. */ private static $arrJSONErrorMap = null; /** * @var bool */ private static $boolHandlerRegistered = false; /** * @var string */ private static $strRoot = null; /** * @var string */ private static $strFileName = null; /** * Prevent Construct. */ final private function __construct() { trigger_error(sprintf('%s() %s is static.', __FUNCTION__, get_class($this)), E_USER_ERROR); } /** * Check if client IP address allowed to use debug. * * @return bool */ public static function isClientIPAddressAllowed() { $strRemoteIPAddress = array_key_exists('REMOTE_ADDR', $_SERVER) ? $_SERVER['REMOTE_ADDR'] : null; $boolIPAllowed = count(TNTOfficiel_Debug::$arrRemoteIPAddressAllowed) === 0 || in_array($strRemoteIPAddress, TNTOfficiel_Debug::$arrRemoteIPAddressAllowed, true) === true; return $boolIPAllowed; } /** * Encode to JSON. * @param $arrDebugInfo * @return string */ public static function encJSON($arrDebugInfo) { $flagJSONEncode = 0; $flagJSONEncode |= defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; $flagJSONEncode |= defined('JSON_UNESCAPED_UNICODE') ? JSON_UNESCAPED_UNICODE : 0; $flagJSONEncode |= defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0; // PHP < 5.3 return null if second parameter is used. return $flagJSONEncode === 0 ? json_encode($arrDebugInfo) : json_encode($arrDebugInfo, $flagJSONEncode); } /** * @param int $intArgType * @return string */ public static function getPHPErrorType($intArgType) { // Generate constant name mapping. if (TNTOfficiel_Debug::$arrPHPErrorMap === null) { TNTOfficiel_Debug::$arrPHPErrorMap = array(); foreach (TNTOfficiel_Debug::$arrPHPErrorNames as $strPHPErrorTypeName) { if (defined($strPHPErrorTypeName)) { $intPHPErrorType = constant($strPHPErrorTypeName); TNTOfficiel_Debug::$arrPHPErrorMap[ $intPHPErrorType ] = $strPHPErrorTypeName; } } } $strPHPErrorType = array_key_exists($intArgType, TNTOfficiel_Debug::$arrPHPErrorMap) ? TNTOfficiel_Debug::$arrPHPErrorMap[ $intArgType ] : (string)$intArgType; return $strPHPErrorType; } /** * @param int $intArgType * @return string */ public static function getJSONErrorType($intArgType) { // Generate constant name mapping. if (TNTOfficiel_Debug::$arrJSONErrorMap === null) { TNTOfficiel_Debug::$arrJSONErrorMap = array(); foreach (TNTOfficiel_Debug::$arrJSONErrorNames as $strJSONErrorTypeName) { if (defined($strJSONErrorTypeName)) { $intJSONErrorType = constant($strJSONErrorTypeName); TNTOfficiel_Debug::$arrJSONErrorMap[ $intJSONErrorType ] = $strJSONErrorTypeName; } } } $strJSONErrorType = array_key_exists($intArgType, TNTOfficiel_Debug::$arrJSONErrorMap) ? TNTOfficiel_Debug::$arrJSONErrorMap[ $intArgType ] : (string)$intArgType; return $strJSONErrorType; } /** * Capture an Error. * * @param int $intArgType * @param string $strArgMessage * @param string $strArgFile * @param int $intArgLine * @param bool $boolArgIsLast * @return boolean */ public static function captureError($intArgType, $strArgMessage, $strArgFile = null, $intArgLine = 0, $arrArgContext = array(), $boolArgIsLast = false) { $arrLogError = array( 'type' => $boolArgIsLast ? 'LastError' : 'Error' ); if ($strArgFile !== null) { $arrLogError['file'] = $strArgFile; $arrLogError['line'] = $intArgLine; } if ($boolArgIsLast) { $arrLogError['trace'] = array(); } $strType = TNTOfficiel_Debug::getPHPErrorType($intArgType); $arrLogError['msg'] = 'Type '.$strType.': '.$strArgMessage; if (array_key_exists($intArgType, TNTOfficiel_Debug::$arrPHPErrorExclude) && is_array(TNTOfficiel_Debug::$arrPHPErrorExclude[$intArgType])) { if (count(TNTOfficiel_Debug::$arrPHPErrorExclude[$intArgType]) === 0) { return false; } foreach (TNTOfficiel_Debug::$arrPHPErrorExclude[$intArgType] as $k => $r) { if (preg_match($r, $strArgMessage) === 1) { return false; } } } TNTOfficiel_Debug::log($arrLogError); // Internal error handler continues (displays/log error, …) return false; } /** * Capture last Error. * Useful at script end for E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, … */ public static function captureLastError() { $arrPHPLastError = error_get_last(); if (is_array($arrPHPLastError)) { TNTOfficiel_Debug::captureError( $arrPHPLastError['type'], $arrPHPLastError['message'], $arrPHPLastError['file'], $arrPHPLastError['line'], array(), true ); } // PHP 5.3.0+ if (function_exists('json_last_error')) { $intTypeJSONLastError = json_last_error(); $strJSONLastErrorType = TNTOfficiel_Debug::getJSONErrorType($intTypeJSONLastError); // PHP 5.5.0+ $strJSONLastErrorMessage = function_exists('json_last_error_msg') ? json_last_error_msg() : 'N/A'; TNTOfficiel_Debug::captureError($strJSONLastErrorType, $strJSONLastErrorMessage, null, 0, array(), true); } } /** * Capture an Exception. * * @param \Exception $objArgException */ public static function captureException($objArgException) { $arrLogException = array( 'type' => 'Exception' ); if ($objArgException->getFile() !== null) { $arrLogException['file'] = $objArgException->getFile(); $arrLogException['line'] = $objArgException->getLine(); } $arrLogException['msg'] = 'Code '.$objArgException->getCode().': '.$objArgException->getMessage(); $arrLogException['trace'] = $objArgException->getTrace(); TNTOfficiel_Debug::log($arrLogException); } /** * Capture connection status. */ public static function captureConnectionStatus() { // is non normal connection status. $intStatus = connection_status(); // connection_aborted() if ($intStatus & 1) { TNTOfficiel_Debug::log(array( 'type' => 'Shutdown', 'msg' => sprintf('Connection was aborted by user.') )); } if ($intStatus & 2) { TNTOfficiel_Debug::log(array( 'type' => 'Shutdown', 'msg' => sprintf('Script exceeded maximum execution time.') )); } } /** * Capture output buffer status. */ public static function captureOutPutBufferStatus() { $msg = 'Output was not sent yet.'; // is output buffer was sent $strOutputBufferFile = null; $intOutputBufferLine = null; $boolOutputBufferSent = headers_sent($strOutputBufferFile, $intOutputBufferLine); if ($boolOutputBufferSent) { $msg = sprintf('Output was sent in \'%s\' on line %s.', $strOutputBufferFile, $intOutputBufferLine); } TNTOfficiel_Debug::log(array( 'type' => 'Shutdown', 'msg' => $msg, 'headers' => headers_list(), 'level' => ob_get_level(), 'trace' => array() )); } /** * Capture at shutdown. */ public static function captureAtShutdown() { TNTOfficiel_Debug::captureLastError(); TNTOfficiel_Debug::captureConnectionStatus(); TNTOfficiel_Debug::captureOutPutBufferStatus(); TNTOfficiel_Debug::addLogContent(']'); } /** * Register capture handlers once. */ public static function registerHandlers() { if (TNTOfficiel_Debug::$boolHandlerRegistered !== true) { TNTOfficiel_Debug::$boolHandlerRegistered = true; set_error_handler(array('TNTOfficiel_Debug', 'captureError')); set_exception_handler(array('TNTOfficiel_Debug', 'captureException')); register_shutdown_function(array('TNTOfficiel_Debug', 'captureAtShutdown')); } } /** * Get the script start time. * * @return float */ public static function getStartTime() { return (float)(array_key_exists('REQUEST_TIME_FLOAT', $_SERVER) ? $_SERVER['REQUEST_TIME_FLOAT'] : $_SERVER['REQUEST_TIME']); } /** * Set log root directory. * * @param $strArgRoot */ public static function setRootDirectory($strArgRoot) { TNTOfficiel_Debug::$strRoot = null; $strRealpath = realpath($strArgRoot); // If not a writable directory. if ($strRealpath === false || !is_dir($strRealpath) || !is_writable($strRealpath)) { return false; } // Add final separator. if (mb_substr($strRealpath, -1) !== DIRECTORY_SEPARATOR) { $strRealpath .= DIRECTORY_SEPARATOR; } // Save. TNTOfficiel_Debug::$strRoot = $strRealpath; return true; } /** * Get log filename. * * @return string */ public static function getFilename() { // If root directory is defined, but not the filename. if (TNTOfficiel_Debug::$strRoot !== null && TNTOfficiel_Debug::$strFileName === null) { $floatScriptTime = TNTOfficiel_Debug::getStartTime(); $strTimeStamp = var_export($floatScriptTime, true); $strTimeStamp = preg_replace('/^([0-9]+(?:\.[0-9]{1,6}))[0-9]*$/ui', '$1', $strTimeStamp); $strTimeStamp = preg_replace('/\./ui', '', $strTimeStamp); $strTimeStamp = sprintf('%-016s',$strTimeStamp); $strFileNameSuffix = $strTimeStamp.'_'.preg_replace('/[^a-z0-9_]+/ui', '-', parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); TNTOfficiel_Debug::$strFileName = TNTOfficiel_Debug::$strRoot.'debug_'.$strFileNameSuffix.'.json'; } return TNTOfficiel_Debug::$strFileName; } /** * Create log file if do not exist. * Log global info at creation. * * @param string $strArgDebugInfo * @return bool */ public static function addLogContent($strArgDebugInfo = '') { $strFileName = TNTOfficiel_Debug::getFilename(); if ($strFileName === null) { return false; } // If file don't already exist. if (!file_exists($strFileName)) { $arrDebugInfo = array( 'type' => 'StartInfo', 'uri' => array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : null, 'referer' => array_key_exists('HTTP_REFERER', $_SERVER) ? $_SERVER['HTTP_REFERER'] : null, 'client' => array_key_exists('REMOTE_ADDR', $_SERVER) ? $_SERVER['REMOTE_ADDR'] : null, 'post' => $_POST, 'get' => $_GET, 'cookie' => $_COOKIE ); $strDebugInfo = '['.TNTOfficiel_Debug::encJSON($arrDebugInfo).','; $strArgDebugInfo = $strDebugInfo.$strArgDebugInfo; } file_put_contents($strFileName, $strArgDebugInfo, FILE_APPEND); } /** * Append log info. * * @param null $arrArg */ public static function log($arrArg = null) { // If not enabled or client IP not allowed. if (TNTOfficiel_Debug::$boolEnabled !== true || TNTOfficiel_Debug::isClientIPAddressAllowed() !== true) { return; } // Set default path. //TNTOfficiel_Debug::setRootDirectory(_PS_ROOT_DIR_.'/log/'); TNTOfficiel_Debug::setRootDirectory(_PS_MODULE_DIR_.TNTOfficiel::MODULE_NAME.'/'.TNTOfficiel::LOG_DIR); // Register handlers if not. TNTOfficiel_Debug::registerHandlers(); if (!is_array($arrArg)) { $arrArg = array('raw' => $arrArg); } // If message, file and line exist, then concat. if (array_key_exists('msg', $arrArg) && array_key_exists('file', $arrArg) && array_key_exists('line', $arrArg)) { $arrArg['msg'] .= ' \''.$arrArg['file'].'\' on line '.$arrArg['line']; unset($arrArg['file']); unset($arrArg['line']); } // If no backtrace and auto backtrace set. if (TNTOfficiel_Debug::$boolBackTraceAuto === true && (!array_key_exists('trace', $arrArg) || !is_array($arrArg['trace'])) ) { // Get current one. $arrArg['trace'] = debug_backtrace(); // Remove trace from this current method. array_shift($arrArg['trace']); } // Process backtrace. if (array_key_exists('trace', $arrArg) && is_array($arrArg['trace'])) { // Final backtrace. $arrTraceStack = array(); // Get each trace, deeper first. while ($arrTrace = array_shift($arrArg['trace'])) { $intDeepIndex = count($arrTraceStack); // Get stack with maximum items. if ($intDeepIndex >= TNTOfficiel_Debug::$intBackTraceMaxDeep) { break; } // function if (array_key_exists('class', $arrTrace) && is_string($arrTrace['class'])) { // Exclude this class. if (in_array($arrTrace['class'], array(__CLASS__), true)) { continue; } $arrTrace['function'] = $arrTrace['class'].$arrTrace['type'].$arrTrace['function']; } // file if (array_key_exists('file', $arrTrace) && is_string($arrTrace['file'])) { $arrTrace['file'] = '\''.$arrTrace['file'].'\' on line '.$arrTrace['line']; } else { $arrTrace['file'] = '[Internal]'; } // arguments $arrCallerArgs = array(); if (array_key_exists('args', $arrTrace) && is_array($arrTrace['args'])) { foreach ($arrTrace['args'] as $k => $mxdTraceArg) { if (is_scalar($mxdTraceArg) || $mxdTraceArg === null) { $arrCallerArgs[ $k ] = $mxdTraceArg; } else { $arrCallerArgs[ $k ] = '('.gettype($mxdTraceArg).')'; if (is_object($mxdTraceArg)) { $arrCallerArgs[ $k ] .= get_class($mxdTraceArg); } } } $arrTrace['function'] .= '('.count($arrCallerArgs).')'; } unset($arrTrace['line']); unset($arrTrace['class']); unset($arrTrace['type']); unset($arrTrace['object']); if (TNTOfficiel_Debug::$boolBackTraceArgsDetail === true) { // Detecting circular reference, etc ... try { serialize($arrTrace['args']); } catch (Exception $e) { $arrTrace['args'] = $arrCallerArgs; } $strArgsJSON = TNTOfficiel_Debug::encJSON($arrTrace['args']); // If unable to encode. if (mb_strlen($strArgsJSON) == 0) { $arrTrace['args'] = $arrCallerArgs; } } else { $arrTrace['args'] = $arrCallerArgs; } // If no arguments. if (count($arrCallerArgs) === 0) { // Remove key (no line output). unset($arrTrace['args']); } // Add trace. $arrTraceStack[ $intDeepIndex ] = $arrTrace; } // Save processed backtrace. $arrArg['trace'] = $arrTraceStack; // Remove backtrace key if empty. if (count($arrArg['trace']) === 0) { unset($arrArg['trace']); } } // Append time and memory consumption. $arrArg += array( 'time' => microtime(true) - TNTOfficiel_Debug::getStartTime(), 'mem' => memory_get_peak_usage() / 1024 / 1024, ); // List of sorted selected key. $arrKeyExistSort = array_intersect_key(array_flip( array('time', 'mem', 'type', 'msg', 'file', 'line', 'trace', 'dump') ), $arrArg); // List of unsorted key left. $arrKeyUnExistUnSort = array_diff_key($arrArg, $arrKeyExistSort); // Append unsorted list to sorted. $arrArg = array_merge($arrKeyExistSort, $arrArg) + $arrKeyUnExistUnSort; $strDebugInfo = TNTOfficiel_Debug::encJSON($arrArg).','; $strDebugInfo = preg_replace('/}\s*,\s*{/ui', '},{', $strDebugInfo); TNTOfficiel_Debug::addLogContent($strDebugInfo); } }