insert($t['chat_history'], [ 'user_message' => wp_kses_post($message), 'ai_response' => '', 'model' => AIWEST_Text_Models::get(), 'mode' => sanitize_text_field($mode), 'target_post_id' => absint($target_post_id), 'command_action' => sanitize_key($command_action), 'status' => 'pending', 'approval_status' => 'not_required', 'approval_note' => '', 'applied_at' => null, 'error_message' => '', 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql'), ]); self::trim_chat(); return intval($wpdb->insert_id); } public static function update_chat($id, $status, $response = '', $error = '') { global $wpdb; $t = AIWEST_Database::tables(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This writes current state to a private history table. $wpdb->update($t['chat_history'], [ 'ai_response' => wp_kses_post($response), 'status' => sanitize_text_field($status), 'error_message' => sanitize_text_field($error), 'updated_at' => current_time('mysql'), ], ['id' => intval($id)]); } public static function recent_chat($limit = null) { global $wpdb; self::ensure_tables(); $t = AIWEST_Database::tables(); $limit = $limit ?: self::limit_chat(); if (!AIWEST_Database::table_exists($t['chat_history'])) return []; // Mutable administrator history must be read without a stale object-cache layer. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_results($wpdb->prepare('SELECT * FROM %i ORDER BY id DESC LIMIT %d', $t['chat_history'], $limit)); } public static function ensure_chat_approval_columns() { // Kept for backward compatibility. Schema upgrades are handled centrally by dbDelta(). self::ensure_tables(); } public static function mark_chat_needs_approval($id, $note = '') { global $wpdb; $t = AIWEST_Database::tables(); $data = [ 'approval_status' => 'waiting', 'approval_note' => sanitize_text_field($note), 'updated_at' => current_time('mysql'), ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This writes current state to a private history table. return $wpdb->update($t['chat_history'], $data, ['id' => intval($id)]); } public static function approve_chat($id, $note = 'Command approved by the administrator.') { global $wpdb; $t = AIWEST_Database::tables(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This writes current state to a private history table. return $wpdb->update($t['chat_history'], [ 'approval_status' => 'applied', 'approval_note' => sanitize_textarea_field($note), 'applied_at' => current_time('mysql'), 'updated_at' => current_time('mysql'), ], ['id' => intval($id)]); } public static function record_chat_decision($id, $status, $note = '') { global $wpdb; $t = AIWEST_Database::tables(); $status = sanitize_key($status); if (!in_array($status, ['recorded', 'failed', 'rejected', 'executing'], true)) { $status = 'recorded'; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This writes current state to a private history table. return $wpdb->update($t['chat_history'], [ 'approval_status' => $status, 'approval_note' => sanitize_textarea_field($note), 'applied_at' => null, 'updated_at' => current_time('mysql'), ], ['id' => intval($id)]); } public static function chat_can_be_approved($row) { if (!$row || empty($row->ai_response) || $row->status === 'error') return false; if (!isset($row->mode) || $row->mode !== 'command') return false; $approval_status = isset($row->approval_status) ? $row->approval_status : 'not_required'; return $approval_status === 'waiting'; } public static function delete_chat($id) { global $wpdb; $t = AIWEST_Database::tables(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This deletes from the plugin's private history table. return $wpdb->delete($t['chat_history'], ['id' => intval($id)], ['%d']); } public static function add_content($source, $topic, $keyword = '', $prompt = '') { global $wpdb; self::ensure_tables(); $t = AIWEST_Database::tables(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert($t['content_history'], [ 'source' => sanitize_text_field($source), 'prompt' => sanitize_text_field(wp_trim_words(wp_strip_all_tags($prompt), 60, '...')), 'topic' => sanitize_text_field($topic), 'keyword' => sanitize_text_field($keyword), 'post_id' => null, 'post_status' => '', 'model' => AIWEST_Text_Models::get(), 'status' => 'pending', 'message' => 'Task accepted for processing.', 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql'), ]); self::trim_content(); return intval($wpdb->insert_id); } public static function update_content($id, $status, $post_id = null, $message = '') { global $wpdb; $t = AIWEST_Database::tables(); $post_status = ''; if ($post_id) { $post_status = get_post_status(intval($post_id)); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This writes current state to a private history table. $wpdb->update($t['content_history'], [ 'post_id' => $post_id ? intval($post_id) : null, 'post_status' => sanitize_text_field($post_status), 'status' => sanitize_text_field($status), 'message' => sanitize_text_field($message), 'updated_at' => current_time('mysql'), ], ['id' => intval($id)]); } public static function recent_content($limit = null) { global $wpdb; self::ensure_tables(); $t = AIWEST_Database::tables(); $limit = $limit ?: self::limit_content(); if (!AIWEST_Database::table_exists($t['content_history'])) return []; // Mutable administrator history must be read without a stale object-cache layer. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_results($wpdb->prepare('SELECT * FROM %i ORDER BY id DESC LIMIT %d', $t['content_history'], $limit)); } public static function delete_content_history($id) { global $wpdb; $t = AIWEST_Database::tables(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This deletes from the plugin's private history table. return $wpdb->delete($t['content_history'], ['id' => intval($id)], ['%d']); } public static function trim_chat() { global $wpdb; $t = AIWEST_Database::tables(); $limit = self::limit_chat(); if (!AIWEST_Database::table_exists($t['chat_history'])) return; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $ids = $wpdb->get_col($wpdb->prepare('SELECT id FROM %i ORDER BY id DESC LIMIT 100000 OFFSET %d', $t['chat_history'], $limit)); if ($ids) { $ids = array_map('intval', $ids); $placeholders = implode(', ', array_fill(0, count($ids), '%d')); $arguments = array_merge([$t['chat_history']], $ids); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The interpolated fragment contains placeholders only. $query = $wpdb->prepare("DELETE FROM %i WHERE id IN ({$placeholders})", $arguments); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above; deletes expired private history rows. $wpdb->query($query); } } public static function trim_content() { global $wpdb; $t = AIWEST_Database::tables(); $limit = self::limit_content(); if (!AIWEST_Database::table_exists($t['content_history'])) return; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $ids = $wpdb->get_col($wpdb->prepare('SELECT id FROM %i ORDER BY id DESC LIMIT 100000 OFFSET %d', $t['content_history'], $limit)); if ($ids) { $ids = array_map('intval', $ids); $placeholders = implode(', ', array_fill(0, count($ids), '%d')); $arguments = array_merge([$t['content_history']], $ids); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The interpolated fragment contains placeholders only. $query = $wpdb->prepare("DELETE FROM %i WHERE id IN ({$placeholders})", $arguments); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above; deletes expired private history rows. $wpdb->query($query); } } }