prefix . self::$table_name; } /** * Create database table */ public static function create_table() { global $wpdb; $table_name = self::get_table_name(); $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, created_at DATETIME NOT NULL, method VARCHAR(10) NOT NULL, url TEXT NOT NULL, request_headers TEXT, request_body LONGTEXT, response_status INT, response_headers TEXT, response_body LONGTEXT, duration_ms INT, PRIMARY KEY (id), KEY created_at (created_at) ) {$charset_collate};"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } /** * Log an API request/response * * @param array $data Log data * @return int|false Insert ID or false on failure */ public static function log($data) { global $wpdb; $table_name = self::get_table_name(); // Prepare data $insert_data = array( 'created_at' => current_time('mysql'), 'method' => isset($data['method']) ? strtoupper($data['method']) : 'GET', 'url' => isset($data['url']) ? $data['url'] : '', 'request_headers' => isset($data['request_headers']) ? (is_array($data['request_headers']) ? wp_json_encode($data['request_headers']) : $data['request_headers']) : null, 'request_body' => isset($data['request_body']) ? (is_array($data['request_body']) ? wp_json_encode($data['request_body']) : $data['request_body']) : null, 'response_status' => isset($data['response_status']) ? intval($data['response_status']) : null, 'response_headers' => isset($data['response_headers']) ? (is_array($data['response_headers']) ? wp_json_encode($data['response_headers']) : $data['response_headers']) : null, 'response_body' => isset($data['response_body']) ? (is_string($data['response_body']) ? $data['response_body'] : wp_json_encode($data['response_body'])) : null, 'duration_ms' => isset($data['duration_ms']) ? intval($data['duration_ms']) : null ); $result = $wpdb->insert($table_name, $insert_data); if ($result === false) { return false; } return $wpdb->insert_id; } /** * Get logs for the last N days * * @param int $days Number of days to fetch (default: 3) * @param int $limit Max number of records (default: 500) * @param string $search Search term for URL * @param string $status_filter Filter by status (success, error, all) * @return array Array of log entries */ public static function get_logs($days = 3, $limit = 500, $search = '', $status_filter = 'all') { global $wpdb; $table_name = self::get_table_name(); // Calculate date threshold $date_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); // Build query — $table_name is derived from $wpdb->prefix, not user input. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query = "SELECT * FROM {$table_name} WHERE created_at >= %s"; $params = array($date_threshold); // Add search filter if (!empty($search)) { $query .= " AND url LIKE %s"; $params[] = '%' . $wpdb->esc_like($search) . '%'; } // Add status filter if ($status_filter === 'success') { $query .= " AND response_status >= 200 AND response_status < 300"; } elseif ($status_filter === 'error') { $query .= " AND (response_status IS NULL OR response_status < 200 OR response_status >= 300)"; } // Order by newest first $query .= " ORDER BY created_at DESC LIMIT %d"; $params[] = $limit; // Prepare and execute query — $query uses only $wpdb->prefix-based table name and standard placeholders. // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $prepared_query = $wpdb->prepare($query, $params); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $results = $wpdb->get_results($prepared_query, ARRAY_A); return $results ? $results : array(); } /** * Get a single log entry by ID * * @param int $id Log ID * @return array|null Log entry or null */ public static function get_log($id) { global $wpdb; $table_name = self::get_table_name(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table_name is derived from $wpdb->prefix, not user input. $query = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is the return value of $wpdb->prepare() above. return $wpdb->get_row($query, ARRAY_A); } /** * Drop the debug logger table */ public static function drop_table() { global $wpdb; $table_name = self::get_table_name(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table_name is $wpdb->prefix based; table names cannot use prepare() parameters. $wpdb->query("DROP TABLE IF EXISTS {$table_name}"); } /** * Clear all logs * * @return int|false Number of rows deleted or false on error */ public static function clear_logs() { global $wpdb; $table_name = self::get_table_name(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table_name is $wpdb->prefix based; TRUNCATE does not support prepare() parameters. return $wpdb->query("TRUNCATE TABLE {$table_name}"); } /** * Clear logs older than N days * * @param int $days Number of days to keep * @return int|false Number of rows deleted or false on error */ public static function clear_old_logs($days = 7) { global $wpdb; $table_name = self::get_table_name(); $date_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); return $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table_name is derived from $wpdb->prefix, not user input. "DELETE FROM {$table_name} WHERE created_at < %s", $date_threshold ) ); } /** * Get log statistics * * @param int $days Number of days to analyze * @return array Statistics */ public static function get_stats($days = 3) { global $wpdb; $table_name = self::get_table_name(); $date_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table_name is derived from $wpdb->prefix, not user input. $stats = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) as total, SUM(CASE WHEN response_status >= 200 AND response_status < 300 THEN 1 ELSE 0 END) as success_count, SUM(CASE WHEN response_status IS NULL OR response_status < 200 OR response_status >= 300 THEN 1 ELSE 0 END) as error_count, AVG(duration_ms) as avg_duration FROM {$table_name} WHERE created_at >= %s", $date_threshold ), ARRAY_A ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $stats ? $stats : array( 'total' => 0, 'success_count' => 0, 'error_count' => 0, 'avg_duration' => 0 ); } }