: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @copyright Copyright (c) 2015, Freemius, Inc.
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
if ( ! defined( 'ABSPATH' ) ) {
private $_file_start = 0;
* @var int PHP Process ID.
private static $_processID;
* @var string PHP Script user name.
private static $_ownerName;
* @var bool Is storage logging turned on.
private static $_isStorageLoggingOn;
* @var int ABSPATH length.
private static $_abspathLength;
private static $LOGGERS = array();
private static $LOG = array();
private static $_HOOKED_FOOTER = false;
private function __construct( $id, $on = false, $echo = false ) {
if ( false !== strpos( $caller['file'], 'plugins' ) ) {
$this->_file_start = strpos( $caller['file'], 'plugins' ) + strlen( 'plugins/' );
$this->_file_start = strpos( $caller['file'], 'themes' ) + strlen( 'themes/' );
public static function get_logger( $id, $on = false, $echo = false ) {
if ( ! isset( self::$_processID ) ) {
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)
private static function init() {
self::$_ownerName = function_exists( 'get_current_user' ) ?
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 ) ) {
private static function hook_footer() {
if ( self::$_HOOKED_FOOTER ) {
add_action( 'admin_footer', 'FS_Logger::dump', 100 );
add_action( 'wp_footer', 'FS_Logger::dump', 100 );
if ( ! function_exists( 'dbDelta' ) ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
return $this->_file_start;
private function _log( &$message, $type, $wrapper = false ) {
if ( ! $this->is_on() ) {
$depth = $wrapper ? 3 : 2;
while ( $depth < count( $bt ) - 1 && 'eval' === $bt[ $depth ]['function'] ) {
* Retrieve the correct call file & line number from backtrace
* when logging from a wrapper method.
if ( empty( $caller['line'] ) ) {
if ( ! empty( $bt[ $depth ]['line'] ) ) {
$caller['line'] = $bt[ $depth ]['line'];
$caller['file'] = $bt[ $depth ]['file'];
$log = array_merge( $caller, array(
'timestamp' => microtime( true ),
if ( self::$_isStorageLoggingOn ) {
$this->db_log( $type, $message, self::$CNT, $caller );
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 );
* @author Vova Feldman (@svovaf)
* @param mixed $api_result
function api_error( $api_result, $wrapper = false ) {
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 ) ) {
} 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 );
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
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 '<div style="font-size: 13px; font-family: monospace; color: #7da767; padding: 8px 3px; background: #000; border-bottom: 1px solid #555;">[' . $log['cnt'] . '] [' . $log['logger']->_id . '] [' . $log['log_type'] . '] <b><code style="color: #c4b1e0;">' . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . '</code> >> <b style="color: #f59330;">' . esc_html( $log['msg'] ) . '</b></b>' . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ')' : '' ) . ' [' . $log['timestamp'] . ']</div>';
<!-- BEGIN: Freemius PHP Console Log -->
<script type="text/javascript">
foreach ( self::$LOG as $log ) {
echo 'console.' . $log['log_type'] . '(' . json_encode( self::format( $log, false ) ) . ')' . "\n";
<!-- END: Freemius PHP Console Log -->
static function get_log() {
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
* @author Vova Feldman (@svovaf)
public static function is_storage_logging_on() {
if ( ! isset( self::$_isStorageLoggingOn ) ) {
self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
return self::$_isStorageLoggingOn;
* Turns on/off database persistent debugging to capture
* multi-session logs to debug complex flows like
* plugin auto-deactivate on premium version activation.
* @todo Check if Theme Check has issues with DB tables for themes.
* @author Vova Feldman (@svovaf)
public static function _set_storage_logging( $is_on = true ) {
$table = "{$wpdb->prefix}fs_logger";
* 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',
`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,
KEY `process_id` (`process_id` ASC),
KEY `process_logger` (`process_id` ASC, `logger` ASC),
KEY `function` (`function` ASC),
KEY `type` (`type` ASC))" );
$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)
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
} else if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
$request_url = WP_FS__IS_HTTP_REQUEST ?
$_SERVER['REQUEST_URI'] :
"{$wpdb->prefix}fs_logger",
'process_id' => self::$_processID,
'user_name' => self::$_ownerName,
'log_order' => $log_order,
'request_type' => $request_type,
'request_url' => $request_url,
'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.
private static $_log_columns = array(
* @author Vova Feldman (@svovaf)
* @param bool $escape_eol
private static function build_db_logs_query(
for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
if ( 'message' !== self::$_log_columns[ $i ] ) {
$select .= self::$_log_columns[ $i ];
$select .= 'REPLACE(message , \'\n\', \' \') AS message';
$query = "SELECT {$select} FROM {$wpdb->prefix}fs_logger";
if ( is_array( $filters ) ) {
if ( ! empty( $filters['type'] ) && 'all' !== $filters['type'] ) {
$filters['type'] = strtolower( $filters['type'] );
switch ( $filters['type'] ) {
$criteria[] = array( 'col' => 'type', 'val' => array( 'warn', 'error' ) );
$criteria[] = array( 'col' => 'type', 'val' => $filters['type'] );
$criteria[] = array( 'col' => 'type', 'val' => array( 'info', 'log' ) );
if ( ! empty( $filters['request_type'] ) ) {
$filters['request_type'] = strtolower( $filters['request_type'] );