table_name = $wpdb->prefix . 'dsz_sync_logs';
}
/**
* Log a message
*
* @param string $level Log level
* @param string $message Log message
* @param array $context Additional context data
* @return bool Success
*/
public function log($level, $message, $context = []) {
global $wpdb;
// Validate level
$valid_levels = [self::LEVEL_INFO, self::LEVEL_WARNING, self::LEVEL_ERROR, self::LEVEL_DEBUG];
if (!in_array($level, $valid_levels)) {
$level = self::LEVEL_INFO;
}
// Prepare context for storage
$context_json = !empty($context) ? wp_json_encode($context) : null;
// Insert log entry
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$result = $wpdb->insert(
$this->table_name,
[
'level' => $level,
'message' => sanitize_text_field($message),
'context' => $context_json,
'created_at' => current_time('mysql'),
],
['%s', '%s', '%s', '%s']
);
// Cleanup old logs periodically
$this->maybe_cleanup();
return $result !== false;
}
/**
* Log info message
*
* @param string $message Log message
* @param array $context Additional context
*/
public function info($message, $context = []) {
$this->log(self::LEVEL_INFO, $message, $context);
}
/**
* Log warning message
*
* @param string $message Log message
* @param array $context Additional context
*/
public function warning($message, $context = []) {
$this->log(self::LEVEL_WARNING, $message, $context);
}
/**
* Log error message
*
* @param string $message Log message
* @param array $context Additional context
*/
public function error($message, $context = []) {
$this->log(self::LEVEL_ERROR, $message, $context);
}
/**
* Log debug message
*
* @param string $message Log message
* @param array $context Additional context
*/
public function debug($message, $context = []) {
if (defined('WP_DEBUG') && WP_DEBUG) {
$this->log(self::LEVEL_DEBUG, $message, $context);
}
}
/**
* Get log entries
*
* @param array $args Query arguments
* @return array Log entries
*/
public function get_logs($args = []) {
global $wpdb;
$defaults = [
'level' => '',
'search' => '',
'limit' => 50,
'offset' => 0,
'orderby' => 'created_at',
'order' => 'DESC',
];
$args = wp_parse_args($args, $defaults);
// Build query
$where = [];
$values = [];
if (!empty($args['level'])) {
$where[] = 'level = %s';
$values[] = $args['level'];
}
if (!empty($args['search'])) {
$where[] = 'message LIKE %s';
$values[] = '%' . $wpdb->esc_like($args['search']) . '%';
}
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
// Sanitize orderby
$allowed_orderby = ['id', 'level', 'message', 'created_at'];
$orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'created_at';
$order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
// Escaping for safe interpolation
$orderby = esc_sql($orderby);
$order = esc_sql($order);
// Build SQL query
$sql = "SELECT * FROM {$this->table_name}";
if (!empty($where)) {
$sql .= " WHERE " . implode(' AND ', $where);
}
$sql .= " ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
$values[] = intval($args['limit']);
$values[] = intval($args['offset']);
// Prepare the query
if (!empty($values)) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$query = $wpdb->prepare($sql, $values);
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$results = $wpdb->get_results($query, ARRAY_A);
} else {
// Fallback (unlikely)
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$results = $wpdb->get_results($sql, ARRAY_A);
}
// Decode context for each result
if ($results) {
foreach ($results as &$row) {
if (!empty($row['context'])) {
$row['context'] = json_decode($row['context'], true);
}
}
}
return $results ?: [];
}
/**
* Get total log count
*
* @param string $level Filter by level (optional)
* @return int Total count
*/
public function get_count($level = '') {
global $wpdb;
if (!empty($level)) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM %i WHERE level = %s",
$this->table_name,
$level
));
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i", $this->table_name));
}
return intval($count);
}
/**
* Clear all logs
*
* @return bool Success
*/
public function clear_logs() {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
return $wpdb->query($wpdb->prepare("TRUNCATE TABLE %i", $this->table_name)) !== false;
}
/**
* Clear logs by level
*
* @param string $level Log level to clear
* @return int|false Number of rows deleted or false
*/
public function clear_by_level($level) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
return $wpdb->delete($this->table_name, ['level' => $level], ['%s']);
}
/**
* Maybe cleanup old logs
*/
private function maybe_cleanup() {
// Only cleanup 1% of the time to reduce overhead
if (wp_rand(1, 100) !== 1) {
return;
}
$this->cleanup_old_logs();
}
/**
* Delete logs older than the given number of days
*
* @param int $days Retention window in days
* @return int|false Rows deleted or false on error
*/
public function cleanup_older_than($days) {
global $wpdb;
$days = max(1, intval($days));
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
return $wpdb->query($wpdb->prepare(
"DELETE FROM %i WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
$this->table_name,
$days
));
}
/**
* Cleanup old logs to maintain max limit
*/
public function cleanup_old_logs() {
global $wpdb;
$count = $this->get_count();
if ($count > $this->max_logs) {
$delete_count = $count - $this->max_logs;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies.
$wpdb->query($wpdb->prepare(
"DELETE FROM %i ORDER BY created_at ASC LIMIT %d",
$this->table_name,
$delete_count
));
}
}
/**
* Get log level badge HTML
*
* @param string $level Log level
* @return string HTML badge
*/
public static function get_level_badge($level) {
$badges = [
self::LEVEL_INFO => 'Info',
self::LEVEL_WARNING => 'Warning',
self::LEVEL_ERROR => 'Error',
self::LEVEL_DEBUG => 'Debug',
];
return isset($badges[$level]) ? $badges[$level] : '' . esc_html($level) . '';
}
/**
* Export logs to CSV
*
* @param array $args Query arguments for filtering
* @return string CSV content
*/
public function export_csv($args = []) {
$args['limit'] = 10000; // Max export
$logs = $this->get_logs($args);
// CSV content
$csv = "ID,Level,Message,Context,Created At\n";
foreach ($logs as $log) {
$context = is_array($log['context']) ? wp_json_encode($log['context']) : $log['context'];
$row = [
$log['id'],
$log['level'],
$log['message'],
$context,
$log['created_at'],
];
// Escape CSV fields
$row = array_map(function($field) {
return '"' . str_replace('"', '""', $field) . '"';
}, $row);
$csv .= implode(',', $row) . "\n";
}
return $csv;
}
}