PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.5
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / rest.php
ai-engine / classes Last commit date
data 11 months ago engines 1 week ago exceptions 11 months ago modules 1 week ago query 1 week ago rest 1 month ago services 1 week ago admin.php 1 month ago api.php 1 month ago core.php 1 week ago discussion.php 11 months ago event.php 11 months ago init.php 7 months ago logging.php 11 months ago reply.php 3 weeks ago rest.php 1 month ago
rest.php
2871 lines
1 <?php
2
3 class Meow_MWAI_Rest {
4 private $core = null;
5 private $namespace = 'mwai/v1';
6
7 public function __construct( $core ) {
8 $this->core = $core;
9 add_action( 'rest_api_init', [ $this, 'rest_init' ] );
10 }
11
12 /**
13 * Retrieve the message from the parameters and optionally sanitize it.
14 *
15 * @param array &$params The parameters array, passed by reference.
16 * @param bool $sanitize Whether to sanitize the message using sanitize_text_field.
17 * @return string The retrieved (and optionally sanitized) message.
18 */
19 public function retrieve_message( &$params, $sanitize = false ): string {
20 $message = $params['message'] ?? '';
21
22 if ( $sanitize ) {
23 $message = sanitize_text_field( $message );
24 }
25
26 return $message;
27 }
28
29 /**
30 * Helper method to create REST responses with automatic token refresh
31 *
32 * @param array $data The response data
33 * @param int $status HTTP status code
34 * @return WP_REST_Response
35 */
36 protected function create_rest_response( $data, $status = 200 ) {
37 // Always check if we need to provide a new nonce
38 $current_nonce = $this->core->get_nonce( true );
39 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
40
41 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
42 // We'll refresh if the nonce is older than 10 hours to be safe
43 $should_refresh = false;
44
45 if ( $request_nonce ) {
46 // Try to determine the age of the nonce
47 // WordPress uses a tick system where each tick is 12 hours
48 // If we're in the second half of the nonce's life, refresh it
49 $time = time();
50 $nonce_tick = wp_nonce_tick();
51
52 // Verify if the nonce is still valid but getting old
53 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
54 if ( $verify === 2 ) {
55 // Nonce is valid but was generated 12-24 hours ago
56 $should_refresh = true;
57 // Log will be written when token is included in response
58 }
59 }
60
61 // If the nonce has changed or should be refreshed, include the new one
62 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
63 $data['new_token'] = $current_nonce;
64
65 // Log if server debug mode is enabled
66 if ( $this->core->get_option( 'server_debug_mode' ) ) {
67 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
68 }
69 }
70
71 return new WP_REST_Response( $data, $status );
72 }
73
74 public function rest_init() {
75 try {
76 // Session Endpoint
77 register_rest_route( $this->namespace, '/start_session', [
78 'methods' => 'POST',
79 'permission_callback' => '__return_true', // Public endpoint for guest users
80 'callback' => [ $this, 'rest_start_session' ],
81 ] );
82
83 // Settings Endpoints
84 register_rest_route( $this->namespace, '/settings/update', [
85 'methods' => 'POST',
86 'permission_callback' => [ $this->core, 'can_access_settings' ],
87 'callback' => [ $this, 'rest_settings_update' ],
88 ] );
89 register_rest_route( $this->namespace, '/settings/options', [
90 'methods' => 'GET',
91 'permission_callback' => [ $this->core, 'can_access_settings' ],
92 'callback' => [ $this, 'rest_settings_list' ],
93 ] );
94 register_rest_route( $this->namespace, '/settings/reset', [
95 'methods' => 'POST',
96 'permission_callback' => [ $this->core, 'can_access_settings' ],
97 'callback' => [ $this, 'rest_settings_reset' ],
98 ] );
99 register_rest_route( $this->namespace, '/settings/chatbots', [
100 'methods' => ['GET', 'POST'],
101 'permission_callback' => [ $this->core, 'can_access_settings' ],
102 'callback' => [ $this, 'rest_settings_chatbots' ],
103 ] );
104 register_rest_route( $this->namespace, '/settings/themes', [
105 'methods' => ['GET', 'POST'],
106 'permission_callback' => [ $this->core, 'can_access_settings' ],
107 'callback' => [ $this, 'rest_settings_themes' ],
108 ] );
109
110 // System Endpoints
111 register_rest_route( $this->namespace, '/system/logs/list', [
112 'methods' => 'POST',
113 'permission_callback' => [ $this->core, 'can_access_settings' ],
114 'callback' => [ $this, 'rest_system_logs_list' ],
115 ] );
116 register_rest_route( $this->namespace, '/system/logs/delete', [
117 'methods' => 'POST',
118 'permission_callback' => [ $this->core, 'can_access_settings' ],
119 'callback' => [ $this, 'rest_system_logs_delete' ],
120 ] );
121 register_rest_route( $this->namespace, '/system/logs/meta', [
122 'methods' => 'POST',
123 'permission_callback' => [ $this->core, 'can_access_settings' ],
124 'callback' => [ $this, 'rest_system_logs_meta_get' ],
125 ] );
126 register_rest_route( $this->namespace, '/system/logs/activity', [
127 'methods' => 'POST',
128 'permission_callback' => [ $this->core, 'can_access_settings' ],
129 'callback' => [ $this, 'rest_system_logs_activity' ],
130 ] );
131 register_rest_route( $this->namespace, '/system/logs/activity_daily', [
132 'methods' => 'POST',
133 'permission_callback' => [ $this->core, 'can_access_settings' ],
134 'callback' => [ $this, 'rest_system_logs_activity_daily' ],
135 ] );
136 register_rest_route( $this->namespace, '/system/templates', [
137 'methods' => 'POST',
138 'permission_callback' => [ $this->core, 'can_access_features' ],
139 'callback' => [ $this, 'rest_system_templates_save' ],
140 ] );
141 register_rest_route( $this->namespace, '/system/templates', [
142 'methods' => 'GET',
143 'permission_callback' => [ $this->core, 'can_access_features' ],
144 'callback' => [ $this, 'rest_system_templates_get' ],
145 ] );
146
147 // AI Endpoints
148 register_rest_route( $this->namespace, '/ai/models', [
149 'methods' => 'POST',
150 'permission_callback' => [ $this->core, 'can_access_features' ],
151 'callback' => [ $this, 'rest_ai_models' ],
152 ] );
153 register_rest_route( $this->namespace, '/ai/test_connection', [
154 'methods' => 'POST',
155 'permission_callback' => [ $this->core, 'can_access_settings' ],
156 'callback' => [ $this, 'rest_ai_test_connection' ],
157 ] );
158 register_rest_route( $this->namespace, '/ai/completions', [
159 'methods' => 'POST',
160 'permission_callback' => [ $this->core, 'can_access_features' ],
161 'callback' => [ $this, 'rest_ai_completions' ],
162 ] );
163 register_rest_route( $this->namespace, '/ai/images', [
164 'methods' => 'POST',
165 'permission_callback' => [ $this->core, 'can_access_features' ],
166 'callback' => [ $this, 'rest_ai_images' ],
167 ] );
168 register_rest_route( $this->namespace, '/ai/image_edit', [
169 'methods' => 'POST',
170 'permission_callback' => [ $this->core, 'can_access_features' ],
171 'callback' => [ $this, 'rest_ai_image_edit' ],
172 ] );
173 register_rest_route( $this->namespace, '/ai/copilot', [
174 'methods' => 'POST',
175 'permission_callback' => [ $this->core, 'can_access_features' ],
176 'callback' => [ $this, 'rest_ai_copilot' ],
177 ] );
178
179 register_rest_route( $this->namespace, '/ai/magic_wand', [
180 'methods' => 'POST',
181 'callback' => [ $this, 'rest_ai_magic_wand' ],
182 'permission_callback' => [ $this->core, 'can_access_features' ],
183 ] );
184 register_rest_route( $this->namespace, '/ai/moderate', [
185 'methods' => 'POST',
186 'permission_callback' => [ $this->core, 'can_access_settings' ],
187 'callback' => [ $this, 'rest_ai_moderate' ],
188 ] );
189 register_rest_route( $this->namespace, '/ai/transcribe_audio', [
190 'methods' => 'POST',
191 'permission_callback' => [ $this->core, 'can_access_settings' ],
192 'callback' => [ $this, 'rest_ai_transcribe_audio' ],
193 ] );
194 register_rest_route( $this->namespace, '/ai/transcribe_image', [
195 'methods' => 'POST',
196 'permission_callback' => [ $this->core, 'can_access_settings' ],
197 'callback' => [ $this, 'rest_ai_transcribe_image' ],
198 ] );
199 register_rest_route( $this->namespace, '/ai/json', [
200 'methods' => 'POST',
201 'permission_callback' => [ $this->core, 'can_access_settings' ],
202 'callback' => [ $this, 'rest_ai_json' ],
203 ] );
204
205 // MCP Endpoints
206 register_rest_route( $this->namespace, '/mcp/functions', [
207 'methods' => 'GET',
208 'permission_callback' => [ $this->core, 'can_access_settings' ],
209 'callback' => [ $this, 'rest_mcp_functions' ],
210 ] );
211 register_rest_route( $this->namespace, '/mcp/self_test', [
212 'methods' => 'POST',
213 'permission_callback' => [ $this->core, 'can_access_settings' ],
214 'callback' => [ $this, 'rest_mcp_self_test' ],
215 ] );
216 register_rest_route( $this->namespace, '/system/mcp_logs/top_tools', [
217 'methods' => 'POST',
218 'permission_callback' => [ $this->core, 'can_access_settings' ],
219 'callback' => [ $this, 'rest_mcp_top_tools' ],
220 ] );
221
222 // Helpers Endpoints
223 register_rest_route( $this->namespace, '/helpers/update_post_title', [
224 'methods' => 'POST',
225 'permission_callback' => [ $this->core, 'can_access_features' ],
226 'callback' => [ $this, 'rest_helpers_update_title' ],
227 ] );
228 register_rest_route( $this->namespace, '/helpers/update_post_excerpt', [
229 'methods' => 'POST',
230 'permission_callback' => [ $this->core, 'can_access_features' ],
231 'callback' => [ $this, 'rest_helpers_update_excerpt' ],
232 ] );
233 register_rest_route( $this->namespace, '/helpers/create_post', [
234 'methods' => 'POST',
235 'permission_callback' => [ $this->core, 'can_access_features' ],
236 'callback' => [ $this, 'rest_helpers_create_post' ],
237 ] );
238 register_rest_route( $this->namespace, '/helpers/create_image', [
239 'methods' => 'POST',
240 'permission_callback' => [ $this->core, 'can_access_features' ],
241 'callback' => [ $this, 'rest_helpers_create_images' ],
242 ] );
243 register_rest_route( $this->namespace, '/helpers/generate_image_meta', [
244 'methods' => 'POST',
245 'permission_callback' => [ $this->core, 'can_access_features' ],
246 'callback' => [ $this, 'rest_helpers_generate_image_meta' ],
247 ] );
248 register_rest_route( $this->namespace, '/helpers/update_media_metadata', [
249 'methods' => 'POST',
250 'permission_callback' => [ $this->core, 'can_access_features' ],
251 'callback' => [ $this, 'rest_helpers_update_media_metadata' ],
252 ] );
253 register_rest_route( $this->namespace, '/helpers/create_video', [
254 'methods' => 'POST',
255 'permission_callback' => [ $this->core, 'can_access_features' ],
256 'callback' => [ $this, 'rest_helpers_create_video' ],
257 ] );
258 register_rest_route( $this->namespace, '/helpers/video_status', [
259 'methods' => 'POST',
260 'permission_callback' => [ $this->core, 'can_access_features' ],
261 'callback' => [ $this, 'rest_helpers_video_status' ],
262 ] );
263 register_rest_route( $this->namespace, '/helpers/download_video', [
264 'methods' => 'POST',
265 'permission_callback' => [ $this->core, 'can_access_features' ],
266 'callback' => [ $this, 'rest_helpers_download_video' ],
267 ] );
268 register_rest_route( $this->namespace, '/helpers/delete_video', [
269 'methods' => 'POST',
270 'permission_callback' => [ $this->core, 'can_access_features' ],
271 'callback' => [ $this, 'rest_helpers_delete_video' ],
272 ] );
273 register_rest_route( $this->namespace, '/helpers/save_video_to_library', [
274 'methods' => 'POST',
275 'permission_callback' => [ $this->core, 'can_access_features' ],
276 'callback' => [ $this, 'rest_helpers_save_video_to_library' ],
277 ] );
278 register_rest_route( $this->namespace, '/helpers/delete_video_from_library', [
279 'methods' => 'POST',
280 'permission_callback' => [ $this->core, 'can_access_features' ],
281 'callback' => [ $this, 'rest_helpers_delete_video_from_library' ],
282 ] );
283 register_rest_route( $this->namespace, '/helpers/list_draft_media', [
284 'methods' => 'GET',
285 'permission_callback' => [ $this->core, 'can_access_features' ],
286 'callback' => [ $this, 'rest_helpers_list_draft_media' ],
287 ] );
288 register_rest_route( $this->namespace, '/helpers/approve_media', [
289 'methods' => 'POST',
290 'permission_callback' => [ $this->core, 'can_access_features' ],
291 'callback' => [ $this, 'rest_helpers_approve_media' ],
292 ] );
293 register_rest_route( $this->namespace, '/helpers/reject_media', [
294 'methods' => 'POST',
295 'permission_callback' => [ $this->core, 'can_access_features' ],
296 'callback' => [ $this, 'rest_helpers_reject_media' ],
297 ] );
298 register_rest_route( $this->namespace, '/helpers/count_posts', [
299 'methods' => 'GET',
300 'permission_callback' => [ $this->core, 'can_access_features' ],
301 'callback' => [ $this, 'rest_helpers_count_posts' ],
302 ] );
303 register_rest_route( $this->namespace, '/helpers/posts_ids', [
304 'methods' => 'GET',
305 'permission_callback' => [ $this->core, 'can_access_features' ],
306 'callback' => [ $this, 'rest_helpers_posts_ids' ],
307 ] );
308 register_rest_route( $this->namespace, '/helpers/post_types', [
309 'methods' => 'GET',
310 'permission_callback' => [ $this->core, 'can_access_features' ],
311 'callback' => [ $this, 'rest_helpers_post_types' ],
312 ] );
313 register_rest_route( $this->namespace, '/helpers/post_content', [
314 'methods' => 'GET',
315 'permission_callback' => [ $this->core, 'can_access_features' ],
316 'callback' => [ $this, 'rest_helpers_post_content' ],
317 ] );
318 register_rest_route( $this->namespace, '/helpers/check_posts_content', [
319 'methods' => 'POST',
320 'permission_callback' => [ $this->core, 'can_access_features' ],
321 'callback' => [ $this, 'rest_helpers_check_posts_content' ],
322 ] );
323 register_rest_route( $this->namespace, '/helpers/run_tasks', [
324 'methods' => 'POST',
325 'permission_callback' => [ $this->core, 'can_access_features' ],
326 'callback' => [ $this, 'rest_helpers_run_tasks' ],
327 ] );
328 register_rest_route( $this->namespace, '/helpers/optimize_database', [
329 'methods' => 'POST',
330 'permission_callback' => [ $this->core, 'can_access_settings' ],
331 'callback' => [ $this, 'rest_helpers_optimize_database' ],
332 ] );
333 register_rest_route( $this->namespace, '/helpers/cron_events', [
334 'methods' => 'GET',
335 'permission_callback' => [ $this->core, 'can_access_features' ],
336 'callback' => [ $this, 'rest_helpers_cron_events' ],
337 ] );
338 register_rest_route( $this->namespace, '/helpers/run_cron', [
339 'methods' => 'POST',
340 'permission_callback' => [ $this->core, 'can_access_features' ],
341 'callback' => [ $this, 'rest_helpers_run_cron' ],
342 ] );
343
344 // OpenAI Endpoints
345 register_rest_route( $this->namespace, '/openai/files/list', [
346 'methods' => 'GET',
347 'permission_callback' => [ $this->core, 'can_access_settings' ],
348 'callback' => [ $this, 'rest_openai_files_get' ],
349 ] );
350 register_rest_route( $this->namespace, '/openai/files/upload', [
351 'methods' => 'POST',
352 'permission_callback' => [ $this->core, 'can_access_settings' ],
353 'callback' => [ $this, 'rest_openai_files_upload' ],
354 ] );
355 register_rest_route( $this->namespace, '/openai/files/delete', [
356 'methods' => 'POST',
357 'permission_callback' => [ $this->core, 'can_access_settings' ],
358 'callback' => [ $this, 'rest_openai_files_delete' ],
359 ] );
360 register_rest_route( $this->namespace, '/openai/files/download', [
361 'methods' => 'POST',
362 'permission_callback' => [ $this->core, 'can_access_settings' ],
363 'callback' => [ $this, 'rest_openai_files_download' ],
364 ] );
365 // TODO: Remove all the /openai/finetunes/* and /openai/files/finetune routes after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
366 register_rest_route( $this->namespace, '/openai/files/finetune', [
367 'methods' => 'POST',
368 'permission_callback' => [ $this->core, 'can_access_settings' ],
369 'callback' => [ $this, 'rest_openai_files_finetune' ],
370 ] );
371 register_rest_route( $this->namespace, '/openai/finetunes/list_deleted', [
372 'methods' => 'GET',
373 'permission_callback' => [ $this->core, 'can_access_settings' ],
374 'callback' => [ $this, 'rest_openai_deleted_finetunes_get' ],
375 ] );
376
377 // register_rest_route( $this->namespace, '/openai/models', array(
378 // 'methods' => 'GET',
379 // 'permission_callback' => [ $this->core, 'can_access_settings' ],
380 // 'callback' => [ $this, 'rest_openai_models_get' ],
381 // ) );
382
383 register_rest_route( $this->namespace, '/openai/finetunes/list', [
384 'methods' => 'GET',
385 'permission_callback' => [ $this->core, 'can_access_settings' ],
386 'callback' => [ $this, 'rest_openai_finetunes_get' ],
387 ] );
388 register_rest_route( $this->namespace, '/openai/finetunes/delete', [
389 'methods' => 'POST',
390 'permission_callback' => [ $this->core, 'can_access_settings' ],
391 'callback' => [ $this, 'rest_openai_finetunes_delete' ],
392 ] );
393 register_rest_route( $this->namespace, '/openai/finetunes/cancel', [
394 'methods' => 'POST',
395 'permission_callback' => [ $this->core, 'can_access_settings' ],
396 'callback' => [ $this, 'rest_openai_finetunes_cancel' ],
397 ] );
398
399 // Logging Endpoints
400 register_rest_route( $this->namespace, '/get_logs', [
401 'methods' => 'GET',
402 'permission_callback' => [ $this->core, 'can_access_features' ],
403 'callback' => [ $this, 'rest_get_logs' ]
404 ] );
405 register_rest_route( $this->namespace, '/clear_logs', [
406 'methods' => 'GET',
407 'permission_callback' => [ $this->core, 'can_access_features' ],
408 'callback' => [ $this, 'rest_clear_logs' ]
409 ] );
410
411 // Forms Endpoints
412 register_rest_route( $this->namespace, '/forms/list', [
413 'methods' => 'GET',
414 'permission_callback' => [ $this->core, 'can_access_settings' ],
415 'callback' => [ $this, 'rest_forms_list' ]
416 ] );
417 register_rest_route( $this->namespace, '/forms/get', [
418 'methods' => 'GET',
419 'permission_callback' => [ $this->core, 'can_access_settings' ],
420 'callback' => [ $this, 'rest_forms_get' ]
421 ] );
422 register_rest_route( $this->namespace, '/forms/create', [
423 'methods' => 'POST',
424 'permission_callback' => [ $this->core, 'can_access_settings' ],
425 'callback' => [ $this, 'rest_forms_create' ]
426 ] );
427 register_rest_route( $this->namespace, '/forms/update', [
428 'methods' => 'POST',
429 'permission_callback' => [ $this->core, 'can_access_settings' ],
430 'callback' => [ $this, 'rest_forms_update' ]
431 ] );
432 register_rest_route( $this->namespace, '/forms/delete', [
433 'methods' => 'POST',
434 'permission_callback' => [ $this->core, 'can_access_settings' ],
435 'callback' => [ $this, 'rest_forms_delete' ]
436 ] );
437 }
438 catch ( Exception $e ) {
439 Meow_MWAI_Logging::error( 'REST API initialization failed: ' . $e->getMessage() );
440 }
441 }
442
443 public function rest_start_session() {
444 try {
445 $sessionId = $this->core->get_session_id();
446 $restNonce = $this->core->get_nonce( true );
447
448 $response = [
449 'success' => true,
450 'sessionId' => $sessionId,
451 'restNonce' => $restNonce
452 ];
453
454 // If in test mode and we have a new token, it will be added by create_rest_response
455 // But we also want to ensure the restNonce matches the test token if available
456 if ( get_option( 'mwai_token_test_mode' ) ) {
457 $token_data = get_option( 'mwai_test_token_data' );
458 if ( $token_data && isset( $token_data['token'] ) ) {
459 $response['restNonce'] = $token_data['token'];
460 }
461 }
462
463 return $this->create_rest_response( $response, 200 );
464 }
465 catch ( Exception $e ) {
466 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
467 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
468 }
469 }
470
471 public function rest_settings_list() {
472 return $this->create_rest_response( [
473 'success' => true,
474 'options' => $this->core->get_all_options()
475 ], 200 );
476 }
477
478 public function rest_helpers_cron_events( $request ) {
479 try {
480 // Only show AI Engine cron events (those starting with mwai_)
481 $cron_events = [];
482 $crons = _get_cron_array();
483
484 // Get transient data for last run status (we'll store this when crons run)
485 $last_run_data = get_transient( 'mwai_cron_last_run' ) ?: [];
486
487 // Get all scheduled events and filter for AI Engine ones
488 foreach ( $crons as $timestamp => $cron ) {
489 foreach ( $cron as $hook => $details ) {
490 // Only process AI Engine hooks (starting with mwai_)
491 if ( strpos( $hook, 'mwai_' ) !== 0 ) {
492 continue;
493 }
494
495 $schedule_key = array_keys( $details )[0];
496 $schedule_info = $details[$schedule_key];
497
498 // Get schedule display name
499 $schedule = $schedule_info['schedule'];
500 $schedules = wp_get_schedules();
501 $schedule_display = isset( $schedules[$schedule]['display'] ) ?
502 $schedules[$schedule]['display'] : $schedule;
503
504 $event_info = [
505 'hook' => $hook,
506 'name' => $this->get_cron_display_name( $hook ),
507 'description' => $this->get_cron_description( $hook ),
508 'next_run' => $timestamp,
509 'next_run_human' => '',
510 'last_run' => isset( $last_run_data[$hook]['time'] ) ? $last_run_data[$hook]['time'] : null,
511 'last_run_human' => isset( $last_run_data[$hook]['time'] ) ?
512 human_time_diff( $last_run_data[$hook]['time'], time() ) . ' ago' :
513 'Never',
514 'last_status' => isset( $last_run_data[$hook]['status'] ) ? $last_run_data[$hook]['status'] : 'unknown',
515 'schedule' => $schedule_display,
516 'is_running' => false,
517 'is_scheduled' => true
518 ];
519
520 // Calculate next run time properly
521 // If we have a last run time and schedule interval, calculate the actual next run
522 if ( isset( $last_run_data[$hook]['time'] ) && isset( $schedules[$schedule]['interval'] ) ) {
523 $interval = $schedules[$schedule]['interval'];
524 $last_run = $last_run_data[$hook]['time'];
525 $expected_next_run = $last_run + $interval;
526
527 // If the scheduled timestamp is in the past but we ran recently,
528 // the next run should be based on the last actual run
529 if ( $timestamp < time() &&
530 $last_run > ( time() - $interval ) ) {
531 // Cron ran recently, calculate next run from last run time
532 $event_info['next_run'] = $expected_next_run;
533 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $expected_next_run );
534 }
535 else if ( $timestamp < time() ) {
536 // Genuinely overdue
537 $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp );
538 }
539 else {
540 // Future scheduled time
541 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp );
542 }
543 }
544 else {
545 // No last run data, use the scheduled timestamp but be conservative about "overdue"
546 if ( $timestamp < time() ) {
547 // Only show as overdue if it's significantly past due (more than the schedule interval)
548 // to avoid false positives for crons that might be running but not tracked
549 $time_past_due = time() - $timestamp;
550 $interval = isset( $schedules[$schedule]['interval'] ) ? $schedules[$schedule]['interval'] : 3600; // Default 1 hour
551
552 if ( $time_past_due > $interval ) {
553 $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp );
554 }
555 else {
556 $event_info['next_run_human'] = 'Due to run';
557 }
558 }
559 else {
560 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp );
561 }
562 }
563
564 // Check if currently running (via transient)
565 $running_transient = get_transient( 'mwai_cron_running_' . $hook );
566 if ( $running_transient ) {
567 $event_info['is_running'] = true;
568 }
569
570 $cron_events[] = $event_info;
571 }
572 }
573
574 return $this->create_rest_response( [ 'success' => true, 'events' => $cron_events ], 200 );
575 }
576 catch ( Exception $e ) {
577 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
578 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
579 }
580 }
581
582 public function rest_helpers_run_cron( $request ) {
583 try {
584 $params = $request->get_json_params();
585 $hook = isset( $params['hook'] ) ? $params['hook'] : null;
586
587 if ( empty( $hook ) ) {
588 return $this->create_rest_response( [ 'success' => false, 'message' => 'No cron hook provided' ], 400 );
589 }
590
591 // Only allow running AI Engine crons (starting with mwai_)
592 if ( strpos( $hook, 'mwai_' ) !== 0 ) {
593 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid cron hook' ], 400 );
594 }
595
596 // Prevent running the Tasks Runner hooks directly - they should only run via cron
597 if ( $hook === 'mwai_tasks_internal_run' || $hook === 'mwai_tasks_internal_dev_run' ) {
598 return $this->create_rest_response( [
599 'success' => false,
600 'message' => 'The Tasks Runner cannot be triggered manually. It runs automatically based on its schedule.'
601 ], 403 );
602 }
603
604 // Check if the hook exists
605 if ( !has_action( $hook ) ) {
606 return $this->create_rest_response( [ 'success' => false, 'message' => 'Cron hook not found' ], 404 );
607 }
608
609 // Run the cron action
610 do_action( $hook );
611
612 return $this->create_rest_response( [
613 'success' => true,
614 'message' => 'Cron executed successfully',
615 'hook' => $hook
616 ], 200 );
617 }
618 catch ( Exception $e ) {
619 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
620 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
621 }
622 }
623
624 private function get_cron_display_name( $hook ) {
625 $names = [
626 'mwai_tasks_internal_run' => 'Tasks Runner',
627 'mwai_tasks_internal_dev_run' => 'Tasks Runner (Dev)',
628 'mwai_cleanup_oauth' => 'OAuth Cleanup',
629 'mwai_files_cleanup' => 'Files Cleanup',
630 'mwai_discussions' => 'Discussions Cleanup'
631 ];
632 return isset( $names[$hook] ) ? $names[$hook] : $hook;
633 }
634
635 private function get_cron_description( $hook ) {
636 $descriptions = [
637 'mwai_tasks_internal_run' => 'Processes background tasks and queued operations.',
638 'mwai_tasks_internal_dev_run' => 'Processes tasks in development mode (every 5 seconds).',
639 'mwai_cleanup_oauth' => 'Cleans up expired OAuth tokens and sessions.',
640 'mwai_files_cleanup' => 'Removes expired files based on expiration dates.',
641 'mwai_discussions' => 'Maintains chat discussions database and removes old entries.'
642 ];
643 return isset( $descriptions[$hook] ) ? $descriptions[$hook] : '';
644 }
645
646 public function rest_settings_update( $request ) {
647 try {
648 $params = $request->get_json_params();
649 $value = $params['options'];
650 $options = $this->core->update_options( $value );
651 $success = !!$options;
652 $message = __( $success ? 'OK' : 'Could not update options.', 'ai-engine' );
653 return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
654 }
655 catch ( Exception $e ) {
656 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
657 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
658 }
659 }
660
661 public function rest_settings_reset() {
662 try {
663 $options = $this->core->reset_options();
664 $success = !!$options;
665 $message = __( $success ? 'OK' : 'Could not reset options.', 'ai-engine' );
666 return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
667 }
668 catch ( Exception $e ) {
669 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
670 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
671 }
672 }
673
674 public function rest_ai_models( $request ) {
675 try {
676 $params = $request->get_json_params();
677 $envId = $params['envId'];
678 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId );
679 $models = $engine->retrieve_models();
680 return $this->create_rest_response( [ 'success' => true, 'models' => $models ], 200 );
681 }
682 catch ( Exception $e ) {
683 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
684 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
685 }
686 }
687
688 public function rest_ai_test_connection( $request ) {
689 try {
690 $params = $request->get_json_params();
691 $envId = $params['env_id'];
692
693 // Get the environment details
694 $env = null;
695 $envs = $this->core->get_option( 'ai_envs' );
696 foreach ( $envs as $e ) {
697 if ( $e['id'] === $envId ) {
698 $env = $e;
699 break;
700 }
701 }
702
703 if ( !$env ) {
704 throw new Exception( __( 'Environment not found.', 'ai-engine' ) );
705 }
706
707 // Get the engine and test connection
708 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId );
709 $result = $engine->connection_check();
710
711 // Format the response based on provider
712 $response = [
713 'success' => true,
714 'provider' => $env['type'],
715 'name' => $env['name'],
716 'data' => $result
717 ];
718
719 return $this->create_rest_response( $response, 200 );
720 }
721 catch ( Exception $e ) {
722 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
723 return $this->create_rest_response( [
724 'success' => false,
725 'error' => $message,
726 'provider' => isset( $env ) ? $env['type'] : 'unknown'
727 ], 200 ); // Return 200 even on error for consistent modal display
728 }
729 }
730
731 public function rest_ai_completions( $request ) {
732 try {
733 $params = $request->get_json_params();
734 $message = $this->retrieve_message( $params );
735 $query = new Meow_MWAI_Query_Text( $message );
736 $query->inject_params( $params );
737
738 // Handle streaming
739 $stream = $params['stream'] ?? false;
740 $streamCallback = null;
741 if ( $stream ) {
742 $streamCallback = function ( $reply ) use ( $query ) {
743 //$raw = _wp_specialchars( $reply, ENT_NOQUOTES, 'UTF-8', true );
744 $raw = $reply;
745 $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ], $query );
746 if ( ob_get_level() > 0 ) {
747 ob_flush();
748 }
749 flush();
750 };
751 if ( headers_sent( $filename, $linenum ) ) {
752 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
753 }
754 header( 'Cache-Control: no-cache' );
755 header( 'Content-Type: text/event-stream' );
756 header( 'X-Accel-Buffering: no' ); // This is useful to disable buffering in nginx through headers.
757 ob_implicit_flush( true );
758 if ( ob_get_level() > 0 ) {
759 ob_end_flush();
760 }
761 }
762
763 // Process Reply
764 $reply = $this->core->run_query( $query, $streamCallback );
765 $restRes = [
766 'success' => true,
767 'data' => $reply->result,
768 'usage' => $reply->usage
769 ];
770 if ( $stream ) {
771 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $restRes ) ], $query );
772 die();
773 }
774 return $this->create_rest_response( $restRes, 200 );
775 }
776 catch ( Exception $e ) {
777 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
778 if ( $stream ) {
779 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
780 }
781 else {
782 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
783 }
784 }
785 }
786
787 public function rest_ai_images( $request ) {
788 try {
789 $params = $request->get_json_params();
790 $message = $this->retrieve_message( $params );
791 $query = new Meow_MWAI_Query_Image( $message );
792 $query->inject_params( $params );
793 $reply = $this->core->run_query( $query );
794 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
795 }
796 catch ( Exception $e ) {
797 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
798 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
799 }
800 }
801
802 public function rest_ai_image_edit( $request ) {
803 try {
804 // Check if this is a multipart request with files
805 $files = $request->get_file_params();
806 $params = null;
807
808 // Debug logging
809 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
810 error_log( '[AI Engine Queries] Image Edit Request - Method: ' . $request->get_method() );
811 $content_type = $request->get_content_type();
812 if ( is_array( $content_type ) ) {
813 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type['value'] );
814 }
815 else {
816 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type );
817 }
818 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes (' . count( $files ) . ')' : 'no' ) );
819 }
820
821 if ( !empty( $files ) ) {
822 // Handle multipart form data - get all params including POST data
823 $params = $request->get_params();
824 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
825 error_log( '[AI Engine Queries] Image Edit Request - Using form data params' );
826 }
827 }
828 else {
829 // Try to get body params first (for form data without files)
830 $body_params = $request->get_body_params();
831 if ( !empty( $body_params ) ) {
832 $params = $body_params;
833 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
834 error_log( '[AI Engine Queries] Image Edit Request - Using body params' );
835 }
836 }
837 else {
838 // Handle JSON request
839 $params = $request->get_json_params();
840 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
841 error_log( '[AI Engine Queries] Image Edit Request - Using JSON params' );
842 }
843 }
844 }
845
846 // Ensure params is always an array
847 if ( empty( $params ) ) {
848 $params = [];
849 }
850
851 // Debug logging
852 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
853 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes' : 'no' ) );
854 error_log( '[AI Engine Queries] Image Edit Request - Params: ' . json_encode( $params ) );
855 }
856
857 $message = $this->retrieve_message( $params );
858 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
859 $query = new Meow_MWAI_Query_EditImage( $message );
860
861 // The inject_params method will handle setting the file from mediaId
862 $query->inject_params( $params );
863
864 // Handle mask file if provided
865 if ( !empty( $files['mask'] ) ) {
866 $mask_file = $files['mask'];
867 if ( $mask_file['error'] === UPLOAD_ERR_OK ) {
868 $mask_data = file_get_contents( $mask_file['tmp_name'] );
869 $query->set_mask( Meow_MWAI_Query_DroppedFile::from_data( $mask_data, 'analysis', $mask_file['type'] ) );
870 }
871 }
872
873 $reply = $this->core->run_query( $query );
874 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
875 }
876 catch ( Exception $e ) {
877 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
878 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
879 }
880 }
881
882 public function rest_ai_magic_wand( $request ) {
883 try {
884 $params = $request->get_json_params();
885 $action = isset( $params['action'] ) ? $params['action'] : null;
886 $data = isset( $params['data'] ) ? $params['data'] : null;
887 if ( empty( $data ) || empty( $action ) ) {
888 return $this->create_rest_response( [ 'success' => false, 'message' => 'An action and some data are required.' ], 500 );
889 }
890 $data = apply_filters( 'mwai_magic_wand_' . $action, '', $data );
891 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
892 }
893 catch ( Exception $e ) {
894 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
895 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
896 }
897 }
898
899 public function rest_ai_copilot( $request ) {
900 try {
901 $params = $request->get_json_params();
902 $action = sanitize_text_field( $params['action'] );
903 $message = $this->retrieve_message( $params, true );
904 $context = sanitize_text_field( $params['context'] );
905 $postId = !empty( $params['postId'] ) ? intval( $params['postId'] ) : null;
906 if ( empty( $action ) || empty( $message ) ) {
907 return $this->create_rest_response( [ 'success' => false, 'message' => 'Copilot needs an action and a prompt.' ], 500 );
908 }
909
910 global $mwai;
911 $result = null;
912 $params = [ 'scope' => 'copilot' ];
913
914 if ( $action === 'text' ) {
915 $prompt = "Here is the current article: \n\n===\n\n" . $context . "\n\n===\n\nIn this article, instead of the [== CURRENT BLOCK ==] placeholder, the author needs additional content. This new content should use the same tone, style, context, it should naturally flow in the article. The author shared additional information for this request:\n\n===\n\n" . $message . "\n\n===\n\nPlease provide the additional content. Only output the additional content, not the entire article, no need for extra information, and no need for the placeholders. Only output the content that should be added.";
916 if ( !empty( $model ) ) {
917 $params['model'] = $model;
918 }
919 $result = $mwai->simpleTextQuery( $prompt, $params );
920 }
921 else if ( $action === 'image' ) {
922 $prompt = "Here is the current article: \n\n===\n\n" . $context . "\n\n===\n\nIn this article, instead of the [== CURRENT BLOCK ==] placeholder, the author needs an image. Please write a detailed description (prompt) for that image that would fit this context. The image should be relevant to the article. The author shared additional information for this request:\n\n===\n\n" . $message . "\n\n===\n\nPlease only output the description for the image, not the entire article, no need for extra information, and no need for the placeholders. Only output the description.";
923
924 // Create the image
925 $simplifiedPrompt = $mwai->simpleTextQuery( $prompt, $params );
926 $media = $mwai->imageQueryForMediaLibrary( $simplifiedPrompt, $params, $postId );
927 $result = [ 'media' => $media ];
928 }
929 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
930 }
931 catch ( Exception $e ) {
932 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
933 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
934 }
935 }
936
937 public function rest_helpers_update_title( $request ) {
938 try {
939 $params = $request->get_json_params();
940 $title = sanitize_text_field( $params['title'] );
941 $postId = intval( $params['postId'] );
942 $post = get_post( $postId );
943 if ( !$post ) {
944 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
945 }
946 $post->post_title = $title;
947 //$post->post_name = sanitize_title( $title );
948 wp_update_post( $post );
949 return $this->create_rest_response( [ 'success' => true, 'message' => 'Title updated.' ], 200 );
950 }
951 catch ( Exception $e ) {
952 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
953 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
954 }
955 }
956
957 public function rest_helpers_update_excerpt( $request ) {
958 try {
959 $params = $request->get_json_params();
960 $excerpt = sanitize_text_field( $params['excerpt'] );
961 $postId = intval( $params['postId'] );
962 $post = get_post( $postId );
963 if ( !$post ) {
964 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
965 }
966 $post->post_excerpt = $excerpt;
967 wp_update_post( $post );
968 return $this->create_rest_response( [ 'success' => true, 'message' => 'Excerpt updated.' ], 200 );
969 }
970 catch ( Exception $e ) {
971 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
972 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
973 }
974 }
975
976 public function rest_helpers_create_post( $request ) {
977 try {
978 $params = $request->get_json_params();
979 $title = sanitize_text_field( $params['title'] );
980 $content = sanitize_textarea_field( $params['content'] );
981 $excerpt = sanitize_text_field( $params['excerpt'] );
982 $postType = sanitize_text_field( $params['postType'] );
983 $post = new stdClass();
984 $post->post_title = $title;
985 $post->post_excerpt = $excerpt;
986 $post->post_content = $content;
987 $post->post_status = 'draft';
988 $post->post_type = isset( $postType ) ? $postType : 'post';
989 // TODO: Let's try to avoid using Markdown to create the Post
990 // Instead, we should create Gutenberg Blocks, or simple HTML.
991 // Then, we can get rid of the library for Markdown.
992 $post->post_content = $this->core->markdown_to_html( $post->post_content );
993 $postId = wp_insert_post( $post );
994 return $this->create_rest_response( [ 'success' => true, 'postId' => $postId ], 200 );
995 }
996 catch ( Exception $e ) {
997 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
998 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
999 }
1000 }
1001
1002 public function rest_helpers_create_images( $request ) {
1003 try {
1004 $params = $request->get_json_params();
1005 $title = sanitize_text_field( $params['title'] );
1006 $caption = sanitize_text_field( $params['caption'] );
1007 $alt = sanitize_text_field( $params['alt'] );
1008 $description = sanitize_text_field( $params['description'] );
1009 $url = $params['url'];
1010 $filename = sanitize_text_field( $params['filename'] );
1011
1012 // Prepare AI metadata
1013 $ai_metadata = [];
1014 if ( !empty( $params['model'] ) ) {
1015 $ai_metadata['model'] = $params['model'];
1016 }
1017 if ( !empty( $params['latency'] ) ) {
1018 $ai_metadata['latency'] = $params['latency'];
1019 }
1020 if ( !empty( $params['env_id'] ) ) {
1021 $ai_metadata['env_id'] = $params['env_id'];
1022 }
1023
1024 // Debug logging
1025 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1026 error_log( '[AI Engine] create_image metadata: ' . json_encode( $ai_metadata ) );
1027 }
1028
1029 // Create as mwai_image post type (draft image)
1030 $attachmentId = $this->core->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, null, 'inherit', 'mwai_image', $ai_metadata );
1031
1032 // Add to user's draft media
1033 $user_id = get_current_user_id();
1034 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
1035 if ( !is_array( $draft_media ) ) {
1036 $draft_media = [];
1037 }
1038 $draft_media[] = [
1039 'attachment_id' => $attachmentId,
1040 'type' => 'image',
1041 'created_at' => time()
1042 ];
1043 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
1044
1045 return $this->create_rest_response( [ 'success' => true, 'attachmentId' => $attachmentId ], 200 );
1046 }
1047 catch ( Exception $e ) {
1048 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1049 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1050 }
1051 }
1052
1053 public function rest_helpers_generate_image_meta( $request ) {
1054 try {
1055 global $mwai;
1056 $params = $request->get_json_params();
1057 $attachment_id = isset( $params['attachmentId'] ) ? intval( $params['attachmentId'] ) : null;
1058
1059 if ( empty( $attachment_id ) ) {
1060 throw new Exception( __( 'The attachment ID is required.', 'ai-engine' ) );
1061 }
1062
1063 // Get the file path from the attachment ID
1064 $file_path = get_attached_file( $attachment_id );
1065 if ( empty( $file_path ) || !file_exists( $file_path ) ) {
1066 throw new Exception( __( 'Could not find the attachment file.', 'ai-engine' ) );
1067 }
1068
1069 $prompt = 'Describe this image and suggest a short title and description. '
1070 . 'Also suggest an SEO-friendly filename (lowercase, ASCII characters only, with hyphens instead of spaces). '
1071 . 'Return a JSON with the keys: title, description, filename.';
1072
1073 // Use file path instead of URL to avoid network issues
1074 $result = $mwai->simpleVisionQuery( $prompt, null, $file_path, [ 'scope' => 'admin-tools' ] );
1075 $result = preg_replace( '/^```json\s*/', '', $result );
1076 $result = preg_replace( '/\s*```$/', '', $result );
1077 if ( is_string( $result ) ) {
1078 $data = json_decode( $result, true );
1079 }
1080 else {
1081 $data = $result;
1082 }
1083 if ( !is_array( $data ) ) {
1084 $data = [];
1085 }
1086 $data = array_merge( [ 'title' => '', 'description' => '', 'filename' => '' ], $data );
1087 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1088 }
1089 catch ( Exception $e ) {
1090 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1091 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1092 }
1093 }
1094
1095 public function rest_helpers_update_media_metadata( $request ) {
1096 try {
1097 $params = $request->get_json_params();
1098 $attachment_id = intval( $params['attachmentId'] );
1099 $title = sanitize_text_field( $params['title'] ?? '' );
1100 $description = sanitize_text_field( $params['description'] ?? '' );
1101 $caption = sanitize_text_field( $params['caption'] ?? '' );
1102 $alt = sanitize_text_field( $params['alt'] ?? '' );
1103 $filename = sanitize_file_name( $params['filename'] ?? '' );
1104
1105 if ( !$attachment_id ) {
1106 throw new Exception( __( 'Attachment ID is required.', 'ai-engine' ) );
1107 }
1108
1109 // Generate slug from filename (without extension)
1110 $slug = '';
1111 if ( !empty( $filename ) ) {
1112 $slug = pathinfo( $filename, PATHINFO_FILENAME );
1113 }
1114
1115 // Update post title, content (description), caption, and slug
1116 $update_data = [
1117 'ID' => $attachment_id,
1118 'post_title' => $title,
1119 'post_content' => $description,
1120 'post_excerpt' => $caption
1121 ];
1122
1123 if ( !empty( $slug ) ) {
1124 $update_data['post_name'] = $slug;
1125 }
1126
1127 wp_update_post( $update_data );
1128
1129 // Update alt text
1130 if ( !empty( $alt ) ) {
1131 update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
1132 }
1133
1134 // Update filename if provided
1135 $new_url = null;
1136 if ( !empty( $filename ) ) {
1137 $file_path = get_attached_file( $attachment_id );
1138 if ( $file_path ) {
1139 // Security: Validate file extension to prevent arbitrary file upload attacks
1140 $original_ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
1141 $new_ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
1142
1143 // Allowlist of safe media extensions (no executable types)
1144 $allowed_extensions = [
1145 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif',
1146 'mp4', 'webm', 'ogg', 'mov', 'avi', 'wmv', 'flv', 'm4v',
1147 'mp3', 'wav', 'flac', 'aac', 'm4a', 'wma',
1148 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'rtf'
1149 ];
1150
1151 // Extension must be in allowlist AND match original extension
1152 if ( !in_array( $new_ext, $allowed_extensions, true ) ) {
1153 throw new Exception( __( 'Invalid file extension. Only media file extensions are allowed.', 'ai-engine' ) );
1154 }
1155 if ( $new_ext !== $original_ext ) {
1156 throw new Exception( __( 'File extension must match the original file type.', 'ai-engine' ) );
1157 }
1158
1159 $path_parts = pathinfo( $file_path );
1160 $new_file_path = $path_parts['dirname'] . '/' . $filename;
1161 if ( rename( $file_path, $new_file_path ) ) {
1162 update_attached_file( $attachment_id, $new_file_path );
1163 // Build new URL from file path for custom post types
1164 $upload_dir = wp_upload_dir();
1165 $new_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $new_file_path );
1166 }
1167 }
1168 }
1169
1170 $response = [ 'success' => true ];
1171 if ( $new_url ) {
1172 $response['url'] = $new_url;
1173 }
1174
1175 return $this->create_rest_response( $response, 200 );
1176 }
1177 catch ( Exception $e ) {
1178 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1179 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1180 }
1181 }
1182
1183 public function rest_openai_files_get() {
1184 try {
1185 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1186 $purposeFilter = isset( $_GET['purpose'] ) ? $_GET['purpose'] : null;
1187 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1188 $files = $openai->list_files( $purposeFilter );
1189 return $this->create_rest_response( [ 'success' => true, 'files' => $files ], 200 );
1190 }
1191 catch ( Exception $e ) {
1192 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1193 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1194 }
1195 }
1196
1197 // TODO: Remove all the rest_openai_*finetune* handlers below after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
1198 public function rest_openai_deleted_finetunes_get() {
1199 try {
1200 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1201 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1202 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1203 $finetunes = $openai->list_deleted_finetunes( $legacy );
1204 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1205 }
1206 catch ( Exception $e ) {
1207 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1208 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1209 }
1210 }
1211
1212 public function rest_openai_finetunes_get() {
1213 try {
1214 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1215 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1216 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1217 $finetunes = $openai->list_finetunes( $legacy );
1218 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1219 }
1220 catch ( Exception $e ) {
1221 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1222 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1223 }
1224 }
1225
1226 public function rest_openai_files_upload( $request ) {
1227 try {
1228 $params = $request->get_json_params();
1229 $envId = $params['envId'];
1230 ;
1231 $filename = sanitize_text_field( $params['filename'] );
1232 $data = $params['data'];
1233 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1234 $file = $openai->upload_file( $filename, $data );
1235 return $this->create_rest_response( [ 'success' => true, 'file' => $file ], 200 );
1236 }
1237 catch ( Exception $e ) {
1238 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1239 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1240 }
1241 }
1242
1243 public function rest_openai_files_delete( $request ) {
1244 try {
1245 $params = $request->get_json_params();
1246 $envId = $params['envId'];
1247 ;
1248 $fileId = $params['fileId'];
1249 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1250 $openai->delete_file( $fileId );
1251 return $this->create_rest_response( [ 'success' => true ], 200 );
1252 }
1253 catch ( Exception $e ) {
1254 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1255 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1256 }
1257 }
1258
1259 public function rest_openai_finetunes_cancel( $request ) {
1260 try {
1261 $params = $request->get_json_params();
1262 $envId = $params['envId'];
1263 ;
1264 $finetuneId = $params['finetuneId'];
1265 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1266 $openai->cancel_finetune( $finetuneId );
1267 return $this->create_rest_response( [ 'success' => true ], 200 );
1268 }
1269 catch ( Exception $e ) {
1270 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1271 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1272 }
1273 }
1274
1275 public function rest_openai_finetunes_delete( $request ) {
1276 try {
1277 $params = $request->get_json_params();
1278 $envId = $params['envId'];
1279 ;
1280 $modelId = $params['modelId'];
1281 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1282 $openai->delete_finetune( $modelId );
1283 return $this->create_rest_response( [ 'success' => true ], 200 );
1284 }
1285 catch ( Exception $e ) {
1286 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1287 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1288 }
1289 }
1290
1291 public function rest_openai_files_download( $request ) {
1292 try {
1293 $params = $request->get_json_params();
1294 $envId = $params['envId'];
1295 ;
1296 $fileId = $params['fileId'];
1297 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1298 $filename = $openai->download_file( $fileId );
1299 $data = file_get_contents( $filename );
1300 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1301 }
1302 catch ( Exception $e ) {
1303 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1304 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1305 }
1306 }
1307
1308 public function rest_openai_files_finetune( $request ) {
1309 try {
1310 $params = $request->get_json_params();
1311 $envId = $params['envId'];
1312 ;
1313 $fileId = $params['fileId'];
1314 $model = $params['model'];
1315 $suffix = $params['suffix'];
1316 $hyperparams = [
1317 'nEpochs' => isset( $params['nEpochs'] ) ? $params['nEpochs'] : null,
1318 'batchSize' => isset( $params['batchSize'] ) ? $params['batchSize'] : null,
1319 ];
1320 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1321 $finetune = $openai->run_finetune( $fileId, $model, $suffix, $hyperparams );
1322 return $this->create_rest_response( [ 'success' => true, 'finetune' => $finetune ], 200 );
1323 }
1324 catch ( Exception $e ) {
1325 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1326 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1327 }
1328 }
1329
1330 public function rest_helpers_count_posts( $request ) {
1331 try {
1332 global $wpdb;
1333 $params = $request->get_query_params();
1334 $postType = $params['postType'];
1335 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1336 $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) );
1337 $ignored_ids = $wpdb->get_col(
1338 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_mwai_embedding_ignore'"
1339 );
1340 $exclude_sql = '';
1341 if ( !empty( $ignored_ids ) ) {
1342 $ignored_ids = array_map( 'intval', $ignored_ids );
1343 $exclude_sql = ' AND p.ID NOT IN (' . implode( ',', $ignored_ids ) . ')';
1344 }
1345 $mimeFilter = '';
1346 if ( $postType === 'attachment' ) {
1347 $mimeFilter = " AND p.post_mime_type LIKE 'image/%'";
1348 }
1349 $query = "SELECT COUNT(*) FROM {$wpdb->posts} p
1350 WHERE p.post_type = %s
1351 AND p.post_status IN ($statusPlaceholders)" . $exclude_sql . $mimeFilter;
1352 $prepareArgs = array_merge( [ $postType ], $postStatus );
1353 $count = (int) $wpdb->get_var( $wpdb->prepare( $query, ...$prepareArgs ) );
1354 return $this->create_rest_response( [ 'success' => true, 'count' => $count ], 200 );
1355 }
1356 catch ( Exception $e ) {
1357 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1358 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1359 }
1360 }
1361
1362 public function rest_helpers_posts_ids( $request ) {
1363 try {
1364 global $wpdb;
1365 $params = $request->get_query_params();
1366 $postType = $params['postType'];
1367 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1368
1369 // Use direct SQL query instead of get_posts to avoid memory issues with large sites
1370 $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) );
1371 $ignored_ids = $wpdb->get_col(
1372 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_mwai_embedding_ignore'"
1373 );
1374 $exclude_sql = '';
1375 if ( !empty( $ignored_ids ) ) {
1376 $ignored_ids = array_map( 'intval', $ignored_ids );
1377 $exclude_sql = ' AND p.ID NOT IN (' . implode( ',', $ignored_ids ) . ')';
1378 }
1379 $mimeFilter = '';
1380 if ( $postType === 'attachment' ) {
1381 $mimeFilter = " AND p.post_mime_type LIKE 'image/%'";
1382 }
1383 $query = "SELECT p.ID FROM {$wpdb->posts} p
1384 WHERE p.post_type = %s
1385 AND p.post_status IN ($statusPlaceholders)" . $exclude_sql . $mimeFilter . '
1386 ORDER BY p.ID ASC';
1387
1388 $prepareArgs = array_merge( [ $postType ], $postStatus );
1389 $postIds = $wpdb->get_col( $wpdb->prepare( $query, ...$prepareArgs ) );
1390 $postIds = array_map( 'intval', $postIds );
1391
1392 return $this->create_rest_response( [ 'success' => true, 'postIds' => $postIds ], 200 );
1393 }
1394 catch ( Exception $e ) {
1395 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1396 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1397 }
1398 }
1399
1400 public function rest_helpers_post_content( $request ) {
1401 try {
1402 $params = $request->get_query_params();
1403 $offset = (int) $params['offset'];
1404 $postType = $params['postType'];
1405 $postStatus = isset( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1406 $postId = (int) $params['postId'];
1407
1408 $post = null;
1409 if ( !empty( $postId ) ) {
1410 $post = get_post( $postId );
1411 if ( $post->post_status !== 'publish' && $post->post_status !== 'future'
1412 && $post->post_status !== 'draft' && $post->post_status !== 'private' ) {
1413 $post = null;
1414 }
1415 }
1416 else {
1417 $posts = get_posts( [
1418 'posts_per_page' => 1,
1419 'post_type' => $postType,
1420 'offset' => $offset,
1421 'post_status' => $postStatus,
1422 ] );
1423 $post = count( $posts ) === 0 ? null : $posts[0];
1424 }
1425 if ( !$post ) {
1426 return $this->create_rest_response( [ 'success' => false, 'message' => 'Post not found' ], 404 );
1427 }
1428 $cleanPost = $this->core->get_post( $post );
1429 return $this->create_rest_response( [ 'success' => true, 'content' => $cleanPost['content'],
1430 'checksum' => $cleanPost['checksum'], 'language' => $cleanPost['language'], 'excerpt' => $cleanPost['excerpt'],
1431 'postId' => $cleanPost['postId'], 'title' => $cleanPost['title'], 'url' => $cleanPost['url'] ], 200 );
1432 }
1433 catch ( Exception $e ) {
1434 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1435 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1436 }
1437 }
1438
1439 // Batch check which posts have content (for Push All optimization)
1440 public function rest_helpers_check_posts_content( $request ) {
1441 try {
1442 $params = $request->get_json_params();
1443 $postIds = isset( $params['postIds'] ) ? $params['postIds'] : [];
1444
1445 if ( empty( $postIds ) || !is_array( $postIds ) ) {
1446 return $this->create_rest_response( [
1447 'success' => false,
1448 'message' => 'postIds array is required'
1449 ], 400 );
1450 }
1451
1452 // Sanitize post IDs
1453 $postIds = array_map( 'intval', $postIds );
1454
1455 // Check content using the mwai_pre_post_content filter to support page builders,
1456 // ACF, and other plugins that store content outside of post_content
1457 $postsWithContent = [];
1458
1459 foreach ( $postIds as $postId ) {
1460 $post = get_post( $postId );
1461 if ( !$post ) {
1462 continue;
1463 }
1464 // Apply the same filter used by get_post_content() in core.php
1465 $content = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
1466 $content = trim( strip_tags( $content ) );
1467 if ( !empty( $content ) ) {
1468 $postsWithContent[] = $postId;
1469 }
1470 }
1471
1472 return $this->create_rest_response( [
1473 'success' => true,
1474 'postsWithContent' => $postsWithContent
1475 ], 200 );
1476 }
1477 catch ( Exception $e ) {
1478 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1479 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1480 }
1481 }
1482
1483 public function rest_helpers_run_tasks( $request ) {
1484 try {
1485 // Prevent concurrent execution with a transient lock
1486 $lock_key = 'mwai_rest_run_tasks_lock';
1487 if ( get_transient( $lock_key ) ) {
1488 // Log excessive calls for debugging
1489 if ( $this->core->get_option( 'dev_mode' ) ) {
1490 error_log( '[AI Engine] WARNING: rest_helpers_run_tasks called while already running' );
1491 }
1492 return $this->create_rest_response( [
1493 'success' => false,
1494 'message' => 'Tasks are already running. Please wait.'
1495 ], 429 ); // 429 Too Many Requests
1496 }
1497
1498 // Set lock for 30 seconds
1499 set_transient( $lock_key, true, 30 );
1500
1501 // Log task execution start
1502 if ( $this->core->get_option( 'dev_mode' ) ) {
1503 error_log( '[AI Engine] rest_helpers_run_tasks triggered via REST API' );
1504 }
1505
1506 try {
1507 do_action( 'mwai_tasks_run' );
1508 delete_transient( $lock_key );
1509 return $this->create_rest_response( [ 'success' => true ], 200 );
1510 }
1511 catch ( Exception $e ) {
1512 delete_transient( $lock_key );
1513 throw $e;
1514 }
1515 }
1516 catch ( Exception $e ) {
1517 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1518 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1519 }
1520 }
1521
1522 public function rest_helpers_optimize_database( $request ) {
1523 try {
1524 global $wpdb;
1525 $results = [];
1526
1527 // Add indexes to optimize query performance
1528 $indexes = [
1529 // mwai_logs indexes
1530 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time', 'columns' => 'time' ],
1531 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_userId', 'columns' => 'userId' ],
1532 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_envId', 'columns' => 'envId' ],
1533 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_refId', 'columns' => 'refId' ],
1534 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time_model', 'columns' => 'time, model' ],
1535
1536 // mwai_logmeta indexes
1537 [ 'table' => 'mwai_logmeta', 'name' => 'idx_mwai_logmeta_log_id', 'columns' => 'log_id' ],
1538
1539 // mwai_vectors indexes
1540 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_envId_status_dbId', 'columns' => 'envId, status, dbId' ],
1541 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_refId', 'columns' => 'refId' ],
1542 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_status', 'columns' => 'status' ],
1543 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_updated', 'columns' => 'updated' ],
1544
1545 // mwai_files indexes
1546 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_expires', 'columns' => 'expires' ],
1547 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_userId', 'columns' => 'userId' ],
1548 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_purpose', 'columns' => 'purpose' ],
1549
1550 // mwai_filemeta indexes
1551 [ 'table' => 'mwai_filemeta', 'name' => 'idx_mwai_filemeta_file_id', 'columns' => 'file_id' ],
1552
1553 // mwai_chats indexes
1554 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_botId', 'columns' => 'chatId, botId' ],
1555 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_userId', 'columns' => 'chatId, userId' ],
1556 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_updated', 'columns' => 'updated' ],
1557 ];
1558
1559 // Add indexes
1560 foreach ( $indexes as $index ) {
1561 $table = $wpdb->prefix . $index['table'];
1562 $index_name = $index['name'];
1563 $columns = $index['columns'];
1564
1565 // Check if index already exists
1566 $existing = $wpdb->get_var( $wpdb->prepare(
1567 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
1568 WHERE table_schema = %s AND table_name = %s AND index_name = %s',
1569 DB_NAME,
1570 $table,
1571 $index_name
1572 ) );
1573
1574 if ( !$existing ) {
1575 $wpdb->query( "ALTER TABLE `$table` ADD INDEX `$index_name` ($columns)" );
1576 $results[] = "Added index $index_name on $table";
1577 }
1578 }
1579
1580 // Clean up old logs (older than 3 months)
1581 $three_months_ago = date( 'Y-m-d H:i:s', strtotime( '-3 months' ) );
1582
1583 // Delete old logs
1584 $deleted_logs = $wpdb->query( $wpdb->prepare(
1585 "DELETE FROM {$wpdb->prefix}mwai_logs WHERE time < %s",
1586 $three_months_ago
1587 ) );
1588 $results[] = "Deleted $deleted_logs old log entries";
1589
1590 // Delete orphaned logmeta
1591 $deleted_logmeta = $wpdb->query(
1592 "DELETE lm FROM {$wpdb->prefix}mwai_logmeta lm
1593 LEFT JOIN {$wpdb->prefix}mwai_logs l ON lm.log_id = l.id
1594 WHERE l.id IS NULL"
1595 );
1596 $results[] = "Deleted $deleted_logmeta orphaned logmeta entries";
1597
1598 // Delete old chats (older than 3 months)
1599 $deleted_chats = $wpdb->query( $wpdb->prepare(
1600 "DELETE FROM {$wpdb->prefix}mwai_chats WHERE updated < %s",
1601 $three_months_ago
1602 ) );
1603 $results[] = "Deleted $deleted_chats old chat discussions";
1604
1605 // Optimize tables
1606 $tables = [ 'mwai_logs', 'mwai_logmeta', 'mwai_vectors', 'mwai_files', 'mwai_filemeta', 'mwai_chats' ];
1607 foreach ( $tables as $table ) {
1608 $wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}$table" );
1609 }
1610 $results[] = 'Optimized all AI Engine tables';
1611
1612 $message = implode( "\n", $results );
1613 return $this->create_rest_response( [ 'success' => true, 'message' => $message ], 200 );
1614 }
1615 catch ( Exception $e ) {
1616 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1617 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1618 }
1619 }
1620
1621 public function rest_system_templates_get( $request ) {
1622 try {
1623 $params = $request->get_query_params();
1624 $category = $params['category'];
1625 $templates = [];
1626 $templates_option = get_option( 'mwai_templates', [] );
1627 if ( !is_array( $templates_option ) ) {
1628 update_option( 'mwai_templates', [] );
1629 $templates_option = [];
1630 }
1631
1632 // Migration: DALL-E was removed (deprecated by OpenAI). Move templates to gpt-image-1.5.
1633 // TODO: Remove after 2027-04 (1 year after the shutdown on 2026-05-12).
1634 $deprecated = [ 'dall-e', 'dall-e-2', 'dall-e-3', 'dall-e-3-hd' ];
1635 $migrated = false;
1636 foreach ( $templates_option as &$group ) {
1637 if ( !empty( $group['templates'] ) && is_array( $group['templates'] ) ) {
1638 foreach ( $group['templates'] as &$template ) {
1639 if ( isset( $template['model'] ) && in_array( $template['model'], $deprecated, true ) ) {
1640 $template['model'] = MWAI_FALLBACK_MODEL_IMAGES;
1641 $migrated = true;
1642 }
1643 }
1644 }
1645 }
1646 unset( $group, $template );
1647 if ( $migrated ) {
1648 update_option( 'mwai_templates', $templates_option );
1649 }
1650
1651 $categories = array_column( $templates_option, 'category' );
1652 $index = array_search( $category, $categories );
1653 $templates = [];
1654 if ( $index !== false ) {
1655 $templates = $templates_option[$index]['templates'];
1656 }
1657 return $this->create_rest_response( [ 'success' => true, 'templates' => $templates ], 200 );
1658 }
1659 catch ( Exception $e ) {
1660 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1661 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1662 }
1663 }
1664
1665 public function rest_system_templates_save( $request ) {
1666 try {
1667 $params = $request->get_json_params();
1668 $category = $params['category'];
1669 $templates = $params['templates'];
1670 $templates_option = get_option( 'mwai_templates', [] );
1671 $categories = array_column( $templates_option, 'category' );
1672 $index = array_search( $category, $categories );
1673 if ( $index !== false && $index >= 0 ) {
1674 $templates_option[$index]['templates'] = $templates;
1675 }
1676 else {
1677 $group = [ 'category' => $category, 'templates' => $templates ];
1678 $templates_option[] = $group;
1679 }
1680
1681 update_option( 'mwai_templates', $templates_option );
1682 return $this->create_rest_response( [ 'success' => true ], 200 );
1683 }
1684 catch ( Exception $e ) {
1685 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1686 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1687 }
1688 }
1689
1690 public function rest_system_logs_list( $request ) {
1691 try {
1692 $params = $request->get_json_params();
1693 $offset = $params['offset'];
1694 $limit = $params['limit'];
1695 $filters = $params['filters'];
1696 $sort = isset( $params['sort'] ) ? $params['sort'] : null;
1697 $logs = apply_filters( 'mwai_stats_logs_list', [], $offset, $limit, $filters, $sort );
1698 return $this->create_rest_response( [ 'success' => true, 'total' => $logs['total'], 'logs' => $logs['rows'] ], 200 );
1699 }
1700 catch ( Exception $e ) {
1701 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1702 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1703 }
1704 }
1705
1706 public function rest_system_logs_delete( $request ) {
1707 try {
1708 $params = $request->get_json_params();
1709 $logIds = $params['logIds'];
1710 $success = apply_filters( 'mwai_stats_logs_delete', true, $logIds );
1711 return $this->create_rest_response( [ 'success' => $success ], 200 );
1712 }
1713 catch ( Exception $e ) {
1714 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1715 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1716 }
1717 }
1718
1719 public function rest_system_logs_meta_get( $request ) {
1720 try {
1721 $params = $request->get_json_params();
1722 $logId = $params['logId'];
1723 $metaKeys = $params['metaKeys'];
1724 $data = apply_filters( 'mwai_stats_logs_meta', [], $logId, $metaKeys );
1725 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1726 }
1727 catch ( Exception $e ) {
1728 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1729 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1730 }
1731 }
1732
1733 public function rest_system_logs_activity( $request ) {
1734 try {
1735 $params = $request->get_json_params();
1736 $hours = isset( $params['hours'] ) ? intval( $params['hours'] ) : 24;
1737 $data = apply_filters( 'mwai_stats_logs_activity', [], $hours );
1738 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1739 }
1740 catch ( Exception $e ) {
1741 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1742 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1743 }
1744 }
1745
1746 public function rest_system_logs_activity_daily( $request ) {
1747 try {
1748 $params = $request->get_json_params();
1749 $days = isset( $params['days'] ) ? intval( $params['days'] ) : 31;
1750 $byModel = isset( $params['byModel'] ) ? (bool) $params['byModel'] : false;
1751 $feature = isset( $params['feature'] ) ? sanitize_text_field( $params['feature'] ) : null;
1752
1753 if ( $byModel ) {
1754 $data = apply_filters( 'mwai_stats_logs_activity_daily_by_model', [], $days );
1755 }
1756 else {
1757 $data = apply_filters( 'mwai_stats_logs_activity_daily', [], $days, $feature );
1758 }
1759
1760 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1761 }
1762 catch ( Exception $e ) {
1763 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1764 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1765 }
1766 }
1767
1768 public function rest_ai_moderate( $request ) {
1769 try {
1770 $params = $request->get_json_params();
1771 $envId = $params['envId'];
1772 $text = $params['text'];
1773 if ( !$text ) {
1774 return $this->create_rest_response( [ 'success' => false, 'message' => 'Text not found.' ], 404 );
1775 }
1776 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1777 $results = $openai->moderate( $text );
1778 return $this->create_rest_response( [ 'success' => true, 'results' => $results ], 200 );
1779 }
1780 catch ( Exception $e ) {
1781 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1782 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1783 }
1784 }
1785
1786 public function rest_ai_transcribe_audio( $request ) {
1787 try {
1788 global $mwai;
1789 $params = $request->get_json_params();
1790 $url = !empty( $params['url'] ) ? $params['url'] : null;
1791 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
1792 $path = !empty( $params['path'] ) ? $params['path'] : null;
1793
1794 // If mediaId is provided, get the file path
1795 if ( !$path && $mediaId > 0 ) {
1796 $path = get_attached_file( $mediaId );
1797 if ( empty( $path ) ) {
1798 throw new Exception( __( 'The media file cannot be found.', 'ai-engine' ) );
1799 }
1800 }
1801
1802 // Set the scope for admin tools
1803 if ( !isset( $params['scope'] ) ) {
1804 $params['scope'] = 'admin-tools';
1805 }
1806
1807 $result = $mwai->simpleTranscribeAudio( $url, $path, $params );
1808 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1809 }
1810 catch ( Exception $e ) {
1811 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1812 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1813 }
1814 }
1815
1816 public function rest_ai_transcribe_image( $request ) {
1817 try {
1818 global $mwai;
1819 $params = $request->get_json_params();
1820 $message = $this->retrieve_message( $params );
1821 $url = !empty( $params['url'] ) ? $params['url'] : null;
1822 // This could lead to a security issue, so let's avoid using path directly.
1823 //$path = !empty( $params['path'] ) ? $params['path'] : null;
1824 $result = $mwai->simpleVisionQuery( $message, $url );
1825 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1826 }
1827 catch ( Exception $e ) {
1828 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1829 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1830 }
1831 }
1832
1833 public function rest_ai_json( $request ) {
1834 try {
1835 global $mwai;
1836 $params = $request->get_json_params();
1837 $message = $this->retrieve_message( $params );
1838 $result = $mwai->simpleJsonQuery( $message );
1839 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1840 }
1841 catch ( Exception $e ) {
1842 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1843 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1844 }
1845 }
1846
1847 public function rest_mcp_functions( $request ) {
1848 try {
1849 // Get all registered MCP tools
1850 $tools = apply_filters( 'mwai_mcp_tools', [] );
1851
1852 // Format the response
1853 $response = [
1854 'success' => true,
1855 'count' => count( $tools ),
1856 'functions' => $tools
1857 ];
1858
1859 return $this->create_rest_response( $response, 200 );
1860 }
1861 catch ( Exception $e ) {
1862 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1863 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1864 }
1865 }
1866
1867 /**
1868 * Loopback test that mimics the way Anthropic's claude.ai connector probes
1869 * the OAuth discovery endpoint, so admins can detect a hosting-layer block
1870 * (typically WP Engine's WAF rejecting the python-httpx User-Agent on
1871 * /.well-known/oauth-* paths) without waiting until they try to connect from
1872 * claude.ai. Hits the site's own public PRM URL with the same UA Anthropic
1873 * uses and reports the result.
1874 */
1875 public function rest_mcp_self_test( $request ) {
1876 try {
1877 $resource_url = rest_url( 'mcp/v1/http' );
1878 $probe_url = home_url( '/.well-known/oauth-protected-resource' . wp_parse_url( $resource_url, PHP_URL_PATH ) );
1879 $reference_url = rest_url( 'mcp/v1/.well-known/oauth-protected-resource' );
1880
1881 $args = [
1882 'timeout' => 10,
1883 'redirection' => 3,
1884 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ),
1885 'user-agent' => 'python-httpx/0.28.1',
1886 'headers' => [
1887 'Accept' => 'application/json',
1888 ],
1889 ];
1890
1891 $probe_response = wp_remote_get( $probe_url, $args );
1892 $reference_args = $args;
1893 $reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0';
1894 $reference_response = wp_remote_get( $reference_url, $reference_args );
1895
1896 $build = function( $url, $response ) {
1897 if ( is_wp_error( $response ) ) {
1898 return [
1899 'url' => $url,
1900 'reachable' => false,
1901 'error' => $response->get_error_message(),
1902 'status' => null,
1903 'content_type' => null,
1904 ];
1905 }
1906 return [
1907 'url' => $url,
1908 'reachable' => true,
1909 'status' => (int) wp_remote_retrieve_response_code( $response ),
1910 'content_type' => wp_remote_retrieve_header( $response, 'content-type' ),
1911 ];
1912 };
1913
1914 $probe = $build( $probe_url, $probe_response );
1915 $reference = $build( $reference_url, $reference_response );
1916
1917 $verdict = 'unknown';
1918 $message = '';
1919 if ( $probe['reachable'] && $probe['status'] === 200 ) {
1920 $verdict = 'ok';
1921 $message = 'Your site allows the python-httpx User-Agent on the OAuth discovery path. Claude.ai\'s connector should be able to reach it.';
1922 } else if ( $probe['reachable'] && $probe['status'] === 403 ) {
1923 $verdict = 'waf_blocks_python_ua';
1924 $message = 'Your host returned 403 to a User-Agent containing "python" on the OAuth discovery path. Claude.ai uses python-httpx as its outbound HTTP client, so its connector will fail with "Couldn\'t reach the MCP server". This is a common default on WP Engine. Fix: add a Cloudflare Transform Rule that rewrites the User-Agent for /.well-known/oauth-* and /wp-json/mcp/v1/* paths before the request reaches your origin. See https://meowapps.com/fix-mcp-wordpress-connection for the full recipe.';
1925 } else if ( $probe['reachable'] && $probe['status'] === 404 ) {
1926 $verdict = 'wellknown_blocked';
1927 $message = 'Your host returned 404 for the host-root /.well-known/oauth-protected-resource path. This usually means your hosting layer (.htaccess, nginx config, or a security plugin) intercepts /.well-known/* paths before WordPress sees them. Adjust rewrites so the path reaches index.php.';
1928 } else if ( !$probe['reachable'] ) {
1929 $verdict = 'unreachable';
1930 $message = 'The loopback request could not reach the site at all (' . esc_html( $probe['error'] ) . '). Check that the site is publicly resolvable and that the server can reach itself over HTTPS.';
1931 } else {
1932 $verdict = 'unexpected_status';
1933 $message = 'Got HTTP ' . $probe['status'] . ' from the loopback probe. Expected 200. Investigate the response in your CDN/origin logs.';
1934 }
1935
1936 return $this->create_rest_response( [
1937 'success' => true,
1938 'verdict' => $verdict,
1939 'message' => $message,
1940 'probe' => $probe,
1941 'reference' => $reference,
1942 ], 200 );
1943 }
1944 catch ( Exception $e ) {
1945 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1946 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1947 }
1948 }
1949
1950 /**
1951 * Top MCP tools by call count over the last N days. Powers the
1952 * "Top Tools" widget in the MCP Logs view of the Insights screen.
1953 * Returns rows shaped as { tool, count, success_count, error_count }.
1954 */
1955 public function rest_mcp_top_tools( $request ) {
1956 try {
1957 $params = $request->get_json_params();
1958 $days = isset( $params['days'] ) ? max( 1, intval( $params['days'] ) ) : 7;
1959 $limit = isset( $params['limit'] ) ? max( 1, min( 50, intval( $params['limit'] ) ) ) : 10;
1960
1961 global $wpdb;
1962 $table = $wpdb->prefix . 'mwai_logs';
1963 $rows = $wpdb->get_results(
1964 $wpdb->prepare(
1965 "SELECT scope AS tool,
1966 COUNT(*) AS count,
1967 SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS success_count,
1968 SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS error_count
1969 FROM $table
1970 WHERE feature = 'mcp_tool'
1971 AND time >= DATE_SUB(NOW(), INTERVAL %d DAY)
1972 GROUP BY scope
1973 ORDER BY count DESC
1974 LIMIT %d",
1975 '%"status":"success"%',
1976 '%"status":"error"%',
1977 $days,
1978 $limit
1979 ),
1980 ARRAY_A
1981 );
1982
1983 foreach ( $rows as &$row ) {
1984 $row['count'] = (int) $row['count'];
1985 $row['success_count'] = (int) $row['success_count'];
1986 $row['error_count'] = (int) $row['error_count'];
1987 }
1988 unset( $row );
1989
1990 return $this->create_rest_response( [ 'success' => true, 'tools' => $rows ?: [] ], 200 );
1991 }
1992 catch ( Exception $e ) {
1993 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1994 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1995 }
1996 }
1997
1998 public function rest_helpers_post_types() {
1999 try {
2000 $postTypes = $this->core->get_post_types();
2001 return $this->create_rest_response( [ 'success' => true, 'postTypes' => $postTypes ], 200 );
2002 }
2003 catch ( Exception $e ) {
2004 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2005 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2006 }
2007 }
2008
2009 public function rest_settings_themes( $request ) {
2010 try {
2011 $method = $request->get_method();
2012 if ( $method === 'GET' ) {
2013 $themes = $this->core->get_themes();
2014 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
2015 }
2016 else if ( $method === 'POST' ) {
2017 $params = $request->get_json_params();
2018 $themes = $params['themes'];
2019 $themes = $this->core->update_themes( $themes );
2020 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
2021 }
2022 }
2023 catch ( Exception $e ) {
2024 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2025 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2026 }
2027 }
2028
2029 public function rest_settings_chatbots( $request ) {
2030 try {
2031 $method = $request->get_method();
2032 if ( $method === 'GET' ) {
2033 $chatbots = $this->core->get_chatbots();
2034 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
2035 }
2036 else if ( $method === 'POST' ) {
2037 $params = $request->get_json_params();
2038 $chatbots = $params['chatbots'];
2039 $chatbots = $this->core->update_chatbots( $chatbots );
2040 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
2041 }
2042 return $this->create_rest_response( [ 'success' => false, 'message' => 'Method not allowed' ], 405 );
2043 }
2044 catch ( Exception $e ) {
2045 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2046 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2047 }
2048 }
2049
2050 #region Logs
2051
2052 public function rest_get_logs() {
2053 $logs = Meow_MWAI_Logging::get();
2054 return $this->create_rest_response( [ 'success' => true, 'data' => $logs ], 200 );
2055 }
2056
2057 public function rest_clear_logs() {
2058 Meow_MWAI_Logging::clear();
2059 return $this->create_rest_response( [ 'success' => true ], 200 );
2060 }
2061
2062 #endregion
2063
2064 #region Forms
2065
2066 public function rest_forms_list( $request ) {
2067 try {
2068 $args = [
2069 'post_type' => 'mwai_form',
2070 'posts_per_page' => 100,
2071 'post_status' => 'any',
2072 'orderby' => 'date',
2073 'order' => 'DESC'
2074 ];
2075
2076 $posts = get_posts( $args );
2077 $forms = array_map( function ( $post ) {
2078 return [
2079 'id' => $post->ID,
2080 'title' => $post->post_title,
2081 'status' => $post->post_status
2082 ];
2083 }, $posts );
2084
2085 return $this->create_rest_response( [ 'success' => true, 'forms' => $forms ], 200 );
2086 }
2087 catch ( Exception $e ) {
2088 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2089 }
2090 }
2091
2092 public function rest_forms_get( $request ) {
2093 try {
2094 $id = intval( $request->get_param( 'id' ) );
2095 if ( !$id ) {
2096 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2097 }
2098
2099 $post = get_post( $id );
2100 if ( !$post || $post->post_type !== 'mwai_form' ) {
2101 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2102 }
2103
2104 $form = [
2105 'id' => $post->ID,
2106 'title' => [
2107 'raw' => $post->post_title,
2108 'rendered' => $post->post_title
2109 ],
2110 'content' => [
2111 'raw' => $post->post_content,
2112 'rendered' => $post->post_content
2113 ],
2114 'status' => $post->post_status
2115 ];
2116
2117 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2118 }
2119 catch ( Exception $e ) {
2120 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2121 }
2122 }
2123
2124 public function rest_forms_create( $request ) {
2125 try {
2126 $params = $request->get_json_params();
2127 $title = isset( $params['title'] ) ? $params['title'] : 'Untitled Form';
2128
2129 // wp_insert_post expects slashed data - it calls wp_unslash() internally, which would
2130 // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and corrupt
2131 // the stored blocks.
2132 $post_data = wp_slash( [
2133 'post_title' => $title,
2134 'post_content' => '',
2135 'post_status' => 'draft',
2136 'post_type' => 'mwai_form'
2137 ] );
2138
2139 $post_id = wp_insert_post( $post_data );
2140
2141 if ( is_wp_error( $post_id ) ) {
2142 return $this->create_rest_response( [ 'success' => false, 'message' => $post_id->get_error_message() ], 500 );
2143 }
2144
2145 $post = get_post( $post_id );
2146 $form = [
2147 'id' => $post->ID,
2148 'title' => [
2149 'raw' => $post->post_title,
2150 'rendered' => $post->post_title
2151 ],
2152 'status' => $post->post_status
2153 ];
2154
2155 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2156 }
2157 catch ( Exception $e ) {
2158 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2159 }
2160 }
2161
2162 public function rest_forms_update( $request ) {
2163 try {
2164 $params = $request->get_json_params();
2165 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2166
2167 if ( !$id ) {
2168 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2169 }
2170
2171 $post = get_post( $id );
2172 if ( !$post || $post->post_type !== 'mwai_form' ) {
2173 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2174 }
2175
2176 $post_data = [ 'ID' => $id ];
2177
2178 if ( isset( $params['title'] ) ) {
2179 $post_data['post_title'] = $params['title'];
2180 }
2181
2182 if ( isset( $params['content'] ) ) {
2183 $post_data['post_content'] = $params['content'];
2184 }
2185
2186 if ( isset( $params['status'] ) ) {
2187 $post_data['post_status'] = $params['status'];
2188 }
2189
2190 // wp_update_post expects slashed data - it calls wp_unslash() internally, which would
2191 // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and break
2192 // Gutenberg blocks on reload.
2193 $result = wp_update_post( wp_slash( $post_data ) );
2194
2195 if ( is_wp_error( $result ) ) {
2196 return $this->create_rest_response( [ 'success' => false, 'message' => $result->get_error_message() ], 500 );
2197 }
2198
2199 $post = get_post( $id );
2200 $form = [
2201 'id' => $post->ID,
2202 'title' => [
2203 'raw' => $post->post_title,
2204 'rendered' => $post->post_title
2205 ],
2206 'content' => [
2207 'raw' => $post->post_content,
2208 'rendered' => $post->post_content
2209 ],
2210 'status' => $post->post_status
2211 ];
2212
2213 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2214 }
2215 catch ( Exception $e ) {
2216 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2217 }
2218 }
2219
2220 public function rest_forms_delete( $request ) {
2221 try {
2222 $params = $request->get_json_params();
2223 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2224
2225 if ( !$id ) {
2226 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2227 }
2228
2229 $post = get_post( $id );
2230 if ( !$post || $post->post_type !== 'mwai_form' ) {
2231 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2232 }
2233
2234 $result = wp_delete_post( $id, true );
2235
2236 if ( !$result ) {
2237 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to delete form' ], 500 );
2238 }
2239
2240 return $this->create_rest_response( [ 'success' => true ], 200 );
2241 }
2242 catch ( Exception $e ) {
2243 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2244 }
2245 }
2246
2247 #endregion
2248
2249 #region Video Generation Helpers
2250
2251 public function rest_helpers_create_video( $request ) {
2252 try {
2253 $params = $request->get_json_params();
2254 $prompt = sanitize_text_field( $params['prompt'] );
2255 $model = sanitize_text_field( $params['model'] ?? 'sora-2' );
2256 $size = sanitize_text_field( $params['size'] ?? '720x1280' );
2257 $seconds = absint( $params['seconds'] ?? 4 );
2258 $envId = sanitize_text_field( $params['envId'] ?? '' );
2259
2260 // Check if envId is provided (defaults not supported for videos yet)
2261 if ( empty( $envId ) ) {
2262 throw new Exception( 'Please select a specific environment and model in the Video Generator. Default environments are not yet supported for video generation.' );
2263 }
2264
2265 // Get API key from environment
2266 $env = $this->core->get_ai_env( $envId );
2267 $api_key = $env['apikey'] ?? '';
2268
2269 if ( empty( $api_key ) ) {
2270 throw new Exception( 'OpenAI API key not found.' );
2271 }
2272
2273 // Prepare multipart boundary
2274 $boundary = wp_generate_password( 24, false );
2275 $body = '';
2276
2277 // Add model
2278 $body .= "--{$boundary}\r\n";
2279 $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n";
2280 $body .= "{$model}\r\n";
2281
2282 // Add prompt
2283 $body .= "--{$boundary}\r\n";
2284 $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n";
2285 $body .= "{$prompt}\r\n";
2286
2287 // Add size
2288 $body .= "--{$boundary}\r\n";
2289 $body .= "Content-Disposition: form-data; name=\"size\"\r\n\r\n";
2290 $body .= "{$size}\r\n";
2291
2292 // Add seconds
2293 $body .= "--{$boundary}\r\n";
2294 $body .= "Content-Disposition: form-data; name=\"seconds\"\r\n\r\n";
2295 $body .= "{$seconds}\r\n";
2296
2297 $body .= "--{$boundary}--\r\n";
2298
2299 // Call OpenAI API to create video
2300 $response = wp_remote_post( 'https://api.openai.com/v1/videos', [
2301 'headers' => [
2302 'Authorization' => 'Bearer ' . $api_key,
2303 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
2304 ],
2305 'body' => $body,
2306 'timeout' => 30
2307 ] );
2308
2309 if ( is_wp_error( $response ) ) {
2310 throw new Exception( $response->get_error_message() );
2311 }
2312
2313 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
2314
2315 if ( isset( $response_body['error'] ) ) {
2316 throw new Exception( $response_body['error']['message'] ?? 'Unknown error' );
2317 }
2318
2319 // Record usage (price is calculated per second)
2320 $usage = $this->core->record_videos_usage( $model, $size, $seconds );
2321
2322 // Log to Query Logs (Statistics)
2323 try {
2324 if ( class_exists( 'MeowPro_MWAI_Stats' ) && class_exists( 'MeowPro_MWAI_Statistics' ) ) {
2325 $statsObject = new MeowPro_MWAI_Stats();
2326 $statsObject->session = $params['session'] ?? null;
2327 $statsObject->scope = 'admin-tools';
2328 $statsObject->feature = 'video-generator';
2329 $statsObject->model = $model;
2330 $statsObject->envId = $envId;
2331 $statsObject->units = $seconds;
2332 $statsObject->type = 'seconds';
2333 $statsObject->price = $usage['price'] ?? 0;
2334 $statsObject->accuracy = $usage['accuracy'] ?? 'full';
2335
2336 $statistics = new MeowPro_MWAI_Statistics();
2337 $statistics->commit_stats( $statsObject );
2338 }
2339 }
2340 catch ( Exception $statsError ) {
2341 // Log the error but don't fail the video creation
2342 error_log( '[AI Engine Video] Failed to log statistics: ' . $statsError->getMessage() );
2343 }
2344
2345 // Store metadata for later retrieval when video completes
2346 if ( isset( $response_body['id'] ) ) {
2347 set_transient( 'mwai_video_metadata_' . $response_body['id'], [
2348 'model' => $model,
2349 'env_id' => $envId,
2350 'created_at' => time()
2351 ], 7 * DAY_IN_SECONDS );
2352 }
2353
2354 return $this->create_rest_response( [ 'success' => true, 'video' => $response_body, 'usage' => $usage ], 200 );
2355 }
2356 catch ( Exception $e ) {
2357 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2358 }
2359 }
2360
2361 public function rest_helpers_video_status( $request ) {
2362 try {
2363 $params = $request->get_json_params();
2364 $video_ids = $params['videoIds'] ?? [];
2365 $envId = sanitize_text_field( $params['envId'] ?? '' );
2366
2367 if ( empty( $video_ids ) ) {
2368 return $this->create_rest_response( [ 'success' => true, 'videos' => [] ], 200 );
2369 }
2370
2371 // Get API key from environment
2372 $env = $this->core->get_ai_env( $envId );
2373 $api_key = $env['apikey'] ?? '';
2374
2375 if ( empty( $api_key ) ) {
2376 throw new Exception( 'OpenAI API key not found.' );
2377 }
2378
2379 $videos = [];
2380 foreach ( $video_ids as $video_id ) {
2381 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id, [
2382 'headers' => [
2383 'Authorization' => 'Bearer ' . $api_key
2384 ],
2385 'timeout' => 15
2386 ] );
2387
2388 if ( !is_wp_error( $response ) ) {
2389 $body = json_decode( wp_remote_retrieve_body( $response ), true );
2390 if ( !isset( $body['error'] ) ) {
2391 // If video is completed and we haven't saved it yet, download and save to media library
2392 if ( $body['status'] === 'completed' && empty( get_transient( 'mwai_video_saved_' . $video_id ) ) ) {
2393 // Retrieve metadata that was stored when video was created
2394 $metadata = get_transient( 'mwai_video_metadata_' . $video_id );
2395 $ai_metadata = [];
2396 if ( $metadata ) {
2397 $ai_metadata = [
2398 'model' => $metadata['model'] ?? null,
2399 'env_id' => $metadata['env_id'] ?? null,
2400 'latency' => isset( $metadata['created_at'] ) ? ( time() - $metadata['created_at'] ) : null
2401 ];
2402 }
2403
2404 $attachment_id = $this->download_and_save_video( $video_id, $api_key, '', '', $ai_metadata );
2405 if ( $attachment_id ) {
2406 $body['attachment_id'] = $attachment_id;
2407 // Build URL from file path for custom post types
2408 $file_path = get_attached_file( $attachment_id );
2409 $upload_dir = wp_upload_dir();
2410 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2411 // Mark as saved so we don't download again
2412 set_transient( 'mwai_video_saved_' . $video_id, $attachment_id, DAY_IN_SECONDS );
2413 // Clean up metadata transient
2414 delete_transient( 'mwai_video_metadata_' . $video_id );
2415 }
2416 }
2417 // Check if we already have this video saved
2418 else if ( $body['status'] === 'completed' ) {
2419 $attachment_id = get_transient( 'mwai_video_saved_' . $video_id );
2420 if ( $attachment_id ) {
2421 $body['attachment_id'] = $attachment_id;
2422 // Build URL from file path for custom post types
2423 $file_path = get_attached_file( $attachment_id );
2424 $upload_dir = wp_upload_dir();
2425 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2426 }
2427 }
2428 $videos[] = $body;
2429 }
2430 else {
2431 // Include error information in the response
2432 error_log( 'AI Engine: Video generation failed for ID ' . $video_id . ': ' . json_encode( $body['error'] ) );
2433 $videos[] = [
2434 'id' => $video_id,
2435 'status' => 'failed',
2436 'error' => $body['error']
2437 ];
2438 }
2439 }
2440 else {
2441 // WP HTTP error
2442 error_log( 'AI Engine: Failed to check video status for ID ' . $video_id . ': ' . $response->get_error_message() );
2443 $videos[] = [
2444 'id' => $video_id,
2445 'status' => 'failed',
2446 'error' => [ 'message' => $response->get_error_message() ]
2447 ];
2448 }
2449 }
2450
2451 return $this->create_rest_response( [ 'success' => true, 'videos' => $videos ], 200 );
2452 }
2453 catch ( Exception $e ) {
2454 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2455 }
2456 }
2457
2458 public function rest_helpers_download_video( $request ) {
2459 try {
2460 $params = $request->get_json_params();
2461 $video_id = sanitize_text_field( $params['videoId'] );
2462 $envId = sanitize_text_field( $params['envId'] ?? '' );
2463
2464 // Get API key from environment
2465 $env = $this->core->get_ai_env( $envId );
2466 $api_key = $env['apikey'] ?? '';
2467
2468 if ( empty( $api_key ) ) {
2469 throw new Exception( 'OpenAI API key not found.' );
2470 }
2471
2472 $temp_file = wp_tempnam( $video_id . '.mp4' );
2473
2474 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2475 'headers' => [
2476 'Authorization' => 'Bearer ' . $api_key
2477 ],
2478 'timeout' => 120,
2479 'stream' => true,
2480 'filename' => $temp_file
2481 ] );
2482
2483 if ( is_wp_error( $response ) ) {
2484 throw new Exception( $response->get_error_message() );
2485 }
2486
2487 $file_data = file_get_contents( $temp_file );
2488 $base64 = base64_encode( $file_data );
2489
2490 unlink( $temp_file );
2491
2492 return $this->create_rest_response( [
2493 'success' => true,
2494 'data' => $base64,
2495 'mimeType' => 'video/mp4'
2496 ], 200 );
2497 }
2498 catch ( Exception $e ) {
2499 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2500 }
2501 }
2502
2503 public function rest_helpers_delete_video( $request ) {
2504 try {
2505 $params = $request->get_json_params();
2506 $video_id = sanitize_text_field( $params['videoId'] );
2507 $envId = sanitize_text_field( $params['envId'] ?? '' );
2508
2509 // Get API key from environment
2510 $env = $this->core->get_ai_env( $envId );
2511 $api_key = $env['apikey'] ?? '';
2512
2513 if ( empty( $api_key ) ) {
2514 throw new Exception( 'OpenAI API key not found.' );
2515 }
2516
2517 $response = wp_remote_request( 'https://api.openai.com/v1/videos/' . $video_id, [
2518 'method' => 'DELETE',
2519 'headers' => [
2520 'Authorization' => 'Bearer ' . $api_key
2521 ],
2522 'timeout' => 15
2523 ] );
2524
2525 if ( is_wp_error( $response ) ) {
2526 throw new Exception( $response->get_error_message() );
2527 }
2528
2529 return $this->create_rest_response( [ 'success' => true ], 200 );
2530 }
2531 catch ( Exception $e ) {
2532 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2533 }
2534 }
2535
2536 private function download_and_save_video( $video_id, $api_key, $title = '', $description = '', $ai_metadata = [] ) {
2537 try {
2538 // Download video content
2539 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2540 'headers' => [
2541 'Authorization' => 'Bearer ' . $api_key
2542 ],
2543 'timeout' => 120
2544 ] );
2545
2546 if ( is_wp_error( $response ) ) {
2547 error_log( 'Error downloading video: ' . $response->get_error_message() );
2548 return false;
2549 }
2550
2551 $video_data = wp_remote_retrieve_body( $response );
2552 if ( empty( $video_data ) ) {
2553 error_log( 'Empty video data received' );
2554 return false;
2555 }
2556
2557 // Generate filename
2558 $filename = $video_id . '.mp4';
2559 $upload_dir = wp_upload_dir();
2560 $file_path = $upload_dir['path'] . '/' . $filename;
2561
2562 // Save to file
2563 file_put_contents( $file_path, $video_data );
2564
2565 // Prepare attachment data - use mwai_video post type (draft video)
2566 $attachment = [
2567 'post_mime_type' => 'video/mp4',
2568 'post_title' => !empty( $title ) ? $title : 'AI Generated Video',
2569 'post_content' => $description,
2570 'post_status' => 'inherit',
2571 'post_type' => 'mwai_video'
2572 ];
2573
2574 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
2575 $attachment_id = wp_insert_post( $attachment );
2576
2577 // Set the attached file manually since we're not using wp_insert_attachment
2578 update_attached_file( $attachment_id, $file_path );
2579
2580 if ( is_wp_error( $attachment_id ) ) {
2581 error_log( 'Error creating attachment: ' . $attachment_id->get_error_message() );
2582 return false;
2583 }
2584
2585 // Generate attachment metadata
2586 require_once ABSPATH . 'wp-admin/includes/image.php';
2587 $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path );
2588 wp_update_attachment_metadata( $attachment_id, $attach_data );
2589
2590 // Store AI-related metadata
2591 if ( !empty( $ai_metadata['model'] ) ) {
2592 update_post_meta( $attachment_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
2593 }
2594 if ( !empty( $ai_metadata['latency'] ) ) {
2595 update_post_meta( $attachment_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
2596 }
2597 if ( !empty( $ai_metadata['env_id'] ) ) {
2598 update_post_meta( $attachment_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
2599 }
2600
2601 // Add to user's draft media
2602 $user_id = get_current_user_id();
2603 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2604 if ( !is_array( $draft_media ) ) {
2605 $draft_media = [];
2606 }
2607 $draft_media[] = [
2608 'attachment_id' => $attachment_id,
2609 'type' => 'video',
2610 'openai_id' => $video_id,
2611 'created_at' => time()
2612 ];
2613 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
2614
2615 return $attachment_id;
2616 }
2617 catch ( Exception $e ) {
2618 error_log( 'Exception in download_and_save_video: ' . $e->getMessage() );
2619 return false;
2620 }
2621 }
2622
2623 public function rest_helpers_save_video_to_library( $request ) {
2624 try {
2625 $params = $request->get_json_params();
2626 $video_id = sanitize_text_field( $params['videoId'] );
2627 $title = sanitize_text_field( $params['title'] );
2628 $description = sanitize_text_field( $params['description'] );
2629 $filename = sanitize_file_name( $params['filename'] );
2630 $envId = sanitize_text_field( $params['envId'] ?? '' );
2631
2632 // Ensure filename has .mp4 extension
2633 if ( !preg_match( '/\.mp4$/i', $filename ) ) {
2634 $filename .= '.mp4';
2635 }
2636
2637 // Get API key from environment
2638 $env = $this->core->get_ai_env( $envId );
2639 $api_key = $env['apikey'] ?? '';
2640
2641 if ( empty( $api_key ) ) {
2642 throw new Exception( 'OpenAI API key not found.' );
2643 }
2644
2645 // Download video content
2646 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2647 'headers' => [
2648 'Authorization' => 'Bearer ' . $api_key
2649 ],
2650 'timeout' => 120
2651 ] );
2652
2653 if ( is_wp_error( $response ) ) {
2654 throw new Exception( $response->get_error_message() );
2655 }
2656
2657 $video_data = wp_remote_retrieve_body( $response );
2658
2659 // Upload to WordPress media library
2660 $upload_dir = wp_upload_dir();
2661 $file_path = $upload_dir['path'] . '/' . $filename;
2662
2663 file_put_contents( $file_path, $video_data );
2664
2665 $attachment = [
2666 'post_mime_type' => 'video/mp4',
2667 'post_title' => $title,
2668 'post_content' => $description,
2669 'post_status' => 'inherit'
2670 ];
2671
2672 $attach_id = wp_insert_attachment( $attachment, $file_path );
2673
2674 require_once( ABSPATH . 'wp-admin/includes/image.php' );
2675 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
2676 wp_update_attachment_metadata( $attach_id, $attach_data );
2677
2678 return $this->create_rest_response( [
2679 'success' => true,
2680 'attachmentId' => $attach_id
2681 ], 200 );
2682 }
2683 catch ( Exception $e ) {
2684 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2685 }
2686 }
2687
2688 public function rest_helpers_delete_video_from_library( $request ) {
2689 try {
2690 $params = $request->get_json_params();
2691 $attachment_id = absint( $params['attachmentId'] );
2692
2693 if ( empty( $attachment_id ) ) {
2694 throw new Exception( 'Attachment ID is required.' );
2695 }
2696
2697 $deleted = wp_delete_attachment( $attachment_id, true );
2698
2699 if ( !$deleted ) {
2700 throw new Exception( 'Failed to delete attachment.' );
2701 }
2702
2703 return $this->create_rest_response( [ 'success' => true ], 200 );
2704 }
2705 catch ( Exception $e ) {
2706 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2707 }
2708 }
2709
2710 public function rest_helpers_list_draft_media( $request ) {
2711 try {
2712 $type = $request->get_param( 'type' ); // 'image', 'video', or null for all
2713 $user_id = get_current_user_id();
2714 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2715
2716 if ( !is_array( $draft_media ) ) {
2717 return $this->create_rest_response( [ 'success' => true, 'media' => [] ], 200 );
2718 }
2719
2720 $media_items = [];
2721 foreach ( $draft_media as $item ) {
2722 // Filter by type if specified
2723 if ( $type && $item['type'] !== $type ) {
2724 continue;
2725 }
2726
2727 $attachment_id = $item['attachment_id'];
2728 $attachment = get_post( $attachment_id );
2729
2730 if ( $attachment ) {
2731 // For custom post types (mwai_image, mwai_video), build URL from file path
2732 $file_path = get_attached_file( $attachment_id );
2733 $upload_dir = wp_upload_dir();
2734 $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2735
2736 $model = get_post_meta( $attachment_id, 'mwai_model', true );
2737 $generation_time = get_post_meta( $attachment_id, 'mwai_latency', true );
2738 $env_id = get_post_meta( $attachment_id, 'mwai_env_id', true );
2739
2740 // Debug logging
2741 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2742 error_log( '[AI Engine] list_draft_media - attachment_id: ' . $attachment_id . ' model: ' . var_export( $model, true ) . ' generation_time: ' . var_export( $generation_time, true ) . ' env_id: ' . var_export( $env_id, true ) );
2743 }
2744
2745 $media_items[] = [
2746 'attachment_id' => $attachment_id,
2747 'type' => $item['type'],
2748 'openai_id' => $item['openai_id'] ?? null,
2749 'url' => $url,
2750 'title' => $attachment->post_title,
2751 'description' => $attachment->post_content,
2752 'filename' => basename( $file_path ),
2753 'created_at' => $item['created_at'],
2754 'model' => $model,
2755 'generation_time' => $generation_time,
2756 'env_id' => $env_id
2757 ];
2758 }
2759 }
2760
2761 return $this->create_rest_response( [ 'success' => true, 'media' => $media_items ], 200 );
2762 }
2763 catch ( Exception $e ) {
2764 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2765 }
2766 }
2767
2768 public function rest_helpers_approve_media( $request ) {
2769 try {
2770 $params = $request->get_json_params();
2771 $attachment_id = absint( $params['attachmentId'] );
2772 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2773 $envId = sanitize_text_field( $params['envId'] ?? '' );
2774
2775 if ( empty( $attachment_id ) ) {
2776 throw new Exception( 'Attachment ID is required.' );
2777 }
2778
2779 // Convert from mwai_image/mwai_video to attachment post type
2780 wp_update_post( [
2781 'ID' => $attachment_id,
2782 'post_type' => 'attachment',
2783 'post_status' => 'inherit'
2784 ] );
2785
2786 // Remove from draft media list
2787 $user_id = get_current_user_id();
2788 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2789 if ( is_array( $draft_media ) ) {
2790 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2791 return $item['attachment_id'] !== $attachment_id;
2792 } );
2793 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2794 }
2795
2796 // Delete video from OpenAI if applicable
2797 if ( !empty( $openai_id ) ) {
2798 $env = $this->core->get_ai_env( $envId );
2799 $api_key = $env['apikey'] ?? '';
2800
2801 if ( !empty( $api_key ) ) {
2802 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2803 'method' => 'DELETE',
2804 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2805 'timeout' => 15
2806 ] );
2807 }
2808 }
2809
2810 return $this->create_rest_response( [ 'success' => true ], 200 );
2811 }
2812 catch ( Exception $e ) {
2813 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2814 }
2815 }
2816
2817 public function rest_helpers_reject_media( $request ) {
2818 try {
2819 $params = $request->get_json_params();
2820 $attachment_id = absint( $params['attachmentId'] );
2821 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2822 $envId = sanitize_text_field( $params['envId'] ?? '' );
2823
2824 if ( empty( $attachment_id ) ) {
2825 throw new Exception( 'Attachment ID is required.' );
2826 }
2827
2828 // Convert from mwai_image/mwai_video to attachment post type first
2829 // This ensures wp_delete_attachment properly deletes the physical file
2830 wp_update_post( [
2831 'ID' => $attachment_id,
2832 'post_type' => 'attachment'
2833 ] );
2834
2835 // Delete attachment from WordPress (now that it's a proper attachment, files will be deleted)
2836 wp_delete_attachment( $attachment_id, true );
2837
2838 // Remove from draft media list
2839 $user_id = get_current_user_id();
2840 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2841 if ( is_array( $draft_media ) ) {
2842 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2843 return $item['attachment_id'] !== $attachment_id;
2844 } );
2845 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2846 }
2847
2848 // Delete video from OpenAI if applicable
2849 if ( !empty( $openai_id ) ) {
2850 $env = $this->core->get_ai_env( $envId );
2851 $api_key = $env['apikey'] ?? '';
2852
2853 if ( !empty( $api_key ) ) {
2854 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2855 'method' => 'DELETE',
2856 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2857 'timeout' => 15
2858 ] );
2859 }
2860 }
2861
2862 return $this->create_rest_response( [ 'success' => true ], 200 );
2863 }
2864 catch ( Exception $e ) {
2865 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2866 }
2867 }
2868
2869 #endregion
2870 }
2871