* @author Christoph Dorn * @link https://TheTempusProject.com/TempusDebugger * @license https://opensource.org/licenses/MIT [MIT LICENSE] * @package TempusDebugger */ namespace TheTempusProject\Canary\Classes; use \ReflectionClass; class TempusDebugger { const VERSION = '1.1'; const LOG = 'LOG'; const INFO = 'INFO'; const WARN = 'WARN'; const ERROR = 'ERROR'; const DUMP = 'DUMP'; const TRACE = 'TRACE'; const EXCEPTION = 'EXCEPTION'; const TABLE = 'TABLE'; const GROUP_START = 'GROUP_START'; const GROUP_END = 'GROUP_END'; protected static $instance = null; protected $inExceptionHandler = false; protected $throwErrorExceptions = true; protected $convertAssertionErrorsToExceptions = true; protected $throwAssertionExceptions = false; protected $messageIndex = 1; protected $options = [ 'maxDepth' => 10, 'maxObjectDepth' => 5, 'maxArrayDepth' => 5, 'useNativeJsonEncode' => true, 'includeLineNumbers' => true ]; protected $objectFilters = [ 'TempusDebugger' => [ 'objectStack', 'instance', 'json_objectStack', ], ]; protected $objectStack = []; protected $enabled = true; protected $logToInsightConsole = null; /** * When the object gets serialized only include specific object members. * * @return array */ public function __sleep() { return [ 'options', 'objectFilters', 'enabled' ]; } /** * Gets singleton instance of TempusDebugger * * @param boolean $autoCreate * @return TempusDebugger */ public static function getInstance( $autoCreate = false ) { if ( $autoCreate === true && !self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Set an Insight console to direct all logging calls to * * @param object $console The console object to log to * @return void */ public function setLogToInsightConsole( $console ) { if ( is_string( $console ) ) { if ( get_class( $this ) != 'TempusDebugger_Insight' && !is_subclass_of( $this, 'TempusDebugger_Insight' ) ) { throw new Exception( 'TempusDebugger instance not an instance or subclass of TempusDebugger_Insight!' ); } $this->logToInsightConsole = $this->to( 'request' )->console( $console ); } else { $this->logToInsightConsole = $console; } } /** * Enable and disable logging to Firebug * * @param boolean $enabled TRUE to enable, FALSE to disable * @return void */ public function setEnabled( $enabled ) { $this->enabled = $enabled; } /** * Check if logging is enabled * * @return boolean TRUE if enabled */ public function getEnabled() { return $this->enabled; } /** * Specify a filter to be used when encoding an object * * Filters are used to exclude object members. * * @param string $class The class name of the object * @param array $filter An array of members to exclude * @return void */ public function setObjectFilter( $class, $filter ) { $this->objectFilters[strtolower( $class )] = $filter; } /** * Set some options for the library * * Options: * - maxDepth: The maximum depth to traverse (default: 10) * - maxObjectDepth: The maximum depth to traverse objects (default: 5) * - maxArrayDepth: The maximum depth to traverse arrays (default: 5) * - useNativeJsonEncode: If true will use json_encode() (default: true) * - includeLineNumbers: If true will include line numbers and filenames (default: true) * * @param array $options The options to be set * @return void */ public function setOptions( $options ) { $this->options = array_merge( $this->options, $options ); } /** * Get options from the library * * @return array The currently set options */ public function getOptions() { return $this->options; } /** * Set an option for the library * * @param string $name * @param mixed $value * @return void * @throws Exception */ public function setOption( $name, $value ) { if ( !isset( $this->options[$name] ) ) { throw $this->newException( 'Unknown option: ' . $name ); } $this->options[$name] = $value; } /** * Get an option from the library * * @param string $name * @return mixed * @throws Exception */ public function getOption( $name ) { if ( !isset( $this->options[$name] ) ) { throw $this->newException( 'Unknown option: ' . $name ); } return $this->options[$name]; } /** * Register TempusDebugger as your error handler * * Will throw exceptions for each php error. * * @return mixed Returns a string containing the previously defined error handler (if any) */ public function registerErrorHandler( $throwErrorExceptions = false ) { //NOTE: The following errors will not be caught by this error handler: // E_ERROR, E_PARSE, E_CORE_ERROR, // E_CORE_WARNING, E_COMPILE_ERROR, // E_COMPILE_WARNING, E_STRICT $this->throwErrorExceptions = $throwErrorExceptions; return set_error_handler( [ $this, 'errorHandler' ] ); } /** * TempusDebugger's error handler * * Throws exception for each php error that will occur. * * @param integer $errno * @param string $errstr * @param string $errfile * @param integer $errline * @param array $errcontext */ public function errorHandler( $errno, $errstr, $errfile, $errline, $errcontext ) { // Don't throw exception if error reporting is switched off if ( error_reporting() == 0 ) { return; } // Only throw exceptions for errors we are asking for if ( error_reporting() && $errno ) { $exception = new ErrorException( $errstr, 0, $errno, $errfile, $errline ); if ( $this->throwErrorExceptions ) { throw $exception; } else { $this->tt( $exception ); } } } /** * Register TempusDebugger as your exception handler * * @return mixed Returns the name of the previously defined exception handler, * or NULL on error. * If no previous handler was defined, NULL is also returned. */ public function registerExceptionHandler() { return set_exception_handler( [ $this, 'exceptionHandler' ] ); } /** * TempusDebugger's exception handler * * Logs all exceptions to your the console and then stops the script. * * @param Exception $exception * @throws Exception */ public function exceptionHandler( $exception ) { $this->inExceptionHandler = true; header( 'HTTP/1.1 500 Internal Server Error' ); try { $this->tt( $exception ); } catch ( Exception $e ) { echo 'We had an exception: ' . $e; } $this->inExceptionHandler = false; } /** * Register TempusDebugger driver as your assert callback * * @param boolean $convertAssertionErrorsToExceptions * @param boolean $throwAssertionExceptions * @return mixed Returns the original setting or FALSE on errors */ public function registerAssertionHandler( $convertAssertionErrorsToExceptions = true, $throwAssertionExceptions = false ) { $this->convertAssertionErrorsToExceptions = $convertAssertionErrorsToExceptions; $this->throwAssertionExceptions = $throwAssertionExceptions; if ( $throwAssertionExceptions && !$convertAssertionErrorsToExceptions ) { throw $this->newException( 'Cannot throw assertion exceptions as assertion errors are not being converted to exceptions!' ); } return assert_options( ASSERT_CALLBACK, [ $this, 'assertionHandler' ] ); } /** * TempusDebugger's assertion handler * * Logs all assertions to your the console and then stops the script. * * @param string $file File source of assertion * @param integer $line Line source of assertion * @param mixed $code Assertion code */ public function assertionHandler( $file, $line, $code ) { if ( $this->convertAssertionErrorsToExceptions ) { $exception = new ErrorException( 'Assertion Failed - Code[ ' . $code . ' ]', 0, null, $file, $line ); if ( $this->throwAssertionExceptions ) { throw $exception; } else { $this->tt( $exception ); } } else { $this->tt( $code, 'Assertion Failed', TempusDebugger::ERROR, [ 'File' => $file, 'Line' => $line ] ); } } /** * Start a group for following messages. * * Options: * Collapsed: [true|false] * Color: [#RRGGBB|ColorName] * * @param string $name * @param array $options OPTIONAL Instructions on how to log the group * @return true * @throws Exception */ public function group( $name, $options = null ) { if ( !$name ) { throw $this->newException( 'You must specify a label for the group!' ); } if ( $options ) { if ( !is_array( $options ) ) { throw $this->newException( 'Options must be defined as an array!' ); } if ( array_key_exists( 'Collapsed', $options ) ) { $options['Collapsed'] = ( $options['Collapsed'] ) ? 'true' : 'false'; } } return $this->tt( null, $name, TempusDebugger::GROUP_START, $options ); } /** * Ends a group you have started before * * @return true * @throws Exception */ public function groupEnd() { return $this->tt( null, null, TempusDebugger::GROUP_END ); } /** * Log object with label to the console * * @see TempusDebugger::LOG * @param mixes $object * @param string $label * @return true * @throws Exception */ public function log( $object, $label = null, $options = [] ) { return $this->tt( $object, $label, TempusDebugger::LOG, $options ); } /** * Log object with label to the console * * @see TempusDebugger::INFO * @param mixes $object * @param string $label * @return true * @throws Exception */ public function info( $object, $label = null, $options = [] ) { return $this->tt( $object, $label, TempusDebugger::INFO, $options ); } /** * Log object with label to the console * * @see TempusDebugger::WARN * @param mixes $object * @param string $label * @return true * @throws Exception */ public function warn( $object, $label = null, $options = [] ) { return $this->tt( $object, $label, TempusDebugger::WARN, $options ); } /** * Log object with label to the console * * @see TempusDebugger::ERROR * @param mixes $object * @param string $label * @return true * @throws Exception */ public function error( $object, $label = null, $options = [] ) { return $this->tt( $object, $label, TempusDebugger::ERROR, $options ); } /** * Dumps key and variable to the console * * @see TempusDebugger::DUMP * @param string $key * @param mixed $variable * @return true * @throws Exception */ public function dump( $key, $variable, $options = [] ) { if ( !is_string( $key ) ) { throw $this->newException( 'Key passed to dump() is not a string' ); } if ( strlen( $key ) > 100 ) { throw $this->newException( 'Key passed to dump() is longer than 100 characters' ); } if ( !preg_match_all( '/^[a-zA-Z0-9-_\.:]*$/', $key, $m ) ) { throw $this->newException( 'Key passed to dump() contains invalid characters [a-zA-Z0-9-_\.:]' ); } return $this->tt( $variable, $key, TempusDebugger::DUMP, $options ); } /** * Log a trace in the console * * @see TempusDebugger::TRACE * @param string $label * @return true * @throws Exception */ public function trace( $label ) { return $this->tt( $label, TempusDebugger::TRACE ); } /** * Log a table in the console * * @see TempusDebugger::TABLE * @param string $label * @param string $table * @return true * @throws Exception */ public function table( $label, $table, $options = [] ) { return $this->tt( $table, $label, TempusDebugger::TABLE, $options ); } /** * Insight API wrapper * * @see Insight_Helper::to() */ public static function to() { $instance = self::getInstance(); if ( !method_exists( $instance, '_to' ) ) { throw new Exception( 'TempusDebugger::to() implementation not loaded' ); } $args = func_get_args(); return call_user_func_array( [ $instance, '_to' ], $args ); } /** * Insight API wrapper * * @see Insight_Helper::plugin() */ public static function plugin() { $instance = self::getInstance(); if ( !method_exists( $instance, '_plugin' ) ) { throw new Exception( 'TempusDebugger::plugin() implementation not loaded' ); } $args = func_get_args(); return call_user_func_array( [ $instance, '_plugin' ], $args ); } /** * Check if TempusDebugger is installed on client * * @return boolean */ public function detectClientExtension() { // Check if TempusDebugger is installed on client via User-Agent header if ( @preg_match_all( '/\sTempusDebugger\/([\.\d]*)\s?/si', $this->getUserAgent(), $m ) && version_compare( $m[1][0], '0.0.6', '>=' ) ) { return true; // Check if TempusDebugger is installed on client via X-TempusDebugger-Version header } elseif ( @preg_match_all( '/^([\.\d]*)$/si', $this->getRequestHeader( 'X-TempusDebugger-Version' ), $m ) && version_compare( $m[1][0], '0.0.6', '>=' ) ) { return true; } return false; } /** * Log varible to the console * * @param mixed $object The variable to be logged * @return boolean Return TRUE if message was added to headers, FALSE otherwise * @throws Exception */ public function tt( $object ) { if ( $this instanceof TempusDebugger_Insight && method_exists( $this, '_logUpgradeClientMessage' ) ) { if ( !TempusDebugger_Insight::$upgradeClientMessageLogged ) { // avoid infinite recursion as _logUpgradeClientMessage() logs a message $this->_logUpgradeClientMessage(); } } static $insightGroupStack = []; if ( !$this->getEnabled() ) { return false; } if ( $this->headersSent( $filename, $linenum ) ) { // If we are logging from within the exception handler we cannot throw another exception if ( $this->inExceptionHandler ) { // Simply echo the error out to the page echo '
TempusDebugger ERROR: Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to TempusDebugger. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.
'; } else { throw $this->newException( 'Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to TempusDebugger. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.' ); } } $type = null; $label = null; $options = []; if ( func_num_args() == 1 ) { } elseif ( func_num_args() == 2 ) { switch ( func_get_arg( 1 ) ) { case self::LOG: case self::INFO: case self::WARN: case self::ERROR: case self::DUMP: case self::TRACE: case self::EXCEPTION: case self::TABLE: case self::GROUP_START: case self::GROUP_END: $type = func_get_arg( 1 ); break; default: $label = func_get_arg( 1 ); break; } } elseif ( func_num_args() == 3 ) { $type = func_get_arg( 2 ); $label = func_get_arg( 1 ); } elseif ( func_num_args() == 4 ) { $type = func_get_arg( 2 ); $label = func_get_arg( 1 ); $options = func_get_arg( 3 ); } else { throw $this->newException( 'Wrong number of arguments to tt() function!' ); } if ( $this->logToInsightConsole !== null && ( get_class( $this ) == 'TempusDebugger_Insight' || is_subclass_of( $this, 'TempusDebugger_Insight' ) ) ) { $trace = debug_backtrace(); if ( !$trace ) { return false; } for ( $i = 0; $i < sizeof( $trace ); $i++ ) { if ( isset( $trace[$i]['class'] ) ) { if ($trace[$i]['class'] == 'TempusDebugger') { continue; } } if ( isset( $trace[$i]['file'] ) ) { $path = $this->_standardizePath( $trace[$i]['file'] ); if ( substr( $path, -15, 15 ) == 'TempusTools.php' || substr( $path, -18, 18 ) == 'TempusDebugger.php' ) { continue; } } if ( isset( $trace[$i]['function'] ) && $trace[$i]['function'] == 'tt' && isset( $trace[$i - 1]['file'] ) && substr( $this->_standardizePath( $trace[$i - 1]['file'] ), -15, 15 ) == 'TempusTools.php' ) { continue; } if ( isset( $trace[$i]['class'] ) && $trace[$i]['class'] == 'TT' && isset( $trace[$i - 1]['file'] ) && substr( $this->_standardizePath( $trace[$i - 1]['file'] ), -15, 15 ) == 'TempusTools.php' ) { continue; } break; } // adjust trace offset $msg = $this->logToInsightConsole->option( 'encoder.trace.offsetAdjustment', $i ); if ( $object instanceof Exception ) { $type = self::EXCEPTION; } if ( $label && $type != self::TABLE && $type != self::GROUP_START ) { $msg = $msg->label( $label ); } switch ( $type ) { case self::DUMP: case self::LOG: return $msg->log( $object ); case self::INFO: return $msg->info( $object ); case self::WARN: return $msg->warn( $object ); case self::ERROR: return $msg->error( $object ); case self::TRACE: return $msg->trace( $object ); case self::EXCEPTION: return $this->plugin( 'error' )->handleException( $object, $msg ); case self::TABLE: if ( isset( $object[0] ) && !is_string( $object[0] ) && $label ) { $object = [ $label, $object ]; } return $msg->table( $object[0], array_slice( $object[1], 1 ), $object[1][0] ); case self::GROUP_START: $insightGroupStack[] = $msg->group( md5( $label ) )->open(); return $msg->log( $label ); case self::GROUP_END: if ( count( $insightGroupStack ) == 0 ) { throw new Error( 'Too many groupEnd() as opposed to group() calls!' ); } $group = array_pop( $insightGroupStack ); return $group->close(); default: return $msg->log( $object ); } } if (!$this->detectClientExtension()) { return false; } $meta = []; $skipFinalObjectEncode = false; if ( $object instanceof Exception ) { $meta['file'] = $this->_escapeTraceFile( $object->getFile() ); $meta['line'] = $object->getLine(); $trace = $object->getTrace(); if ( $object instanceof ErrorException && isset( $trace[0]['function'] ) && $trace[0]['function'] == 'errorHandler' && isset( $trace[0]['class'] ) && $trace[0]['class'] == 'TempusDebugger' ) { $severity = false; switch ( $object->getSeverity() ) { case E_WARNING: $severity = 'E_WARNING'; break; case E_NOTICE: $severity = 'E_NOTICE'; break; case E_USER_ERROR: $severity = 'E_USER_ERROR'; break; case E_USER_WARNING: $severity = 'E_USER_WARNING'; break; case E_USER_NOTICE: $severity = 'E_USER_NOTICE'; break; case E_STRICT: $severity = 'E_STRICT'; break; case E_RECOVERABLE_ERROR: $severity = 'E_RECOVERABLE_ERROR'; break; case E_DEPRECATED: $severity = 'E_DEPRECATED'; break; case E_USER_DEPRECATED: $severity = 'E_USER_DEPRECATED'; break; } $object = [ 'Class' => get_class( $object ), 'Message' => $severity . ': ' . $object->getMessage(), 'File' => $this->_escapeTraceFile( $object->getFile() ), 'Line' => $object->getLine(), 'Type' => 'trigger', 'Trace' => $this->_escapeTrace( array_splice( $trace, 2 ) ) ]; $skipFinalObjectEncode = true; } else { $object = [ 'Class' => get_class( $object ), 'Message' => $object->getMessage(), 'File' => $this->_escapeTraceFile( $object->getFile() ), 'Line' => $object->getLine(), 'Type' => 'throw', 'Trace' => $this->_escapeTrace( $trace ) ]; $skipFinalObjectEncode = true; } $type = self::EXCEPTION; } elseif ( $type == self::TRACE ) { $trace = debug_backtrace(); if ( !$trace ) { return false; } for ( $i = 0; $i < sizeof( $trace ); $i++ ) { if ( isset( $trace[$i]['class'] ) && isset( $trace[$i]['file'] ) && ( $trace[$i]['class'] == 'TempusDebugger' || $trace[$i]['class'] == 'FB' ) && ( substr( $this->_standardizePath( $trace[$i]['file'] ), -15, 15 ) == 'TempusTools.php' || substr( $this->_standardizePath( $trace[$i]['file'] ), -18, 18 ) == 'TempusDebugger.php' ) ) { } elseif ( isset( $trace[$i]['class'] ) && isset( $trace[$i + 1]['file'] ) && $trace[$i]['class'] == 'TempusDebugger' && substr( $this->_standardizePath( $trace[$i + 1]['file'] ), -15, 15 ) == 'TempusTools.php' ) { /* Skip tt() */ } elseif ( $trace[$i]['function'] == 'tt' || $trace[$i]['function'] == 'trace' || $trace[$i]['function'] == 'send' ) { $object = [ 'Class' => isset( $trace[$i]['class'] ) ? $trace[$i]['class'] : '', 'Type' => isset( $trace[$i]['type'] ) ? $trace[$i]['type'] : '', 'Function' => isset( $trace[$i]['function'] ) ? $trace[$i]['function'] : '', 'Message' => $trace[$i]['args'][0], 'File' => isset( $trace[$i]['file'] ) ? $this->_escapeTraceFile( $trace[$i]['file'] ) : '', 'Line' => isset( $trace[$i]['line'] ) ? $trace[$i]['line'] : '', 'Args' => isset( $trace[$i]['args'] ) ? $this->encodeObject( $trace[$i]['args'] ) : '', 'Trace' => $this->_escapeTrace( array_splice( $trace, $i + 1 ) ) ]; $skipFinalObjectEncode = true; $meta['file'] = isset( $trace[$i]['file'] ) ? $this->_escapeTraceFile( $trace[$i]['file'] ) : ''; $meta['line'] = isset( $trace[$i]['line'] ) ? $trace[$i]['line'] : ''; break; } } } elseif ( $type == self::TABLE ) { if ( isset( $object[0] ) && is_string( $object[0] ) ) { $object[1] = $this->encodeTable( $object[1] ); } else { $object = $this->encodeTable( $object ); } $skipFinalObjectEncode = true; } elseif ( $type == self::GROUP_START ) { if ( !$label ) { throw $this->newException( 'You must specify a label for the group!' ); } } else { if ( $type === null ) { $type = self::LOG; } } if ( $this->options['includeLineNumbers'] ) { if ( !isset( $meta['file'] ) || !isset( $meta['line'] ) ) { $trace = debug_backtrace(); for ( $i = 0; $trace && $i < sizeof( $trace ); $i++ ) { if ( isset( $trace[$i]['class'] ) && isset( $trace[$i]['file'] ) && ( $trace[$i]['class'] == 'TempusDebugger' || $trace[$i]['class'] == 'FB' ) && ( substr( $this->_standardizePath( $trace[$i]['file'] ), -15, 15 ) == 'TempusTools.php' || substr( $this->_standardizePath( $trace[$i]['file'] ), -18, 18 ) == 'TempusDebugger.php' ) ) { /* Skip - TempusTools::trace(), TempusTools::send(), $firephp->trace(), $firephp->tt() */ } elseif ( isset( $trace[$i]['class'] ) && isset( $trace[$i + 1]['file'] ) && $trace[$i]['class'] == 'TempusDebugger' && substr( $this->_standardizePath( $trace[$i + 1]['file'] ), -15, 15 ) == 'TempusTools.php' ) { /* Skip tt() */ } elseif ( isset( $trace[$i]['file'] ) && substr( $this->_standardizePath( $trace[$i]['file'] ), -15, 15 ) == 'TempusTools.php' ) { /* Skip TempusTools::tt() */ } else { $meta['file'] = isset( $trace[$i]['file'] ) ? $this->_escapeTraceFile( $trace[$i]['file'] ) : ''; $meta['line'] = isset( $trace[$i]['line'] ) ? $trace[$i]['line'] : ''; break; } } } } else { unset( $meta['file'], $meta['line'] ); } $this->setHeader( 'X-Wf-Protocol-1', 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2' ); /* $this->setHeader('X-Wf-1-Plugin-1', 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/' . self::VERSION); $structureIndex = 1; if ($type == self::DUMP) { $structureIndex = 2; $this->setHeader('X-Wf-1-Structure-2', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1'); } else { $this->setHeader('X-Wf-1-Structure-1', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'); } */ if ( $type == self::DUMP ) { $msg = '{"' . $label . '":' . $this->jsonEncode( $object, $skipFinalObjectEncode ) . '}'; } else { $msgMeta = $options; $msgMeta['Type'] = $type; if ( $label !== null ) { $msgMeta['Label'] = $label; } if ( isset( $meta['file'] ) && !isset( $msgMeta['File'] ) ) { $msgMeta['File'] = $meta['file']; } if ( isset( $meta['line'] ) && !isset( $msgMeta['Line'] ) ) { $msgMeta['Line'] = $meta['line']; } $msg = '[' . $this->jsonEncode( $msgMeta ) . ',' . $this->jsonEncode( $object, $skipFinalObjectEncode ) . ']'; } $parts = explode( "\n", chunk_split( $msg, 5000, "\n" ) ); for ( $i = 0; $i < count( $parts ); $i++ ) { $part = $parts[$i]; if ( $part ) { if ( count( $parts ) > 2 ) { // Message needs to be split into multiple parts $this->setHeader( 'x-td-1-' . $structureIndex . '-' . '1-' . $this->messageIndex, ( ( $i == 0 ) ? strlen( $msg ) : '' ) . '|' . $part . '|' . ( ( $i < count( $parts ) - 2 ) ? '\\' : '' ) ); } else { $this->setHeader( 'x-td-1-' . $structureIndex . '-' . '1-' . $this->messageIndex, strlen( $part ) . '|' . $part . '|' ); } $this->messageIndex++; if ( $this->messageIndex > 99999 ) { throw $this->newException( 'Maximum number (99,999) of messages reached!' ); } } } $this->setHeader('x-td-1-Index', $this->messageIndex - 1); return true; } /** * Standardizes path for windows systems. * * @param string $path * @return string */ protected function _standardizePath( $path ) { return preg_replace( '/\\\\+/', '/', $path ); } /** * Escape trace path for windows systems * * @param array $trace * @return array */ protected function _escapeTrace( $trace ) { if ( !$trace ) { return $trace; } for ( $i = 0; $i < sizeof( $trace ); $i++ ) { if ( isset( $trace[$i]['file'] ) ) { $trace[$i]['file'] = $this->_escapeTraceFile( $trace[$i]['file'] ); } if ( isset( $trace[$i]['args'] ) ) { $trace[$i]['args'] = $this->encodeObject( $trace[$i]['args'] ); } } return $trace; } /** * Escape file information of trace for windows systems * * @param string $file * @return string */ protected function _escapeTraceFile( $file ) { if ( strpos( $file, '\\' ) ) { /* First strip down to single \ */ $file = preg_replace( '/\\\\+/', '\\', $file ); } return $file; } /** * Check if headers have already been sent * * @param string $filename * @param integer $linenum */ protected function headersSent( &$filename, &$linenum ) { return headers_sent( $filename, $linenum ); } /** * Send header * * @param string $name * @param string $value */ protected function setHeader( $name, $value ) { return header( $name . ': ' . $value ); } /** * Get user agent * * @return string|false */ protected function getUserAgent() { if ( !isset( $_SERVER['HTTP_USER_AGENT'] ) ) { return false; } return $_SERVER['HTTP_USER_AGENT']; } /** * Get all request headers * * @return array */ public static function getAllRequestHeaders() { static $_cachedHeaders = false; if ( $_cachedHeaders !== false ) { return $_cachedHeaders; } $headers = []; if ( function_exists( 'getallheaders' ) ) { foreach ( getallheaders() as $name => $value ) { $headers[strtolower( $name )] = $value; } } else { foreach ( $_SERVER as $name => $value ) { if ( substr( $name, 0, 5 ) == 'HTTP_' ) { $headers[strtolower( str_replace( ' ', '-', str_replace( '_', ' ', substr( $name, 5 ) ) ) )] = $value; } } } return $_cachedHeaders = $headers; } /** * Get a request header * * @return string|false */ protected function getRequestHeader( $name ) { $headers = self::getAllRequestHeaders(); if ( isset( $headers[strtolower( $name )] ) ) { return $headers[strtolower( $name )]; } return false; } /** * Returns a new exception * * @param string $message * @return Exception */ protected function newException( $message ) { return new Exception( $message ); } /** * Encode an object into a JSON string * * Uses PHP's jeson_encode() if available * * @param object $object The object to be encoded * @param boolean $skipObjectEncode * @return string The JSON string */ public function jsonEncode( $object, $skipObjectEncode = false ) { if ( !$skipObjectEncode ) { $object = $this->encodeObject( $object ); } return json_encode( $object ); } /** * Encodes a table by encoding each row and column with encodeObject() * * @param array $table The table to be encoded * @return array */ protected function encodeTable( $table ) { if ( !$table ) { return $table; } $newTable = []; foreach ( $table as $row ) { if ( is_array( $row ) ) { $newRow = []; foreach ( $row as $item ) { $newRow[] = $this->encodeObject( $item ); } $newTable[] = $newRow; } } return $newTable; } /** * Encodes an object including members with * protected and private visibility * * @param object $object The object to be encoded * @param integer $Depth The current traversal depth * @return array All members of the object */ protected function encodeObject( $object, $objectDepth = 1, $arrayDepth = 1, $maxDepth = 1 ) { if ( $maxDepth > $this->options['maxDepth'] ) { return '** Max Depth (' . $this->options['maxDepth'] . ') **'; } $return = []; if ( is_resource( $object ) ) { return '** ' . (string) $object . ' **'; } elseif ( is_object( $object ) ) { if ( $objectDepth > $this->options['maxObjectDepth'] ) { return '** Max Object Depth (' . $this->options['maxObjectDepth'] . ') **'; } foreach ( $this->objectStack as $refVal ) { if ( $refVal === $object ) { return '** Recursion (' . get_class( $object ) . ') **'; } } array_push( $this->objectStack, $object ); $return['__className'] = $class = get_class( $object ); $classLower = strtolower( $class ); $reflectionClass = new ReflectionClass( $class ); $properties = []; foreach ( $reflectionClass->getProperties() as $property ) { $properties[$property->getName()] = $property; } $members = (array)$object; foreach ( $properties as $plainName => $property ) { $name = $rawName = $plainName; if ( $property->isStatic() ) { $name = 'static:' . $name; } if ( $property->isPublic() ) { $name = 'public:' . $name; } elseif ( $property->isPrivate() ) { $name = 'private:' . $name; $rawName = "\0" . $class . "\0" . $rawName; } elseif ( $property->isProtected() ) { $name = 'protected:' . $name; $rawName = "\0" . '*' . "\0" . $rawName; } if ( !( isset( $this->objectFilters[$classLower] ) && is_array( $this->objectFilters[$classLower] ) && in_array( $plainName, $this->objectFilters[$classLower] ) ) ) { if ( array_key_exists( $rawName, $members ) && !$property->isStatic() ) { $return[$name] = $this->encodeObject( $members[$rawName], $objectDepth + 1, 1, $maxDepth + 1 ); } else { if ( method_exists( $property, 'setAccessible' ) ) { $property->setAccessible( true ); $return[$name] = $this->encodeObject( $property->getValue( $object ), $objectDepth + 1, 1, $maxDepth + 1 ); } elseif ( $property->isPublic() ) { $return[$name] = $this->encodeObject( $property->getValue( $object ), $objectDepth + 1, 1, $maxDepth + 1 ); } else { $return[$name] = '** Need PHP 5.3 to get value **'; } } } else { $return[$name] = '** Excluded by Filter **'; } } // Include all members that are not defined in the class // but exist in the object foreach ( $members as $rawName => $value ) { $name = $rawName; if ( $name[0] == "\0" ) { $parts = explode( "\0", $name ); $name = $parts[2]; } $plainName = $name; if ( !isset( $properties[$name] ) ) { $name = 'undeclared:' . $name; if ( !( isset( $this->objectFilters[$classLower] ) && is_array( $this->objectFilters[$classLower] ) && in_array( $plainName, $this->objectFilters[$classLower] ) ) ) { $return[$name] = $this->encodeObject( $value, $objectDepth + 1, 1, $maxDepth + 1 ); } else { $return[$name] = '** Excluded by Filter **'; } } } array_pop( $this->objectStack ); } elseif ( is_array( $object ) ) { if ( $arrayDepth > $this->options['maxArrayDepth'] ) { return '** Max Array Depth (' . $this->options['maxArrayDepth'] . ') **'; } foreach ( $object as $key => $val ) { // Encoding the $GLOBALS PHP array causes an infinite loop // if the recursion is not reset here as it contains // a reference to itself. This is the only way I have come up // with to stop infinite recursion in this case. if ( $key == 'GLOBALS' && is_array( $val ) && array_key_exists( 'GLOBALS', $val ) ) { $val['GLOBALS'] = '** Recursion (GLOBALS) **'; } if ( !$this->is_utf8( $key ) ) { $key = encodeUtfEight( $key ); } $return[$key] = $this->encodeObject( $val, 1, $arrayDepth + 1, $maxDepth + 1 ); } } else { if ( $this->is_utf8( $object ) ) { return $object; } else { return encodeUtfEight( $object ); } } return $return; } protected function encodeUtfEight( $str ) { return array_map( function ( $item ) { return mb_convert_encoding( $item, "UTF-8", mb_detect_encoding( $item ) ); }, $str ); } protected function is_utf8( $str ) { return mb_check_encoding($string, 'UTF-8'); } }