PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.9
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 4 months ago exceptions 1 year ago modules 4 months ago query 5 months ago rest 5 months ago services 7 months ago admin.php 5 months ago api.php 7 months ago core.php 5 months ago discussion.php 1 year ago event.php 1 year ago init.php 8 months ago logging.php 1 year ago reply.php 7 months ago rest.php 5 months ago
rest.php
2683 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 if ( isset( $params['message'] ) ) {
21 $message = $params['message'];
22 }
23 // TODO: Remove after March 2026 - Legacy "prompt" parameter support
24 elseif ( isset( $params['prompt'] ) ) {
25 $message = $params['prompt'];
26 unset( $params['prompt'] );
27 $params['message'] = $message;
28 Meow_MWAI_Logging::deprecated( '"prompt" is deprecated, please use "message" instead.' );
29 }
30 else {
31 $message = '';
32 }
33
34 if ( $sanitize ) {
35 $message = sanitize_text_field( $message );
36 }
37
38 return $message;
39 }
40
41 /**
42 * Helper method to create REST responses with automatic token refresh
43 *
44 * @param array $data The response data
45 * @param int $status HTTP status code
46 * @return WP_REST_Response
47 */
48 protected function create_rest_response( $data, $status = 200 ) {
49 // Always check if we need to provide a new nonce
50 $current_nonce = $this->core->get_nonce( true );
51 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
52
53 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
54 // We'll refresh if the nonce is older than 10 hours to be safe
55 $should_refresh = false;
56
57 if ( $request_nonce ) {
58 // Try to determine the age of the nonce
59 // WordPress uses a tick system where each tick is 12 hours
60 // If we're in the second half of the nonce's life, refresh it
61 $time = time();
62 $nonce_tick = wp_nonce_tick();
63
64 // Verify if the nonce is still valid but getting old
65 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
66 if ( $verify === 2 ) {
67 // Nonce is valid but was generated 12-24 hours ago
68 $should_refresh = true;
69 // Log will be written when token is included in response
70 }
71 }
72
73 // If the nonce has changed or should be refreshed, include the new one
74 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
75 $data['new_token'] = $current_nonce;
76
77 // Log if server debug mode is enabled
78 if ( $this->core->get_option( 'server_debug_mode' ) ) {
79 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
80 }
81 }
82
83 return new WP_REST_Response( $data, $status );
84 }
85
86 public function rest_init() {
87 try {
88 // Session Endpoint
89 register_rest_route( $this->namespace, '/start_session', [
90 'methods' => 'POST',
91 'permission_callback' => '__return_true', // Public endpoint for guest users
92 'callback' => [ $this, 'rest_start_session' ],
93 ] );
94
95 // Settings Endpoints
96 register_rest_route( $this->namespace, '/settings/update', [
97 'methods' => 'POST',
98 'permission_callback' => [ $this->core, 'can_access_settings' ],
99 'callback' => [ $this, 'rest_settings_update' ],
100 ] );
101 register_rest_route( $this->namespace, '/settings/options', [
102 'methods' => 'GET',
103 'permission_callback' => [ $this->core, 'can_access_settings' ],
104 'callback' => [ $this, 'rest_settings_list' ],
105 ] );
106 register_rest_route( $this->namespace, '/settings/reset', [
107 'methods' => 'POST',
108 'permission_callback' => [ $this->core, 'can_access_settings' ],
109 'callback' => [ $this, 'rest_settings_reset' ],
110 ] );
111 register_rest_route( $this->namespace, '/settings/chatbots', [
112 'methods' => ['GET', 'POST'],
113 'permission_callback' => [ $this->core, 'can_access_settings' ],
114 'callback' => [ $this, 'rest_settings_chatbots' ],
115 ] );
116 register_rest_route( $this->namespace, '/settings/themes', [
117 'methods' => ['GET', 'POST'],
118 'permission_callback' => [ $this->core, 'can_access_settings' ],
119 'callback' => [ $this, 'rest_settings_themes' ],
120 ] );
121
122 // System Endpoints
123 register_rest_route( $this->namespace, '/system/logs/list', [
124 'methods' => 'POST',
125 'permission_callback' => [ $this->core, 'can_access_settings' ],
126 'callback' => [ $this, 'rest_system_logs_list' ],
127 ] );
128 register_rest_route( $this->namespace, '/system/logs/delete', [
129 'methods' => 'POST',
130 'permission_callback' => [ $this->core, 'can_access_settings' ],
131 'callback' => [ $this, 'rest_system_logs_delete' ],
132 ] );
133 register_rest_route( $this->namespace, '/system/logs/meta', [
134 'methods' => 'POST',
135 'permission_callback' => [ $this->core, 'can_access_settings' ],
136 'callback' => [ $this, 'rest_system_logs_meta_get' ],
137 ] );
138 register_rest_route( $this->namespace, '/system/logs/activity', [
139 'methods' => 'POST',
140 'permission_callback' => [ $this->core, 'can_access_settings' ],
141 'callback' => [ $this, 'rest_system_logs_activity' ],
142 ] );
143 register_rest_route( $this->namespace, '/system/logs/activity_daily', [
144 'methods' => 'POST',
145 'permission_callback' => [ $this->core, 'can_access_settings' ],
146 'callback' => [ $this, 'rest_system_logs_activity_daily' ],
147 ] );
148 register_rest_route( $this->namespace, '/system/templates', [
149 'methods' => 'POST',
150 'permission_callback' => [ $this->core, 'can_access_features' ],
151 'callback' => [ $this, 'rest_system_templates_save' ],
152 ] );
153 register_rest_route( $this->namespace, '/system/templates', [
154 'methods' => 'GET',
155 'permission_callback' => [ $this->core, 'can_access_features' ],
156 'callback' => [ $this, 'rest_system_templates_get' ],
157 ] );
158
159 // AI Endpoints
160 register_rest_route( $this->namespace, '/ai/models', [
161 'methods' => 'POST',
162 'permission_callback' => [ $this->core, 'can_access_features' ],
163 'callback' => [ $this, 'rest_ai_models' ],
164 ] );
165 register_rest_route( $this->namespace, '/ai/test_connection', [
166 'methods' => 'POST',
167 'permission_callback' => [ $this->core, 'can_access_settings' ],
168 'callback' => [ $this, 'rest_ai_test_connection' ],
169 ] );
170 register_rest_route( $this->namespace, '/ai/completions', [
171 'methods' => 'POST',
172 'permission_callback' => [ $this->core, 'can_access_features' ],
173 'callback' => [ $this, 'rest_ai_completions' ],
174 ] );
175 register_rest_route( $this->namespace, '/ai/images', [
176 'methods' => 'POST',
177 'permission_callback' => [ $this->core, 'can_access_features' ],
178 'callback' => [ $this, 'rest_ai_images' ],
179 ] );
180 register_rest_route( $this->namespace, '/ai/image_edit', [
181 'methods' => 'POST',
182 'permission_callback' => [ $this->core, 'can_access_features' ],
183 'callback' => [ $this, 'rest_ai_image_edit' ],
184 ] );
185 register_rest_route( $this->namespace, '/ai/copilot', [
186 'methods' => 'POST',
187 'permission_callback' => [ $this->core, 'can_access_features' ],
188 'callback' => [ $this, 'rest_ai_copilot' ],
189 ] );
190
191 register_rest_route( $this->namespace, '/ai/magic_wand', [
192 'methods' => 'POST',
193 'callback' => [ $this, 'rest_ai_magic_wand' ],
194 'permission_callback' => [ $this->core, 'can_access_features' ],
195 ] );
196 register_rest_route( $this->namespace, '/ai/moderate', [
197 'methods' => 'POST',
198 'permission_callback' => [ $this->core, 'can_access_settings' ],
199 'callback' => [ $this, 'rest_ai_moderate' ],
200 ] );
201 register_rest_route( $this->namespace, '/ai/transcribe_audio', [
202 'methods' => 'POST',
203 'permission_callback' => [ $this->core, 'can_access_settings' ],
204 'callback' => [ $this, 'rest_ai_transcribe_audio' ],
205 ] );
206 register_rest_route( $this->namespace, '/ai/transcribe_image', [
207 'methods' => 'POST',
208 'permission_callback' => [ $this->core, 'can_access_settings' ],
209 'callback' => [ $this, 'rest_ai_transcribe_image' ],
210 ] );
211 register_rest_route( $this->namespace, '/ai/json', [
212 'methods' => 'POST',
213 'permission_callback' => [ $this->core, 'can_access_settings' ],
214 'callback' => [ $this, 'rest_ai_json' ],
215 ] );
216
217 // MCP Endpoints
218 register_rest_route( $this->namespace, '/mcp/functions', [
219 'methods' => 'GET',
220 'permission_callback' => [ $this->core, 'can_access_settings' ],
221 'callback' => [ $this, 'rest_mcp_functions' ],
222 ] );
223
224 // Helpers Endpoints
225 register_rest_route( $this->namespace, '/helpers/update_post_title', [
226 'methods' => 'POST',
227 'permission_callback' => [ $this->core, 'can_access_features' ],
228 'callback' => [ $this, 'rest_helpers_update_title' ],
229 ] );
230 register_rest_route( $this->namespace, '/helpers/update_post_excerpt', [
231 'methods' => 'POST',
232 'permission_callback' => [ $this->core, 'can_access_features' ],
233 'callback' => [ $this, 'rest_helpers_update_excerpt' ],
234 ] );
235 register_rest_route( $this->namespace, '/helpers/create_post', [
236 'methods' => 'POST',
237 'permission_callback' => [ $this->core, 'can_access_features' ],
238 'callback' => [ $this, 'rest_helpers_create_post' ],
239 ] );
240 register_rest_route( $this->namespace, '/helpers/create_image', [
241 'methods' => 'POST',
242 'permission_callback' => [ $this->core, 'can_access_features' ],
243 'callback' => [ $this, 'rest_helpers_create_images' ],
244 ] );
245 register_rest_route( $this->namespace, '/helpers/generate_image_meta', [
246 'methods' => 'POST',
247 'permission_callback' => [ $this->core, 'can_access_features' ],
248 'callback' => [ $this, 'rest_helpers_generate_image_meta' ],
249 ] );
250 register_rest_route( $this->namespace, '/helpers/update_media_metadata', [
251 'methods' => 'POST',
252 'permission_callback' => [ $this->core, 'can_access_features' ],
253 'callback' => [ $this, 'rest_helpers_update_media_metadata' ],
254 ] );
255 register_rest_route( $this->namespace, '/helpers/create_video', [
256 'methods' => 'POST',
257 'permission_callback' => [ $this->core, 'can_access_features' ],
258 'callback' => [ $this, 'rest_helpers_create_video' ],
259 ] );
260 register_rest_route( $this->namespace, '/helpers/video_status', [
261 'methods' => 'POST',
262 'permission_callback' => [ $this->core, 'can_access_features' ],
263 'callback' => [ $this, 'rest_helpers_video_status' ],
264 ] );
265 register_rest_route( $this->namespace, '/helpers/download_video', [
266 'methods' => 'POST',
267 'permission_callback' => [ $this->core, 'can_access_features' ],
268 'callback' => [ $this, 'rest_helpers_download_video' ],
269 ] );
270 register_rest_route( $this->namespace, '/helpers/delete_video', [
271 'methods' => 'POST',
272 'permission_callback' => [ $this->core, 'can_access_features' ],
273 'callback' => [ $this, 'rest_helpers_delete_video' ],
274 ] );
275 register_rest_route( $this->namespace, '/helpers/save_video_to_library', [
276 'methods' => 'POST',
277 'permission_callback' => [ $this->core, 'can_access_features' ],
278 'callback' => [ $this, 'rest_helpers_save_video_to_library' ],
279 ] );
280 register_rest_route( $this->namespace, '/helpers/delete_video_from_library', [
281 'methods' => 'POST',
282 'permission_callback' => [ $this->core, 'can_access_features' ],
283 'callback' => [ $this, 'rest_helpers_delete_video_from_library' ],
284 ] );
285 register_rest_route( $this->namespace, '/helpers/list_draft_media', [
286 'methods' => 'GET',
287 'permission_callback' => [ $this->core, 'can_access_features' ],
288 'callback' => [ $this, 'rest_helpers_list_draft_media' ],
289 ] );
290 register_rest_route( $this->namespace, '/helpers/approve_media', [
291 'methods' => 'POST',
292 'permission_callback' => [ $this->core, 'can_access_features' ],
293 'callback' => [ $this, 'rest_helpers_approve_media' ],
294 ] );
295 register_rest_route( $this->namespace, '/helpers/reject_media', [
296 'methods' => 'POST',
297 'permission_callback' => [ $this->core, 'can_access_features' ],
298 'callback' => [ $this, 'rest_helpers_reject_media' ],
299 ] );
300 register_rest_route( $this->namespace, '/helpers/count_posts', [
301 'methods' => 'GET',
302 'permission_callback' => [ $this->core, 'can_access_features' ],
303 'callback' => [ $this, 'rest_helpers_count_posts' ],
304 ] );
305 register_rest_route( $this->namespace, '/helpers/posts_ids', [
306 'methods' => 'GET',
307 'permission_callback' => [ $this->core, 'can_access_features' ],
308 'callback' => [ $this, 'rest_helpers_posts_ids' ],
309 ] );
310 register_rest_route( $this->namespace, '/helpers/post_types', [
311 'methods' => 'GET',
312 'permission_callback' => [ $this->core, 'can_access_features' ],
313 'callback' => [ $this, 'rest_helpers_post_types' ],
314 ] );
315 register_rest_route( $this->namespace, '/helpers/post_content', [
316 'methods' => 'GET',
317 'permission_callback' => [ $this->core, 'can_access_features' ],
318 'callback' => [ $this, 'rest_helpers_post_content' ],
319 ] );
320 register_rest_route( $this->namespace, '/helpers/check_posts_content', [
321 'methods' => 'POST',
322 'permission_callback' => [ $this->core, 'can_access_features' ],
323 'callback' => [ $this, 'rest_helpers_check_posts_content' ],
324 ] );
325 register_rest_route( $this->namespace, '/helpers/run_tasks', [
326 'methods' => 'POST',
327 'permission_callback' => [ $this->core, 'can_access_features' ],
328 'callback' => [ $this, 'rest_helpers_run_tasks' ],
329 ] );
330 register_rest_route( $this->namespace, '/helpers/optimize_database', [
331 'methods' => 'POST',
332 'permission_callback' => [ $this->core, 'can_access_settings' ],
333 'callback' => [ $this, 'rest_helpers_optimize_database' ],
334 ] );
335 register_rest_route( $this->namespace, '/helpers/cron_events', [
336 'methods' => 'GET',
337 'permission_callback' => [ $this->core, 'can_access_features' ],
338 'callback' => [ $this, 'rest_helpers_cron_events' ],
339 ] );
340 register_rest_route( $this->namespace, '/helpers/run_cron', [
341 'methods' => 'POST',
342 'permission_callback' => [ $this->core, 'can_access_features' ],
343 'callback' => [ $this, 'rest_helpers_run_cron' ],
344 ] );
345
346 // OpenAI Endpoints
347 register_rest_route( $this->namespace, '/openai/files/list', [
348 'methods' => 'GET',
349 'permission_callback' => [ $this->core, 'can_access_settings' ],
350 'callback' => [ $this, 'rest_openai_files_get' ],
351 ] );
352 register_rest_route( $this->namespace, '/openai/files/upload', [
353 'methods' => 'POST',
354 'permission_callback' => [ $this->core, 'can_access_settings' ],
355 'callback' => [ $this, 'rest_openai_files_upload' ],
356 ] );
357 register_rest_route( $this->namespace, '/openai/files/delete', [
358 'methods' => 'POST',
359 'permission_callback' => [ $this->core, 'can_access_settings' ],
360 'callback' => [ $this, 'rest_openai_files_delete' ],
361 ] );
362 register_rest_route( $this->namespace, '/openai/files/download', [
363 'methods' => 'POST',
364 'permission_callback' => [ $this->core, 'can_access_settings' ],
365 'callback' => [ $this, 'rest_openai_files_download' ],
366 ] );
367 register_rest_route( $this->namespace, '/openai/files/finetune', [
368 'methods' => 'POST',
369 'permission_callback' => [ $this->core, 'can_access_settings' ],
370 'callback' => [ $this, 'rest_openai_files_finetune' ],
371 ] );
372 register_rest_route( $this->namespace, '/openai/finetunes/list_deleted', [
373 'methods' => 'GET',
374 'permission_callback' => [ $this->core, 'can_access_settings' ],
375 'callback' => [ $this, 'rest_openai_deleted_finetunes_get' ],
376 ] );
377
378 // register_rest_route( $this->namespace, '/openai/models', array(
379 // 'methods' => 'GET',
380 // 'permission_callback' => [ $this->core, 'can_access_settings' ],
381 // 'callback' => [ $this, 'rest_openai_models_get' ],
382 // ) );
383
384 register_rest_route( $this->namespace, '/openai/finetunes/list', [
385 'methods' => 'GET',
386 'permission_callback' => [ $this->core, 'can_access_settings' ],
387 'callback' => [ $this, 'rest_openai_finetunes_get' ],
388 ] );
389 register_rest_route( $this->namespace, '/openai/finetunes/delete', [
390 'methods' => 'POST',
391 'permission_callback' => [ $this->core, 'can_access_settings' ],
392 'callback' => [ $this, 'rest_openai_finetunes_delete' ],
393 ] );
394 register_rest_route( $this->namespace, '/openai/finetunes/cancel', [
395 'methods' => 'POST',
396 'permission_callback' => [ $this->core, 'can_access_settings' ],
397 'callback' => [ $this, 'rest_openai_finetunes_cancel' ],
398 ] );
399
400 // Logging Endpoints
401 register_rest_route( $this->namespace, '/get_logs', [
402 'methods' => 'GET',
403 'permission_callback' => [ $this->core, 'can_access_features' ],
404 'callback' => [ $this, 'rest_get_logs' ]
405 ] );
406 register_rest_route( $this->namespace, '/clear_logs', [
407 'methods' => 'GET',
408 'permission_callback' => [ $this->core, 'can_access_features' ],
409 'callback' => [ $this, 'rest_clear_logs' ]
410 ] );
411
412 // Forms Endpoints
413 register_rest_route( $this->namespace, '/forms/list', [
414 'methods' => 'GET',
415 'permission_callback' => [ $this->core, 'can_access_settings' ],
416 'callback' => [ $this, 'rest_forms_list' ]
417 ] );
418 register_rest_route( $this->namespace, '/forms/get', [
419 'methods' => 'GET',
420 'permission_callback' => [ $this->core, 'can_access_settings' ],
421 'callback' => [ $this, 'rest_forms_get' ]
422 ] );
423 register_rest_route( $this->namespace, '/forms/create', [
424 'methods' => 'POST',
425 'permission_callback' => [ $this->core, 'can_access_settings' ],
426 'callback' => [ $this, 'rest_forms_create' ]
427 ] );
428 register_rest_route( $this->namespace, '/forms/update', [
429 'methods' => 'POST',
430 'permission_callback' => [ $this->core, 'can_access_settings' ],
431 'callback' => [ $this, 'rest_forms_update' ]
432 ] );
433 register_rest_route( $this->namespace, '/forms/delete', [
434 'methods' => 'POST',
435 'permission_callback' => [ $this->core, 'can_access_settings' ],
436 'callback' => [ $this, 'rest_forms_delete' ]
437 ] );
438 }
439 catch ( Exception $e ) {
440 Meow_MWAI_Logging::error( 'REST API initialization failed: ' . $e->getMessage() );
441 }
442 }
443
444 public function rest_start_session() {
445 try {
446 $sessionId = $this->core->get_session_id();
447 $restNonce = $this->core->get_nonce( true );
448
449 $response = [
450 'success' => true,
451 'sessionId' => $sessionId,
452 'restNonce' => $restNonce
453 ];
454
455 // If in test mode and we have a new token, it will be added by create_rest_response
456 // But we also want to ensure the restNonce matches the test token if available
457 if ( get_option( 'mwai_token_test_mode' ) ) {
458 $token_data = get_option( 'mwai_test_token_data' );
459 if ( $token_data && isset( $token_data['token'] ) ) {
460 $response['restNonce'] = $token_data['token'];
461 }
462 }
463
464 return $this->create_rest_response( $response, 200 );
465 }
466 catch ( Exception $e ) {
467 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
468 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
469 }
470 }
471
472 public function rest_settings_list() {
473 return $this->create_rest_response( [
474 'success' => true,
475 'options' => $this->core->get_all_options()
476 ], 200 );
477 }
478
479 public function rest_helpers_cron_events( $request ) {
480 try {
481 // Only show AI Engine cron events (those starting with mwai_)
482 $cron_events = [];
483 $crons = _get_cron_array();
484
485 // Get transient data for last run status (we'll store this when crons run)
486 $last_run_data = get_transient( 'mwai_cron_last_run' ) ?: [];
487
488 // Get all scheduled events and filter for AI Engine ones
489 foreach ( $crons as $timestamp => $cron ) {
490 foreach ( $cron as $hook => $details ) {
491 // Only process AI Engine hooks (starting with mwai_)
492 if ( strpos( $hook, 'mwai_' ) !== 0 ) {
493 continue;
494 }
495
496 $schedule_key = array_keys( $details )[0];
497 $schedule_info = $details[$schedule_key];
498
499 // Get schedule display name
500 $schedule = $schedule_info['schedule'];
501 $schedules = wp_get_schedules();
502 $schedule_display = isset( $schedules[$schedule]['display'] ) ?
503 $schedules[$schedule]['display'] : $schedule;
504
505 $event_info = [
506 'hook' => $hook,
507 'name' => $this->get_cron_display_name( $hook ),
508 'description' => $this->get_cron_description( $hook ),
509 'next_run' => $timestamp,
510 'next_run_human' => '',
511 'last_run' => isset( $last_run_data[$hook]['time'] ) ? $last_run_data[$hook]['time'] : null,
512 'last_run_human' => isset( $last_run_data[$hook]['time'] ) ?
513 human_time_diff( $last_run_data[$hook]['time'], time() ) . ' ago' :
514 'Never',
515 'last_status' => isset( $last_run_data[$hook]['status'] ) ? $last_run_data[$hook]['status'] : 'unknown',
516 'schedule' => $schedule_display,
517 'is_running' => false,
518 'is_scheduled' => true
519 ];
520
521 // Calculate next run time properly
522 // If we have a last run time and schedule interval, calculate the actual next run
523 if ( isset( $last_run_data[$hook]['time'] ) && isset( $schedules[$schedule]['interval'] ) ) {
524 $interval = $schedules[$schedule]['interval'];
525 $last_run = $last_run_data[$hook]['time'];
526 $expected_next_run = $last_run + $interval;
527
528 // If the scheduled timestamp is in the past but we ran recently,
529 // the next run should be based on the last actual run
530 if ( $timestamp < time() &&
531 $last_run > ( time() - $interval ) ) {
532 // Cron ran recently, calculate next run from last run time
533 $event_info['next_run'] = $expected_next_run;
534 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $expected_next_run );
535 }
536 else if ( $timestamp < time() ) {
537 // Genuinely overdue
538 $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp );
539 }
540 else {
541 // Future scheduled time
542 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp );
543 }
544 }
545 else {
546 // No last run data, use the scheduled timestamp but be conservative about "overdue"
547 if ( $timestamp < time() ) {
548 // Only show as overdue if it's significantly past due (more than the schedule interval)
549 // to avoid false positives for crons that might be running but not tracked
550 $time_past_due = time() - $timestamp;
551 $interval = isset( $schedules[$schedule]['interval'] ) ? $schedules[$schedule]['interval'] : 3600; // Default 1 hour
552
553 if ( $time_past_due > $interval ) {
554 $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp );
555 }
556 else {
557 $event_info['next_run_human'] = 'Due to run';
558 }
559 }
560 else {
561 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp );
562 }
563 }
564
565 // Check if currently running (via transient)
566 $running_transient = get_transient( 'mwai_cron_running_' . $hook );
567 if ( $running_transient ) {
568 $event_info['is_running'] = true;
569 }
570
571 $cron_events[] = $event_info;
572 }
573 }
574
575 return $this->create_rest_response( [ 'success' => true, 'events' => $cron_events ], 200 );
576 }
577 catch ( Exception $e ) {
578 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
579 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
580 }
581 }
582
583 public function rest_helpers_run_cron( $request ) {
584 try {
585 $params = $request->get_json_params();
586 $hook = isset( $params['hook'] ) ? $params['hook'] : null;
587
588 if ( empty( $hook ) ) {
589 return $this->create_rest_response( [ 'success' => false, 'message' => 'No cron hook provided' ], 400 );
590 }
591
592 // Only allow running AI Engine crons (starting with mwai_)
593 if ( strpos( $hook, 'mwai_' ) !== 0 ) {
594 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid cron hook' ], 400 );
595 }
596
597 // Prevent running the Tasks Runner hooks directly - they should only run via cron
598 if ( $hook === 'mwai_tasks_internal_run' || $hook === 'mwai_tasks_internal_dev_run' ) {
599 return $this->create_rest_response( [
600 'success' => false,
601 'message' => 'The Tasks Runner cannot be triggered manually. It runs automatically based on its schedule.'
602 ], 403 );
603 }
604
605 // Check if the hook exists
606 if ( !has_action( $hook ) ) {
607 return $this->create_rest_response( [ 'success' => false, 'message' => 'Cron hook not found' ], 404 );
608 }
609
610 // Run the cron action
611 do_action( $hook );
612
613 return $this->create_rest_response( [
614 'success' => true,
615 'message' => 'Cron executed successfully',
616 'hook' => $hook
617 ], 200 );
618 }
619 catch ( Exception $e ) {
620 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
621 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
622 }
623 }
624
625 private function get_cron_display_name( $hook ) {
626 $names = [
627 'mwai_tasks_internal_run' => 'Tasks Runner',
628 'mwai_tasks_internal_dev_run' => 'Tasks Runner (Dev)',
629 'mwai_cleanup_oauth' => 'OAuth Cleanup',
630 'mwai_files_cleanup' => 'Files Cleanup',
631 'mwai_discussions' => 'Discussions Cleanup'
632 ];
633 return isset( $names[$hook] ) ? $names[$hook] : $hook;
634 }
635
636 private function get_cron_description( $hook ) {
637 $descriptions = [
638 'mwai_tasks_internal_run' => 'Processes background tasks and queued operations.',
639 'mwai_tasks_internal_dev_run' => 'Processes tasks in development mode (every 5 seconds).',
640 'mwai_cleanup_oauth' => 'Cleans up expired OAuth tokens and sessions.',
641 'mwai_files_cleanup' => 'Removes expired files based on expiration dates.',
642 'mwai_discussions' => 'Maintains chat discussions database and removes old entries.'
643 ];
644 return isset( $descriptions[$hook] ) ? $descriptions[$hook] : '';
645 }
646
647 public function rest_settings_update( $request ) {
648 try {
649 $params = $request->get_json_params();
650 $value = $params['options'];
651 $options = $this->core->update_options( $value );
652 $success = !!$options;
653 $message = __( $success ? 'OK' : 'Could not update options.', 'ai-engine' );
654 return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
655 }
656 catch ( Exception $e ) {
657 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
658 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
659 }
660 }
661
662 public function rest_settings_reset() {
663 try {
664 $options = $this->core->reset_options();
665 $success = !!$options;
666 $message = __( $success ? 'OK' : 'Could not reset options.', 'ai-engine' );
667 return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
668 }
669 catch ( Exception $e ) {
670 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
671 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
672 }
673 }
674
675 public function rest_ai_models( $request ) {
676 try {
677 $params = $request->get_json_params();
678 $envId = $params['envId'];
679 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId );
680 $models = $engine->retrieve_models();
681 return $this->create_rest_response( [ 'success' => true, 'models' => $models ], 200 );
682 }
683 catch ( Exception $e ) {
684 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
685 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
686 }
687 }
688
689 public function rest_ai_test_connection( $request ) {
690 try {
691 $params = $request->get_json_params();
692 $envId = $params['env_id'];
693
694 // Get the environment details
695 $env = null;
696 $envs = $this->core->get_option( 'ai_envs' );
697 foreach ( $envs as $e ) {
698 if ( $e['id'] === $envId ) {
699 $env = $e;
700 break;
701 }
702 }
703
704 if ( !$env ) {
705 throw new Exception( __( 'Environment not found.', 'ai-engine' ) );
706 }
707
708 // Get the engine and test connection
709 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId );
710 $result = $engine->connection_check();
711
712 // Format the response based on provider
713 $response = [
714 'success' => true,
715 'provider' => $env['type'],
716 'name' => $env['name'],
717 'data' => $result
718 ];
719
720 return $this->create_rest_response( $response, 200 );
721 }
722 catch ( Exception $e ) {
723 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
724 return $this->create_rest_response( [
725 'success' => false,
726 'error' => $message,
727 'provider' => isset( $env ) ? $env['type'] : 'unknown'
728 ], 200 ); // Return 200 even on error for consistent modal display
729 }
730 }
731
732 public function rest_ai_completions( $request ) {
733 try {
734 $params = $request->get_json_params();
735 $message = $this->retrieve_message( $params );
736 $query = new Meow_MWAI_Query_Text( $message );
737 $query->inject_params( $params );
738
739 // Handle streaming
740 $stream = $params['stream'] ?? false;
741 $streamCallback = null;
742 if ( $stream ) {
743 $streamCallback = function ( $reply ) use ( $query ) {
744 //$raw = _wp_specialchars( $reply, ENT_NOQUOTES, 'UTF-8', true );
745 $raw = $reply;
746 $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ], $query );
747 if ( ob_get_level() > 0 ) {
748 ob_flush();
749 }
750 flush();
751 };
752 if ( headers_sent( $filename, $linenum ) ) {
753 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
754 }
755 header( 'Cache-Control: no-cache' );
756 header( 'Content-Type: text/event-stream' );
757 header( 'X-Accel-Buffering: no' ); // This is useful to disable buffering in nginx through headers.
758 ob_implicit_flush( true );
759 if ( ob_get_level() > 0 ) {
760 ob_end_flush();
761 }
762 }
763
764 // Process Reply
765 $reply = $this->core->run_query( $query, $streamCallback );
766 $restRes = [
767 'success' => true,
768 'data' => $reply->result,
769 'usage' => $reply->usage
770 ];
771 if ( $stream ) {
772 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $restRes ) ], $query );
773 die();
774 }
775 return $this->create_rest_response( $restRes, 200 );
776 }
777 catch ( Exception $e ) {
778 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
779 if ( $stream ) {
780 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
781 }
782 else {
783 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
784 }
785 }
786 }
787
788 public function rest_ai_images( $request ) {
789 try {
790 $params = $request->get_json_params();
791 $message = $this->retrieve_message( $params );
792 $query = new Meow_MWAI_Query_Image( $message );
793 $query->inject_params( $params );
794 $reply = $this->core->run_query( $query );
795 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
796 }
797 catch ( Exception $e ) {
798 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
799 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
800 }
801 }
802
803 public function rest_ai_image_edit( $request ) {
804 try {
805 // Check if this is a multipart request with files
806 $files = $request->get_file_params();
807 $params = null;
808
809 // Debug logging
810 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
811 error_log( '[AI Engine Queries] Image Edit Request - Method: ' . $request->get_method() );
812 $content_type = $request->get_content_type();
813 if ( is_array( $content_type ) ) {
814 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type['value'] );
815 }
816 else {
817 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type );
818 }
819 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes (' . count( $files ) . ')' : 'no' ) );
820 }
821
822 if ( !empty( $files ) ) {
823 // Handle multipart form data - get all params including POST data
824 $params = $request->get_params();
825 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
826 error_log( '[AI Engine Queries] Image Edit Request - Using form data params' );
827 }
828 }
829 else {
830 // Try to get body params first (for form data without files)
831 $body_params = $request->get_body_params();
832 if ( !empty( $body_params ) ) {
833 $params = $body_params;
834 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
835 error_log( '[AI Engine Queries] Image Edit Request - Using body params' );
836 }
837 }
838 else {
839 // Handle JSON request
840 $params = $request->get_json_params();
841 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
842 error_log( '[AI Engine Queries] Image Edit Request - Using JSON params' );
843 }
844 }
845 }
846
847 // Ensure params is always an array
848 if ( empty( $params ) ) {
849 $params = [];
850 }
851
852 // Debug logging
853 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
854 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes' : 'no' ) );
855 error_log( '[AI Engine Queries] Image Edit Request - Params: ' . json_encode( $params ) );
856 }
857
858 $message = $this->retrieve_message( $params );
859 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
860 $query = new Meow_MWAI_Query_EditImage( $message );
861
862 // The inject_params method will handle setting the file from mediaId
863 $query->inject_params( $params );
864
865 // Handle mask file if provided
866 if ( !empty( $files['mask'] ) ) {
867 $mask_file = $files['mask'];
868 if ( $mask_file['error'] === UPLOAD_ERR_OK ) {
869 $mask_data = file_get_contents( $mask_file['tmp_name'] );
870 $query->set_mask( Meow_MWAI_Query_DroppedFile::from_data( $mask_data, 'vision', $mask_file['type'] ) );
871 }
872 }
873
874 $reply = $this->core->run_query( $query );
875 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
876 }
877 catch ( Exception $e ) {
878 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
879 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
880 }
881 }
882
883 public function rest_ai_magic_wand( $request ) {
884 try {
885 $params = $request->get_json_params();
886 $action = isset( $params['action'] ) ? $params['action'] : null;
887 $data = isset( $params['data'] ) ? $params['data'] : null;
888 if ( empty( $data ) || empty( $action ) ) {
889 return $this->create_rest_response( [ 'success' => false, 'message' => 'An action and some data are required.' ], 500 );
890 }
891 $data = apply_filters( 'mwai_magic_wand_' . $action, '', $data );
892 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
893 }
894 catch ( Exception $e ) {
895 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
896 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
897 }
898 }
899
900 public function rest_ai_copilot( $request ) {
901 try {
902 $params = $request->get_json_params();
903 $action = sanitize_text_field( $params['action'] );
904 $message = $this->retrieve_message( $params, true );
905 $context = sanitize_text_field( $params['context'] );
906 $postId = !empty( $params['postId'] ) ? intval( $params['postId'] ) : null;
907 if ( empty( $action ) || empty( $message ) ) {
908 return $this->create_rest_response( [ 'success' => false, 'message' => 'Copilot needs an action and a prompt.' ], 500 );
909 }
910
911 global $mwai;
912 $result = null;
913 $params = [ 'scope' => 'copilot' ];
914
915 if ( $action === 'text' ) {
916 $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.";
917 if ( !empty( $model ) ) {
918 $params['model'] = $model;
919 }
920 $result = $mwai->simpleTextQuery( $prompt, $params );
921 }
922 else if ( $action === 'image' ) {
923 $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.";
924
925 // Create the image
926 $simplifiedPrompt = $mwai->simpleTextQuery( $prompt, $params );
927 $media = $mwai->imageQueryForMediaLibrary( $simplifiedPrompt, $params, $postId );
928 $result = [ 'media' => $media ];
929 }
930 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
931 }
932 catch ( Exception $e ) {
933 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
934 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
935 }
936 }
937
938 public function rest_helpers_update_title( $request ) {
939 try {
940 $params = $request->get_json_params();
941 $title = sanitize_text_field( $params['title'] );
942 $postId = intval( $params['postId'] );
943 $post = get_post( $postId );
944 if ( !$post ) {
945 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
946 }
947 $post->post_title = $title;
948 //$post->post_name = sanitize_title( $title );
949 wp_update_post( $post );
950 return $this->create_rest_response( [ 'success' => true, 'message' => 'Title updated.' ], 200 );
951 }
952 catch ( Exception $e ) {
953 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
954 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
955 }
956 }
957
958 public function rest_helpers_update_excerpt( $request ) {
959 try {
960 $params = $request->get_json_params();
961 $excerpt = sanitize_text_field( $params['excerpt'] );
962 $postId = intval( $params['postId'] );
963 $post = get_post( $postId );
964 if ( !$post ) {
965 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
966 }
967 $post->post_excerpt = $excerpt;
968 wp_update_post( $post );
969 return $this->create_rest_response( [ 'success' => true, 'message' => 'Excerpt updated.' ], 200 );
970 }
971 catch ( Exception $e ) {
972 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
973 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
974 }
975 }
976
977 public function rest_helpers_create_post( $request ) {
978 try {
979 $params = $request->get_json_params();
980 $title = sanitize_text_field( $params['title'] );
981 $content = sanitize_textarea_field( $params['content'] );
982 $excerpt = sanitize_text_field( $params['excerpt'] );
983 $postType = sanitize_text_field( $params['postType'] );
984 $post = new stdClass();
985 $post->post_title = $title;
986 $post->post_excerpt = $excerpt;
987 $post->post_content = $content;
988 $post->post_status = 'draft';
989 $post->post_type = isset( $postType ) ? $postType : 'post';
990 // TODO: Let's try to avoid using Markdown to create the Post
991 // Instead, we should create Gutenberg Blocks, or simple HTML.
992 // Then, we can get rid of the library for Markdown.
993 $post->post_content = $this->core->markdown_to_html( $post->post_content );
994 $postId = wp_insert_post( $post );
995 return $this->create_rest_response( [ 'success' => true, 'postId' => $postId ], 200 );
996 }
997 catch ( Exception $e ) {
998 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
999 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1000 }
1001 }
1002
1003 public function rest_helpers_create_images( $request ) {
1004 try {
1005 $params = $request->get_json_params();
1006 $title = sanitize_text_field( $params['title'] );
1007 $caption = sanitize_text_field( $params['caption'] );
1008 $alt = sanitize_text_field( $params['alt'] );
1009 $description = sanitize_text_field( $params['description'] );
1010 $url = $params['url'];
1011 $filename = sanitize_text_field( $params['filename'] );
1012
1013 // Prepare AI metadata
1014 $ai_metadata = [];
1015 if ( !empty( $params['model'] ) ) {
1016 $ai_metadata['model'] = $params['model'];
1017 }
1018 if ( !empty( $params['latency'] ) ) {
1019 $ai_metadata['latency'] = $params['latency'];
1020 }
1021 if ( !empty( $params['env_id'] ) ) {
1022 $ai_metadata['env_id'] = $params['env_id'];
1023 }
1024
1025 // Debug logging
1026 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1027 error_log( '[AI Engine] create_image metadata: ' . json_encode( $ai_metadata ) );
1028 }
1029
1030 // Create as mwai_image post type (draft image)
1031 $attachmentId = $this->core->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, null, 'inherit', 'mwai_image', $ai_metadata );
1032
1033 // Add to user's draft media
1034 $user_id = get_current_user_id();
1035 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
1036 if ( !is_array( $draft_media ) ) {
1037 $draft_media = [];
1038 }
1039 $draft_media[] = [
1040 'attachment_id' => $attachmentId,
1041 'type' => 'image',
1042 'created_at' => time()
1043 ];
1044 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
1045
1046 return $this->create_rest_response( [ 'success' => true, 'attachmentId' => $attachmentId ], 200 );
1047 }
1048 catch ( Exception $e ) {
1049 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1050 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1051 }
1052 }
1053
1054 public function rest_helpers_generate_image_meta( $request ) {
1055 try {
1056 global $mwai;
1057 $params = $request->get_json_params();
1058 $attachment_id = isset( $params['attachmentId'] ) ? intval( $params['attachmentId'] ) : null;
1059
1060 if ( empty( $attachment_id ) ) {
1061 throw new Exception( __( 'The attachment ID is required.', 'ai-engine' ) );
1062 }
1063
1064 // Get the file path from the attachment ID
1065 $file_path = get_attached_file( $attachment_id );
1066 if ( empty( $file_path ) || !file_exists( $file_path ) ) {
1067 throw new Exception( __( 'Could not find the attachment file.', 'ai-engine' ) );
1068 }
1069
1070 $prompt = 'Describe this image and suggest a short title and description. '
1071 . 'Also suggest an SEO-friendly filename (lowercase, ASCII characters only, with hyphens instead of spaces). '
1072 . 'Return a JSON with the keys: title, description, filename.';
1073
1074 // Use file path instead of URL to avoid network issues
1075 $result = $mwai->simpleVisionQuery( $prompt, null, $file_path, [ 'scope' => 'admin-tools' ] );
1076 $result = preg_replace( '/^```json\s*/', '', $result );
1077 $result = preg_replace( '/\s*```$/', '', $result );
1078 if ( is_string( $result ) ) {
1079 $data = json_decode( $result, true );
1080 }
1081 else {
1082 $data = $result;
1083 }
1084 if ( !is_array( $data ) ) {
1085 $data = [];
1086 }
1087 $data = array_merge( [ 'title' => '', 'description' => '', 'filename' => '' ], $data );
1088 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1089 }
1090 catch ( Exception $e ) {
1091 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1092 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1093 }
1094 }
1095
1096 public function rest_helpers_update_media_metadata( $request ) {
1097 try {
1098 $params = $request->get_json_params();
1099 $attachment_id = intval( $params['attachmentId'] );
1100 $title = sanitize_text_field( $params['title'] ?? '' );
1101 $description = sanitize_text_field( $params['description'] ?? '' );
1102 $caption = sanitize_text_field( $params['caption'] ?? '' );
1103 $alt = sanitize_text_field( $params['alt'] ?? '' );
1104 $filename = sanitize_file_name( $params['filename'] ?? '' );
1105
1106 if ( !$attachment_id ) {
1107 throw new Exception( __( 'Attachment ID is required.', 'ai-engine' ) );
1108 }
1109
1110 // Generate slug from filename (without extension)
1111 $slug = '';
1112 if ( !empty( $filename ) ) {
1113 $slug = pathinfo( $filename, PATHINFO_FILENAME );
1114 }
1115
1116 // Update post title, content (description), caption, and slug
1117 $update_data = [
1118 'ID' => $attachment_id,
1119 'post_title' => $title,
1120 'post_content' => $description,
1121 'post_excerpt' => $caption
1122 ];
1123
1124 if ( !empty( $slug ) ) {
1125 $update_data['post_name'] = $slug;
1126 }
1127
1128 wp_update_post( $update_data );
1129
1130 // Update alt text
1131 if ( !empty( $alt ) ) {
1132 update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
1133 }
1134
1135 // Update filename if provided
1136 $new_url = null;
1137 if ( !empty( $filename ) ) {
1138 $file_path = get_attached_file( $attachment_id );
1139 if ( $file_path ) {
1140 // Security: Validate file extension to prevent arbitrary file upload attacks
1141 $original_ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
1142 $new_ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
1143
1144 // Allowlist of safe media extensions (no executable types)
1145 $allowed_extensions = [
1146 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif',
1147 'mp4', 'webm', 'ogg', 'mov', 'avi', 'wmv', 'flv', 'm4v',
1148 'mp3', 'wav', 'flac', 'aac', 'm4a', 'wma',
1149 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'rtf'
1150 ];
1151
1152 // Extension must be in allowlist AND match original extension
1153 if ( !in_array( $new_ext, $allowed_extensions, true ) ) {
1154 throw new Exception( __( 'Invalid file extension. Only media file extensions are allowed.', 'ai-engine' ) );
1155 }
1156 if ( $new_ext !== $original_ext ) {
1157 throw new Exception( __( 'File extension must match the original file type.', 'ai-engine' ) );
1158 }
1159
1160 $path_parts = pathinfo( $file_path );
1161 $new_file_path = $path_parts['dirname'] . '/' . $filename;
1162 if ( rename( $file_path, $new_file_path ) ) {
1163 update_attached_file( $attachment_id, $new_file_path );
1164 // Build new URL from file path for custom post types
1165 $upload_dir = wp_upload_dir();
1166 $new_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $new_file_path );
1167 }
1168 }
1169 }
1170
1171 $response = [ 'success' => true ];
1172 if ( $new_url ) {
1173 $response['url'] = $new_url;
1174 }
1175
1176 return $this->create_rest_response( $response, 200 );
1177 }
1178 catch ( Exception $e ) {
1179 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1180 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1181 }
1182 }
1183
1184 public function rest_openai_files_get() {
1185 try {
1186 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1187 $purposeFilter = isset( $_GET['purpose'] ) ? $_GET['purpose'] : null;
1188 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1189 $files = $openai->list_files( $purposeFilter );
1190 return $this->create_rest_response( [ 'success' => true, 'files' => $files ], 200 );
1191 }
1192 catch ( Exception $e ) {
1193 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1194 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1195 }
1196 }
1197
1198 public function rest_openai_deleted_finetunes_get() {
1199 try {
1200 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1201 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1202 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1203 $finetunes = $openai->list_deleted_finetunes( $legacy );
1204 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1205 }
1206 catch ( Exception $e ) {
1207 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1208 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1209 }
1210 }
1211
1212 public function rest_openai_finetunes_get() {
1213 try {
1214 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1215 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1216 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1217 $finetunes = $openai->list_finetunes( $legacy );
1218 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1219 }
1220 catch ( Exception $e ) {
1221 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1222 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1223 }
1224 }
1225
1226 public function rest_openai_files_upload( $request ) {
1227 try {
1228 $params = $request->get_json_params();
1229 $envId = $params['envId'];
1230 ;
1231 $filename = sanitize_text_field( $params['filename'] );
1232 $data = $params['data'];
1233 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1234 $file = $openai->upload_file( $filename, $data );
1235 return $this->create_rest_response( [ 'success' => true, 'file' => $file ], 200 );
1236 }
1237 catch ( Exception $e ) {
1238 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1239 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1240 }
1241 }
1242
1243 public function rest_openai_files_delete( $request ) {
1244 try {
1245 $params = $request->get_json_params();
1246 $envId = $params['envId'];
1247 ;
1248 $fileId = $params['fileId'];
1249 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1250 $openai->delete_file( $fileId );
1251 return $this->create_rest_response( [ 'success' => true ], 200 );
1252 }
1253 catch ( Exception $e ) {
1254 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1255 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1256 }
1257 }
1258
1259 public function rest_openai_finetunes_cancel( $request ) {
1260 try {
1261 $params = $request->get_json_params();
1262 $envId = $params['envId'];
1263 ;
1264 $finetuneId = $params['finetuneId'];
1265 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1266 $openai->cancel_finetune( $finetuneId );
1267 return $this->create_rest_response( [ 'success' => true ], 200 );
1268 }
1269 catch ( Exception $e ) {
1270 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1271 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1272 }
1273 }
1274
1275 public function rest_openai_finetunes_delete( $request ) {
1276 try {
1277 $params = $request->get_json_params();
1278 $envId = $params['envId'];
1279 ;
1280 $modelId = $params['modelId'];
1281 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1282 $openai->delete_finetune( $modelId );
1283 return $this->create_rest_response( [ 'success' => true ], 200 );
1284 }
1285 catch ( Exception $e ) {
1286 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1287 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1288 }
1289 }
1290
1291 public function rest_openai_files_download( $request ) {
1292 try {
1293 $params = $request->get_json_params();
1294 $envId = $params['envId'];
1295 ;
1296 $fileId = $params['fileId'];
1297 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1298 $filename = $openai->download_file( $fileId );
1299 $data = file_get_contents( $filename );
1300 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1301 }
1302 catch ( Exception $e ) {
1303 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1304 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1305 }
1306 }
1307
1308 public function rest_openai_files_finetune( $request ) {
1309 try {
1310 $params = $request->get_json_params();
1311 $envId = $params['envId'];
1312 ;
1313 $fileId = $params['fileId'];
1314 $model = $params['model'];
1315 $suffix = $params['suffix'];
1316 $hyperparams = [
1317 'nEpochs' => isset( $params['nEpochs'] ) ? $params['nEpochs'] : null,
1318 'batchSize' => isset( $params['batchSize'] ) ? $params['batchSize'] : null,
1319 ];
1320 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1321 $finetune = $openai->run_finetune( $fileId, $model, $suffix, $hyperparams );
1322 return $this->create_rest_response( [ 'success' => true, 'finetune' => $finetune ], 200 );
1323 }
1324 catch ( Exception $e ) {
1325 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1326 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1327 }
1328 }
1329
1330 public function rest_helpers_count_posts( $request ) {
1331 try {
1332 $params = $request->get_query_params();
1333 $postType = $params['postType'];
1334 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1335 $count = wp_count_posts( $postType );
1336 $count = array_sum( array_intersect_key( (array) $count, array_flip( $postStatus ) ) );
1337 return $this->create_rest_response( [ 'success' => true, 'count' => $count ], 200 );
1338 }
1339 catch ( Exception $e ) {
1340 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1341 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1342 }
1343 }
1344
1345 public function rest_helpers_posts_ids( $request ) {
1346 try {
1347 global $wpdb;
1348 $params = $request->get_query_params();
1349 $postType = $params['postType'];
1350 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1351
1352 // Use direct SQL query instead of get_posts to avoid memory issues with large sites
1353 $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) );
1354 $query = "SELECT ID FROM {$wpdb->posts}
1355 WHERE post_type = %s
1356 AND post_status IN ($statusPlaceholders)
1357 ORDER BY ID ASC";
1358
1359 $prepareArgs = array_merge( [ $postType ], $postStatus );
1360 $postIds = $wpdb->get_col( $wpdb->prepare( $query, ...$prepareArgs ) );
1361 $postIds = array_map( 'intval', $postIds );
1362
1363 return $this->create_rest_response( [ 'success' => true, 'postIds' => $postIds ], 200 );
1364 }
1365 catch ( Exception $e ) {
1366 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1367 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1368 }
1369 }
1370
1371 public function rest_helpers_post_content( $request ) {
1372 try {
1373 $params = $request->get_query_params();
1374 $offset = (int) $params['offset'];
1375 $postType = $params['postType'];
1376 $postStatus = isset( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1377 $postId = (int) $params['postId'];
1378
1379 $post = null;
1380 if ( !empty( $postId ) ) {
1381 $post = get_post( $postId );
1382 if ( $post->post_status !== 'publish' && $post->post_status !== 'future'
1383 && $post->post_status !== 'draft' && $post->post_status !== 'private' ) {
1384 $post = null;
1385 }
1386 }
1387 else {
1388 $posts = get_posts( [
1389 'posts_per_page' => 1,
1390 'post_type' => $postType,
1391 'offset' => $offset,
1392 'post_status' => $postStatus,
1393 ] );
1394 $post = count( $posts ) === 0 ? null : $posts[0];
1395 }
1396 if ( !$post ) {
1397 return $this->create_rest_response( [ 'success' => false, 'message' => 'Post not found' ], 404 );
1398 }
1399 $cleanPost = $this->core->get_post( $post );
1400 return $this->create_rest_response( [ 'success' => true, 'content' => $cleanPost['content'],
1401 'checksum' => $cleanPost['checksum'], 'language' => $cleanPost['language'], 'excerpt' => $cleanPost['excerpt'],
1402 'postId' => $cleanPost['postId'], 'title' => $cleanPost['title'], 'url' => $cleanPost['url'] ], 200 );
1403 }
1404 catch ( Exception $e ) {
1405 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1406 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1407 }
1408 }
1409
1410 // Batch check which posts have content (for Push All optimization)
1411 public function rest_helpers_check_posts_content( $request ) {
1412 try {
1413 $params = $request->get_json_params();
1414 $postIds = isset( $params['postIds'] ) ? $params['postIds'] : [];
1415
1416 if ( empty( $postIds ) || !is_array( $postIds ) ) {
1417 return $this->create_rest_response( [
1418 'success' => false,
1419 'message' => 'postIds array is required'
1420 ], 400 );
1421 }
1422
1423 // Sanitize post IDs
1424 $postIds = array_map( 'intval', $postIds );
1425
1426 // Check content using the mwai_pre_post_content filter to support page builders,
1427 // ACF, and other plugins that store content outside of post_content
1428 $postsWithContent = [];
1429
1430 foreach ( $postIds as $postId ) {
1431 $post = get_post( $postId );
1432 if ( !$post ) {
1433 continue;
1434 }
1435 // Apply the same filter used by get_post_content() in core.php
1436 $content = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
1437 $content = trim( strip_tags( $content ) );
1438 if ( !empty( $content ) ) {
1439 $postsWithContent[] = $postId;
1440 }
1441 }
1442
1443 return $this->create_rest_response( [
1444 'success' => true,
1445 'postsWithContent' => $postsWithContent
1446 ], 200 );
1447 }
1448 catch ( Exception $e ) {
1449 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1450 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1451 }
1452 }
1453
1454 public function rest_helpers_run_tasks( $request ) {
1455 try {
1456 // Prevent concurrent execution with a transient lock
1457 $lock_key = 'mwai_rest_run_tasks_lock';
1458 if ( get_transient( $lock_key ) ) {
1459 // Log excessive calls for debugging
1460 if ( $this->core->get_option( 'dev_mode' ) ) {
1461 error_log( '[AI Engine] WARNING: rest_helpers_run_tasks called while already running' );
1462 }
1463 return $this->create_rest_response( [
1464 'success' => false,
1465 'message' => 'Tasks are already running. Please wait.'
1466 ], 429 ); // 429 Too Many Requests
1467 }
1468
1469 // Set lock for 30 seconds
1470 set_transient( $lock_key, true, 30 );
1471
1472 // Log task execution start
1473 if ( $this->core->get_option( 'dev_mode' ) ) {
1474 error_log( '[AI Engine] rest_helpers_run_tasks triggered via REST API' );
1475 }
1476
1477 try {
1478 do_action( 'mwai_tasks_run' );
1479 delete_transient( $lock_key );
1480 return $this->create_rest_response( [ 'success' => true ], 200 );
1481 }
1482 catch ( Exception $e ) {
1483 delete_transient( $lock_key );
1484 throw $e;
1485 }
1486 }
1487 catch ( Exception $e ) {
1488 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1489 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1490 }
1491 }
1492
1493 public function rest_helpers_optimize_database( $request ) {
1494 try {
1495 global $wpdb;
1496 $results = [];
1497
1498 // Add indexes to optimize query performance
1499 $indexes = [
1500 // mwai_logs indexes
1501 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time', 'columns' => 'time' ],
1502 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_userId', 'columns' => 'userId' ],
1503 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_envId', 'columns' => 'envId' ],
1504 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_refId', 'columns' => 'refId' ],
1505 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time_model', 'columns' => 'time, model' ],
1506
1507 // mwai_logmeta indexes
1508 [ 'table' => 'mwai_logmeta', 'name' => 'idx_mwai_logmeta_log_id', 'columns' => 'log_id' ],
1509
1510 // mwai_vectors indexes
1511 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_envId_status_dbId', 'columns' => 'envId, status, dbId' ],
1512 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_refId', 'columns' => 'refId' ],
1513 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_status', 'columns' => 'status' ],
1514 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_updated', 'columns' => 'updated' ],
1515
1516 // mwai_files indexes
1517 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_expires', 'columns' => 'expires' ],
1518 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_userId', 'columns' => 'userId' ],
1519 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_purpose', 'columns' => 'purpose' ],
1520
1521 // mwai_filemeta indexes
1522 [ 'table' => 'mwai_filemeta', 'name' => 'idx_mwai_filemeta_file_id', 'columns' => 'file_id' ],
1523
1524 // mwai_chats indexes
1525 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_botId', 'columns' => 'chatId, botId' ],
1526 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_userId', 'columns' => 'chatId, userId' ],
1527 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_updated', 'columns' => 'updated' ],
1528 ];
1529
1530 // Add indexes
1531 foreach ( $indexes as $index ) {
1532 $table = $wpdb->prefix . $index['table'];
1533 $index_name = $index['name'];
1534 $columns = $index['columns'];
1535
1536 // Check if index already exists
1537 $existing = $wpdb->get_var( $wpdb->prepare(
1538 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
1539 WHERE table_schema = %s AND table_name = %s AND index_name = %s',
1540 DB_NAME,
1541 $table,
1542 $index_name
1543 ) );
1544
1545 if ( !$existing ) {
1546 $wpdb->query( "ALTER TABLE `$table` ADD INDEX `$index_name` ($columns)" );
1547 $results[] = "Added index $index_name on $table";
1548 }
1549 }
1550
1551 // Clean up old logs (older than 3 months)
1552 $three_months_ago = date( 'Y-m-d H:i:s', strtotime( '-3 months' ) );
1553
1554 // Delete old logs
1555 $deleted_logs = $wpdb->query( $wpdb->prepare(
1556 "DELETE FROM {$wpdb->prefix}mwai_logs WHERE time < %s",
1557 $three_months_ago
1558 ) );
1559 $results[] = "Deleted $deleted_logs old log entries";
1560
1561 // Delete orphaned logmeta
1562 $deleted_logmeta = $wpdb->query(
1563 "DELETE lm FROM {$wpdb->prefix}mwai_logmeta lm
1564 LEFT JOIN {$wpdb->prefix}mwai_logs l ON lm.log_id = l.id
1565 WHERE l.id IS NULL"
1566 );
1567 $results[] = "Deleted $deleted_logmeta orphaned logmeta entries";
1568
1569 // Delete old chats (older than 3 months)
1570 $deleted_chats = $wpdb->query( $wpdb->prepare(
1571 "DELETE FROM {$wpdb->prefix}mwai_chats WHERE updated < %s",
1572 $three_months_ago
1573 ) );
1574 $results[] = "Deleted $deleted_chats old chat discussions";
1575
1576 // Optimize tables
1577 $tables = [ 'mwai_logs', 'mwai_logmeta', 'mwai_vectors', 'mwai_files', 'mwai_filemeta', 'mwai_chats' ];
1578 foreach ( $tables as $table ) {
1579 $wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}$table" );
1580 }
1581 $results[] = 'Optimized all AI Engine tables';
1582
1583 $message = implode( "\n", $results );
1584 return $this->create_rest_response( [ 'success' => true, 'message' => $message ], 200 );
1585 }
1586 catch ( Exception $e ) {
1587 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1588 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1589 }
1590 }
1591
1592 public function rest_system_templates_get( $request ) {
1593 try {
1594 $params = $request->get_query_params();
1595 $category = $params['category'];
1596 $templates = [];
1597 $templates_option = get_option( 'mwai_templates', [] );
1598 if ( !is_array( $templates_option ) ) {
1599 update_option( 'mwai_templates', [] );
1600 }
1601 $categories = array_column( $templates_option, 'category' );
1602 $index = array_search( $category, $categories );
1603 $templates = [];
1604 if ( $index !== false ) {
1605 $templates = $templates_option[$index]['templates'];
1606 }
1607 return $this->create_rest_response( [ 'success' => true, 'templates' => $templates ], 200 );
1608 }
1609 catch ( Exception $e ) {
1610 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1611 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1612 }
1613 }
1614
1615 public function rest_system_templates_save( $request ) {
1616 try {
1617 $params = $request->get_json_params();
1618 $category = $params['category'];
1619 $templates = $params['templates'];
1620 $templates_option = get_option( 'mwai_templates', [] );
1621 $categories = array_column( $templates_option, 'category' );
1622 $index = array_search( $category, $categories );
1623 if ( $index !== false && $index >= 0 ) {
1624 $templates_option[$index]['templates'] = $templates;
1625 }
1626 else {
1627 $group = [ 'category' => $category, 'templates' => $templates ];
1628 $templates_option[] = $group;
1629 }
1630
1631 update_option( 'mwai_templates', $templates_option );
1632 return $this->create_rest_response( [ 'success' => true ], 200 );
1633 }
1634 catch ( Exception $e ) {
1635 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1636 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1637 }
1638 }
1639
1640 public function rest_system_logs_list( $request ) {
1641 try {
1642 $params = $request->get_json_params();
1643 $offset = $params['offset'];
1644 $limit = $params['limit'];
1645 $filters = $params['filters'];
1646 $sort = isset( $params['sort'] ) ? $params['sort'] : null;
1647 $logs = apply_filters( 'mwai_stats_logs_list', [], $offset, $limit, $filters, $sort );
1648 return $this->create_rest_response( [ 'success' => true, 'total' => $logs['total'], 'logs' => $logs['rows'] ], 200 );
1649 }
1650 catch ( Exception $e ) {
1651 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1652 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1653 }
1654 }
1655
1656 public function rest_system_logs_delete( $request ) {
1657 try {
1658 $params = $request->get_json_params();
1659 $logIds = $params['logIds'];
1660 $success = apply_filters( 'mwai_stats_logs_delete', true, $logIds );
1661 return $this->create_rest_response( [ 'success' => $success ], 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_logs_meta_get( $request ) {
1670 try {
1671 $params = $request->get_json_params();
1672 $logId = $params['logId'];
1673 $metaKeys = $params['metaKeys'];
1674 $data = apply_filters( 'mwai_stats_logs_meta', [], $logId, $metaKeys );
1675 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1676 }
1677 catch ( Exception $e ) {
1678 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1679 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1680 }
1681 }
1682
1683 public function rest_system_logs_activity( $request ) {
1684 try {
1685 $params = $request->get_json_params();
1686 $hours = isset( $params['hours'] ) ? intval( $params['hours'] ) : 24;
1687 $data = apply_filters( 'mwai_stats_logs_activity', [], $hours );
1688 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1689 }
1690 catch ( Exception $e ) {
1691 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1692 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1693 }
1694 }
1695
1696 public function rest_system_logs_activity_daily( $request ) {
1697 try {
1698 $params = $request->get_json_params();
1699 $days = isset( $params['days'] ) ? intval( $params['days'] ) : 31;
1700 $byModel = isset( $params['byModel'] ) ? (bool) $params['byModel'] : false;
1701
1702 if ( $byModel ) {
1703 $data = apply_filters( 'mwai_stats_logs_activity_daily_by_model', [], $days );
1704 }
1705 else {
1706 $data = apply_filters( 'mwai_stats_logs_activity_daily', [], $days );
1707 }
1708
1709 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1710 }
1711 catch ( Exception $e ) {
1712 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1713 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1714 }
1715 }
1716
1717 public function rest_ai_moderate( $request ) {
1718 try {
1719 $params = $request->get_json_params();
1720 $envId = $params['envId'];
1721 $text = $params['text'];
1722 if ( !$text ) {
1723 return $this->create_rest_response( [ 'success' => false, 'message' => 'Text not found.' ], 404 );
1724 }
1725 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1726 $results = $openai->moderate( $text );
1727 return $this->create_rest_response( [ 'success' => true, 'results' => $results ], 200 );
1728 }
1729 catch ( Exception $e ) {
1730 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1731 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1732 }
1733 }
1734
1735 public function rest_ai_transcribe_audio( $request ) {
1736 try {
1737 global $mwai;
1738 $params = $request->get_json_params();
1739 $url = !empty( $params['url'] ) ? $params['url'] : null;
1740 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
1741 $path = !empty( $params['path'] ) ? $params['path'] : null;
1742
1743 // If mediaId is provided, get the file path
1744 if ( !$path && $mediaId > 0 ) {
1745 $path = get_attached_file( $mediaId );
1746 if ( empty( $path ) ) {
1747 throw new Exception( __( 'The media file cannot be found.', 'ai-engine' ) );
1748 }
1749 }
1750
1751 // Set the scope for admin tools
1752 if ( !isset( $params['scope'] ) ) {
1753 $params['scope'] = 'admin-tools';
1754 }
1755
1756 $result = $mwai->simpleTranscribeAudio( $url, $path, $params );
1757 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1758 }
1759 catch ( Exception $e ) {
1760 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1761 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1762 }
1763 }
1764
1765 public function rest_ai_transcribe_image( $request ) {
1766 try {
1767 global $mwai;
1768 $params = $request->get_json_params();
1769 $message = $this->retrieve_message( $params );
1770 $url = !empty( $params['url'] ) ? $params['url'] : null;
1771 // This could lead to a security issue, so let's avoid using path directly.
1772 //$path = !empty( $params['path'] ) ? $params['path'] : null;
1773 $result = $mwai->simpleVisionQuery( $message, $url );
1774 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1775 }
1776 catch ( Exception $e ) {
1777 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1778 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1779 }
1780 }
1781
1782 public function rest_ai_json( $request ) {
1783 try {
1784 global $mwai;
1785 $params = $request->get_json_params();
1786 $message = $this->retrieve_message( $params );
1787 $result = $mwai->simpleJsonQuery( $message );
1788 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1789 }
1790 catch ( Exception $e ) {
1791 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1792 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1793 }
1794 }
1795
1796 public function rest_mcp_functions( $request ) {
1797 try {
1798 // Get all registered MCP tools
1799 $tools = apply_filters( 'mwai_mcp_tools', [] );
1800
1801 // Format the response
1802 $response = [
1803 'success' => true,
1804 'count' => count( $tools ),
1805 'functions' => $tools
1806 ];
1807
1808 return $this->create_rest_response( $response, 200 );
1809 }
1810 catch ( Exception $e ) {
1811 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1812 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1813 }
1814 }
1815
1816 public function rest_helpers_post_types() {
1817 try {
1818 $postTypes = $this->core->get_post_types();
1819 return $this->create_rest_response( [ 'success' => true, 'postTypes' => $postTypes ], 200 );
1820 }
1821 catch ( Exception $e ) {
1822 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1823 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1824 }
1825 }
1826
1827 public function rest_settings_themes( $request ) {
1828 try {
1829 $method = $request->get_method();
1830 if ( $method === 'GET' ) {
1831 $themes = $this->core->get_themes();
1832 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
1833 }
1834 else if ( $method === 'POST' ) {
1835 $params = $request->get_json_params();
1836 $themes = $params['themes'];
1837 $themes = $this->core->update_themes( $themes );
1838 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
1839 }
1840 }
1841 catch ( Exception $e ) {
1842 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1843 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1844 }
1845 }
1846
1847 public function rest_settings_chatbots( $request ) {
1848 try {
1849 $method = $request->get_method();
1850 if ( $method === 'GET' ) {
1851 $chatbots = $this->core->get_chatbots();
1852 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
1853 }
1854 else if ( $method === 'POST' ) {
1855 $params = $request->get_json_params();
1856 $chatbots = $params['chatbots'];
1857 $chatbots = $this->core->update_chatbots( $chatbots );
1858 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
1859 }
1860 return $this->create_rest_response( [ 'success' => false, 'message' => 'Method not allowed' ], 405 );
1861 }
1862 catch ( Exception $e ) {
1863 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1864 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1865 }
1866 }
1867
1868 #region Logs
1869
1870 public function rest_get_logs() {
1871 $logs = Meow_MWAI_Logging::get();
1872 return $this->create_rest_response( [ 'success' => true, 'data' => $logs ], 200 );
1873 }
1874
1875 public function rest_clear_logs() {
1876 Meow_MWAI_Logging::clear();
1877 return $this->create_rest_response( [ 'success' => true ], 200 );
1878 }
1879
1880 #endregion
1881
1882 #region Forms
1883
1884 public function rest_forms_list( $request ) {
1885 try {
1886 $args = [
1887 'post_type' => 'mwai_form',
1888 'posts_per_page' => 100,
1889 'post_status' => 'any',
1890 'orderby' => 'date',
1891 'order' => 'DESC'
1892 ];
1893
1894 $posts = get_posts( $args );
1895 $forms = array_map( function ( $post ) {
1896 return [
1897 'id' => $post->ID,
1898 'title' => $post->post_title,
1899 'status' => $post->post_status
1900 ];
1901 }, $posts );
1902
1903 return $this->create_rest_response( [ 'success' => true, 'forms' => $forms ], 200 );
1904 }
1905 catch ( Exception $e ) {
1906 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
1907 }
1908 }
1909
1910 public function rest_forms_get( $request ) {
1911 try {
1912 $id = intval( $request->get_param( 'id' ) );
1913 if ( !$id ) {
1914 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
1915 }
1916
1917 $post = get_post( $id );
1918 if ( !$post || $post->post_type !== 'mwai_form' ) {
1919 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
1920 }
1921
1922 $form = [
1923 'id' => $post->ID,
1924 'title' => [
1925 'raw' => $post->post_title,
1926 'rendered' => $post->post_title
1927 ],
1928 'content' => [
1929 'raw' => $post->post_content,
1930 'rendered' => $post->post_content
1931 ],
1932 'status' => $post->post_status
1933 ];
1934
1935 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
1936 }
1937 catch ( Exception $e ) {
1938 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
1939 }
1940 }
1941
1942 public function rest_forms_create( $request ) {
1943 try {
1944 $params = $request->get_json_params();
1945 $title = isset( $params['title'] ) ? $params['title'] : 'Untitled Form';
1946
1947 $post_data = [
1948 'post_title' => $title,
1949 'post_content' => '',
1950 'post_status' => 'draft',
1951 'post_type' => 'mwai_form'
1952 ];
1953
1954 $post_id = wp_insert_post( $post_data );
1955
1956 if ( is_wp_error( $post_id ) ) {
1957 return $this->create_rest_response( [ 'success' => false, 'message' => $post_id->get_error_message() ], 500 );
1958 }
1959
1960 $post = get_post( $post_id );
1961 $form = [
1962 'id' => $post->ID,
1963 'title' => [
1964 'raw' => $post->post_title,
1965 'rendered' => $post->post_title
1966 ],
1967 'status' => $post->post_status
1968 ];
1969
1970 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
1971 }
1972 catch ( Exception $e ) {
1973 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
1974 }
1975 }
1976
1977 public function rest_forms_update( $request ) {
1978 try {
1979 $params = $request->get_json_params();
1980 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
1981
1982 if ( !$id ) {
1983 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
1984 }
1985
1986 $post = get_post( $id );
1987 if ( !$post || $post->post_type !== 'mwai_form' ) {
1988 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
1989 }
1990
1991 $post_data = [ 'ID' => $id ];
1992
1993 if ( isset( $params['title'] ) ) {
1994 $post_data['post_title'] = $params['title'];
1995 }
1996
1997 if ( isset( $params['content'] ) ) {
1998 $post_data['post_content'] = $params['content'];
1999 }
2000
2001 if ( isset( $params['status'] ) ) {
2002 $post_data['post_status'] = $params['status'];
2003 }
2004
2005 $result = wp_update_post( $post_data );
2006
2007 if ( is_wp_error( $result ) ) {
2008 return $this->create_rest_response( [ 'success' => false, 'message' => $result->get_error_message() ], 500 );
2009 }
2010
2011 $post = get_post( $id );
2012 $form = [
2013 'id' => $post->ID,
2014 'title' => [
2015 'raw' => $post->post_title,
2016 'rendered' => $post->post_title
2017 ],
2018 'content' => [
2019 'raw' => $post->post_content,
2020 'rendered' => $post->post_content
2021 ],
2022 'status' => $post->post_status
2023 ];
2024
2025 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2026 }
2027 catch ( Exception $e ) {
2028 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2029 }
2030 }
2031
2032 public function rest_forms_delete( $request ) {
2033 try {
2034 $params = $request->get_json_params();
2035 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2036
2037 if ( !$id ) {
2038 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2039 }
2040
2041 $post = get_post( $id );
2042 if ( !$post || $post->post_type !== 'mwai_form' ) {
2043 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2044 }
2045
2046 $result = wp_delete_post( $id, true );
2047
2048 if ( !$result ) {
2049 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to delete form' ], 500 );
2050 }
2051
2052 return $this->create_rest_response( [ 'success' => true ], 200 );
2053 }
2054 catch ( Exception $e ) {
2055 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2056 }
2057 }
2058
2059 #endregion
2060
2061 #region Video Generation Helpers
2062
2063 public function rest_helpers_create_video( $request ) {
2064 try {
2065 $params = $request->get_json_params();
2066 $prompt = sanitize_text_field( $params['prompt'] );
2067 $model = sanitize_text_field( $params['model'] ?? 'sora-2' );
2068 $size = sanitize_text_field( $params['size'] ?? '720x1280' );
2069 $seconds = absint( $params['seconds'] ?? 4 );
2070 $envId = sanitize_text_field( $params['envId'] ?? '' );
2071
2072 // Check if envId is provided (defaults not supported for videos yet)
2073 if ( empty( $envId ) ) {
2074 throw new Exception( 'Please select a specific environment and model in the Video Generator. Default environments are not yet supported for video generation.' );
2075 }
2076
2077 // Get API key from environment
2078 $env = $this->core->get_ai_env( $envId );
2079 $api_key = $env['apikey'] ?? '';
2080
2081 if ( empty( $api_key ) ) {
2082 throw new Exception( 'OpenAI API key not found.' );
2083 }
2084
2085 // Prepare multipart boundary
2086 $boundary = wp_generate_password( 24, false );
2087 $body = '';
2088
2089 // Add model
2090 $body .= "--{$boundary}\r\n";
2091 $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n";
2092 $body .= "{$model}\r\n";
2093
2094 // Add prompt
2095 $body .= "--{$boundary}\r\n";
2096 $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n";
2097 $body .= "{$prompt}\r\n";
2098
2099 // Add size
2100 $body .= "--{$boundary}\r\n";
2101 $body .= "Content-Disposition: form-data; name=\"size\"\r\n\r\n";
2102 $body .= "{$size}\r\n";
2103
2104 // Add seconds
2105 $body .= "--{$boundary}\r\n";
2106 $body .= "Content-Disposition: form-data; name=\"seconds\"\r\n\r\n";
2107 $body .= "{$seconds}\r\n";
2108
2109 $body .= "--{$boundary}--\r\n";
2110
2111 // Call OpenAI API to create video
2112 $response = wp_remote_post( 'https://api.openai.com/v1/videos', [
2113 'headers' => [
2114 'Authorization' => 'Bearer ' . $api_key,
2115 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
2116 ],
2117 'body' => $body,
2118 'timeout' => 30
2119 ] );
2120
2121 if ( is_wp_error( $response ) ) {
2122 throw new Exception( $response->get_error_message() );
2123 }
2124
2125 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
2126
2127 if ( isset( $response_body['error'] ) ) {
2128 throw new Exception( $response_body['error']['message'] ?? 'Unknown error' );
2129 }
2130
2131 // Record usage (price is calculated per second)
2132 $usage = $this->core->record_videos_usage( $model, $size, $seconds );
2133
2134 // Log to Query Logs (Statistics)
2135 try {
2136 if ( class_exists( 'MeowPro_MWAI_Stats' ) && class_exists( 'MeowPro_MWAI_Statistics' ) ) {
2137 $statsObject = new MeowPro_MWAI_Stats();
2138 $statsObject->session = $params['session'] ?? null;
2139 $statsObject->scope = 'admin-tools';
2140 $statsObject->feature = 'video-generator';
2141 $statsObject->model = $model;
2142 $statsObject->envId = $envId;
2143 $statsObject->units = $seconds;
2144 $statsObject->type = 'seconds';
2145 $statsObject->price = $usage['price'] ?? 0;
2146 $statsObject->accuracy = $usage['accuracy'] ?? 'full';
2147
2148 $statistics = new MeowPro_MWAI_Statistics();
2149 $statistics->commit_stats( $statsObject );
2150 }
2151 }
2152 catch ( Exception $statsError ) {
2153 // Log the error but don't fail the video creation
2154 error_log( '[AI Engine Video] Failed to log statistics: ' . $statsError->getMessage() );
2155 }
2156
2157 // Store metadata for later retrieval when video completes
2158 if ( isset( $response_body['id'] ) ) {
2159 set_transient( 'mwai_video_metadata_' . $response_body['id'], [
2160 'model' => $model,
2161 'env_id' => $envId,
2162 'created_at' => time()
2163 ], 7 * DAY_IN_SECONDS );
2164 }
2165
2166 return $this->create_rest_response( [ 'success' => true, 'video' => $response_body, 'usage' => $usage ], 200 );
2167 }
2168 catch ( Exception $e ) {
2169 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2170 }
2171 }
2172
2173 public function rest_helpers_video_status( $request ) {
2174 try {
2175 $params = $request->get_json_params();
2176 $video_ids = $params['videoIds'] ?? [];
2177 $envId = sanitize_text_field( $params['envId'] ?? '' );
2178
2179 if ( empty( $video_ids ) ) {
2180 return $this->create_rest_response( [ 'success' => true, 'videos' => [] ], 200 );
2181 }
2182
2183 // Get API key from environment
2184 $env = $this->core->get_ai_env( $envId );
2185 $api_key = $env['apikey'] ?? '';
2186
2187 if ( empty( $api_key ) ) {
2188 throw new Exception( 'OpenAI API key not found.' );
2189 }
2190
2191 $videos = [];
2192 foreach ( $video_ids as $video_id ) {
2193 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id, [
2194 'headers' => [
2195 'Authorization' => 'Bearer ' . $api_key
2196 ],
2197 'timeout' => 15
2198 ] );
2199
2200 if ( !is_wp_error( $response ) ) {
2201 $body = json_decode( wp_remote_retrieve_body( $response ), true );
2202 if ( !isset( $body['error'] ) ) {
2203 // If video is completed and we haven't saved it yet, download and save to media library
2204 if ( $body['status'] === 'completed' && empty( get_transient( 'mwai_video_saved_' . $video_id ) ) ) {
2205 // Retrieve metadata that was stored when video was created
2206 $metadata = get_transient( 'mwai_video_metadata_' . $video_id );
2207 $ai_metadata = [];
2208 if ( $metadata ) {
2209 $ai_metadata = [
2210 'model' => $metadata['model'] ?? null,
2211 'env_id' => $metadata['env_id'] ?? null,
2212 'latency' => isset( $metadata['created_at'] ) ? ( time() - $metadata['created_at'] ) : null
2213 ];
2214 }
2215
2216 $attachment_id = $this->download_and_save_video( $video_id, $api_key, '', '', $ai_metadata );
2217 if ( $attachment_id ) {
2218 $body['attachment_id'] = $attachment_id;
2219 // Build URL from file path for custom post types
2220 $file_path = get_attached_file( $attachment_id );
2221 $upload_dir = wp_upload_dir();
2222 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2223 // Mark as saved so we don't download again
2224 set_transient( 'mwai_video_saved_' . $video_id, $attachment_id, DAY_IN_SECONDS );
2225 // Clean up metadata transient
2226 delete_transient( 'mwai_video_metadata_' . $video_id );
2227 }
2228 }
2229 // Check if we already have this video saved
2230 else if ( $body['status'] === 'completed' ) {
2231 $attachment_id = get_transient( 'mwai_video_saved_' . $video_id );
2232 if ( $attachment_id ) {
2233 $body['attachment_id'] = $attachment_id;
2234 // Build URL from file path for custom post types
2235 $file_path = get_attached_file( $attachment_id );
2236 $upload_dir = wp_upload_dir();
2237 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2238 }
2239 }
2240 $videos[] = $body;
2241 }
2242 else {
2243 // Include error information in the response
2244 error_log( 'AI Engine: Video generation failed for ID ' . $video_id . ': ' . json_encode( $body['error'] ) );
2245 $videos[] = [
2246 'id' => $video_id,
2247 'status' => 'failed',
2248 'error' => $body['error']
2249 ];
2250 }
2251 }
2252 else {
2253 // WP HTTP error
2254 error_log( 'AI Engine: Failed to check video status for ID ' . $video_id . ': ' . $response->get_error_message() );
2255 $videos[] = [
2256 'id' => $video_id,
2257 'status' => 'failed',
2258 'error' => [ 'message' => $response->get_error_message() ]
2259 ];
2260 }
2261 }
2262
2263 return $this->create_rest_response( [ 'success' => true, 'videos' => $videos ], 200 );
2264 }
2265 catch ( Exception $e ) {
2266 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2267 }
2268 }
2269
2270 public function rest_helpers_download_video( $request ) {
2271 try {
2272 $params = $request->get_json_params();
2273 $video_id = sanitize_text_field( $params['videoId'] );
2274 $envId = sanitize_text_field( $params['envId'] ?? '' );
2275
2276 // Get API key from environment
2277 $env = $this->core->get_ai_env( $envId );
2278 $api_key = $env['apikey'] ?? '';
2279
2280 if ( empty( $api_key ) ) {
2281 throw new Exception( 'OpenAI API key not found.' );
2282 }
2283
2284 $temp_file = wp_tempnam( $video_id . '.mp4' );
2285
2286 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2287 'headers' => [
2288 'Authorization' => 'Bearer ' . $api_key
2289 ],
2290 'timeout' => 120,
2291 'stream' => true,
2292 'filename' => $temp_file
2293 ] );
2294
2295 if ( is_wp_error( $response ) ) {
2296 throw new Exception( $response->get_error_message() );
2297 }
2298
2299 $file_data = file_get_contents( $temp_file );
2300 $base64 = base64_encode( $file_data );
2301
2302 unlink( $temp_file );
2303
2304 return $this->create_rest_response( [
2305 'success' => true,
2306 'data' => $base64,
2307 'mimeType' => 'video/mp4'
2308 ], 200 );
2309 }
2310 catch ( Exception $e ) {
2311 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2312 }
2313 }
2314
2315 public function rest_helpers_delete_video( $request ) {
2316 try {
2317 $params = $request->get_json_params();
2318 $video_id = sanitize_text_field( $params['videoId'] );
2319 $envId = sanitize_text_field( $params['envId'] ?? '' );
2320
2321 // Get API key from environment
2322 $env = $this->core->get_ai_env( $envId );
2323 $api_key = $env['apikey'] ?? '';
2324
2325 if ( empty( $api_key ) ) {
2326 throw new Exception( 'OpenAI API key not found.' );
2327 }
2328
2329 $response = wp_remote_request( 'https://api.openai.com/v1/videos/' . $video_id, [
2330 'method' => 'DELETE',
2331 'headers' => [
2332 'Authorization' => 'Bearer ' . $api_key
2333 ],
2334 'timeout' => 15
2335 ] );
2336
2337 if ( is_wp_error( $response ) ) {
2338 throw new Exception( $response->get_error_message() );
2339 }
2340
2341 return $this->create_rest_response( [ 'success' => true ], 200 );
2342 }
2343 catch ( Exception $e ) {
2344 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2345 }
2346 }
2347
2348 private function download_and_save_video( $video_id, $api_key, $title = '', $description = '', $ai_metadata = [] ) {
2349 try {
2350 // Download video content
2351 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2352 'headers' => [
2353 'Authorization' => 'Bearer ' . $api_key
2354 ],
2355 'timeout' => 120
2356 ] );
2357
2358 if ( is_wp_error( $response ) ) {
2359 error_log( 'Error downloading video: ' . $response->get_error_message() );
2360 return false;
2361 }
2362
2363 $video_data = wp_remote_retrieve_body( $response );
2364 if ( empty( $video_data ) ) {
2365 error_log( 'Empty video data received' );
2366 return false;
2367 }
2368
2369 // Generate filename
2370 $filename = $video_id . '.mp4';
2371 $upload_dir = wp_upload_dir();
2372 $file_path = $upload_dir['path'] . '/' . $filename;
2373
2374 // Save to file
2375 file_put_contents( $file_path, $video_data );
2376
2377 // Prepare attachment data - use mwai_video post type (draft video)
2378 $attachment = [
2379 'post_mime_type' => 'video/mp4',
2380 'post_title' => !empty( $title ) ? $title : 'AI Generated Video',
2381 'post_content' => $description,
2382 'post_status' => 'inherit',
2383 'post_type' => 'mwai_video'
2384 ];
2385
2386 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
2387 $attachment_id = wp_insert_post( $attachment );
2388
2389 // Set the attached file manually since we're not using wp_insert_attachment
2390 update_attached_file( $attachment_id, $file_path );
2391
2392 if ( is_wp_error( $attachment_id ) ) {
2393 error_log( 'Error creating attachment: ' . $attachment_id->get_error_message() );
2394 return false;
2395 }
2396
2397 // Generate attachment metadata
2398 require_once ABSPATH . 'wp-admin/includes/image.php';
2399 $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path );
2400 wp_update_attachment_metadata( $attachment_id, $attach_data );
2401
2402 // Store AI-related metadata
2403 if ( !empty( $ai_metadata['model'] ) ) {
2404 update_post_meta( $attachment_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
2405 }
2406 if ( !empty( $ai_metadata['latency'] ) ) {
2407 update_post_meta( $attachment_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
2408 }
2409 if ( !empty( $ai_metadata['env_id'] ) ) {
2410 update_post_meta( $attachment_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
2411 }
2412
2413 // Add to user's draft media
2414 $user_id = get_current_user_id();
2415 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2416 if ( !is_array( $draft_media ) ) {
2417 $draft_media = [];
2418 }
2419 $draft_media[] = [
2420 'attachment_id' => $attachment_id,
2421 'type' => 'video',
2422 'openai_id' => $video_id,
2423 'created_at' => time()
2424 ];
2425 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
2426
2427 return $attachment_id;
2428 }
2429 catch ( Exception $e ) {
2430 error_log( 'Exception in download_and_save_video: ' . $e->getMessage() );
2431 return false;
2432 }
2433 }
2434
2435 public function rest_helpers_save_video_to_library( $request ) {
2436 try {
2437 $params = $request->get_json_params();
2438 $video_id = sanitize_text_field( $params['videoId'] );
2439 $title = sanitize_text_field( $params['title'] );
2440 $description = sanitize_text_field( $params['description'] );
2441 $filename = sanitize_file_name( $params['filename'] );
2442 $envId = sanitize_text_field( $params['envId'] ?? '' );
2443
2444 // Ensure filename has .mp4 extension
2445 if ( !preg_match( '/\.mp4$/i', $filename ) ) {
2446 $filename .= '.mp4';
2447 }
2448
2449 // Get API key from environment
2450 $env = $this->core->get_ai_env( $envId );
2451 $api_key = $env['apikey'] ?? '';
2452
2453 if ( empty( $api_key ) ) {
2454 throw new Exception( 'OpenAI API key not found.' );
2455 }
2456
2457 // Download video content
2458 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2459 'headers' => [
2460 'Authorization' => 'Bearer ' . $api_key
2461 ],
2462 'timeout' => 120
2463 ] );
2464
2465 if ( is_wp_error( $response ) ) {
2466 throw new Exception( $response->get_error_message() );
2467 }
2468
2469 $video_data = wp_remote_retrieve_body( $response );
2470
2471 // Upload to WordPress media library
2472 $upload_dir = wp_upload_dir();
2473 $file_path = $upload_dir['path'] . '/' . $filename;
2474
2475 file_put_contents( $file_path, $video_data );
2476
2477 $attachment = [
2478 'post_mime_type' => 'video/mp4',
2479 'post_title' => $title,
2480 'post_content' => $description,
2481 'post_status' => 'inherit'
2482 ];
2483
2484 $attach_id = wp_insert_attachment( $attachment, $file_path );
2485
2486 require_once( ABSPATH . 'wp-admin/includes/image.php' );
2487 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
2488 wp_update_attachment_metadata( $attach_id, $attach_data );
2489
2490 return $this->create_rest_response( [
2491 'success' => true,
2492 'attachmentId' => $attach_id
2493 ], 200 );
2494 }
2495 catch ( Exception $e ) {
2496 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2497 }
2498 }
2499
2500 public function rest_helpers_delete_video_from_library( $request ) {
2501 try {
2502 $params = $request->get_json_params();
2503 $attachment_id = absint( $params['attachmentId'] );
2504
2505 if ( empty( $attachment_id ) ) {
2506 throw new Exception( 'Attachment ID is required.' );
2507 }
2508
2509 $deleted = wp_delete_attachment( $attachment_id, true );
2510
2511 if ( !$deleted ) {
2512 throw new Exception( 'Failed to delete attachment.' );
2513 }
2514
2515 return $this->create_rest_response( [ 'success' => true ], 200 );
2516 }
2517 catch ( Exception $e ) {
2518 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2519 }
2520 }
2521
2522 public function rest_helpers_list_draft_media( $request ) {
2523 try {
2524 $type = $request->get_param( 'type' ); // 'image', 'video', or null for all
2525 $user_id = get_current_user_id();
2526 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2527
2528 if ( !is_array( $draft_media ) ) {
2529 return $this->create_rest_response( [ 'success' => true, 'media' => [] ], 200 );
2530 }
2531
2532 $media_items = [];
2533 foreach ( $draft_media as $item ) {
2534 // Filter by type if specified
2535 if ( $type && $item['type'] !== $type ) {
2536 continue;
2537 }
2538
2539 $attachment_id = $item['attachment_id'];
2540 $attachment = get_post( $attachment_id );
2541
2542 if ( $attachment ) {
2543 // For custom post types (mwai_image, mwai_video), build URL from file path
2544 $file_path = get_attached_file( $attachment_id );
2545 $upload_dir = wp_upload_dir();
2546 $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2547
2548 $model = get_post_meta( $attachment_id, 'mwai_model', true );
2549 $generation_time = get_post_meta( $attachment_id, 'mwai_latency', true );
2550 $env_id = get_post_meta( $attachment_id, 'mwai_env_id', true );
2551
2552 // Debug logging
2553 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2554 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 ) );
2555 }
2556
2557 $media_items[] = [
2558 'attachment_id' => $attachment_id,
2559 'type' => $item['type'],
2560 'openai_id' => $item['openai_id'] ?? null,
2561 'url' => $url,
2562 'title' => $attachment->post_title,
2563 'description' => $attachment->post_content,
2564 'filename' => basename( $file_path ),
2565 'created_at' => $item['created_at'],
2566 'model' => $model,
2567 'generation_time' => $generation_time,
2568 'env_id' => $env_id
2569 ];
2570 }
2571 }
2572
2573 return $this->create_rest_response( [ 'success' => true, 'media' => $media_items ], 200 );
2574 }
2575 catch ( Exception $e ) {
2576 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2577 }
2578 }
2579
2580 public function rest_helpers_approve_media( $request ) {
2581 try {
2582 $params = $request->get_json_params();
2583 $attachment_id = absint( $params['attachmentId'] );
2584 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2585 $envId = sanitize_text_field( $params['envId'] ?? '' );
2586
2587 if ( empty( $attachment_id ) ) {
2588 throw new Exception( 'Attachment ID is required.' );
2589 }
2590
2591 // Convert from mwai_image/mwai_video to attachment post type
2592 wp_update_post( [
2593 'ID' => $attachment_id,
2594 'post_type' => 'attachment',
2595 'post_status' => 'inherit'
2596 ] );
2597
2598 // Remove from draft media list
2599 $user_id = get_current_user_id();
2600 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2601 if ( is_array( $draft_media ) ) {
2602 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2603 return $item['attachment_id'] !== $attachment_id;
2604 } );
2605 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2606 }
2607
2608 // Delete video from OpenAI if applicable
2609 if ( !empty( $openai_id ) ) {
2610 $env = $this->core->get_ai_env( $envId );
2611 $api_key = $env['apikey'] ?? '';
2612
2613 if ( !empty( $api_key ) ) {
2614 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2615 'method' => 'DELETE',
2616 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2617 'timeout' => 15
2618 ] );
2619 }
2620 }
2621
2622 return $this->create_rest_response( [ 'success' => true ], 200 );
2623 }
2624 catch ( Exception $e ) {
2625 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2626 }
2627 }
2628
2629 public function rest_helpers_reject_media( $request ) {
2630 try {
2631 $params = $request->get_json_params();
2632 $attachment_id = absint( $params['attachmentId'] );
2633 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2634 $envId = sanitize_text_field( $params['envId'] ?? '' );
2635
2636 if ( empty( $attachment_id ) ) {
2637 throw new Exception( 'Attachment ID is required.' );
2638 }
2639
2640 // Convert from mwai_image/mwai_video to attachment post type first
2641 // This ensures wp_delete_attachment properly deletes the physical file
2642 wp_update_post( [
2643 'ID' => $attachment_id,
2644 'post_type' => 'attachment'
2645 ] );
2646
2647 // Delete attachment from WordPress (now that it's a proper attachment, files will be deleted)
2648 wp_delete_attachment( $attachment_id, true );
2649
2650 // Remove from draft media list
2651 $user_id = get_current_user_id();
2652 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2653 if ( is_array( $draft_media ) ) {
2654 $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) {
2655 return $item['attachment_id'] !== $attachment_id;
2656 } );
2657 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2658 }
2659
2660 // Delete video from OpenAI if applicable
2661 if ( !empty( $openai_id ) ) {
2662 $env = $this->core->get_ai_env( $envId );
2663 $api_key = $env['apikey'] ?? '';
2664
2665 if ( !empty( $api_key ) ) {
2666 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2667 'method' => 'DELETE',
2668 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2669 'timeout' => 15
2670 ] );
2671 }
2672 }
2673
2674 return $this->create_rest_response( [ 'success' => true ], 200 );
2675 }
2676 catch ( Exception $e ) {
2677 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2678 }
2679 }
2680
2681 #endregion
2682 }
2683