PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.6.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.6.4
3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 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 1 year ago engines 1 day ago exceptions 2 days ago modules 1 day ago query 2 days ago services 1 day ago admin.php 1 day ago api.php 3 weeks ago core.php 2 days ago discussion.php 1 year ago event.php 1 year ago init.php 2 days ago logging.php 1 year ago reply.php 2 days ago rest.php 1 day ago
rest.php
2942 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 // Finish the request even if the client disconnects, so usage/credits are
762 // still recorded after the stream completes. Otherwise an abort mid-stream
763 // kills the script before accounting runs. See chat_submit for details.
764 ignore_user_abort( true );
765 }
766
767 // Process Reply
768 $reply = $this->core->run_query( $query, $streamCallback );
769 $restRes = [
770 'success' => true,
771 'data' => $reply->result,
772 'usage' => $reply->usage
773 ];
774 if ( $stream ) {
775 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $restRes ) ], $query );
776 die();
777 }
778 return $this->create_rest_response( $restRes, 200 );
779 }
780 catch ( Exception $e ) {
781 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
782 if ( $stream ) {
783 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
784 }
785 else {
786 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
787 }
788 }
789 }
790
791 public function rest_ai_images( $request ) {
792 try {
793 $params = $request->get_json_params();
794 $message = $this->retrieve_message( $params );
795 $query = new Meow_MWAI_Query_Image( $message );
796 $query->inject_params( $params );
797 $reply = $this->core->run_query( $query );
798 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
799 }
800 catch ( Exception $e ) {
801 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
802 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
803 }
804 }
805
806 public function rest_ai_image_edit( $request ) {
807 try {
808 // Check if this is a multipart request with files
809 $files = $request->get_file_params();
810 $params = null;
811
812 // Debug logging
813 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
814 error_log( '[AI Engine Queries] Image Edit Request - Method: ' . $request->get_method() );
815 $content_type = $request->get_content_type();
816 if ( is_array( $content_type ) ) {
817 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type['value'] );
818 }
819 else {
820 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type );
821 }
822 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes (' . count( $files ) . ')' : 'no' ) );
823 }
824
825 if ( !empty( $files ) ) {
826 // Handle multipart form data - get all params including POST data
827 $params = $request->get_params();
828 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
829 error_log( '[AI Engine Queries] Image Edit Request - Using form data params' );
830 }
831 }
832 else {
833 // Try to get body params first (for form data without files)
834 $body_params = $request->get_body_params();
835 if ( !empty( $body_params ) ) {
836 $params = $body_params;
837 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
838 error_log( '[AI Engine Queries] Image Edit Request - Using body params' );
839 }
840 }
841 else {
842 // Handle JSON request
843 $params = $request->get_json_params();
844 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
845 error_log( '[AI Engine Queries] Image Edit Request - Using JSON params' );
846 }
847 }
848 }
849
850 // Ensure params is always an array
851 if ( empty( $params ) ) {
852 $params = [];
853 }
854
855 // Debug logging
856 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
857 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes' : 'no' ) );
858 error_log( '[AI Engine Queries] Image Edit Request - Params: ' . json_encode( $params ) );
859 }
860
861 $message = $this->retrieve_message( $params );
862 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
863 $query = new Meow_MWAI_Query_EditImage( $message );
864
865 // The inject_params method will handle setting the file from mediaId
866 $query->inject_params( $params );
867
868 // Handle mask file if provided
869 if ( !empty( $files['mask'] ) ) {
870 $mask_file = $files['mask'];
871 if ( $mask_file['error'] === UPLOAD_ERR_OK ) {
872 $mask_data = file_get_contents( $mask_file['tmp_name'] );
873 $query->set_mask( Meow_MWAI_Query_DroppedFile::from_data( $mask_data, 'analysis', $mask_file['type'] ) );
874 }
875 }
876
877 $reply = $this->core->run_query( $query );
878 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
879 }
880 catch ( Exception $e ) {
881 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
882 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
883 }
884 }
885
886 public function rest_ai_magic_wand( $request ) {
887 try {
888 $params = $request->get_json_params();
889 $action = isset( $params['action'] ) ? $params['action'] : null;
890 $data = isset( $params['data'] ) ? $params['data'] : null;
891 if ( empty( $data ) || empty( $action ) ) {
892 return $this->create_rest_response( [ 'success' => false, 'message' => 'An action and some data are required.' ], 500 );
893 }
894 $data = apply_filters( 'mwai_magic_wand_' . $action, '', $data );
895 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
896 }
897 catch ( Exception $e ) {
898 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
899 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
900 }
901 }
902
903 public function rest_ai_copilot( $request ) {
904 try {
905 $params = $request->get_json_params();
906 $action = sanitize_text_field( $params['action'] );
907 $message = $this->retrieve_message( $params, true );
908 $context = sanitize_text_field( $params['context'] );
909 $postId = !empty( $params['postId'] ) ? intval( $params['postId'] ) : null;
910 if ( empty( $action ) || empty( $message ) ) {
911 return $this->create_rest_response( [ 'success' => false, 'message' => 'Copilot needs an action and a prompt.' ], 500 );
912 }
913
914 global $mwai;
915 $result = null;
916 $params = [ 'scope' => 'copilot' ];
917
918 if ( $action === 'text' ) {
919 $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.";
920 if ( !empty( $model ) ) {
921 $params['model'] = $model;
922 }
923 $result = $mwai->simpleTextQuery( $prompt, $params );
924 }
925 else if ( $action === 'image' ) {
926 $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.";
927
928 // Create the image
929 $simplifiedPrompt = $mwai->simpleTextQuery( $prompt, $params );
930 $media = $mwai->imageQueryForMediaLibrary( $simplifiedPrompt, $params, $postId );
931 $result = [ 'media' => $media ];
932 }
933 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
934 }
935 catch ( Exception $e ) {
936 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
937 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
938 }
939 }
940
941 public function rest_helpers_update_title( $request ) {
942 try {
943 $params = $request->get_json_params();
944 $title = sanitize_text_field( $params['title'] );
945 $postId = intval( $params['postId'] );
946 $post = get_post( $postId );
947 if ( !$post ) {
948 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
949 }
950 $post->post_title = $title;
951 //$post->post_name = sanitize_title( $title );
952 wp_update_post( $post );
953 return $this->create_rest_response( [ 'success' => true, 'message' => 'Title updated.' ], 200 );
954 }
955 catch ( Exception $e ) {
956 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
957 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
958 }
959 }
960
961 public function rest_helpers_update_excerpt( $request ) {
962 try {
963 $params = $request->get_json_params();
964 $excerpt = sanitize_text_field( $params['excerpt'] );
965 $postId = intval( $params['postId'] );
966 $post = get_post( $postId );
967 if ( !$post ) {
968 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
969 }
970 $post->post_excerpt = $excerpt;
971 wp_update_post( $post );
972 return $this->create_rest_response( [ 'success' => true, 'message' => 'Excerpt updated.' ], 200 );
973 }
974 catch ( Exception $e ) {
975 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
976 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
977 }
978 }
979
980 public function rest_helpers_create_post( $request ) {
981 try {
982 $params = $request->get_json_params();
983 $title = sanitize_text_field( $params['title'] );
984 $content = sanitize_textarea_field( $params['content'] );
985 $excerpt = sanitize_text_field( $params['excerpt'] );
986 $postType = sanitize_text_field( $params['postType'] );
987 $post = new stdClass();
988 $post->post_title = $title;
989 $post->post_excerpt = $excerpt;
990 $post->post_content = $content;
991 $post->post_status = 'draft';
992 $post->post_type = isset( $postType ) ? $postType : 'post';
993 // TODO: Let's try to avoid using Markdown to create the Post
994 // Instead, we should create Gutenberg Blocks, or simple HTML.
995 // Then, we can get rid of the library for Markdown.
996 $post->post_content = $this->core->markdown_to_html( $post->post_content );
997 $postId = wp_insert_post( $post );
998 return $this->create_rest_response( [ 'success' => true, 'postId' => $postId ], 200 );
999 }
1000 catch ( Exception $e ) {
1001 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1002 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1003 }
1004 }
1005
1006 public function rest_helpers_create_images( $request ) {
1007 try {
1008 $params = $request->get_json_params();
1009 $title = sanitize_text_field( $params['title'] );
1010 $caption = sanitize_text_field( $params['caption'] );
1011 $alt = sanitize_text_field( $params['alt'] );
1012 $description = sanitize_text_field( $params['description'] );
1013 $url = $params['url'];
1014 $filename = sanitize_text_field( $params['filename'] );
1015
1016 // Prepare AI metadata
1017 $ai_metadata = [];
1018 if ( !empty( $params['model'] ) ) {
1019 $ai_metadata['model'] = $params['model'];
1020 }
1021 if ( !empty( $params['latency'] ) ) {
1022 $ai_metadata['latency'] = $params['latency'];
1023 }
1024 if ( !empty( $params['env_id'] ) ) {
1025 $ai_metadata['env_id'] = $params['env_id'];
1026 }
1027
1028 // Debug logging
1029 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1030 error_log( '[AI Engine] create_image metadata: ' . json_encode( $ai_metadata ) );
1031 }
1032
1033 // Create as mwai_image post type (draft image)
1034 $attachmentId = $this->core->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, null, 'inherit', 'mwai_image', $ai_metadata );
1035
1036 // Add to user's draft media
1037 $user_id = get_current_user_id();
1038 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
1039 if ( !is_array( $draft_media ) ) {
1040 $draft_media = [];
1041 }
1042 $draft_media[] = [
1043 'attachment_id' => $attachmentId,
1044 'type' => 'image',
1045 'created_at' => time()
1046 ];
1047 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
1048
1049 return $this->create_rest_response( [ 'success' => true, 'attachmentId' => $attachmentId ], 200 );
1050 }
1051 catch ( Exception $e ) {
1052 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1053 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1054 }
1055 }
1056
1057 public function rest_helpers_generate_image_meta( $request ) {
1058 try {
1059 global $mwai;
1060 $params = $request->get_json_params();
1061 $attachment_id = isset( $params['attachmentId'] ) ? intval( $params['attachmentId'] ) : null;
1062
1063 if ( empty( $attachment_id ) ) {
1064 throw new Exception( __( 'The attachment ID is required.', 'ai-engine' ) );
1065 }
1066
1067 // Get the file path from the attachment ID
1068 $file_path = get_attached_file( $attachment_id );
1069 if ( empty( $file_path ) || !file_exists( $file_path ) ) {
1070 throw new Exception( __( 'Could not find the attachment file.', 'ai-engine' ) );
1071 }
1072
1073 $prompt = 'Describe this image and suggest a short title and description. '
1074 . 'Also suggest an SEO-friendly filename (lowercase, ASCII characters only, with hyphens instead of spaces). '
1075 . 'Return a JSON with the keys: title, description, filename.';
1076
1077 // Use file path instead of URL to avoid network issues
1078 $result = $mwai->simpleVisionQuery( $prompt, null, $file_path, [ 'scope' => 'admin-tools' ] );
1079 $result = preg_replace( '/^```json\s*/', '', $result );
1080 $result = preg_replace( '/\s*```$/', '', $result );
1081 if ( is_string( $result ) ) {
1082 $data = json_decode( $result, true );
1083 }
1084 else {
1085 $data = $result;
1086 }
1087 if ( !is_array( $data ) ) {
1088 $data = [];
1089 }
1090 $data = array_merge( [ 'title' => '', 'description' => '', 'filename' => '' ], $data );
1091 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1092 }
1093 catch ( Exception $e ) {
1094 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1095 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1096 }
1097 }
1098
1099 public function rest_helpers_update_media_metadata( $request ) {
1100 try {
1101 $params = $request->get_json_params();
1102 $attachment_id = intval( $params['attachmentId'] );
1103 $title = sanitize_text_field( $params['title'] ?? '' );
1104 $description = sanitize_text_field( $params['description'] ?? '' );
1105 $caption = sanitize_text_field( $params['caption'] ?? '' );
1106 $alt = sanitize_text_field( $params['alt'] ?? '' );
1107 $filename = sanitize_file_name( $params['filename'] ?? '' );
1108
1109 if ( !$attachment_id ) {
1110 throw new Exception( __( 'Attachment ID is required.', 'ai-engine' ) );
1111 }
1112
1113 // Generate slug from filename (without extension)
1114 $slug = '';
1115 if ( !empty( $filename ) ) {
1116 $slug = pathinfo( $filename, PATHINFO_FILENAME );
1117 }
1118
1119 // Update post title, content (description), caption, and slug
1120 $update_data = [
1121 'ID' => $attachment_id,
1122 'post_title' => $title,
1123 'post_content' => $description,
1124 'post_excerpt' => $caption
1125 ];
1126
1127 if ( !empty( $slug ) ) {
1128 $update_data['post_name'] = $slug;
1129 }
1130
1131 wp_update_post( $update_data );
1132
1133 // Update alt text
1134 if ( !empty( $alt ) ) {
1135 update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
1136 }
1137
1138 // Update filename if provided
1139 $new_url = null;
1140 if ( !empty( $filename ) ) {
1141 $file_path = get_attached_file( $attachment_id );
1142 if ( $file_path ) {
1143 // Security: Validate file extension to prevent arbitrary file upload attacks
1144 $original_ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
1145 $new_ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
1146
1147 // Allowlist of safe media extensions (no executable types)
1148 $allowed_extensions = [
1149 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif',
1150 'mp4', 'webm', 'ogg', 'mov', 'avi', 'wmv', 'flv', 'm4v',
1151 'mp3', 'wav', 'flac', 'aac', 'm4a', 'wma',
1152 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'rtf'
1153 ];
1154
1155 // Extension must be in allowlist AND match original extension
1156 if ( !in_array( $new_ext, $allowed_extensions, true ) ) {
1157 throw new Exception( __( 'Invalid file extension. Only media file extensions are allowed.', 'ai-engine' ) );
1158 }
1159 if ( $new_ext !== $original_ext ) {
1160 throw new Exception( __( 'File extension must match the original file type.', 'ai-engine' ) );
1161 }
1162
1163 $path_parts = pathinfo( $file_path );
1164 $new_file_path = $path_parts['dirname'] . '/' . $filename;
1165 if ( rename( $file_path, $new_file_path ) ) {
1166 update_attached_file( $attachment_id, $new_file_path );
1167 // Build new URL from file path for custom post types
1168 $upload_dir = wp_upload_dir();
1169 $new_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $new_file_path );
1170 }
1171 }
1172 }
1173
1174 $response = [ 'success' => true ];
1175 if ( $new_url ) {
1176 $response['url'] = $new_url;
1177 }
1178
1179 return $this->create_rest_response( $response, 200 );
1180 }
1181 catch ( Exception $e ) {
1182 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1183 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1184 }
1185 }
1186
1187 public function rest_openai_files_get() {
1188 try {
1189 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1190 $purposeFilter = isset( $_GET['purpose'] ) ? $_GET['purpose'] : null;
1191 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1192 $files = $openai->list_files( $purposeFilter );
1193 return $this->create_rest_response( [ 'success' => true, 'files' => $files ], 200 );
1194 }
1195 catch ( Exception $e ) {
1196 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1197 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1198 }
1199 }
1200
1201 // TODO: Remove all the rest_openai_*finetune* handlers below after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
1202 public function rest_openai_deleted_finetunes_get() {
1203 try {
1204 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1205 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1206 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1207 $finetunes = $openai->list_deleted_finetunes( $legacy );
1208 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1209 }
1210 catch ( Exception $e ) {
1211 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1212 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1213 }
1214 }
1215
1216 public function rest_openai_finetunes_get() {
1217 try {
1218 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1219 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1220 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1221 $finetunes = $openai->list_finetunes( $legacy );
1222 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1223 }
1224 catch ( Exception $e ) {
1225 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1226 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1227 }
1228 }
1229
1230 public function rest_openai_files_upload( $request ) {
1231 try {
1232 $params = $request->get_json_params();
1233 $envId = $params['envId'];
1234 ;
1235 $filename = sanitize_text_field( $params['filename'] );
1236 $data = $params['data'];
1237 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1238 $file = $openai->upload_file( $filename, $data );
1239 return $this->create_rest_response( [ 'success' => true, 'file' => $file ], 200 );
1240 }
1241 catch ( Exception $e ) {
1242 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1243 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1244 }
1245 }
1246
1247 public function rest_openai_files_delete( $request ) {
1248 try {
1249 $params = $request->get_json_params();
1250 $envId = $params['envId'];
1251 ;
1252 $fileId = $params['fileId'];
1253 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1254 $openai->delete_file( $fileId );
1255 return $this->create_rest_response( [ 'success' => true ], 200 );
1256 }
1257 catch ( Exception $e ) {
1258 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1259 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1260 }
1261 }
1262
1263 public function rest_openai_finetunes_cancel( $request ) {
1264 try {
1265 $params = $request->get_json_params();
1266 $envId = $params['envId'];
1267 ;
1268 $finetuneId = $params['finetuneId'];
1269 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1270 $openai->cancel_finetune( $finetuneId );
1271 return $this->create_rest_response( [ 'success' => true ], 200 );
1272 }
1273 catch ( Exception $e ) {
1274 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1275 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1276 }
1277 }
1278
1279 public function rest_openai_finetunes_delete( $request ) {
1280 try {
1281 $params = $request->get_json_params();
1282 $envId = $params['envId'];
1283 ;
1284 $modelId = $params['modelId'];
1285 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1286 $openai->delete_finetune( $modelId );
1287 return $this->create_rest_response( [ 'success' => true ], 200 );
1288 }
1289 catch ( Exception $e ) {
1290 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1291 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1292 }
1293 }
1294
1295 public function rest_openai_files_download( $request ) {
1296 try {
1297 $params = $request->get_json_params();
1298 $envId = $params['envId'];
1299 ;
1300 $fileId = $params['fileId'];
1301 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1302 $filename = $openai->download_file( $fileId );
1303 $data = file_get_contents( $filename );
1304 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1305 }
1306 catch ( Exception $e ) {
1307 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1308 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1309 }
1310 }
1311
1312 public function rest_openai_files_finetune( $request ) {
1313 try {
1314 $params = $request->get_json_params();
1315 $envId = $params['envId'];
1316 ;
1317 $fileId = $params['fileId'];
1318 $model = $params['model'];
1319 $suffix = $params['suffix'];
1320 $hyperparams = [
1321 'nEpochs' => isset( $params['nEpochs'] ) ? $params['nEpochs'] : null,
1322 'batchSize' => isset( $params['batchSize'] ) ? $params['batchSize'] : null,
1323 ];
1324 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1325 $finetune = $openai->run_finetune( $fileId, $model, $suffix, $hyperparams );
1326 return $this->create_rest_response( [ 'success' => true, 'finetune' => $finetune ], 200 );
1327 }
1328 catch ( Exception $e ) {
1329 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1330 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1331 }
1332 }
1333
1334 public function rest_helpers_count_posts( $request ) {
1335 try {
1336 global $wpdb;
1337 $params = $request->get_query_params();
1338 $postType = $params['postType'];
1339 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1340 $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) );
1341 $ignored_ids = $wpdb->get_col(
1342 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_mwai_embedding_ignore'"
1343 );
1344 $exclude_sql = '';
1345 if ( !empty( $ignored_ids ) ) {
1346 $ignored_ids = array_map( 'intval', $ignored_ids );
1347 $exclude_sql = ' AND p.ID NOT IN (' . implode( ',', $ignored_ids ) . ')';
1348 }
1349 $mimeFilter = '';
1350 if ( $postType === 'attachment' ) {
1351 $mimeFilter = " AND p.post_mime_type LIKE 'image/%'";
1352 }
1353 $query = "SELECT COUNT(*) FROM {$wpdb->posts} p
1354 WHERE p.post_type = %s
1355 AND p.post_status IN ($statusPlaceholders)" . $exclude_sql . $mimeFilter;
1356 $prepareArgs = array_merge( [ $postType ], $postStatus );
1357 $count = (int) $wpdb->get_var( $wpdb->prepare( $query, ...$prepareArgs ) );
1358 return $this->create_rest_response( [ 'success' => true, 'count' => $count ], 200 );
1359 }
1360 catch ( Exception $e ) {
1361 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1362 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1363 }
1364 }
1365
1366 public function rest_helpers_posts_ids( $request ) {
1367 try {
1368 global $wpdb;
1369 $params = $request->get_query_params();
1370 $postType = $params['postType'];
1371 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1372
1373 // Use direct SQL query instead of get_posts to avoid memory issues with large sites
1374 $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) );
1375 $ignored_ids = $wpdb->get_col(
1376 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_mwai_embedding_ignore'"
1377 );
1378 $exclude_sql = '';
1379 if ( !empty( $ignored_ids ) ) {
1380 $ignored_ids = array_map( 'intval', $ignored_ids );
1381 $exclude_sql = ' AND p.ID NOT IN (' . implode( ',', $ignored_ids ) . ')';
1382 }
1383 $mimeFilter = '';
1384 if ( $postType === 'attachment' ) {
1385 $mimeFilter = " AND p.post_mime_type LIKE 'image/%'";
1386 }
1387 $query = "SELECT p.ID FROM {$wpdb->posts} p
1388 WHERE p.post_type = %s
1389 AND p.post_status IN ($statusPlaceholders)" . $exclude_sql . $mimeFilter . '
1390 ORDER BY p.ID ASC';
1391
1392 $prepareArgs = array_merge( [ $postType ], $postStatus );
1393 $postIds = $wpdb->get_col( $wpdb->prepare( $query, ...$prepareArgs ) );
1394 $postIds = array_map( 'intval', $postIds );
1395
1396 return $this->create_rest_response( [ 'success' => true, 'postIds' => $postIds ], 200 );
1397 }
1398 catch ( Exception $e ) {
1399 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1400 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1401 }
1402 }
1403
1404 public function rest_helpers_post_content( $request ) {
1405 try {
1406 $params = $request->get_query_params();
1407 $offset = (int) $params['offset'];
1408 $postType = $params['postType'];
1409 $postStatus = isset( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1410 $postId = (int) $params['postId'];
1411
1412 $post = null;
1413 if ( !empty( $postId ) ) {
1414 $post = get_post( $postId );
1415 if ( $post->post_status !== 'publish' && $post->post_status !== 'future'
1416 && $post->post_status !== 'draft' && $post->post_status !== 'private' ) {
1417 $post = null;
1418 }
1419 }
1420 else {
1421 $posts = get_posts( [
1422 'posts_per_page' => 1,
1423 'post_type' => $postType,
1424 'offset' => $offset,
1425 'post_status' => $postStatus,
1426 ] );
1427 $post = count( $posts ) === 0 ? null : $posts[0];
1428 }
1429 if ( !$post ) {
1430 return $this->create_rest_response( [ 'success' => false, 'message' => 'Post not found' ], 404 );
1431 }
1432 $cleanPost = $this->core->get_post( $post );
1433 return $this->create_rest_response( [ 'success' => true, 'content' => $cleanPost['content'],
1434 'checksum' => $cleanPost['checksum'], 'language' => $cleanPost['language'], 'excerpt' => $cleanPost['excerpt'],
1435 'postId' => $cleanPost['postId'], 'title' => $cleanPost['title'], 'url' => $cleanPost['url'] ], 200 );
1436 }
1437 catch ( Exception $e ) {
1438 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1439 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1440 }
1441 }
1442
1443 // Batch check which posts have content (for Push All optimization)
1444 public function rest_helpers_check_posts_content( $request ) {
1445 try {
1446 $params = $request->get_json_params();
1447 $postIds = isset( $params['postIds'] ) ? $params['postIds'] : [];
1448
1449 if ( empty( $postIds ) || !is_array( $postIds ) ) {
1450 return $this->create_rest_response( [
1451 'success' => false,
1452 'message' => 'postIds array is required'
1453 ], 400 );
1454 }
1455
1456 // Sanitize post IDs
1457 $postIds = array_map( 'intval', $postIds );
1458
1459 // Check content using the mwai_pre_post_content filter to support page builders,
1460 // ACF, and other plugins that store content outside of post_content
1461 $postsWithContent = [];
1462
1463 foreach ( $postIds as $postId ) {
1464 $post = get_post( $postId );
1465 if ( !$post ) {
1466 continue;
1467 }
1468 // Apply the same filter used by get_post_content() in core.php
1469 $content = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
1470 $content = trim( strip_tags( $content ) );
1471 if ( !empty( $content ) ) {
1472 $postsWithContent[] = $postId;
1473 }
1474 }
1475
1476 return $this->create_rest_response( [
1477 'success' => true,
1478 'postsWithContent' => $postsWithContent
1479 ], 200 );
1480 }
1481 catch ( Exception $e ) {
1482 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1483 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1484 }
1485 }
1486
1487 public function rest_helpers_run_tasks( $request ) {
1488 try {
1489 // Prevent concurrent execution with a transient lock
1490 $lock_key = 'mwai_rest_run_tasks_lock';
1491 if ( get_transient( $lock_key ) ) {
1492 // Log excessive calls for debugging
1493 if ( $this->core->get_option( 'dev_mode' ) ) {
1494 error_log( '[AI Engine] WARNING: rest_helpers_run_tasks called while already running' );
1495 }
1496 return $this->create_rest_response( [
1497 'success' => false,
1498 'message' => 'Tasks are already running. Please wait.'
1499 ], 429 ); // 429 Too Many Requests
1500 }
1501
1502 // Set lock for 30 seconds
1503 set_transient( $lock_key, true, 30 );
1504
1505 // Log task execution start
1506 if ( $this->core->get_option( 'dev_mode' ) ) {
1507 error_log( '[AI Engine] rest_helpers_run_tasks triggered via REST API' );
1508 }
1509
1510 try {
1511 do_action( 'mwai_tasks_run' );
1512 delete_transient( $lock_key );
1513 return $this->create_rest_response( [ 'success' => true ], 200 );
1514 }
1515 catch ( Exception $e ) {
1516 delete_transient( $lock_key );
1517 throw $e;
1518 }
1519 }
1520 catch ( Exception $e ) {
1521 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1522 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1523 }
1524 }
1525
1526 public function rest_helpers_optimize_database( $request ) {
1527 try {
1528 global $wpdb;
1529 $results = [];
1530
1531 // Add indexes to optimize query performance
1532 $indexes = [
1533 // mwai_logs indexes
1534 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time', 'columns' => 'time' ],
1535 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_userId', 'columns' => 'userId' ],
1536 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_envId', 'columns' => 'envId' ],
1537 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_refId', 'columns' => 'refId' ],
1538 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time_model', 'columns' => 'time, model' ],
1539
1540 // mwai_logmeta indexes
1541 [ 'table' => 'mwai_logmeta', 'name' => 'idx_mwai_logmeta_log_id', 'columns' => 'log_id' ],
1542
1543 // mwai_vectors indexes
1544 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_envId_status_dbId', 'columns' => 'envId, status, dbId' ],
1545 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_refId', 'columns' => 'refId' ],
1546 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_status', 'columns' => 'status' ],
1547 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_updated', 'columns' => 'updated' ],
1548
1549 // mwai_files indexes
1550 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_expires', 'columns' => 'expires' ],
1551 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_userId', 'columns' => 'userId' ],
1552 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_purpose', 'columns' => 'purpose' ],
1553
1554 // mwai_filemeta indexes
1555 [ 'table' => 'mwai_filemeta', 'name' => 'idx_mwai_filemeta_file_id', 'columns' => 'file_id' ],
1556
1557 // mwai_chats indexes
1558 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_botId', 'columns' => 'chatId, botId' ],
1559 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_userId', 'columns' => 'chatId, userId' ],
1560 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_updated', 'columns' => 'updated' ],
1561 ];
1562
1563 // Add indexes
1564 foreach ( $indexes as $index ) {
1565 $table = $wpdb->prefix . $index['table'];
1566 $index_name = $index['name'];
1567 $columns = $index['columns'];
1568
1569 // Check if index already exists
1570 $existing = $wpdb->get_var( $wpdb->prepare(
1571 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
1572 WHERE table_schema = %s AND table_name = %s AND index_name = %s',
1573 DB_NAME,
1574 $table,
1575 $index_name
1576 ) );
1577
1578 if ( !$existing ) {
1579 $wpdb->query( "ALTER TABLE `$table` ADD INDEX `$index_name` ($columns)" );
1580 $results[] = "Added index $index_name on $table";
1581 }
1582 }
1583
1584 // Clean up old logs (older than 3 months)
1585 $three_months_ago = date( 'Y-m-d H:i:s', strtotime( '-3 months' ) );
1586
1587 // Delete old logs
1588 $deleted_logs = $wpdb->query( $wpdb->prepare(
1589 "DELETE FROM {$wpdb->prefix}mwai_logs WHERE time < %s",
1590 $three_months_ago
1591 ) );
1592 $results[] = "Deleted $deleted_logs old log entries";
1593
1594 // Delete orphaned logmeta
1595 $deleted_logmeta = $wpdb->query(
1596 "DELETE lm FROM {$wpdb->prefix}mwai_logmeta lm
1597 LEFT JOIN {$wpdb->prefix}mwai_logs l ON lm.log_id = l.id
1598 WHERE l.id IS NULL"
1599 );
1600 $results[] = "Deleted $deleted_logmeta orphaned logmeta entries";
1601
1602 // Delete old chats (older than 3 months)
1603 $deleted_chats = $wpdb->query( $wpdb->prepare(
1604 "DELETE FROM {$wpdb->prefix}mwai_chats WHERE updated < %s",
1605 $three_months_ago
1606 ) );
1607 $results[] = "Deleted $deleted_chats old chat discussions";
1608
1609 // Optimize tables
1610 $tables = [ 'mwai_logs', 'mwai_logmeta', 'mwai_vectors', 'mwai_files', 'mwai_filemeta', 'mwai_chats' ];
1611 foreach ( $tables as $table ) {
1612 $wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}$table" );
1613 }
1614 $results[] = 'Optimized all AI Engine tables';
1615
1616 $message = implode( "\n", $results );
1617 return $this->create_rest_response( [ 'success' => true, 'message' => $message ], 200 );
1618 }
1619 catch ( Exception $e ) {
1620 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1621 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1622 }
1623 }
1624
1625 public function rest_system_templates_get( $request ) {
1626 try {
1627 $params = $request->get_query_params();
1628 $category = $params['category'];
1629 $templates = [];
1630 $templates_option = get_option( 'mwai_templates', [] );
1631 if ( !is_array( $templates_option ) ) {
1632 update_option( 'mwai_templates', [] );
1633 $templates_option = [];
1634 }
1635
1636 // Migration: DALL-E was removed (deprecated by OpenAI). Move templates to gpt-image-1.5.
1637 // TODO: Remove after 2027-04 (1 year after the shutdown on 2026-05-12).
1638 $deprecated = [ 'dall-e', 'dall-e-2', 'dall-e-3', 'dall-e-3-hd' ];
1639 $migrated = false;
1640 foreach ( $templates_option as &$group ) {
1641 if ( !empty( $group['templates'] ) && is_array( $group['templates'] ) ) {
1642 foreach ( $group['templates'] as &$template ) {
1643 if ( isset( $template['model'] ) && in_array( $template['model'], $deprecated, true ) ) {
1644 $template['model'] = MWAI_FALLBACK_MODEL_IMAGES;
1645 $migrated = true;
1646 }
1647 }
1648 }
1649 }
1650 unset( $group, $template );
1651 if ( $migrated ) {
1652 update_option( 'mwai_templates', $templates_option );
1653 }
1654
1655 $categories = array_column( $templates_option, 'category' );
1656 $index = array_search( $category, $categories );
1657 $templates = [];
1658 if ( $index !== false ) {
1659 $templates = $templates_option[$index]['templates'];
1660 }
1661 return $this->create_rest_response( [ 'success' => true, 'templates' => $templates ], 200 );
1662 }
1663 catch ( Exception $e ) {
1664 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1665 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1666 }
1667 }
1668
1669 public function rest_system_templates_save( $request ) {
1670 try {
1671 $params = $request->get_json_params();
1672 $category = $params['category'];
1673 $templates = $params['templates'];
1674 $templates_option = get_option( 'mwai_templates', [] );
1675 $categories = array_column( $templates_option, 'category' );
1676 $index = array_search( $category, $categories );
1677 if ( $index !== false && $index >= 0 ) {
1678 $templates_option[$index]['templates'] = $templates;
1679 }
1680 else {
1681 $group = [ 'category' => $category, 'templates' => $templates ];
1682 $templates_option[] = $group;
1683 }
1684
1685 update_option( 'mwai_templates', $templates_option );
1686 return $this->create_rest_response( [ 'success' => true ], 200 );
1687 }
1688 catch ( Exception $e ) {
1689 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1690 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1691 }
1692 }
1693
1694 public function rest_system_logs_list( $request ) {
1695 try {
1696 $params = $request->get_json_params();
1697 $offset = $params['offset'];
1698 $limit = $params['limit'];
1699 $filters = $params['filters'];
1700 $sort = isset( $params['sort'] ) ? $params['sort'] : null;
1701 $logs = apply_filters( 'mwai_stats_logs_list', [], $offset, $limit, $filters, $sort );
1702 return $this->create_rest_response( [ 'success' => true, 'total' => $logs['total'], 'logs' => $logs['rows'] ], 200 );
1703 }
1704 catch ( Exception $e ) {
1705 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1706 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1707 }
1708 }
1709
1710 public function rest_system_logs_delete( $request ) {
1711 try {
1712 $params = $request->get_json_params();
1713 $logIds = $params['logIds'];
1714 $success = apply_filters( 'mwai_stats_logs_delete', true, $logIds );
1715 return $this->create_rest_response( [ 'success' => $success ], 200 );
1716 }
1717 catch ( Exception $e ) {
1718 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1719 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1720 }
1721 }
1722
1723 public function rest_system_logs_meta_get( $request ) {
1724 try {
1725 $params = $request->get_json_params();
1726 $logId = $params['logId'];
1727 $metaKeys = $params['metaKeys'];
1728 $data = apply_filters( 'mwai_stats_logs_meta', [], $logId, $metaKeys );
1729 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1730 }
1731 catch ( Exception $e ) {
1732 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1733 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1734 }
1735 }
1736
1737 public function rest_system_logs_activity( $request ) {
1738 try {
1739 $params = $request->get_json_params();
1740 $hours = isset( $params['hours'] ) ? intval( $params['hours'] ) : 24;
1741 $data = apply_filters( 'mwai_stats_logs_activity', [], $hours );
1742 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1743 }
1744 catch ( Exception $e ) {
1745 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1746 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1747 }
1748 }
1749
1750 public function rest_system_logs_activity_daily( $request ) {
1751 try {
1752 $params = $request->get_json_params();
1753 $days = isset( $params['days'] ) ? intval( $params['days'] ) : 31;
1754 $byModel = isset( $params['byModel'] ) ? (bool) $params['byModel'] : false;
1755 $feature = isset( $params['feature'] ) ? sanitize_text_field( $params['feature'] ) : null;
1756
1757 if ( $byModel ) {
1758 $data = apply_filters( 'mwai_stats_logs_activity_daily_by_model', [], $days );
1759 }
1760 else {
1761 $data = apply_filters( 'mwai_stats_logs_activity_daily', [], $days, $feature );
1762 }
1763
1764 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1765 }
1766 catch ( Exception $e ) {
1767 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1768 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1769 }
1770 }
1771
1772 public function rest_ai_moderate( $request ) {
1773 try {
1774 $params = $request->get_json_params();
1775 $envId = $params['envId'];
1776 $text = $params['text'];
1777 if ( !$text ) {
1778 return $this->create_rest_response( [ 'success' => false, 'message' => 'Text not found.' ], 404 );
1779 }
1780 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1781 $results = $openai->moderate( $text );
1782 return $this->create_rest_response( [ 'success' => true, 'results' => $results ], 200 );
1783 }
1784 catch ( Exception $e ) {
1785 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1786 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1787 }
1788 }
1789
1790 public function rest_ai_transcribe_audio( $request ) {
1791 try {
1792 global $mwai;
1793 $params = $request->get_json_params();
1794 $url = !empty( $params['url'] ) ? $params['url'] : null;
1795 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
1796 // A client-supplied path is deliberately ignored. It used to reach
1797 // file_get_contents() with only a stream-wrapper blocklist in front of it, so an
1798 // absolute path or ../ went straight through and the bytes were forwarded to the
1799 // configured transcription endpoint. On multisite that let a subsite administrator
1800 // read the network wp-config.php and exfiltrate the auth salts off-host. This is
1801 // the same class as CVE-2024-38791, which the image handler below already dropped
1802 // its path for. mediaId stays: it resolves through get_attached_file() on a real
1803 // attachment instead of an arbitrary string.
1804 $path = null;
1805
1806 // If mediaId is provided, get the file path
1807 if ( !$path && $mediaId > 0 ) {
1808 $path = get_attached_file( $mediaId );
1809 if ( empty( $path ) ) {
1810 throw new Exception( __( 'The media file cannot be found.', 'ai-engine' ) );
1811 }
1812 }
1813
1814 // Set the scope for admin tools
1815 if ( !isset( $params['scope'] ) ) {
1816 $params['scope'] = 'admin-tools';
1817 }
1818
1819 $result = $mwai->simpleTranscribeAudio( $url, $path, $params );
1820 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1821 }
1822 catch ( Exception $e ) {
1823 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1824 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1825 }
1826 }
1827
1828 public function rest_ai_transcribe_image( $request ) {
1829 try {
1830 global $mwai;
1831 $params = $request->get_json_params();
1832 $message = $this->retrieve_message( $params );
1833 $url = !empty( $params['url'] ) ? $params['url'] : null;
1834 // This could lead to a security issue, so let's avoid using path directly.
1835 //$path = !empty( $params['path'] ) ? $params['path'] : null;
1836 $result = $mwai->simpleVisionQuery( $message, $url );
1837 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1838 }
1839 catch ( Exception $e ) {
1840 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1841 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1842 }
1843 }
1844
1845 public function rest_ai_json( $request ) {
1846 try {
1847 global $mwai;
1848 $params = $request->get_json_params();
1849 $message = $this->retrieve_message( $params );
1850 $result = $mwai->simpleJsonQuery( $message );
1851 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1852 }
1853 catch ( Exception $e ) {
1854 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1855 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1856 }
1857 }
1858
1859 public function rest_mcp_functions( $request ) {
1860 try {
1861 // Get all registered MCP tools. Handlers key entries by tool name, so the
1862 // array is associative; array_values() forces a JSON array (not an object)
1863 // for the client, which groups them with functions.reduce().
1864 $tools = array_values( apply_filters( 'mwai_mcp_tools', [] ) );
1865
1866 // Format the response
1867 $response = [
1868 'success' => true,
1869 'count' => count( $tools ),
1870 'functions' => $tools
1871 ];
1872
1873 return $this->create_rest_response( $response, 200 );
1874 }
1875 catch ( Exception $e ) {
1876 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1877 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1878 }
1879 }
1880
1881 /**
1882 * Loopback test that mimics the way Anthropic's claude.ai connector reaches
1883 * the site, so admins can detect a hosting-layer block without waiting until
1884 * they try to connect from claude.ai. Two stages, because WAFs filter them
1885 * independently and real tickets showed both patterns:
1886 *
1887 * 1. GET on the OAuth discovery path with the python-httpx User-Agent
1888 * (WP Engine's WAF blocks this one).
1889 * 2. POST on /wp-json/mcp/v1/http, python-httpx vs neutral User-Agent
1890 * (Bluehost's mod_security lets discovery GETs through but 403s the
1891 * POSTs, so OAuth client registration and every MCP call fail while
1892 * stage 1 looks fine). Both POSTs are unauthenticated, so WordPress
1893 * itself answers them identically: any python-only difference in
1894 * status or content type is the firewall talking.
1895 */
1896 public function rest_mcp_self_test( $request ) {
1897 try {
1898 $resource_url = rest_url( 'mcp/v1/http' );
1899 $probe_url = home_url( '/.well-known/oauth-protected-resource' . wp_parse_url( $resource_url, PHP_URL_PATH ) );
1900 $reference_url = rest_url( 'mcp/v1/.well-known/oauth-protected-resource' );
1901
1902 $args = [
1903 'timeout' => 10,
1904 'redirection' => 3,
1905 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ),
1906 'user-agent' => 'python-httpx/0.28.1',
1907 'headers' => [
1908 'Accept' => 'application/json',
1909 ],
1910 ];
1911
1912 $probe_response = wp_remote_get( $probe_url, $args );
1913 $reference_args = $args;
1914 $reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0';
1915 $reference_response = wp_remote_get( $reference_url, $reference_args );
1916
1917 $build = function ( $url, $response ) {
1918 if ( is_wp_error( $response ) ) {
1919 return [
1920 'url' => $url,
1921 'reachable' => false,
1922 'error' => $response->get_error_message(),
1923 'status' => null,
1924 'content_type' => null,
1925 ];
1926 }
1927 return [
1928 'url' => $url,
1929 'reachable' => true,
1930 'status' => (int) wp_remote_retrieve_response_code( $response ),
1931 'content_type' => wp_remote_retrieve_header( $response, 'content-type' ),
1932 ];
1933 };
1934
1935 $probe = $build( $probe_url, $probe_response );
1936 $reference = $build( $reference_url, $reference_response );
1937
1938 // Stage 2: the POST pair. An unauthenticated initialize is harmless and
1939 // never reaches a tool; we only care whether the firewall lets it in.
1940 $post_body = wp_json_encode( [
1941 'jsonrpc' => '2.0',
1942 'id' => 1,
1943 'method' => 'initialize',
1944 'params' => [
1945 'protocolVersion' => '2025-03-26',
1946 'capabilities' => new stdClass(),
1947 'clientInfo' => [ 'name' => 'ai-engine-self-test', 'version' => '1.0' ],
1948 ],
1949 ] );
1950 $post_args = [
1951 'timeout' => 10,
1952 'redirection' => 3,
1953 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ),
1954 'user-agent' => 'python-httpx/0.28.1',
1955 'headers' => [
1956 'Accept' => 'application/json, text/event-stream',
1957 'Content-Type' => 'application/json',
1958 ],
1959 'body' => $post_body,
1960 ];
1961 $post_probe = $build( $resource_url, wp_remote_post( $resource_url, $post_args ) );
1962 $post_reference_args = $post_args;
1963 $post_reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0';
1964 $post_reference = $build( $resource_url, wp_remote_post( $resource_url, $post_reference_args ) );
1965
1966 $is_json = function ( $probe ) {
1967 return $probe['content_type'] && strpos( $probe['content_type'], 'json' ) !== false;
1968 };
1969 // The firewall reveals itself by treating the python UA differently from
1970 // the neutral one: different status, or a block page instead of the JSON
1971 // that WordPress returns to both unauthenticated POSTs.
1972 $post_blocked = ( $post_reference['reachable'] && (
1973 ( !$post_probe['reachable'] )
1974 || ( $post_probe['status'] !== $post_reference['status'] && in_array( $post_probe['status'], [ 403, 406, 418, 429 ], true ) )
1975 || ( $is_json( $post_reference ) && !$is_json( $post_probe ) )
1976 ) );
1977
1978 $verdict = 'unknown';
1979 $message = '';
1980 if ( $probe['reachable'] && $probe['status'] === 403 ) {
1981 $verdict = 'waf_blocks_python_ua';
1982 $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.';
1983 }
1984 else if ( $probe['reachable'] && $probe['status'] === 404 ) {
1985 $verdict = 'wellknown_blocked';
1986 $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.';
1987 }
1988 else if ( !$probe['reachable'] ) {
1989 $verdict = 'unreachable';
1990 $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.';
1991 }
1992 else if ( $probe['status'] === 200 && $post_blocked ) {
1993 $verdict = 'waf_blocks_python_post';
1994 $message = 'OAuth discovery works, but your host blocks POST requests from the python-httpx User-Agent on /wp-json/mcp/v1/*. Claude.ai\'s connector will pass discovery and then fail at client registration ("Couldn\'t register with your sign-in service") or on the first MCP call. This pattern is common with mod_security on shared hosts (seen on Bluehost). Fix: ask your host to whitelist POST requests on the /wp-json/mcp/v1/ path, or add a Cloudflare Transform Rule that rewrites the User-Agent for /.well-known/oauth-* and /wp-json/mcp/v1/* before the request reaches your origin. See https://meowapps.com/fix-mcp-wordpress-connection for the full recipe.';
1995 }
1996 else if ( $probe['status'] === 200 ) {
1997 $verdict = 'ok';
1998 $message = 'Your site accepts the python-httpx User-Agent on both the OAuth discovery path (GET) and the MCP endpoint (POST). Claude.ai\'s connector should be able to reach it. If connecting still fails, run the same probes from an external machine: some servers reach themselves without going through the edge firewall.';
1999 }
2000 else {
2001 $verdict = 'unexpected_status';
2002 $message = 'Got HTTP ' . $probe['status'] . ' from the loopback probe. Expected 200. Investigate the response in your CDN/origin logs.';
2003 }
2004
2005 return $this->create_rest_response( [
2006 'success' => true,
2007 'verdict' => $verdict,
2008 'message' => $message,
2009 'probe' => $probe,
2010 'reference' => $reference,
2011 'post_probe' => $post_probe,
2012 'post_reference' => $post_reference,
2013 ], 200 );
2014 }
2015 catch ( Exception $e ) {
2016 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2017 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2018 }
2019 }
2020
2021 /**
2022 * Top MCP tools by call count over the last N days. Powers the
2023 * "Top Tools" widget in the MCP Logs view of the Insights screen.
2024 * Returns rows shaped as { tool, count, success_count, error_count }.
2025 */
2026 public function rest_mcp_top_tools( $request ) {
2027 try {
2028 $params = $request->get_json_params();
2029 $days = isset( $params['days'] ) ? max( 1, intval( $params['days'] ) ) : 7;
2030 $limit = isset( $params['limit'] ) ? max( 1, min( 50, intval( $params['limit'] ) ) ) : 10;
2031
2032 global $wpdb;
2033 $table = $wpdb->prefix . 'mwai_logs';
2034 $rows = $wpdb->get_results(
2035 $wpdb->prepare(
2036 "SELECT scope AS tool,
2037 COUNT(*) AS count,
2038 SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS success_count,
2039 SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS error_count
2040 FROM $table
2041 WHERE feature = 'mcp_tool'
2042 AND time >= DATE_SUB(NOW(), INTERVAL %d DAY)
2043 GROUP BY scope
2044 ORDER BY count DESC
2045 LIMIT %d",
2046 '%"status":"success"%',
2047 '%"status":"error"%',
2048 $days,
2049 $limit
2050 ),
2051 ARRAY_A
2052 );
2053
2054 foreach ( $rows as &$row ) {
2055 $row['count'] = (int) $row['count'];
2056 $row['success_count'] = (int) $row['success_count'];
2057 $row['error_count'] = (int) $row['error_count'];
2058 }
2059 unset( $row );
2060
2061 return $this->create_rest_response( [ 'success' => true, 'tools' => $rows ?: [] ], 200 );
2062 }
2063 catch ( Exception $e ) {
2064 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2065 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2066 }
2067 }
2068
2069 public function rest_helpers_post_types() {
2070 try {
2071 $postTypes = $this->core->get_post_types();
2072 return $this->create_rest_response( [ 'success' => true, 'postTypes' => $postTypes ], 200 );
2073 }
2074 catch ( Exception $e ) {
2075 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2076 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2077 }
2078 }
2079
2080 public function rest_settings_themes( $request ) {
2081 try {
2082 $method = $request->get_method();
2083 if ( $method === 'GET' ) {
2084 $themes = $this->core->get_themes();
2085 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
2086 }
2087 else if ( $method === 'POST' ) {
2088 $params = $request->get_json_params();
2089 $themes = $params['themes'];
2090 $themes = $this->core->update_themes( $themes );
2091 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
2092 }
2093 }
2094 catch ( Exception $e ) {
2095 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2096 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2097 }
2098 }
2099
2100 public function rest_settings_chatbots( $request ) {
2101 try {
2102 $method = $request->get_method();
2103 if ( $method === 'GET' ) {
2104 $chatbots = $this->core->get_chatbots();
2105 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
2106 }
2107 else if ( $method === 'POST' ) {
2108 $params = $request->get_json_params();
2109 $chatbots = $params['chatbots'];
2110 $chatbots = $this->core->update_chatbots( $chatbots );
2111 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
2112 }
2113 return $this->create_rest_response( [ 'success' => false, 'message' => 'Method not allowed' ], 405 );
2114 }
2115 catch ( Exception $e ) {
2116 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2117 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2118 }
2119 }
2120
2121 #region Logs
2122
2123 public function rest_get_logs() {
2124 $logs = Meow_MWAI_Logging::get();
2125 return $this->create_rest_response( [ 'success' => true, 'data' => $logs ], 200 );
2126 }
2127
2128 public function rest_clear_logs() {
2129 Meow_MWAI_Logging::clear();
2130 return $this->create_rest_response( [ 'success' => true ], 200 );
2131 }
2132
2133 #endregion
2134
2135 #region Forms
2136
2137 public function rest_forms_list( $request ) {
2138 try {
2139 $args = [
2140 'post_type' => 'mwai_form',
2141 'posts_per_page' => 100,
2142 'post_status' => 'any',
2143 'orderby' => 'date',
2144 'order' => 'DESC'
2145 ];
2146
2147 $posts = get_posts( $args );
2148 $forms = array_map( function ( $post ) {
2149 return [
2150 'id' => $post->ID,
2151 'title' => $post->post_title,
2152 'status' => $post->post_status
2153 ];
2154 }, $posts );
2155
2156 return $this->create_rest_response( [ 'success' => true, 'forms' => $forms ], 200 );
2157 }
2158 catch ( Exception $e ) {
2159 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2160 }
2161 }
2162
2163 public function rest_forms_get( $request ) {
2164 try {
2165 $id = intval( $request->get_param( 'id' ) );
2166 if ( !$id ) {
2167 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2168 }
2169
2170 $post = get_post( $id );
2171 if ( !$post || $post->post_type !== 'mwai_form' ) {
2172 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2173 }
2174
2175 $form = [
2176 'id' => $post->ID,
2177 'title' => [
2178 'raw' => $post->post_title,
2179 'rendered' => $post->post_title
2180 ],
2181 'content' => [
2182 'raw' => $post->post_content,
2183 'rendered' => $post->post_content
2184 ],
2185 'status' => $post->post_status
2186 ];
2187
2188 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2189 }
2190 catch ( Exception $e ) {
2191 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2192 }
2193 }
2194
2195 public function rest_forms_create( $request ) {
2196 try {
2197 $params = $request->get_json_params();
2198 $title = isset( $params['title'] ) ? $params['title'] : 'Untitled Form';
2199
2200 // wp_insert_post expects slashed data - it calls wp_unslash() internally, which would
2201 // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and corrupt
2202 // the stored blocks.
2203 $post_data = wp_slash( [
2204 'post_title' => $title,
2205 'post_content' => '',
2206 'post_status' => 'draft',
2207 'post_type' => 'mwai_form'
2208 ] );
2209
2210 $post_id = wp_insert_post( $post_data );
2211
2212 if ( is_wp_error( $post_id ) ) {
2213 return $this->create_rest_response( [ 'success' => false, 'message' => $post_id->get_error_message() ], 500 );
2214 }
2215
2216 $post = get_post( $post_id );
2217 $form = [
2218 'id' => $post->ID,
2219 'title' => [
2220 'raw' => $post->post_title,
2221 'rendered' => $post->post_title
2222 ],
2223 'status' => $post->post_status
2224 ];
2225
2226 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2227 }
2228 catch ( Exception $e ) {
2229 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2230 }
2231 }
2232
2233 public function rest_forms_update( $request ) {
2234 try {
2235 $params = $request->get_json_params();
2236 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2237
2238 if ( !$id ) {
2239 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2240 }
2241
2242 $post = get_post( $id );
2243 if ( !$post || $post->post_type !== 'mwai_form' ) {
2244 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2245 }
2246
2247 $post_data = [ 'ID' => $id ];
2248
2249 if ( isset( $params['title'] ) ) {
2250 $post_data['post_title'] = $params['title'];
2251 }
2252
2253 if ( isset( $params['content'] ) ) {
2254 $post_data['post_content'] = $params['content'];
2255 }
2256
2257 if ( isset( $params['status'] ) ) {
2258 $post_data['post_status'] = $params['status'];
2259 }
2260
2261 // wp_update_post expects slashed data - it calls wp_unslash() internally, which would
2262 // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and break
2263 // Gutenberg blocks on reload.
2264 $result = wp_update_post( wp_slash( $post_data ) );
2265
2266 if ( is_wp_error( $result ) ) {
2267 return $this->create_rest_response( [ 'success' => false, 'message' => $result->get_error_message() ], 500 );
2268 }
2269
2270 $post = get_post( $id );
2271 $form = [
2272 'id' => $post->ID,
2273 'title' => [
2274 'raw' => $post->post_title,
2275 'rendered' => $post->post_title
2276 ],
2277 'content' => [
2278 'raw' => $post->post_content,
2279 'rendered' => $post->post_content
2280 ],
2281 'status' => $post->post_status
2282 ];
2283
2284 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2285 }
2286 catch ( Exception $e ) {
2287 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2288 }
2289 }
2290
2291 public function rest_forms_delete( $request ) {
2292 try {
2293 $params = $request->get_json_params();
2294 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2295
2296 if ( !$id ) {
2297 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2298 }
2299
2300 $post = get_post( $id );
2301 if ( !$post || $post->post_type !== 'mwai_form' ) {
2302 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2303 }
2304
2305 $result = wp_delete_post( $id, true );
2306
2307 if ( !$result ) {
2308 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to delete form' ], 500 );
2309 }
2310
2311 return $this->create_rest_response( [ 'success' => true ], 200 );
2312 }
2313 catch ( Exception $e ) {
2314 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2315 }
2316 }
2317
2318 #endregion
2319
2320 #region Video Generation Helpers
2321
2322 public function rest_helpers_create_video( $request ) {
2323 try {
2324 $params = $request->get_json_params();
2325 $prompt = sanitize_text_field( $params['prompt'] );
2326 $model = sanitize_text_field( $params['model'] ?? 'sora-2' );
2327 $size = sanitize_text_field( $params['size'] ?? '720x1280' );
2328 $seconds = absint( $params['seconds'] ?? 4 );
2329 $envId = sanitize_text_field( $params['envId'] ?? '' );
2330
2331 // Check if envId is provided (defaults not supported for videos yet)
2332 if ( empty( $envId ) ) {
2333 throw new Exception( 'Please select a specific environment and model in the Video Generator. Default environments are not yet supported for video generation.' );
2334 }
2335
2336 // Get API key from environment
2337 $env = $this->core->get_ai_env( $envId );
2338 $api_key = $env['apikey'] ?? '';
2339
2340 if ( empty( $api_key ) ) {
2341 throw new Exception( 'OpenAI API key not found.' );
2342 }
2343
2344 // Prepare multipart boundary
2345 $boundary = wp_generate_password( 24, false );
2346 $body = '';
2347
2348 // Add model
2349 $body .= "--{$boundary}\r\n";
2350 $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n";
2351 $body .= "{$model}\r\n";
2352
2353 // Add prompt
2354 $body .= "--{$boundary}\r\n";
2355 $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n";
2356 $body .= "{$prompt}\r\n";
2357
2358 // Add size
2359 $body .= "--{$boundary}\r\n";
2360 $body .= "Content-Disposition: form-data; name=\"size\"\r\n\r\n";
2361 $body .= "{$size}\r\n";
2362
2363 // Add seconds
2364 $body .= "--{$boundary}\r\n";
2365 $body .= "Content-Disposition: form-data; name=\"seconds\"\r\n\r\n";
2366 $body .= "{$seconds}\r\n";
2367
2368 $body .= "--{$boundary}--\r\n";
2369
2370 // Call OpenAI API to create video
2371 $response = wp_remote_post( 'https://api.openai.com/v1/videos', [
2372 'headers' => [
2373 'Authorization' => 'Bearer ' . $api_key,
2374 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
2375 ],
2376 'body' => $body,
2377 'timeout' => 30
2378 ] );
2379
2380 if ( is_wp_error( $response ) ) {
2381 throw new Exception( $response->get_error_message() );
2382 }
2383
2384 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
2385
2386 if ( isset( $response_body['error'] ) ) {
2387 throw new Exception( $response_body['error']['message'] ?? 'Unknown error' );
2388 }
2389
2390 // Record usage (price is calculated per second)
2391 $usage = $this->core->record_videos_usage( $model, $size, $seconds );
2392
2393 // Log to Query Logs (Statistics)
2394 try {
2395 if ( class_exists( 'MeowPro_MWAI_Stats' ) && class_exists( 'MeowPro_MWAI_Statistics' ) ) {
2396 $statsObject = new MeowPro_MWAI_Stats();
2397 $statsObject->session = $params['session'] ?? null;
2398 $statsObject->scope = 'admin-tools';
2399 $statsObject->feature = 'video-generator';
2400 $statsObject->model = $model;
2401 $statsObject->envId = $envId;
2402 $statsObject->units = $seconds;
2403 $statsObject->type = 'seconds';
2404 $statsObject->price = $usage['price'] ?? 0;
2405 $statsObject->accuracy = $usage['accuracy'] ?? 'full';
2406
2407 $statistics = new MeowPro_MWAI_Statistics();
2408 $statistics->commit_stats( $statsObject );
2409 }
2410 }
2411 catch ( Exception $statsError ) {
2412 // Log the error but don't fail the video creation
2413 error_log( '[AI Engine Video] Failed to log statistics: ' . $statsError->getMessage() );
2414 }
2415
2416 // Store metadata for later retrieval when video completes
2417 if ( isset( $response_body['id'] ) ) {
2418 set_transient( 'mwai_video_metadata_' . $response_body['id'], [
2419 'model' => $model,
2420 'env_id' => $envId,
2421 'created_at' => time()
2422 ], 7 * DAY_IN_SECONDS );
2423 }
2424
2425 return $this->create_rest_response( [ 'success' => true, 'video' => $response_body, 'usage' => $usage ], 200 );
2426 }
2427 catch ( Exception $e ) {
2428 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2429 }
2430 }
2431
2432 public function rest_helpers_video_status( $request ) {
2433 try {
2434 $params = $request->get_json_params();
2435 $video_ids = $params['videoIds'] ?? [];
2436 $envId = sanitize_text_field( $params['envId'] ?? '' );
2437
2438 if ( empty( $video_ids ) ) {
2439 return $this->create_rest_response( [ 'success' => true, 'videos' => [] ], 200 );
2440 }
2441
2442 // Get API key from environment
2443 $env = $this->core->get_ai_env( $envId );
2444 $api_key = $env['apikey'] ?? '';
2445
2446 if ( empty( $api_key ) ) {
2447 throw new Exception( 'OpenAI API key not found.' );
2448 }
2449
2450 $videos = [];
2451 foreach ( $video_ids as $video_id ) {
2452 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id, [
2453 'headers' => [
2454 'Authorization' => 'Bearer ' . $api_key
2455 ],
2456 'timeout' => 15
2457 ] );
2458
2459 if ( !is_wp_error( $response ) ) {
2460 $body = json_decode( wp_remote_retrieve_body( $response ), true );
2461 if ( !isset( $body['error'] ) ) {
2462 // If video is completed and we haven't saved it yet, download and save to media library
2463 if ( $body['status'] === 'completed' && empty( get_transient( 'mwai_video_saved_' . $video_id ) ) ) {
2464 // Retrieve metadata that was stored when video was created
2465 $metadata = get_transient( 'mwai_video_metadata_' . $video_id );
2466 $ai_metadata = [];
2467 if ( $metadata ) {
2468 $ai_metadata = [
2469 'model' => $metadata['model'] ?? null,
2470 'env_id' => $metadata['env_id'] ?? null,
2471 'latency' => isset( $metadata['created_at'] ) ? ( time() - $metadata['created_at'] ) : null
2472 ];
2473 }
2474
2475 $attachment_id = $this->download_and_save_video( $video_id, $api_key, '', '', $ai_metadata );
2476 if ( $attachment_id ) {
2477 $body['attachment_id'] = $attachment_id;
2478 // Build URL from file path for custom post types
2479 $file_path = get_attached_file( $attachment_id );
2480 $upload_dir = wp_upload_dir();
2481 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2482 // Mark as saved so we don't download again
2483 set_transient( 'mwai_video_saved_' . $video_id, $attachment_id, DAY_IN_SECONDS );
2484 // Clean up metadata transient
2485 delete_transient( 'mwai_video_metadata_' . $video_id );
2486 }
2487 }
2488 // Check if we already have this video saved
2489 else if ( $body['status'] === 'completed' ) {
2490 $attachment_id = get_transient( 'mwai_video_saved_' . $video_id );
2491 if ( $attachment_id ) {
2492 $body['attachment_id'] = $attachment_id;
2493 // Build URL from file path for custom post types
2494 $file_path = get_attached_file( $attachment_id );
2495 $upload_dir = wp_upload_dir();
2496 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2497 }
2498 }
2499 $videos[] = $body;
2500 }
2501 else {
2502 // Include error information in the response
2503 error_log( 'AI Engine: Video generation failed for ID ' . $video_id . ': ' . json_encode( $body['error'] ) );
2504 $videos[] = [
2505 'id' => $video_id,
2506 'status' => 'failed',
2507 'error' => $body['error']
2508 ];
2509 }
2510 }
2511 else {
2512 // WP HTTP error
2513 error_log( 'AI Engine: Failed to check video status for ID ' . $video_id . ': ' . $response->get_error_message() );
2514 $videos[] = [
2515 'id' => $video_id,
2516 'status' => 'failed',
2517 'error' => [ 'message' => $response->get_error_message() ]
2518 ];
2519 }
2520 }
2521
2522 return $this->create_rest_response( [ 'success' => true, 'videos' => $videos ], 200 );
2523 }
2524 catch ( Exception $e ) {
2525 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2526 }
2527 }
2528
2529 public function rest_helpers_download_video( $request ) {
2530 try {
2531 $params = $request->get_json_params();
2532 $video_id = sanitize_text_field( $params['videoId'] );
2533 $envId = sanitize_text_field( $params['envId'] ?? '' );
2534
2535 // Get API key from environment
2536 $env = $this->core->get_ai_env( $envId );
2537 $api_key = $env['apikey'] ?? '';
2538
2539 if ( empty( $api_key ) ) {
2540 throw new Exception( 'OpenAI API key not found.' );
2541 }
2542
2543 $temp_file = wp_tempnam( $video_id . '.mp4' );
2544
2545 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2546 'headers' => [
2547 'Authorization' => 'Bearer ' . $api_key
2548 ],
2549 'timeout' => 120,
2550 'stream' => true,
2551 'filename' => $temp_file
2552 ] );
2553
2554 if ( is_wp_error( $response ) ) {
2555 throw new Exception( $response->get_error_message() );
2556 }
2557
2558 $file_data = file_get_contents( $temp_file );
2559 $base64 = base64_encode( $file_data );
2560
2561 unlink( $temp_file );
2562
2563 return $this->create_rest_response( [
2564 'success' => true,
2565 'data' => $base64,
2566 'mimeType' => 'video/mp4'
2567 ], 200 );
2568 }
2569 catch ( Exception $e ) {
2570 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2571 }
2572 }
2573
2574 public function rest_helpers_delete_video( $request ) {
2575 try {
2576 $params = $request->get_json_params();
2577 $video_id = sanitize_text_field( $params['videoId'] );
2578 $envId = sanitize_text_field( $params['envId'] ?? '' );
2579
2580 // Get API key from environment
2581 $env = $this->core->get_ai_env( $envId );
2582 $api_key = $env['apikey'] ?? '';
2583
2584 if ( empty( $api_key ) ) {
2585 throw new Exception( 'OpenAI API key not found.' );
2586 }
2587
2588 $response = wp_remote_request( 'https://api.openai.com/v1/videos/' . $video_id, [
2589 'method' => 'DELETE',
2590 'headers' => [
2591 'Authorization' => 'Bearer ' . $api_key
2592 ],
2593 'timeout' => 15
2594 ] );
2595
2596 if ( is_wp_error( $response ) ) {
2597 throw new Exception( $response->get_error_message() );
2598 }
2599
2600 return $this->create_rest_response( [ 'success' => true ], 200 );
2601 }
2602 catch ( Exception $e ) {
2603 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2604 }
2605 }
2606
2607 private function download_and_save_video( $video_id, $api_key, $title = '', $description = '', $ai_metadata = [] ) {
2608 try {
2609 // Download video content
2610 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2611 'headers' => [
2612 'Authorization' => 'Bearer ' . $api_key
2613 ],
2614 'timeout' => 120
2615 ] );
2616
2617 if ( is_wp_error( $response ) ) {
2618 error_log( 'Error downloading video: ' . $response->get_error_message() );
2619 return false;
2620 }
2621
2622 $video_data = wp_remote_retrieve_body( $response );
2623 if ( empty( $video_data ) ) {
2624 error_log( 'Empty video data received' );
2625 return false;
2626 }
2627
2628 // Generate filename
2629 $filename = $video_id . '.mp4';
2630 $upload_dir = wp_upload_dir();
2631 $file_path = $upload_dir['path'] . '/' . $filename;
2632
2633 // Save to file
2634 file_put_contents( $file_path, $video_data );
2635
2636 // Prepare attachment data - use mwai_video post type (draft video)
2637 $attachment = [
2638 'post_mime_type' => 'video/mp4',
2639 'post_title' => !empty( $title ) ? $title : 'AI Generated Video',
2640 'post_content' => $description,
2641 'post_status' => 'inherit',
2642 'post_type' => 'mwai_video'
2643 ];
2644
2645 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
2646 $attachment_id = wp_insert_post( $attachment );
2647
2648 // Set the attached file manually since we're not using wp_insert_attachment
2649 update_attached_file( $attachment_id, $file_path );
2650
2651 if ( is_wp_error( $attachment_id ) ) {
2652 error_log( 'Error creating attachment: ' . $attachment_id->get_error_message() );
2653 return false;
2654 }
2655
2656 // Generate attachment metadata
2657 require_once ABSPATH . 'wp-admin/includes/image.php';
2658 $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path );
2659 wp_update_attachment_metadata( $attachment_id, $attach_data );
2660
2661 // Store AI-related metadata
2662 if ( !empty( $ai_metadata['model'] ) ) {
2663 update_post_meta( $attachment_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
2664 }
2665 if ( !empty( $ai_metadata['latency'] ) ) {
2666 update_post_meta( $attachment_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
2667 }
2668 if ( !empty( $ai_metadata['env_id'] ) ) {
2669 update_post_meta( $attachment_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
2670 }
2671
2672 // Add to user's draft media
2673 $user_id = get_current_user_id();
2674 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2675 if ( !is_array( $draft_media ) ) {
2676 $draft_media = [];
2677 }
2678 $draft_media[] = [
2679 'attachment_id' => $attachment_id,
2680 'type' => 'video',
2681 'openai_id' => $video_id,
2682 'created_at' => time()
2683 ];
2684 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
2685
2686 return $attachment_id;
2687 }
2688 catch ( Exception $e ) {
2689 error_log( 'Exception in download_and_save_video: ' . $e->getMessage() );
2690 return false;
2691 }
2692 }
2693
2694 public function rest_helpers_save_video_to_library( $request ) {
2695 try {
2696 $params = $request->get_json_params();
2697 $video_id = sanitize_text_field( $params['videoId'] );
2698 $title = sanitize_text_field( $params['title'] );
2699 $description = sanitize_text_field( $params['description'] );
2700 $filename = sanitize_file_name( $params['filename'] );
2701 $envId = sanitize_text_field( $params['envId'] ?? '' );
2702
2703 // Ensure filename has .mp4 extension
2704 if ( !preg_match( '/\.mp4$/i', $filename ) ) {
2705 $filename .= '.mp4';
2706 }
2707
2708 // Get API key from environment
2709 $env = $this->core->get_ai_env( $envId );
2710 $api_key = $env['apikey'] ?? '';
2711
2712 if ( empty( $api_key ) ) {
2713 throw new Exception( 'OpenAI API key not found.' );
2714 }
2715
2716 // Download video content
2717 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2718 'headers' => [
2719 'Authorization' => 'Bearer ' . $api_key
2720 ],
2721 'timeout' => 120
2722 ] );
2723
2724 if ( is_wp_error( $response ) ) {
2725 throw new Exception( $response->get_error_message() );
2726 }
2727
2728 $video_data = wp_remote_retrieve_body( $response );
2729
2730 // Upload to WordPress media library
2731 $upload_dir = wp_upload_dir();
2732 $file_path = $upload_dir['path'] . '/' . $filename;
2733
2734 file_put_contents( $file_path, $video_data );
2735
2736 $attachment = [
2737 'post_mime_type' => 'video/mp4',
2738 'post_title' => $title,
2739 'post_content' => $description,
2740 'post_status' => 'inherit'
2741 ];
2742
2743 $attach_id = wp_insert_attachment( $attachment, $file_path );
2744
2745 require_once( ABSPATH . 'wp-admin/includes/image.php' );
2746 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
2747 wp_update_attachment_metadata( $attach_id, $attach_data );
2748
2749 return $this->create_rest_response( [
2750 'success' => true,
2751 'attachmentId' => $attach_id
2752 ], 200 );
2753 }
2754 catch ( Exception $e ) {
2755 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2756 }
2757 }
2758
2759 public function rest_helpers_delete_video_from_library( $request ) {
2760 try {
2761 $params = $request->get_json_params();
2762 $attachment_id = absint( $params['attachmentId'] );
2763
2764 if ( empty( $attachment_id ) ) {
2765 throw new Exception( 'Attachment ID is required.' );
2766 }
2767
2768 $deleted = wp_delete_attachment( $attachment_id, true );
2769
2770 if ( !$deleted ) {
2771 throw new Exception( 'Failed to delete attachment.' );
2772 }
2773
2774 return $this->create_rest_response( [ 'success' => true ], 200 );
2775 }
2776 catch ( Exception $e ) {
2777 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2778 }
2779 }
2780
2781 public function rest_helpers_list_draft_media( $request ) {
2782 try {
2783 $type = $request->get_param( 'type' ); // 'image', 'video', or null for all
2784 $user_id = get_current_user_id();
2785 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2786
2787 if ( !is_array( $draft_media ) ) {
2788 return $this->create_rest_response( [ 'success' => true, 'media' => [] ], 200 );
2789 }
2790
2791 $media_items = [];
2792 foreach ( $draft_media as $item ) {
2793 // Filter by type if specified
2794 if ( $type && $item['type'] !== $type ) {
2795 continue;
2796 }
2797
2798 $attachment_id = $item['attachment_id'];
2799 $attachment = get_post( $attachment_id );
2800
2801 if ( $attachment ) {
2802 // For custom post types (mwai_image, mwai_video), build URL from file path
2803 $file_path = get_attached_file( $attachment_id );
2804 $upload_dir = wp_upload_dir();
2805 $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2806
2807 $model = get_post_meta( $attachment_id, 'mwai_model', true );
2808 $generation_time = get_post_meta( $attachment_id, 'mwai_latency', true );
2809 $env_id = get_post_meta( $attachment_id, 'mwai_env_id', true );
2810
2811 // Debug logging
2812 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2813 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 ) );
2814 }
2815
2816 $media_items[] = [
2817 'attachment_id' => $attachment_id,
2818 'type' => $item['type'],
2819 'openai_id' => $item['openai_id'] ?? null,
2820 'url' => $url,
2821 'title' => $attachment->post_title,
2822 'description' => $attachment->post_content,
2823 'filename' => basename( $file_path ),
2824 'created_at' => $item['created_at'],
2825 'model' => $model,
2826 'generation_time' => $generation_time,
2827 'env_id' => $env_id
2828 ];
2829 }
2830 }
2831
2832 return $this->create_rest_response( [ 'success' => true, 'media' => $media_items ], 200 );
2833 }
2834 catch ( Exception $e ) {
2835 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2836 }
2837 }
2838
2839 public function rest_helpers_approve_media( $request ) {
2840 try {
2841 $params = $request->get_json_params();
2842 $attachment_id = absint( $params['attachmentId'] );
2843 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2844 $envId = sanitize_text_field( $params['envId'] ?? '' );
2845
2846 if ( empty( $attachment_id ) ) {
2847 throw new Exception( 'Attachment ID is required.' );
2848 }
2849
2850 // Convert from mwai_image/mwai_video to attachment post type
2851 wp_update_post( [
2852 'ID' => $attachment_id,
2853 'post_type' => 'attachment',
2854 'post_status' => 'inherit'
2855 ] );
2856
2857 // Remove from draft media list
2858 $user_id = get_current_user_id();
2859 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2860 if ( is_array( $draft_media ) ) {
2861 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2862 return $item['attachment_id'] !== $attachment_id;
2863 } );
2864 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2865 }
2866
2867 // Delete video from OpenAI if applicable
2868 if ( !empty( $openai_id ) ) {
2869 $env = $this->core->get_ai_env( $envId );
2870 $api_key = $env['apikey'] ?? '';
2871
2872 if ( !empty( $api_key ) ) {
2873 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2874 'method' => 'DELETE',
2875 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2876 'timeout' => 15
2877 ] );
2878 }
2879 }
2880
2881 return $this->create_rest_response( [ 'success' => true ], 200 );
2882 }
2883 catch ( Exception $e ) {
2884 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2885 }
2886 }
2887
2888 public function rest_helpers_reject_media( $request ) {
2889 try {
2890 $params = $request->get_json_params();
2891 $attachment_id = absint( $params['attachmentId'] );
2892 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2893 $envId = sanitize_text_field( $params['envId'] ?? '' );
2894
2895 if ( empty( $attachment_id ) ) {
2896 throw new Exception( 'Attachment ID is required.' );
2897 }
2898
2899 // Convert from mwai_image/mwai_video to attachment post type first
2900 // This ensures wp_delete_attachment properly deletes the physical file
2901 wp_update_post( [
2902 'ID' => $attachment_id,
2903 'post_type' => 'attachment'
2904 ] );
2905
2906 // Delete attachment from WordPress (now that it's a proper attachment, files will be deleted)
2907 wp_delete_attachment( $attachment_id, true );
2908
2909 // Remove from draft media list
2910 $user_id = get_current_user_id();
2911 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2912 if ( is_array( $draft_media ) ) {
2913 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2914 return $item['attachment_id'] !== $attachment_id;
2915 } );
2916 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2917 }
2918
2919 // Delete video from OpenAI if applicable
2920 if ( !empty( $openai_id ) ) {
2921 $env = $this->core->get_ai_env( $envId );
2922 $api_key = $env['apikey'] ?? '';
2923
2924 if ( !empty( $api_key ) ) {
2925 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2926 'method' => 'DELETE',
2927 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2928 'timeout' => 15
2929 ] );
2930 }
2931 }
2932
2933 return $this->create_rest_response( [ 'success' => true ], 200 );
2934 }
2935 catch ( Exception $e ) {
2936 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2937 }
2938 }
2939
2940 #endregion
2941 }
2942