PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.6.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.6.3
3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 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 week ago exceptions 1 week ago modules 1 week ago query 1 week ago services 1 week ago admin.php 1 week ago api.php 4 weeks ago core.php 1 week ago discussion.php 1 year ago event.php 1 year ago init.php 1 week ago logging.php 1 year ago reply.php 1 week ago rest.php 1 week ago
rest.php
2934 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 $path = !empty( $params['path'] ) ? $params['path'] : null;
1797
1798 // If mediaId is provided, get the file path
1799 if ( !$path && $mediaId > 0 ) {
1800 $path = get_attached_file( $mediaId );
1801 if ( empty( $path ) ) {
1802 throw new Exception( __( 'The media file cannot be found.', 'ai-engine' ) );
1803 }
1804 }
1805
1806 // Set the scope for admin tools
1807 if ( !isset( $params['scope'] ) ) {
1808 $params['scope'] = 'admin-tools';
1809 }
1810
1811 $result = $mwai->simpleTranscribeAudio( $url, $path, $params );
1812 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1813 }
1814 catch ( Exception $e ) {
1815 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1816 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1817 }
1818 }
1819
1820 public function rest_ai_transcribe_image( $request ) {
1821 try {
1822 global $mwai;
1823 $params = $request->get_json_params();
1824 $message = $this->retrieve_message( $params );
1825 $url = !empty( $params['url'] ) ? $params['url'] : null;
1826 // This could lead to a security issue, so let's avoid using path directly.
1827 //$path = !empty( $params['path'] ) ? $params['path'] : null;
1828 $result = $mwai->simpleVisionQuery( $message, $url );
1829 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1830 }
1831 catch ( Exception $e ) {
1832 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1833 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1834 }
1835 }
1836
1837 public function rest_ai_json( $request ) {
1838 try {
1839 global $mwai;
1840 $params = $request->get_json_params();
1841 $message = $this->retrieve_message( $params );
1842 $result = $mwai->simpleJsonQuery( $message );
1843 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1844 }
1845 catch ( Exception $e ) {
1846 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1847 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1848 }
1849 }
1850
1851 public function rest_mcp_functions( $request ) {
1852 try {
1853 // Get all registered MCP tools. Handlers key entries by tool name, so the
1854 // array is associative; array_values() forces a JSON array (not an object)
1855 // for the client, which groups them with functions.reduce().
1856 $tools = array_values( apply_filters( 'mwai_mcp_tools', [] ) );
1857
1858 // Format the response
1859 $response = [
1860 'success' => true,
1861 'count' => count( $tools ),
1862 'functions' => $tools
1863 ];
1864
1865 return $this->create_rest_response( $response, 200 );
1866 }
1867 catch ( Exception $e ) {
1868 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1869 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1870 }
1871 }
1872
1873 /**
1874 * Loopback test that mimics the way Anthropic's claude.ai connector reaches
1875 * the site, so admins can detect a hosting-layer block without waiting until
1876 * they try to connect from claude.ai. Two stages, because WAFs filter them
1877 * independently and real tickets showed both patterns:
1878 *
1879 * 1. GET on the OAuth discovery path with the python-httpx User-Agent
1880 * (WP Engine's WAF blocks this one).
1881 * 2. POST on /wp-json/mcp/v1/http, python-httpx vs neutral User-Agent
1882 * (Bluehost's mod_security lets discovery GETs through but 403s the
1883 * POSTs, so OAuth client registration and every MCP call fail while
1884 * stage 1 looks fine). Both POSTs are unauthenticated, so WordPress
1885 * itself answers them identically: any python-only difference in
1886 * status or content type is the firewall talking.
1887 */
1888 public function rest_mcp_self_test( $request ) {
1889 try {
1890 $resource_url = rest_url( 'mcp/v1/http' );
1891 $probe_url = home_url( '/.well-known/oauth-protected-resource' . wp_parse_url( $resource_url, PHP_URL_PATH ) );
1892 $reference_url = rest_url( 'mcp/v1/.well-known/oauth-protected-resource' );
1893
1894 $args = [
1895 'timeout' => 10,
1896 'redirection' => 3,
1897 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ),
1898 'user-agent' => 'python-httpx/0.28.1',
1899 'headers' => [
1900 'Accept' => 'application/json',
1901 ],
1902 ];
1903
1904 $probe_response = wp_remote_get( $probe_url, $args );
1905 $reference_args = $args;
1906 $reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0';
1907 $reference_response = wp_remote_get( $reference_url, $reference_args );
1908
1909 $build = function ( $url, $response ) {
1910 if ( is_wp_error( $response ) ) {
1911 return [
1912 'url' => $url,
1913 'reachable' => false,
1914 'error' => $response->get_error_message(),
1915 'status' => null,
1916 'content_type' => null,
1917 ];
1918 }
1919 return [
1920 'url' => $url,
1921 'reachable' => true,
1922 'status' => (int) wp_remote_retrieve_response_code( $response ),
1923 'content_type' => wp_remote_retrieve_header( $response, 'content-type' ),
1924 ];
1925 };
1926
1927 $probe = $build( $probe_url, $probe_response );
1928 $reference = $build( $reference_url, $reference_response );
1929
1930 // Stage 2: the POST pair. An unauthenticated initialize is harmless and
1931 // never reaches a tool; we only care whether the firewall lets it in.
1932 $post_body = wp_json_encode( [
1933 'jsonrpc' => '2.0',
1934 'id' => 1,
1935 'method' => 'initialize',
1936 'params' => [
1937 'protocolVersion' => '2025-03-26',
1938 'capabilities' => new stdClass(),
1939 'clientInfo' => [ 'name' => 'ai-engine-self-test', 'version' => '1.0' ],
1940 ],
1941 ] );
1942 $post_args = [
1943 'timeout' => 10,
1944 'redirection' => 3,
1945 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ),
1946 'user-agent' => 'python-httpx/0.28.1',
1947 'headers' => [
1948 'Accept' => 'application/json, text/event-stream',
1949 'Content-Type' => 'application/json',
1950 ],
1951 'body' => $post_body,
1952 ];
1953 $post_probe = $build( $resource_url, wp_remote_post( $resource_url, $post_args ) );
1954 $post_reference_args = $post_args;
1955 $post_reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0';
1956 $post_reference = $build( $resource_url, wp_remote_post( $resource_url, $post_reference_args ) );
1957
1958 $is_json = function ( $probe ) {
1959 return $probe['content_type'] && strpos( $probe['content_type'], 'json' ) !== false;
1960 };
1961 // The firewall reveals itself by treating the python UA differently from
1962 // the neutral one: different status, or a block page instead of the JSON
1963 // that WordPress returns to both unauthenticated POSTs.
1964 $post_blocked = ( $post_reference['reachable'] && (
1965 ( !$post_probe['reachable'] )
1966 || ( $post_probe['status'] !== $post_reference['status'] && in_array( $post_probe['status'], [ 403, 406, 418, 429 ], true ) )
1967 || ( $is_json( $post_reference ) && !$is_json( $post_probe ) )
1968 ) );
1969
1970 $verdict = 'unknown';
1971 $message = '';
1972 if ( $probe['reachable'] && $probe['status'] === 403 ) {
1973 $verdict = 'waf_blocks_python_ua';
1974 $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.';
1975 }
1976 else if ( $probe['reachable'] && $probe['status'] === 404 ) {
1977 $verdict = 'wellknown_blocked';
1978 $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.';
1979 }
1980 else if ( !$probe['reachable'] ) {
1981 $verdict = 'unreachable';
1982 $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.';
1983 }
1984 else if ( $probe['status'] === 200 && $post_blocked ) {
1985 $verdict = 'waf_blocks_python_post';
1986 $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.';
1987 }
1988 else if ( $probe['status'] === 200 ) {
1989 $verdict = 'ok';
1990 $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.';
1991 }
1992 else {
1993 $verdict = 'unexpected_status';
1994 $message = 'Got HTTP ' . $probe['status'] . ' from the loopback probe. Expected 200. Investigate the response in your CDN/origin logs.';
1995 }
1996
1997 return $this->create_rest_response( [
1998 'success' => true,
1999 'verdict' => $verdict,
2000 'message' => $message,
2001 'probe' => $probe,
2002 'reference' => $reference,
2003 'post_probe' => $post_probe,
2004 'post_reference' => $post_reference,
2005 ], 200 );
2006 }
2007 catch ( Exception $e ) {
2008 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2009 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2010 }
2011 }
2012
2013 /**
2014 * Top MCP tools by call count over the last N days. Powers the
2015 * "Top Tools" widget in the MCP Logs view of the Insights screen.
2016 * Returns rows shaped as { tool, count, success_count, error_count }.
2017 */
2018 public function rest_mcp_top_tools( $request ) {
2019 try {
2020 $params = $request->get_json_params();
2021 $days = isset( $params['days'] ) ? max( 1, intval( $params['days'] ) ) : 7;
2022 $limit = isset( $params['limit'] ) ? max( 1, min( 50, intval( $params['limit'] ) ) ) : 10;
2023
2024 global $wpdb;
2025 $table = $wpdb->prefix . 'mwai_logs';
2026 $rows = $wpdb->get_results(
2027 $wpdb->prepare(
2028 "SELECT scope AS tool,
2029 COUNT(*) AS count,
2030 SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS success_count,
2031 SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS error_count
2032 FROM $table
2033 WHERE feature = 'mcp_tool'
2034 AND time >= DATE_SUB(NOW(), INTERVAL %d DAY)
2035 GROUP BY scope
2036 ORDER BY count DESC
2037 LIMIT %d",
2038 '%"status":"success"%',
2039 '%"status":"error"%',
2040 $days,
2041 $limit
2042 ),
2043 ARRAY_A
2044 );
2045
2046 foreach ( $rows as &$row ) {
2047 $row['count'] = (int) $row['count'];
2048 $row['success_count'] = (int) $row['success_count'];
2049 $row['error_count'] = (int) $row['error_count'];
2050 }
2051 unset( $row );
2052
2053 return $this->create_rest_response( [ 'success' => true, 'tools' => $rows ?: [] ], 200 );
2054 }
2055 catch ( Exception $e ) {
2056 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2057 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2058 }
2059 }
2060
2061 public function rest_helpers_post_types() {
2062 try {
2063 $postTypes = $this->core->get_post_types();
2064 return $this->create_rest_response( [ 'success' => true, 'postTypes' => $postTypes ], 200 );
2065 }
2066 catch ( Exception $e ) {
2067 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2068 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2069 }
2070 }
2071
2072 public function rest_settings_themes( $request ) {
2073 try {
2074 $method = $request->get_method();
2075 if ( $method === 'GET' ) {
2076 $themes = $this->core->get_themes();
2077 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
2078 }
2079 else if ( $method === 'POST' ) {
2080 $params = $request->get_json_params();
2081 $themes = $params['themes'];
2082 $themes = $this->core->update_themes( $themes );
2083 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
2084 }
2085 }
2086 catch ( Exception $e ) {
2087 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2088 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2089 }
2090 }
2091
2092 public function rest_settings_chatbots( $request ) {
2093 try {
2094 $method = $request->get_method();
2095 if ( $method === 'GET' ) {
2096 $chatbots = $this->core->get_chatbots();
2097 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
2098 }
2099 else if ( $method === 'POST' ) {
2100 $params = $request->get_json_params();
2101 $chatbots = $params['chatbots'];
2102 $chatbots = $this->core->update_chatbots( $chatbots );
2103 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
2104 }
2105 return $this->create_rest_response( [ 'success' => false, 'message' => 'Method not allowed' ], 405 );
2106 }
2107 catch ( Exception $e ) {
2108 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
2109 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
2110 }
2111 }
2112
2113 #region Logs
2114
2115 public function rest_get_logs() {
2116 $logs = Meow_MWAI_Logging::get();
2117 return $this->create_rest_response( [ 'success' => true, 'data' => $logs ], 200 );
2118 }
2119
2120 public function rest_clear_logs() {
2121 Meow_MWAI_Logging::clear();
2122 return $this->create_rest_response( [ 'success' => true ], 200 );
2123 }
2124
2125 #endregion
2126
2127 #region Forms
2128
2129 public function rest_forms_list( $request ) {
2130 try {
2131 $args = [
2132 'post_type' => 'mwai_form',
2133 'posts_per_page' => 100,
2134 'post_status' => 'any',
2135 'orderby' => 'date',
2136 'order' => 'DESC'
2137 ];
2138
2139 $posts = get_posts( $args );
2140 $forms = array_map( function ( $post ) {
2141 return [
2142 'id' => $post->ID,
2143 'title' => $post->post_title,
2144 'status' => $post->post_status
2145 ];
2146 }, $posts );
2147
2148 return $this->create_rest_response( [ 'success' => true, 'forms' => $forms ], 200 );
2149 }
2150 catch ( Exception $e ) {
2151 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2152 }
2153 }
2154
2155 public function rest_forms_get( $request ) {
2156 try {
2157 $id = intval( $request->get_param( 'id' ) );
2158 if ( !$id ) {
2159 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2160 }
2161
2162 $post = get_post( $id );
2163 if ( !$post || $post->post_type !== 'mwai_form' ) {
2164 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2165 }
2166
2167 $form = [
2168 'id' => $post->ID,
2169 'title' => [
2170 'raw' => $post->post_title,
2171 'rendered' => $post->post_title
2172 ],
2173 'content' => [
2174 'raw' => $post->post_content,
2175 'rendered' => $post->post_content
2176 ],
2177 'status' => $post->post_status
2178 ];
2179
2180 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2181 }
2182 catch ( Exception $e ) {
2183 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2184 }
2185 }
2186
2187 public function rest_forms_create( $request ) {
2188 try {
2189 $params = $request->get_json_params();
2190 $title = isset( $params['title'] ) ? $params['title'] : 'Untitled Form';
2191
2192 // wp_insert_post expects slashed data - it calls wp_unslash() internally, which would
2193 // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and corrupt
2194 // the stored blocks.
2195 $post_data = wp_slash( [
2196 'post_title' => $title,
2197 'post_content' => '',
2198 'post_status' => 'draft',
2199 'post_type' => 'mwai_form'
2200 ] );
2201
2202 $post_id = wp_insert_post( $post_data );
2203
2204 if ( is_wp_error( $post_id ) ) {
2205 return $this->create_rest_response( [ 'success' => false, 'message' => $post_id->get_error_message() ], 500 );
2206 }
2207
2208 $post = get_post( $post_id );
2209 $form = [
2210 'id' => $post->ID,
2211 'title' => [
2212 'raw' => $post->post_title,
2213 'rendered' => $post->post_title
2214 ],
2215 'status' => $post->post_status
2216 ];
2217
2218 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2219 }
2220 catch ( Exception $e ) {
2221 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2222 }
2223 }
2224
2225 public function rest_forms_update( $request ) {
2226 try {
2227 $params = $request->get_json_params();
2228 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2229
2230 if ( !$id ) {
2231 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2232 }
2233
2234 $post = get_post( $id );
2235 if ( !$post || $post->post_type !== 'mwai_form' ) {
2236 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2237 }
2238
2239 $post_data = [ 'ID' => $id ];
2240
2241 if ( isset( $params['title'] ) ) {
2242 $post_data['post_title'] = $params['title'];
2243 }
2244
2245 if ( isset( $params['content'] ) ) {
2246 $post_data['post_content'] = $params['content'];
2247 }
2248
2249 if ( isset( $params['status'] ) ) {
2250 $post_data['post_status'] = $params['status'];
2251 }
2252
2253 // wp_update_post expects slashed data - it calls wp_unslash() internally, which would
2254 // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and break
2255 // Gutenberg blocks on reload.
2256 $result = wp_update_post( wp_slash( $post_data ) );
2257
2258 if ( is_wp_error( $result ) ) {
2259 return $this->create_rest_response( [ 'success' => false, 'message' => $result->get_error_message() ], 500 );
2260 }
2261
2262 $post = get_post( $id );
2263 $form = [
2264 'id' => $post->ID,
2265 'title' => [
2266 'raw' => $post->post_title,
2267 'rendered' => $post->post_title
2268 ],
2269 'content' => [
2270 'raw' => $post->post_content,
2271 'rendered' => $post->post_content
2272 ],
2273 'status' => $post->post_status
2274 ];
2275
2276 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2277 }
2278 catch ( Exception $e ) {
2279 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2280 }
2281 }
2282
2283 public function rest_forms_delete( $request ) {
2284 try {
2285 $params = $request->get_json_params();
2286 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2287
2288 if ( !$id ) {
2289 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2290 }
2291
2292 $post = get_post( $id );
2293 if ( !$post || $post->post_type !== 'mwai_form' ) {
2294 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2295 }
2296
2297 $result = wp_delete_post( $id, true );
2298
2299 if ( !$result ) {
2300 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to delete form' ], 500 );
2301 }
2302
2303 return $this->create_rest_response( [ 'success' => true ], 200 );
2304 }
2305 catch ( Exception $e ) {
2306 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2307 }
2308 }
2309
2310 #endregion
2311
2312 #region Video Generation Helpers
2313
2314 public function rest_helpers_create_video( $request ) {
2315 try {
2316 $params = $request->get_json_params();
2317 $prompt = sanitize_text_field( $params['prompt'] );
2318 $model = sanitize_text_field( $params['model'] ?? 'sora-2' );
2319 $size = sanitize_text_field( $params['size'] ?? '720x1280' );
2320 $seconds = absint( $params['seconds'] ?? 4 );
2321 $envId = sanitize_text_field( $params['envId'] ?? '' );
2322
2323 // Check if envId is provided (defaults not supported for videos yet)
2324 if ( empty( $envId ) ) {
2325 throw new Exception( 'Please select a specific environment and model in the Video Generator. Default environments are not yet supported for video generation.' );
2326 }
2327
2328 // Get API key from environment
2329 $env = $this->core->get_ai_env( $envId );
2330 $api_key = $env['apikey'] ?? '';
2331
2332 if ( empty( $api_key ) ) {
2333 throw new Exception( 'OpenAI API key not found.' );
2334 }
2335
2336 // Prepare multipart boundary
2337 $boundary = wp_generate_password( 24, false );
2338 $body = '';
2339
2340 // Add model
2341 $body .= "--{$boundary}\r\n";
2342 $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n";
2343 $body .= "{$model}\r\n";
2344
2345 // Add prompt
2346 $body .= "--{$boundary}\r\n";
2347 $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n";
2348 $body .= "{$prompt}\r\n";
2349
2350 // Add size
2351 $body .= "--{$boundary}\r\n";
2352 $body .= "Content-Disposition: form-data; name=\"size\"\r\n\r\n";
2353 $body .= "{$size}\r\n";
2354
2355 // Add seconds
2356 $body .= "--{$boundary}\r\n";
2357 $body .= "Content-Disposition: form-data; name=\"seconds\"\r\n\r\n";
2358 $body .= "{$seconds}\r\n";
2359
2360 $body .= "--{$boundary}--\r\n";
2361
2362 // Call OpenAI API to create video
2363 $response = wp_remote_post( 'https://api.openai.com/v1/videos', [
2364 'headers' => [
2365 'Authorization' => 'Bearer ' . $api_key,
2366 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
2367 ],
2368 'body' => $body,
2369 'timeout' => 30
2370 ] );
2371
2372 if ( is_wp_error( $response ) ) {
2373 throw new Exception( $response->get_error_message() );
2374 }
2375
2376 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
2377
2378 if ( isset( $response_body['error'] ) ) {
2379 throw new Exception( $response_body['error']['message'] ?? 'Unknown error' );
2380 }
2381
2382 // Record usage (price is calculated per second)
2383 $usage = $this->core->record_videos_usage( $model, $size, $seconds );
2384
2385 // Log to Query Logs (Statistics)
2386 try {
2387 if ( class_exists( 'MeowPro_MWAI_Stats' ) && class_exists( 'MeowPro_MWAI_Statistics' ) ) {
2388 $statsObject = new MeowPro_MWAI_Stats();
2389 $statsObject->session = $params['session'] ?? null;
2390 $statsObject->scope = 'admin-tools';
2391 $statsObject->feature = 'video-generator';
2392 $statsObject->model = $model;
2393 $statsObject->envId = $envId;
2394 $statsObject->units = $seconds;
2395 $statsObject->type = 'seconds';
2396 $statsObject->price = $usage['price'] ?? 0;
2397 $statsObject->accuracy = $usage['accuracy'] ?? 'full';
2398
2399 $statistics = new MeowPro_MWAI_Statistics();
2400 $statistics->commit_stats( $statsObject );
2401 }
2402 }
2403 catch ( Exception $statsError ) {
2404 // Log the error but don't fail the video creation
2405 error_log( '[AI Engine Video] Failed to log statistics: ' . $statsError->getMessage() );
2406 }
2407
2408 // Store metadata for later retrieval when video completes
2409 if ( isset( $response_body['id'] ) ) {
2410 set_transient( 'mwai_video_metadata_' . $response_body['id'], [
2411 'model' => $model,
2412 'env_id' => $envId,
2413 'created_at' => time()
2414 ], 7 * DAY_IN_SECONDS );
2415 }
2416
2417 return $this->create_rest_response( [ 'success' => true, 'video' => $response_body, 'usage' => $usage ], 200 );
2418 }
2419 catch ( Exception $e ) {
2420 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2421 }
2422 }
2423
2424 public function rest_helpers_video_status( $request ) {
2425 try {
2426 $params = $request->get_json_params();
2427 $video_ids = $params['videoIds'] ?? [];
2428 $envId = sanitize_text_field( $params['envId'] ?? '' );
2429
2430 if ( empty( $video_ids ) ) {
2431 return $this->create_rest_response( [ 'success' => true, 'videos' => [] ], 200 );
2432 }
2433
2434 // Get API key from environment
2435 $env = $this->core->get_ai_env( $envId );
2436 $api_key = $env['apikey'] ?? '';
2437
2438 if ( empty( $api_key ) ) {
2439 throw new Exception( 'OpenAI API key not found.' );
2440 }
2441
2442 $videos = [];
2443 foreach ( $video_ids as $video_id ) {
2444 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id, [
2445 'headers' => [
2446 'Authorization' => 'Bearer ' . $api_key
2447 ],
2448 'timeout' => 15
2449 ] );
2450
2451 if ( !is_wp_error( $response ) ) {
2452 $body = json_decode( wp_remote_retrieve_body( $response ), true );
2453 if ( !isset( $body['error'] ) ) {
2454 // If video is completed and we haven't saved it yet, download and save to media library
2455 if ( $body['status'] === 'completed' && empty( get_transient( 'mwai_video_saved_' . $video_id ) ) ) {
2456 // Retrieve metadata that was stored when video was created
2457 $metadata = get_transient( 'mwai_video_metadata_' . $video_id );
2458 $ai_metadata = [];
2459 if ( $metadata ) {
2460 $ai_metadata = [
2461 'model' => $metadata['model'] ?? null,
2462 'env_id' => $metadata['env_id'] ?? null,
2463 'latency' => isset( $metadata['created_at'] ) ? ( time() - $metadata['created_at'] ) : null
2464 ];
2465 }
2466
2467 $attachment_id = $this->download_and_save_video( $video_id, $api_key, '', '', $ai_metadata );
2468 if ( $attachment_id ) {
2469 $body['attachment_id'] = $attachment_id;
2470 // Build URL from file path for custom post types
2471 $file_path = get_attached_file( $attachment_id );
2472 $upload_dir = wp_upload_dir();
2473 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2474 // Mark as saved so we don't download again
2475 set_transient( 'mwai_video_saved_' . $video_id, $attachment_id, DAY_IN_SECONDS );
2476 // Clean up metadata transient
2477 delete_transient( 'mwai_video_metadata_' . $video_id );
2478 }
2479 }
2480 // Check if we already have this video saved
2481 else if ( $body['status'] === 'completed' ) {
2482 $attachment_id = get_transient( 'mwai_video_saved_' . $video_id );
2483 if ( $attachment_id ) {
2484 $body['attachment_id'] = $attachment_id;
2485 // Build URL from file path for custom post types
2486 $file_path = get_attached_file( $attachment_id );
2487 $upload_dir = wp_upload_dir();
2488 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2489 }
2490 }
2491 $videos[] = $body;
2492 }
2493 else {
2494 // Include error information in the response
2495 error_log( 'AI Engine: Video generation failed for ID ' . $video_id . ': ' . json_encode( $body['error'] ) );
2496 $videos[] = [
2497 'id' => $video_id,
2498 'status' => 'failed',
2499 'error' => $body['error']
2500 ];
2501 }
2502 }
2503 else {
2504 // WP HTTP error
2505 error_log( 'AI Engine: Failed to check video status for ID ' . $video_id . ': ' . $response->get_error_message() );
2506 $videos[] = [
2507 'id' => $video_id,
2508 'status' => 'failed',
2509 'error' => [ 'message' => $response->get_error_message() ]
2510 ];
2511 }
2512 }
2513
2514 return $this->create_rest_response( [ 'success' => true, 'videos' => $videos ], 200 );
2515 }
2516 catch ( Exception $e ) {
2517 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2518 }
2519 }
2520
2521 public function rest_helpers_download_video( $request ) {
2522 try {
2523 $params = $request->get_json_params();
2524 $video_id = sanitize_text_field( $params['videoId'] );
2525 $envId = sanitize_text_field( $params['envId'] ?? '' );
2526
2527 // Get API key from environment
2528 $env = $this->core->get_ai_env( $envId );
2529 $api_key = $env['apikey'] ?? '';
2530
2531 if ( empty( $api_key ) ) {
2532 throw new Exception( 'OpenAI API key not found.' );
2533 }
2534
2535 $temp_file = wp_tempnam( $video_id . '.mp4' );
2536
2537 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2538 'headers' => [
2539 'Authorization' => 'Bearer ' . $api_key
2540 ],
2541 'timeout' => 120,
2542 'stream' => true,
2543 'filename' => $temp_file
2544 ] );
2545
2546 if ( is_wp_error( $response ) ) {
2547 throw new Exception( $response->get_error_message() );
2548 }
2549
2550 $file_data = file_get_contents( $temp_file );
2551 $base64 = base64_encode( $file_data );
2552
2553 unlink( $temp_file );
2554
2555 return $this->create_rest_response( [
2556 'success' => true,
2557 'data' => $base64,
2558 'mimeType' => 'video/mp4'
2559 ], 200 );
2560 }
2561 catch ( Exception $e ) {
2562 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2563 }
2564 }
2565
2566 public function rest_helpers_delete_video( $request ) {
2567 try {
2568 $params = $request->get_json_params();
2569 $video_id = sanitize_text_field( $params['videoId'] );
2570 $envId = sanitize_text_field( $params['envId'] ?? '' );
2571
2572 // Get API key from environment
2573 $env = $this->core->get_ai_env( $envId );
2574 $api_key = $env['apikey'] ?? '';
2575
2576 if ( empty( $api_key ) ) {
2577 throw new Exception( 'OpenAI API key not found.' );
2578 }
2579
2580 $response = wp_remote_request( 'https://api.openai.com/v1/videos/' . $video_id, [
2581 'method' => 'DELETE',
2582 'headers' => [
2583 'Authorization' => 'Bearer ' . $api_key
2584 ],
2585 'timeout' => 15
2586 ] );
2587
2588 if ( is_wp_error( $response ) ) {
2589 throw new Exception( $response->get_error_message() );
2590 }
2591
2592 return $this->create_rest_response( [ 'success' => true ], 200 );
2593 }
2594 catch ( Exception $e ) {
2595 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2596 }
2597 }
2598
2599 private function download_and_save_video( $video_id, $api_key, $title = '', $description = '', $ai_metadata = [] ) {
2600 try {
2601 // Download video content
2602 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2603 'headers' => [
2604 'Authorization' => 'Bearer ' . $api_key
2605 ],
2606 'timeout' => 120
2607 ] );
2608
2609 if ( is_wp_error( $response ) ) {
2610 error_log( 'Error downloading video: ' . $response->get_error_message() );
2611 return false;
2612 }
2613
2614 $video_data = wp_remote_retrieve_body( $response );
2615 if ( empty( $video_data ) ) {
2616 error_log( 'Empty video data received' );
2617 return false;
2618 }
2619
2620 // Generate filename
2621 $filename = $video_id . '.mp4';
2622 $upload_dir = wp_upload_dir();
2623 $file_path = $upload_dir['path'] . '/' . $filename;
2624
2625 // Save to file
2626 file_put_contents( $file_path, $video_data );
2627
2628 // Prepare attachment data - use mwai_video post type (draft video)
2629 $attachment = [
2630 'post_mime_type' => 'video/mp4',
2631 'post_title' => !empty( $title ) ? $title : 'AI Generated Video',
2632 'post_content' => $description,
2633 'post_status' => 'inherit',
2634 'post_type' => 'mwai_video'
2635 ];
2636
2637 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
2638 $attachment_id = wp_insert_post( $attachment );
2639
2640 // Set the attached file manually since we're not using wp_insert_attachment
2641 update_attached_file( $attachment_id, $file_path );
2642
2643 if ( is_wp_error( $attachment_id ) ) {
2644 error_log( 'Error creating attachment: ' . $attachment_id->get_error_message() );
2645 return false;
2646 }
2647
2648 // Generate attachment metadata
2649 require_once ABSPATH . 'wp-admin/includes/image.php';
2650 $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path );
2651 wp_update_attachment_metadata( $attachment_id, $attach_data );
2652
2653 // Store AI-related metadata
2654 if ( !empty( $ai_metadata['model'] ) ) {
2655 update_post_meta( $attachment_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
2656 }
2657 if ( !empty( $ai_metadata['latency'] ) ) {
2658 update_post_meta( $attachment_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
2659 }
2660 if ( !empty( $ai_metadata['env_id'] ) ) {
2661 update_post_meta( $attachment_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
2662 }
2663
2664 // Add to user's draft media
2665 $user_id = get_current_user_id();
2666 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2667 if ( !is_array( $draft_media ) ) {
2668 $draft_media = [];
2669 }
2670 $draft_media[] = [
2671 'attachment_id' => $attachment_id,
2672 'type' => 'video',
2673 'openai_id' => $video_id,
2674 'created_at' => time()
2675 ];
2676 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
2677
2678 return $attachment_id;
2679 }
2680 catch ( Exception $e ) {
2681 error_log( 'Exception in download_and_save_video: ' . $e->getMessage() );
2682 return false;
2683 }
2684 }
2685
2686 public function rest_helpers_save_video_to_library( $request ) {
2687 try {
2688 $params = $request->get_json_params();
2689 $video_id = sanitize_text_field( $params['videoId'] );
2690 $title = sanitize_text_field( $params['title'] );
2691 $description = sanitize_text_field( $params['description'] );
2692 $filename = sanitize_file_name( $params['filename'] );
2693 $envId = sanitize_text_field( $params['envId'] ?? '' );
2694
2695 // Ensure filename has .mp4 extension
2696 if ( !preg_match( '/\.mp4$/i', $filename ) ) {
2697 $filename .= '.mp4';
2698 }
2699
2700 // Get API key from environment
2701 $env = $this->core->get_ai_env( $envId );
2702 $api_key = $env['apikey'] ?? '';
2703
2704 if ( empty( $api_key ) ) {
2705 throw new Exception( 'OpenAI API key not found.' );
2706 }
2707
2708 // Download video content
2709 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2710 'headers' => [
2711 'Authorization' => 'Bearer ' . $api_key
2712 ],
2713 'timeout' => 120
2714 ] );
2715
2716 if ( is_wp_error( $response ) ) {
2717 throw new Exception( $response->get_error_message() );
2718 }
2719
2720 $video_data = wp_remote_retrieve_body( $response );
2721
2722 // Upload to WordPress media library
2723 $upload_dir = wp_upload_dir();
2724 $file_path = $upload_dir['path'] . '/' . $filename;
2725
2726 file_put_contents( $file_path, $video_data );
2727
2728 $attachment = [
2729 'post_mime_type' => 'video/mp4',
2730 'post_title' => $title,
2731 'post_content' => $description,
2732 'post_status' => 'inherit'
2733 ];
2734
2735 $attach_id = wp_insert_attachment( $attachment, $file_path );
2736
2737 require_once( ABSPATH . 'wp-admin/includes/image.php' );
2738 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
2739 wp_update_attachment_metadata( $attach_id, $attach_data );
2740
2741 return $this->create_rest_response( [
2742 'success' => true,
2743 'attachmentId' => $attach_id
2744 ], 200 );
2745 }
2746 catch ( Exception $e ) {
2747 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2748 }
2749 }
2750
2751 public function rest_helpers_delete_video_from_library( $request ) {
2752 try {
2753 $params = $request->get_json_params();
2754 $attachment_id = absint( $params['attachmentId'] );
2755
2756 if ( empty( $attachment_id ) ) {
2757 throw new Exception( 'Attachment ID is required.' );
2758 }
2759
2760 $deleted = wp_delete_attachment( $attachment_id, true );
2761
2762 if ( !$deleted ) {
2763 throw new Exception( 'Failed to delete attachment.' );
2764 }
2765
2766 return $this->create_rest_response( [ 'success' => true ], 200 );
2767 }
2768 catch ( Exception $e ) {
2769 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2770 }
2771 }
2772
2773 public function rest_helpers_list_draft_media( $request ) {
2774 try {
2775 $type = $request->get_param( 'type' ); // 'image', 'video', or null for all
2776 $user_id = get_current_user_id();
2777 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2778
2779 if ( !is_array( $draft_media ) ) {
2780 return $this->create_rest_response( [ 'success' => true, 'media' => [] ], 200 );
2781 }
2782
2783 $media_items = [];
2784 foreach ( $draft_media as $item ) {
2785 // Filter by type if specified
2786 if ( $type && $item['type'] !== $type ) {
2787 continue;
2788 }
2789
2790 $attachment_id = $item['attachment_id'];
2791 $attachment = get_post( $attachment_id );
2792
2793 if ( $attachment ) {
2794 // For custom post types (mwai_image, mwai_video), build URL from file path
2795 $file_path = get_attached_file( $attachment_id );
2796 $upload_dir = wp_upload_dir();
2797 $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2798
2799 $model = get_post_meta( $attachment_id, 'mwai_model', true );
2800 $generation_time = get_post_meta( $attachment_id, 'mwai_latency', true );
2801 $env_id = get_post_meta( $attachment_id, 'mwai_env_id', true );
2802
2803 // Debug logging
2804 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2805 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 ) );
2806 }
2807
2808 $media_items[] = [
2809 'attachment_id' => $attachment_id,
2810 'type' => $item['type'],
2811 'openai_id' => $item['openai_id'] ?? null,
2812 'url' => $url,
2813 'title' => $attachment->post_title,
2814 'description' => $attachment->post_content,
2815 'filename' => basename( $file_path ),
2816 'created_at' => $item['created_at'],
2817 'model' => $model,
2818 'generation_time' => $generation_time,
2819 'env_id' => $env_id
2820 ];
2821 }
2822 }
2823
2824 return $this->create_rest_response( [ 'success' => true, 'media' => $media_items ], 200 );
2825 }
2826 catch ( Exception $e ) {
2827 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2828 }
2829 }
2830
2831 public function rest_helpers_approve_media( $request ) {
2832 try {
2833 $params = $request->get_json_params();
2834 $attachment_id = absint( $params['attachmentId'] );
2835 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2836 $envId = sanitize_text_field( $params['envId'] ?? '' );
2837
2838 if ( empty( $attachment_id ) ) {
2839 throw new Exception( 'Attachment ID is required.' );
2840 }
2841
2842 // Convert from mwai_image/mwai_video to attachment post type
2843 wp_update_post( [
2844 'ID' => $attachment_id,
2845 'post_type' => 'attachment',
2846 'post_status' => 'inherit'
2847 ] );
2848
2849 // Remove from draft media list
2850 $user_id = get_current_user_id();
2851 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2852 if ( is_array( $draft_media ) ) {
2853 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2854 return $item['attachment_id'] !== $attachment_id;
2855 } );
2856 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2857 }
2858
2859 // Delete video from OpenAI if applicable
2860 if ( !empty( $openai_id ) ) {
2861 $env = $this->core->get_ai_env( $envId );
2862 $api_key = $env['apikey'] ?? '';
2863
2864 if ( !empty( $api_key ) ) {
2865 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2866 'method' => 'DELETE',
2867 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2868 'timeout' => 15
2869 ] );
2870 }
2871 }
2872
2873 return $this->create_rest_response( [ 'success' => true ], 200 );
2874 }
2875 catch ( Exception $e ) {
2876 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2877 }
2878 }
2879
2880 public function rest_helpers_reject_media( $request ) {
2881 try {
2882 $params = $request->get_json_params();
2883 $attachment_id = absint( $params['attachmentId'] );
2884 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2885 $envId = sanitize_text_field( $params['envId'] ?? '' );
2886
2887 if ( empty( $attachment_id ) ) {
2888 throw new Exception( 'Attachment ID is required.' );
2889 }
2890
2891 // Convert from mwai_image/mwai_video to attachment post type first
2892 // This ensures wp_delete_attachment properly deletes the physical file
2893 wp_update_post( [
2894 'ID' => $attachment_id,
2895 'post_type' => 'attachment'
2896 ] );
2897
2898 // Delete attachment from WordPress (now that it's a proper attachment, files will be deleted)
2899 wp_delete_attachment( $attachment_id, true );
2900
2901 // Remove from draft media list
2902 $user_id = get_current_user_id();
2903 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2904 if ( is_array( $draft_media ) ) {
2905 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2906 return $item['attachment_id'] !== $attachment_id;
2907 } );
2908 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2909 }
2910
2911 // Delete video from OpenAI if applicable
2912 if ( !empty( $openai_id ) ) {
2913 $env = $this->core->get_ai_env( $envId );
2914 $api_key = $env['apikey'] ?? '';
2915
2916 if ( !empty( $api_key ) ) {
2917 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2918 'method' => 'DELETE',
2919 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2920 'timeout' => 15
2921 ] );
2922 }
2923 }
2924
2925 return $this->create_rest_response( [ 'success' => true ], 200 );
2926 }
2927 catch ( Exception $e ) {
2928 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2929 }
2930 }
2931
2932 #endregion
2933 }
2934