_id = $id; $caller = $bt[2]; if ( false !== strpos( $caller['file'], 'plugins' ) ) { $this->_file_start = strpos( $caller['file'], 'plugins' ) + strlen( 'plugins/' ); } else { $this->_file_start = strpos( $caller['file'], 'themes' ) + strlen( 'themes/' ); } if ( $on ) { $this->on(); } if ( $echo ) { $this->echo_on(); } } /** * @param string $id * @param bool $on * @param bool $echo * * @return FS_Logger */ public static function get_logger( $id, $on = false, $echo = false ) { $id = strtolower( $id ); if ( ! isset( self::$_processID ) ) { self::init(); } if ( ! isset( self::$LOGGERS[ $id ] ) ) { self::$LOGGERS[ $id ] = new FS_Logger( $id, $on, $echo ); } return self::$LOGGERS[ $id ]; } /** * Initialize logging global info. * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 */ private static function init() { self::$_ownerName = function_exists( 'get_current_user' ) ? get_current_user() : 'unknown'; self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) ); self::$_abspathLength = strlen( ABSPATH ); self::$_processID = mt_rand( 0, 32000 ); // Process ID may be `false` on errors. if ( ! is_numeric( self::$_processID ) ) { self::$_processID = 0; } } private static function hook_footer() { if ( self::$_HOOKED_FOOTER ) { return; } if ( is_admin() ) { add_action( 'admin_footer', 'FS_Logger::dump', 100 ); } else { add_action( 'wp_footer', 'FS_Logger::dump', 100 ); } } function is_on() { return $this->_on; } function on() { $this->_on = true; if ( ! function_exists( 'dbDelta' ) ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } self::hook_footer(); } function echo_on() { $this->on(); $this->_echo = true; } function is_echo_on() { return $this->_echo; } function get_id() { return $this->_id; } function get_file() { return $this->_file_start; } private function _log( &$message, $type, $wrapper = false ) { if ( ! $this->is_on() ) { return; } $bt = debug_backtrace(); $depth = $wrapper ? 3 : 2; while ( $depth < count( $bt ) - 1 && 'eval' === $bt[ $depth ]['function'] ) { $depth ++; } $caller = $bt[ $depth ]; /** * Retrieve the correct call file & line number from backtrace * when logging from a wrapper method. * * @author Vova Feldman * @since 1.2.1.6 */ if ( empty( $caller['line'] ) ) { $depth --; while ( $depth >= 0 ) { if ( ! empty( $bt[ $depth ]['line'] ) ) { $caller['line'] = $bt[ $depth ]['line']; $caller['file'] = $bt[ $depth ]['file']; break; } } } $log = array_merge( $caller, array( 'cnt' => self::$CNT ++, 'logger' => $this, 'timestamp' => microtime( true ), 'log_type' => $type, 'msg' => $message, ) ); if ( self::$_isStorageLoggingOn ) { $this->db_log( $type, $message, self::$CNT, $caller ); } self::$LOG[] = $log; if ( $this->is_echo_on() && ! Freemius::is_ajax() ) { echo self::format_html( $log ) . "\n"; } } function log( $message, $wrapper = false ) { $this->_log( $message, 'log', $wrapper ); } function info( $message, $wrapper = false ) { $this->_log( $message, 'info', $wrapper ); } function warn( $message, $wrapper = false ) { $this->_log( $message, 'warn', $wrapper ); } function error( $message, $wrapper = false ) { $this->_log( $message, 'error', $wrapper ); } /** * Log API error. * * @author Vova Feldman (@svovaf) * @since 1.2.1.5 * * @param mixed $api_result * @param bool $wrapper */ function api_error( $api_result, $wrapper = false ) { $message = ''; if ( is_object( $api_result ) && ! empty( $api_result->error ) && ! empty( $api_result->error->message ) ) { $message = $api_result->error->message; } else if ( is_object( $api_result ) ) { $message = var_export( $api_result, true ); } else if ( is_string( $api_result ) ) { $message = $api_result; } else if ( empty( $api_result ) ) { $message = 'Empty API result.'; } $message = 'API Error: ' . $message; $this->_log( $message, 'error', $wrapper ); } function entrance( $message = '', $wrapper = false ) { $msg = 'Entrance' . ( empty( $message ) ? '' : ' > ' ) . $message; $this->_log( $msg, 'log', $wrapper ); } function departure( $message = '', $wrapper = false ) { $msg = 'Departure' . ( empty( $message ) ? '' : ' > ' ) . $message; $this->_log( $msg, 'log', $wrapper ); } #-------------------------------------------------------------------------------- #region Log Formatting #-------------------------------------------------------------------------------- private static function format( $log, $show_type = true ) { return '[' . str_pad( $log['cnt'], strlen( self::$CNT ), '0', STR_PAD_LEFT ) . '] [' . $log['logger']->_id . '] ' . ( $show_type ? '[' . $log['log_type'] . ']' : '' ) . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . ' >> ' . $log['msg'] . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ') ' : '' ) . ' [' . $log['timestamp'] . ']'; } private static function format_html( $log ) { return '
[' . $log['cnt'] . '] [' . $log['logger']->_id . '] [' . $log['log_type'] . '] ' . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . ' >> ' . esc_html( $log['msg'] ) . '' . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ')' : '' ) . ' [' . $log['timestamp'] . ']
'; } #endregion static function dump() { ?> prefix}fs_logger"; if ( $is_on ) { /** * Create logging table. * * NOTE: * dbDelta must use KEY and not INDEX for indexes. * * @link https://core.trac.wordpress.org/ticket/2695 */ $result = $wpdb->query( "CREATE TABLE {$table} ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, `process_id` INT UNSIGNED NOT NULL, `user_name` VARCHAR(64) NOT NULL, `logger` VARCHAR(128) NOT NULL, `log_order` INT UNSIGNED NOT NULL, `type` ENUM('log','info','warn','error') NOT NULL DEFAULT 'log', `message` TEXT NOT NULL, `file` VARCHAR(256) NOT NULL, `line` INT UNSIGNED NOT NULL, `function` VARCHAR(256) NOT NULL, `request_type` ENUM('call','ajax','cron') NOT NULL DEFAULT 'call', `request_url` VARCHAR(1024) NOT NULL, `created` DECIMAL(16, 6) NOT NULL, PRIMARY KEY (`id`), KEY `process_id` (`process_id` ASC), KEY `process_logger` (`process_id` ASC, `logger` ASC), KEY `function` (`function` ASC), KEY `type` (`type` ASC))" ); } else { /** * Drop logging table. */ $result = $wpdb->query( "DROP TABLE IF EXISTS $table;" ); } if ( false !== $result ) { update_option( 'fs_storage_logger', ( $is_on ? 1 : 0 ) ); } return ( false !== $result ); } /** * @author Vova Feldman (@svovaf) * @since 1.2.1.6 * * @param string $type * @param string $message * @param int $log_order * @param array $caller * * @return false|int */ private function db_log( &$type, &$message, &$log_order, &$caller ) { global $wpdb; $request_type = 'call'; if ( defined( 'DOING_CRON' ) && DOING_CRON ) { $request_type = 'cron'; } else if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { $request_type = 'ajax'; } $request_url = WP_FS__IS_HTTP_REQUEST ? $_SERVER['REQUEST_URI'] : ''; return $wpdb->insert( "{$wpdb->prefix}fs_logger", array( 'process_id' => self::$_processID, 'user_name' => self::$_ownerName, 'logger' => $this->_id, 'log_order' => $log_order, 'type' => $type, 'request_type' => $request_type, 'request_url' => $request_url, 'message' => $message, 'file' => isset( $caller['file'] ) ? substr( $caller['file'], self::$_abspathLength ) : '', 'line' => $caller['line'], 'function' => ( ! empty( $caller['class'] ) ? $caller['class'] . $caller['type'] : '' ) . $caller['function'], 'created' => microtime( true ), ) ); } /** * Persistent DB logger columns. * * @var array */ private static $_log_columns = array( 'id', 'process_id', 'user_name', 'logger', 'log_order', 'type', 'message', 'file', 'line', 'function', 'request_type', 'request_url', 'created', ); /** * Create DB logs query. * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 * * @param bool $filters * @param int $limit * @param int $offset * @param bool $order * @param bool $escape_eol * * @return string */ private static function build_db_logs_query( $filters = false, $limit = 200, $offset = 0, $order = false, $escape_eol = false ) { global $wpdb; $select = '*'; if ( $escape_eol ) { $select = ''; for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) { if ( $i > 0 ) { $select .= ', '; } if ( 'message' !== self::$_log_columns[ $i ] ) { $select .= self::$_log_columns[ $i ]; } else { $select .= 'REPLACE(message , \'\n\', \' \') AS message'; } } } $query = "SELECT {$select} FROM {$wpdb->prefix}fs_logger"; if ( is_array( $filters ) ) { $criteria = array(); if ( ! empty( $filters['type'] ) && 'all' !== $filters['type'] ) { $filters['type'] = strtolower( $filters['type'] ); switch ( $filters['type'] ) { case 'warn_error': $criteria[] = array( 'col' => 'type', 'val' => array( 'warn', 'error' ) ); break; case 'error': case 'warn': $criteria[] = array( 'col' => 'type', 'val' => $filters['type'] ); break; case 'info': default: $criteria[] = array( 'col' => 'type', 'val' => array( 'info', 'log' ) ); break; } } if ( ! empty( $filters['request_type'] ) ) { $filters['request_type'] = strtolower( $filters['request_type'] ); if ( in_array( $filters['request_type'], array( 'call', 'ajax', 'cron' ) ) ) { $criteria[] = array( 'col' => 'request_type', 'val' => $filters['request_type'] ); } } if ( ! empty( $filters['file'] ) ) { $criteria[] = array( 'col' => 'file', 'op' => 'LIKE', 'val' => '%' . esc_sql( $filters['file'] ), ); } if ( ! empty( $filters['function'] ) ) { $criteria[] = array( 'col' => 'function', 'op' => 'LIKE', 'val' => '%' . esc_sql( $filters['function'] ), ); } if ( ! empty( $filters['process_id'] ) && is_numeric( $filters['process_id'] ) ) { $criteria[] = array( 'col' => 'process_id', 'val' => $filters['process_id'] ); } if ( ! empty( $filters['logger'] ) ) { $criteria[] = array( 'col' => 'logger', 'op' => 'LIKE', 'val' => '%' . esc_sql( $filters['logger'] ) . '%', ); } if ( ! empty( $filters['message'] ) ) { $criteria[] = array( 'col' => 'message', 'op' => 'LIKE', 'val' => '%' . esc_sql( $filters['message'] ) . '%', ); } if ( 0 < count( $criteria ) ) { $query .= "\nWHERE\n"; $first = true; foreach ( $criteria as $c ) { if ( ! $first ) { $query .= "AND\n"; } if ( is_array( $c['val'] ) ) { $operator = 'IN'; for ( $i = 0, $len = count( $c['val'] ); $i < $len; $i ++ ) { $c['val'][ $i ] = "'" . esc_sql( $c['val'][ $i ] ) . "'"; } $val = '(' . implode( ',', $c['val'] ) . ')'; } else { $operator = ! empty( $c['op'] ) ? $c['op'] : '='; $val = "'" . esc_sql( $c['val'] ) . "'"; } $query .= "`{$c['col']}` {$operator} {$val}\n"; $first = false; } } } if ( ! is_array( $order ) ) { $order = array( 'col' => 'id', 'order' => 'desc' ); } $query .= " ORDER BY {$order['col']} {$order['order']} LIMIT {$offset},{$limit}"; return $query; } /** * Load logs from DB. * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 * * @param bool $filters * @param int $limit * @param int $offset * @param bool $order * * @return object[]|null */ public static function load_db_logs( $filters = false, $limit = 200, $offset = 0, $order = false ) { global $wpdb; $query = self::build_db_logs_query( $filters, $limit, $offset, $order ); return $wpdb->get_results( $query ); } /** * Load logs from DB. * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 * * @param bool $filters * @param string $filename * @param int $limit * @param int $offset * @param bool $order * * @return false|string File download URL or false on failure. */ public static function download_db_logs( $filters = false, $filename = '', $limit = 10000, $offset = 0, $order = false ) { global $wpdb; $query = self::build_db_logs_query( $filters, $limit, $offset, $order, true ); $upload_dir = wp_upload_dir(); if ( empty( $filename ) ) { $filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv'; } $filepath = rtrim( $upload_dir['path'], '/' ) . "/{$filename}"; $query .= " INTO OUTFILE '{$filepath}' FIELDS TERMINATED BY '\t' ESCAPED BY '\\\\' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\\n'"; $columns = ''; for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) { if ( $i > 0 ) { $columns .= ', '; } $columns .= "'" . self::$_log_columns[ $i ] . "'"; } $query = "SELECT {$columns} UNION ALL " . $query; $result = $wpdb->query( $query ); if ( false === $result ) { return false; } return rtrim( $upload_dir['url'], '/' ) . '/' . $filename; } /** * @author Vova Feldman (@svovaf) * @since 1.2.1.6 * * @param string $filename * * @return string */ public static function get_logs_download_url( $filename = '' ) { $upload_dir = wp_upload_dir(); if ( empty( $filename ) ) { $filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv'; } return rtrim( $upload_dir['url'], '/' ) . $filename; } #endregion }