: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* WPMU DEV Logger - A simple logger module
* @author WPMU DEV (Thobk)
* It's created based on Hummingbird\Core\Logger.
* This logger lib will handle the old messages based on the expected size.
* This means, it will try to get rid of the old messages if the file size is larger than the max size of the log file.
* $logger = WDEV_Logger::create(array(
* 'max_log_size' => 10,//10MB
* 'expected_log_size_in_percent' => 0.7,//70%
* 'log_dir' => 'uploads/your_plugin_name',
* 'is_private' => true,//-log.php,
* 'log_dir' => 'uploads/specific/log_dir',
* 'max_log_size' => 5,//5MB
* $logger->foo()->error('Log an error into foo module');//[...DATE...] Error: Log an error into foo module. (uploads/specific/log_dir/foo-log.php)
* $logger->foo()->warning('...a warning...'); $logger->foo()->notice('...a notice...'); $logger->foo()->info('..info...');
* # Global module: $logger->error('Log an error into the main log file');...(uploads/your_plugin_name/index-debug.log).
if ( ! defined( 'WP_CONTENT_DIR' ) ) {
if ( ! class_exists( 'WDEV_Logger' ) ) {
* @var WP_Error|bool $error
private $modules = array();
* Un-lock some limit actions.
* Use this to allow some actions which shouldn't call directly.
const NONCE_NAME = '_wdevnonce';
* Register a new debug log level.
* It will have full control.
const WPMUDEV_DEBUG_LEVEL = 10;
* We use constant WP_DEBUG to define the debug level, e.g:
* define('WP_DEBUG', LOG_DEBUG );
* Add backtrace for debug levels:
* LOG_ERR or 3 => Only for Error type.
* LOG_WARNING or 4 => Only for Warning type.
* LOG_NOTICE or 5 => Only for Notice type.
* LOG_INFO or 6 => Only for Info type.
* LOG_DEBUG or 7 => For Error, Warning and Notice type.
* self::WPMUDEV_DEBUG_LEVEL or 10 => for all message types.
* We use constant WP_DEBUG_LOG to define the log level, e.g:
* define('WP_DEBUG_LOG', LOG_DEBUG );
* And by default, we will log all message types. But we can limit it by defining WP_DEBUG_LOG_LOG:
* LOG_ERR or 3 => Only log Error type.
* LOG_WARNING or 4 => Only log Warning type.
* LOG_NOTICE or 5 => Only log Notice type.
* LOG_INFO or 6 => Only log Info type.
* LOG_DEBUG or 7 => Log Error, Warning and Notice type.
* self::WPMUDEV_DEBUG_LEVEL or 10 or TRUE => for all message types.
* @type boolean use_native_filesystem_api
* If we can't connect to the Filesystem API, enable this to try to use default PHP functions (WP_Filesystem_Direct).
* Maximum file size for each log file in MB.
* Note, set it large might make your site run slower while writing a log or clean the log file.
* @type float expected_log_size_in_percent
* Set the expected file log size in percent ( base on $max_log_size ).
* E.g. If the log file size is larger than 10MB (15MB) => we will need to reduce (15 - 10 * 70/100 = 8MB).
* Log directory, a sub-folder inside WP_CONTENT_DIR
* [WP_CONTENT_DIR]/[log_dir]
* @type boolean add_subsite_dir Allow to add sub-site folder in the MU site.
* By default, we will add a standard module (index), add a empty module to overwrite it. And we can use it to log the general case.
* e.g $logger->index()->log('Something for the general case');
* Module option inherit option from the parent.
* These are some new option:
* @type boolean is_private
* Set is_private is TRUE to use save the log to php file instead of normal .log type.
* Set is_global_module is TRUE to use it as a global/general module.
* By default, we will auto register a new global module "index".
* With global module we can access the method directly, e.g:
* $logger->error('Log an error');
* 'use_native_filesystem_api' => true,
* 'expected_log_size_in_percent' => 0.7,
* 'log_dir' => 'wpmudev',
* 'add_subsite_dir' => true,
* Default is wdev_logger_[plugin_name]
* Return the plugin instance
* @param array $option Logger option.
* @param string|null $option_key Option key name.
* If $option_key is null we will try to use the plugin folder name instead.
* @see self::get_option_key()
public static function create( $option, $option_key = null ) {
return new self( $option, $option_key );
* @param array $option Logger option.
* @param string $option_key Option key name.
public function __construct( $option, $option_key ) {
$this->option_key = $this->get_option_key( $option_key );
$this->parse_option( $option );
// disable for empty option.
if ( ! empty( $option ) ) {
add_action( 'wp_ajax_wdev_logger_action', array( $this, 'process_actions' ) );
// Add cron schedule to clean out outdated logs.
add_action( 'wdev_logger_clear_logs', array( $this, 'clear_logs' ) );
add_action( 'admin_init', array( $this, 'check_cron_schedule' ) );
* Set the current module and we can use this to call some actions.
* $logger->your_module_1()->error('An error.');
* $logger->your_module_1()->notice('A notice.');
* $logger->your_module_2()->delete();//delete the log file.
* @param string $name Method name.
* @param array $arguments Arguments.
public function __call( $name, $arguments ) {
if ( $this->set_current_module( $name ) ) {
} elseif ( $this->enabling_debug_log_mode() ) {
error_log( sprintf( 'Module "%1$s" does not exists, list of registered modules are ["%2$s"]. Continue with global module "%3$s".', $name, join( '", "', array_keys( $this->modules ) ), $this->option['global_module'] ) );//phpcs:ignore
public function enabling_debug_log_mode() {
return defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG;
* @param mixed $message Data to write to log.
* @param string|null $type Log type (Error, Warning, Notice, etc).
public function log( $message, $type = null ) {
$this->maybe_active_global_module();
if ( ! $this->should_log( $message, $type ) ) {
return $this->write_log_file( $this->format_message( $message, $type ) );
* Format the message to be logged.
* @param string $message Message to be logged.
* @param string $type Message type.
private function format_message( $message, $type ) {
if ( ! is_string( $message ) ) {
if ( ! is_scalar( $message ) ) {
$message = PHP_EOL . print_r( $message, true );
$message = print_r( $message, true );
if ( ! empty( $type ) && is_string( $type ) ) {
$type = strtolower( $type );
$message = ucfirst( $type ) . ': ' . $message;
$message = '[' . date( 'c' ) . '] ' . $message;//phpcs:ignore
if ( $this->get_debug_level() && is_int( $this->debug_level ) && $this->level_can_do( $this->debug_level, $type ) ) {
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 20 );//phpcs:ignore
$backtrace = array_filter(
return ! isset( $trace['file'] ) || __FILE__ !== $trace['file'] && false !== strpos( $trace['file'], WP_CONTENT_DIR );
$message .= PHP_EOL .'['. date('c') .'] Stack trace: '. PHP_EOL . print_r( $backtrace, true );//phpcs:ignore
* Error: [an error message].
* @param mixed $message Data to write to log.
public function error( $message ) {
return $this->log( $message, 'Error' );
* Warning: [a notice message].
* @param mixed $message Data to write to log.
public function notice( $message ) {
return $this->log( $message, 'Notice' );
* Warning: [a warning message].
* @param mixed $message Data to write to log.
public function warning( $message ) {
return $this->log( $message, 'Warning' );
* Info: [a info message].
* @param mixed $message Data to write to log.
public function info( $message ) {
return $this->log( $message, 'Info' );
* Retrieve download link for a log module.
* @param string $module Module slug.
* @return string A nonce url to download the module log.
public function get_download_link( $module = null ) {
$this->switch_module( $module );
'action' => 'wdev_logger_action',
'log_action' => 'download',
'log_module' => $this->current_module,
admin_url( 'admin-ajax.php' )
$this->get_log_action_name(),
* Process logger actions.
* Accepts module name (slug) and action. So far only 'download' and 'delete' actions is supported.
public function process_actions() {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
! isset( $_REQUEST['log_action'], $_REQUEST['log_module'], $_REQUEST[ self::NONCE_NAME ] ) ||
! wp_verify_nonce( wp_unslash( $_REQUEST[ self::NONCE_NAME ] ), $this->get_log_action_name() )
// Invalid action, return.
$action = sanitize_text_field( wp_unslash( $_REQUEST['log_action'] ) ); // Input var ok.
$module = sanitize_text_field( wp_unslash( $_REQUEST['log_module'] ) ); // Input var ok.
// Not called by a registered module.
if ( ! isset( $this->modules[ $module ] ) ) {
/* translators: %s Method name */
wp_send_json_error( sprintf( __( 'Module %s does not exist.', 'wpmudev' ), $module ) );
// Only allow these actions.
if ( in_array( $action, array( 'download', 'delete' ), true ) && method_exists( $this, $action ) ) {
$should_return = isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'];
$result = call_user_func( array( $this, $action ), $module, $should_return );
wp_send_json_success( $result );
/* translators: %s Method name */
wp_send_json_error( sprintf( __( 'Method %s does not exist.', 'wpmudev' ), $action ) );
* Delete current log file.
* @param string $module Module slug.
* @return bool True on success or false on failure.
public function delete( $module = null ) {
if ( ! $this->connect_fs() ) {
$this->switch_module( $module );
if ( ! $wp_filesystem->exists( $this->get_file() ) ) {
return $wp_filesystem->delete( $this->get_file(), false, 'f' );
* Retrieve option by key.
* @param string $name Key name.
* @return mixed Returns option value.
public function get_option( $name ) {
$this->maybe_active_global_module();
return $this->get_module_option( $name );
* Retrieve current module option by key.
* @param string $name Key name.
* @param mixed $value Default value.
* @return mixed Returns option value.
private function get_module_option( $name, $value = null ) {
if ( $this->current_module && isset( $this->modules[ $this->current_module ][ $name ] ) ) {
$value = $this->modules[ $this->current_module ][ $name ];
} elseif ( isset( $this->option[ $name ] ) ) {
$value = $this->option[ $name ];
return apply_filters( "wdev_logger_get_option_{$name}", $value, $this->current_module, $this->option );
* Clean up the log dir and delete the option.
* That's useful to use it while uninstalling plugin.
public function cleanup() {
if ( empty( $this->modules ) || ! $this->connect_fs() ) {
foreach ( $this->modules as $module => $module_option ) {
$this->delete( $module );
* @param int $debug_level Debug level to set.
public function set_debug_level( $debug_level = LOG_DEBUG ) {
$is_global_settings = ! $this->un_lock;
$this->maybe_active_global_module();
return $this->set_level( $debug_level, 'debug_level', $is_global_settings );
* @param int $log_level Log level to set.
public function set_log_level( $log_level = LOG_DEBUG ) {
$is_global_settings = ! $this->un_lock;
$this->maybe_active_global_module();
return $this->set_level( $log_level, 'log_level', $is_global_settings );