data
1 year ago
engines
6 days ago
exceptions
4 weeks ago
modules
6 days ago
query
13 hours ago
services
3 weeks ago
admin.php
1 week ago
api.php
3 weeks ago
core.php
13 hours ago
discussion.php
1 year ago
event.php
1 year ago
init.php
1 week ago
logging.php
1 year ago
reply.php
4 weeks ago
rest.php
13 hours ago
rest.php
3107 lines
| 1 | <?php |
| 2 | |
| 3 | class Meow_MWAI_Rest { |
| 4 | private $core = null; |
| 5 | private $namespace = 'mwai/v1'; |
| 6 | |
| 7 | public function __construct( $core ) { |
| 8 | $this->core = $core; |
| 9 | add_action( 'rest_api_init', [ $this, 'rest_init' ] ); |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Retrieve the message from the parameters and optionally sanitize it. |
| 14 | * |
| 15 | * @param array &$params The parameters array, passed by reference. |
| 16 | * @param bool $sanitize Whether to sanitize the message using sanitize_text_field. |
| 17 | * @return string The retrieved (and optionally sanitized) message. |
| 18 | */ |
| 19 | public function retrieve_message( &$params, $sanitize = false ): string { |
| 20 | $message = $params['message'] ?? ''; |
| 21 | |
| 22 | if ( $sanitize ) { |
| 23 | $message = sanitize_text_field( $message ); |
| 24 | } |
| 25 | |
| 26 | return $message; |
| 27 | } |
| 28 | |
| 29 | /** |
| 30 | * Helper method to create REST responses with automatic token refresh |
| 31 | * |
| 32 | * @param array $data The response data |
| 33 | * @param int $status HTTP status code |
| 34 | * @return WP_REST_Response |
| 35 | */ |
| 36 | protected function create_rest_response( $data, $status = 200 ) { |
| 37 | // Always check if we need to provide a new nonce |
| 38 | $current_nonce = $this->core->get_nonce( true ); |
| 39 | $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null; |
| 40 | |
| 41 | // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours) |
| 42 | // We'll refresh if the nonce is older than 10 hours to be safe |
| 43 | $should_refresh = false; |
| 44 | |
| 45 | if ( $request_nonce ) { |
| 46 | // Try to determine the age of the nonce |
| 47 | // WordPress uses a tick system where each tick is 12 hours |
| 48 | // If we're in the second half of the nonce's life, refresh it |
| 49 | $time = time(); |
| 50 | $nonce_tick = wp_nonce_tick(); |
| 51 | |
| 52 | // Verify if the nonce is still valid but getting old |
| 53 | $verify = wp_verify_nonce( $request_nonce, 'wp_rest' ); |
| 54 | if ( $verify === 2 ) { |
| 55 | // Nonce is valid but was generated 12-24 hours ago |
| 56 | $should_refresh = true; |
| 57 | // Log will be written when token is included in response |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // If the nonce has changed or should be refreshed, include the new one |
| 62 | if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) { |
| 63 | $data['new_token'] = $current_nonce; |
| 64 | |
| 65 | // Log if server debug mode is enabled |
| 66 | if ( $this->core->get_option( 'server_debug_mode' ) ) { |
| 67 | error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' ); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | return new WP_REST_Response( $data, $status ); |
| 72 | } |
| 73 | |
| 74 | public function rest_init() { |
| 75 | try { |
| 76 | // Session Endpoint |
| 77 | register_rest_route( $this->namespace, '/start_session', [ |
| 78 | 'methods' => 'POST', |
| 79 | 'permission_callback' => '__return_true', // Public endpoint for guest users |
| 80 | 'callback' => [ $this, 'rest_start_session' ], |
| 81 | ] ); |
| 82 | |
| 83 | // Settings Endpoints |
| 84 | register_rest_route( $this->namespace, '/settings/update', [ |
| 85 | 'methods' => 'POST', |
| 86 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 87 | 'callback' => [ $this, 'rest_settings_update' ], |
| 88 | ] ); |
| 89 | register_rest_route( $this->namespace, '/settings/options', [ |
| 90 | 'methods' => 'GET', |
| 91 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 92 | 'callback' => [ $this, 'rest_settings_list' ], |
| 93 | ] ); |
| 94 | register_rest_route( $this->namespace, '/settings/reset', [ |
| 95 | 'methods' => 'POST', |
| 96 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 97 | 'callback' => [ $this, 'rest_settings_reset' ], |
| 98 | ] ); |
| 99 | register_rest_route( $this->namespace, '/settings/chatbots', [ |
| 100 | 'methods' => ['GET', 'POST'], |
| 101 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 102 | 'callback' => [ $this, 'rest_settings_chatbots' ], |
| 103 | ] ); |
| 104 | register_rest_route( $this->namespace, '/settings/themes', [ |
| 105 | 'methods' => ['GET', 'POST'], |
| 106 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 107 | 'callback' => [ $this, 'rest_settings_themes' ], |
| 108 | ] ); |
| 109 | |
| 110 | // System Endpoints |
| 111 | register_rest_route( $this->namespace, '/system/logs/list', [ |
| 112 | 'methods' => 'POST', |
| 113 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 114 | 'callback' => [ $this, 'rest_system_logs_list' ], |
| 115 | ] ); |
| 116 | register_rest_route( $this->namespace, '/system/logs/delete', [ |
| 117 | 'methods' => 'POST', |
| 118 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 119 | 'callback' => [ $this, 'rest_system_logs_delete' ], |
| 120 | ] ); |
| 121 | register_rest_route( $this->namespace, '/system/logs/meta', [ |
| 122 | 'methods' => 'POST', |
| 123 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 124 | 'callback' => [ $this, 'rest_system_logs_meta_get' ], |
| 125 | ] ); |
| 126 | register_rest_route( $this->namespace, '/system/logs/activity', [ |
| 127 | 'methods' => 'POST', |
| 128 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 129 | 'callback' => [ $this, 'rest_system_logs_activity' ], |
| 130 | ] ); |
| 131 | register_rest_route( $this->namespace, '/system/logs/activity_daily', [ |
| 132 | 'methods' => 'POST', |
| 133 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 134 | 'callback' => [ $this, 'rest_system_logs_activity_daily' ], |
| 135 | ] ); |
| 136 | register_rest_route( $this->namespace, '/system/templates', [ |
| 137 | 'methods' => 'POST', |
| 138 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 139 | 'callback' => [ $this, 'rest_system_templates_save' ], |
| 140 | ] ); |
| 141 | register_rest_route( $this->namespace, '/system/templates', [ |
| 142 | 'methods' => 'GET', |
| 143 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 144 | 'callback' => [ $this, 'rest_system_templates_get' ], |
| 145 | ] ); |
| 146 | |
| 147 | // AI Endpoints |
| 148 | register_rest_route( $this->namespace, '/ai/models', [ |
| 149 | 'methods' => 'POST', |
| 150 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 151 | 'callback' => [ $this, 'rest_ai_models' ], |
| 152 | ] ); |
| 153 | register_rest_route( $this->namespace, '/ai/test_connection', [ |
| 154 | 'methods' => 'POST', |
| 155 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 156 | 'callback' => [ $this, 'rest_ai_test_connection' ], |
| 157 | ] ); |
| 158 | register_rest_route( $this->namespace, '/ai/completions', [ |
| 159 | 'methods' => 'POST', |
| 160 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 161 | 'callback' => [ $this, 'rest_ai_completions' ], |
| 162 | ] ); |
| 163 | register_rest_route( $this->namespace, '/ai/images', [ |
| 164 | 'methods' => 'POST', |
| 165 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 166 | 'callback' => [ $this, 'rest_ai_images' ], |
| 167 | ] ); |
| 168 | register_rest_route( $this->namespace, '/ai/image_edit', [ |
| 169 | 'methods' => 'POST', |
| 170 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 171 | 'callback' => [ $this, 'rest_ai_image_edit' ], |
| 172 | ] ); |
| 173 | register_rest_route( $this->namespace, '/ai/copilot', [ |
| 174 | 'methods' => 'POST', |
| 175 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 176 | 'callback' => [ $this, 'rest_ai_copilot' ], |
| 177 | ] ); |
| 178 | |
| 179 | register_rest_route( $this->namespace, '/ai/magic_wand', [ |
| 180 | 'methods' => 'POST', |
| 181 | 'callback' => [ $this, 'rest_ai_magic_wand' ], |
| 182 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 183 | ] ); |
| 184 | register_rest_route( $this->namespace, '/ai/moderate', [ |
| 185 | 'methods' => 'POST', |
| 186 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 187 | 'callback' => [ $this, 'rest_ai_moderate' ], |
| 188 | ] ); |
| 189 | register_rest_route( $this->namespace, '/ai/transcribe_audio', [ |
| 190 | 'methods' => 'POST', |
| 191 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 192 | 'callback' => [ $this, 'rest_ai_transcribe_audio' ], |
| 193 | ] ); |
| 194 | register_rest_route( $this->namespace, '/ai/transcribe_image', [ |
| 195 | 'methods' => 'POST', |
| 196 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 197 | 'callback' => [ $this, 'rest_ai_transcribe_image' ], |
| 198 | ] ); |
| 199 | register_rest_route( $this->namespace, '/ai/json', [ |
| 200 | 'methods' => 'POST', |
| 201 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 202 | 'callback' => [ $this, 'rest_ai_json' ], |
| 203 | ] ); |
| 204 | |
| 205 | // MCP Endpoints |
| 206 | register_rest_route( $this->namespace, '/mcp/functions', [ |
| 207 | 'methods' => 'GET', |
| 208 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 209 | 'callback' => [ $this, 'rest_mcp_functions' ], |
| 210 | ] ); |
| 211 | register_rest_route( $this->namespace, '/mcp/self_test', [ |
| 212 | 'methods' => 'POST', |
| 213 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 214 | 'callback' => [ $this, 'rest_mcp_self_test' ], |
| 215 | ] ); |
| 216 | register_rest_route( $this->namespace, '/system/mcp_logs/top_tools', [ |
| 217 | 'methods' => 'POST', |
| 218 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 219 | 'callback' => [ $this, 'rest_mcp_top_tools' ], |
| 220 | ] ); |
| 221 | |
| 222 | // Helpers Endpoints |
| 223 | register_rest_route( $this->namespace, '/helpers/update_post_title', [ |
| 224 | 'methods' => 'POST', |
| 225 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 226 | 'callback' => [ $this, 'rest_helpers_update_title' ], |
| 227 | ] ); |
| 228 | register_rest_route( $this->namespace, '/helpers/update_post_excerpt', [ |
| 229 | 'methods' => 'POST', |
| 230 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 231 | 'callback' => [ $this, 'rest_helpers_update_excerpt' ], |
| 232 | ] ); |
| 233 | register_rest_route( $this->namespace, '/helpers/create_post', [ |
| 234 | 'methods' => 'POST', |
| 235 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 236 | 'callback' => [ $this, 'rest_helpers_create_post' ], |
| 237 | ] ); |
| 238 | register_rest_route( $this->namespace, '/helpers/create_image', [ |
| 239 | 'methods' => 'POST', |
| 240 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 241 | 'callback' => [ $this, 'rest_helpers_create_images' ], |
| 242 | ] ); |
| 243 | register_rest_route( $this->namespace, '/helpers/generate_image_meta', [ |
| 244 | 'methods' => 'POST', |
| 245 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 246 | 'callback' => [ $this, 'rest_helpers_generate_image_meta' ], |
| 247 | ] ); |
| 248 | register_rest_route( $this->namespace, '/helpers/update_media_metadata', [ |
| 249 | 'methods' => 'POST', |
| 250 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 251 | 'callback' => [ $this, 'rest_helpers_update_media_metadata' ], |
| 252 | ] ); |
| 253 | register_rest_route( $this->namespace, '/helpers/create_video', [ |
| 254 | 'methods' => 'POST', |
| 255 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 256 | 'callback' => [ $this, 'rest_helpers_create_video' ], |
| 257 | ] ); |
| 258 | register_rest_route( $this->namespace, '/helpers/video_status', [ |
| 259 | 'methods' => 'POST', |
| 260 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 261 | 'callback' => [ $this, 'rest_helpers_video_status' ], |
| 262 | ] ); |
| 263 | register_rest_route( $this->namespace, '/helpers/download_video', [ |
| 264 | 'methods' => 'POST', |
| 265 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 266 | 'callback' => [ $this, 'rest_helpers_download_video' ], |
| 267 | ] ); |
| 268 | register_rest_route( $this->namespace, '/helpers/delete_video', [ |
| 269 | 'methods' => 'POST', |
| 270 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 271 | 'callback' => [ $this, 'rest_helpers_delete_video' ], |
| 272 | ] ); |
| 273 | register_rest_route( $this->namespace, '/helpers/save_video_to_library', [ |
| 274 | 'methods' => 'POST', |
| 275 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 276 | 'callback' => [ $this, 'rest_helpers_save_video_to_library' ], |
| 277 | ] ); |
| 278 | register_rest_route( $this->namespace, '/helpers/delete_video_from_library', [ |
| 279 | 'methods' => 'POST', |
| 280 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 281 | 'callback' => [ $this, 'rest_helpers_delete_video_from_library' ], |
| 282 | ] ); |
| 283 | register_rest_route( $this->namespace, '/helpers/list_draft_media', [ |
| 284 | 'methods' => 'GET', |
| 285 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 286 | 'callback' => [ $this, 'rest_helpers_list_draft_media' ], |
| 287 | ] ); |
| 288 | register_rest_route( $this->namespace, '/helpers/approve_media', [ |
| 289 | 'methods' => 'POST', |
| 290 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 291 | 'callback' => [ $this, 'rest_helpers_approve_media' ], |
| 292 | ] ); |
| 293 | register_rest_route( $this->namespace, '/helpers/reject_media', [ |
| 294 | 'methods' => 'POST', |
| 295 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 296 | 'callback' => [ $this, 'rest_helpers_reject_media' ], |
| 297 | ] ); |
| 298 | register_rest_route( $this->namespace, '/helpers/count_posts', [ |
| 299 | 'methods' => 'GET', |
| 300 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 301 | 'callback' => [ $this, 'rest_helpers_count_posts' ], |
| 302 | ] ); |
| 303 | register_rest_route( $this->namespace, '/helpers/posts_ids', [ |
| 304 | 'methods' => 'GET', |
| 305 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 306 | 'callback' => [ $this, 'rest_helpers_posts_ids' ], |
| 307 | ] ); |
| 308 | register_rest_route( $this->namespace, '/helpers/post_types', [ |
| 309 | 'methods' => 'GET', |
| 310 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 311 | 'callback' => [ $this, 'rest_helpers_post_types' ], |
| 312 | ] ); |
| 313 | register_rest_route( $this->namespace, '/helpers/post_content', [ |
| 314 | 'methods' => 'GET', |
| 315 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 316 | 'callback' => [ $this, 'rest_helpers_post_content' ], |
| 317 | ] ); |
| 318 | register_rest_route( $this->namespace, '/helpers/check_posts_content', [ |
| 319 | 'methods' => 'POST', |
| 320 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 321 | 'callback' => [ $this, 'rest_helpers_check_posts_content' ], |
| 322 | ] ); |
| 323 | register_rest_route( $this->namespace, '/helpers/run_tasks', [ |
| 324 | 'methods' => 'POST', |
| 325 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 326 | 'callback' => [ $this, 'rest_helpers_run_tasks' ], |
| 327 | ] ); |
| 328 | register_rest_route( $this->namespace, '/helpers/optimize_database', [ |
| 329 | 'methods' => 'POST', |
| 330 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 331 | 'callback' => [ $this, 'rest_helpers_optimize_database' ], |
| 332 | ] ); |
| 333 | register_rest_route( $this->namespace, '/helpers/cron_events', [ |
| 334 | 'methods' => 'GET', |
| 335 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 336 | 'callback' => [ $this, 'rest_helpers_cron_events' ], |
| 337 | ] ); |
| 338 | register_rest_route( $this->namespace, '/helpers/run_cron', [ |
| 339 | 'methods' => 'POST', |
| 340 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 341 | 'callback' => [ $this, 'rest_helpers_run_cron' ], |
| 342 | ] ); |
| 343 | |
| 344 | // OpenAI Endpoints |
| 345 | register_rest_route( $this->namespace, '/openai/files/list', [ |
| 346 | 'methods' => 'GET', |
| 347 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 348 | 'callback' => [ $this, 'rest_openai_files_get' ], |
| 349 | ] ); |
| 350 | register_rest_route( $this->namespace, '/openai/files/upload', [ |
| 351 | 'methods' => 'POST', |
| 352 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 353 | 'callback' => [ $this, 'rest_openai_files_upload' ], |
| 354 | ] ); |
| 355 | register_rest_route( $this->namespace, '/openai/files/delete', [ |
| 356 | 'methods' => 'POST', |
| 357 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 358 | 'callback' => [ $this, 'rest_openai_files_delete' ], |
| 359 | ] ); |
| 360 | register_rest_route( $this->namespace, '/openai/files/download', [ |
| 361 | 'methods' => 'POST', |
| 362 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 363 | 'callback' => [ $this, 'rest_openai_files_download' ], |
| 364 | ] ); |
| 365 | // TODO: Remove all the /openai/finetunes/* and /openai/files/finetune routes after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06). |
| 366 | register_rest_route( $this->namespace, '/openai/files/finetune', [ |
| 367 | 'methods' => 'POST', |
| 368 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 369 | 'callback' => [ $this, 'rest_openai_files_finetune' ], |
| 370 | ] ); |
| 371 | register_rest_route( $this->namespace, '/openai/finetunes/list_deleted', [ |
| 372 | 'methods' => 'GET', |
| 373 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 374 | 'callback' => [ $this, 'rest_openai_deleted_finetunes_get' ], |
| 375 | ] ); |
| 376 | |
| 377 | // register_rest_route( $this->namespace, '/openai/models', array( |
| 378 | // 'methods' => 'GET', |
| 379 | // 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 380 | // 'callback' => [ $this, 'rest_openai_models_get' ], |
| 381 | // ) ); |
| 382 | |
| 383 | register_rest_route( $this->namespace, '/openai/finetunes/list', [ |
| 384 | 'methods' => 'GET', |
| 385 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 386 | 'callback' => [ $this, 'rest_openai_finetunes_get' ], |
| 387 | ] ); |
| 388 | register_rest_route( $this->namespace, '/openai/finetunes/delete', [ |
| 389 | 'methods' => 'POST', |
| 390 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 391 | 'callback' => [ $this, 'rest_openai_finetunes_delete' ], |
| 392 | ] ); |
| 393 | register_rest_route( $this->namespace, '/openai/finetunes/cancel', [ |
| 394 | 'methods' => 'POST', |
| 395 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 396 | 'callback' => [ $this, 'rest_openai_finetunes_cancel' ], |
| 397 | ] ); |
| 398 | |
| 399 | // Logging Endpoints |
| 400 | register_rest_route( $this->namespace, '/get_logs', [ |
| 401 | 'methods' => 'GET', |
| 402 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 403 | 'callback' => [ $this, 'rest_get_logs' ] |
| 404 | ] ); |
| 405 | register_rest_route( $this->namespace, '/clear_logs', [ |
| 406 | 'methods' => 'GET', |
| 407 | 'permission_callback' => [ $this->core, 'can_access_features' ], |
| 408 | 'callback' => [ $this, 'rest_clear_logs' ] |
| 409 | ] ); |
| 410 | |
| 411 | // Forms Endpoints |
| 412 | register_rest_route( $this->namespace, '/forms/list', [ |
| 413 | 'methods' => 'GET', |
| 414 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 415 | 'callback' => [ $this, 'rest_forms_list' ] |
| 416 | ] ); |
| 417 | register_rest_route( $this->namespace, '/forms/get', [ |
| 418 | 'methods' => 'GET', |
| 419 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 420 | 'callback' => [ $this, 'rest_forms_get' ] |
| 421 | ] ); |
| 422 | register_rest_route( $this->namespace, '/forms/create', [ |
| 423 | 'methods' => 'POST', |
| 424 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 425 | 'callback' => [ $this, 'rest_forms_create' ] |
| 426 | ] ); |
| 427 | register_rest_route( $this->namespace, '/forms/update', [ |
| 428 | 'methods' => 'POST', |
| 429 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 430 | 'callback' => [ $this, 'rest_forms_update' ] |
| 431 | ] ); |
| 432 | register_rest_route( $this->namespace, '/forms/delete', [ |
| 433 | 'methods' => 'POST', |
| 434 | 'permission_callback' => [ $this->core, 'can_access_settings' ], |
| 435 | 'callback' => [ $this, 'rest_forms_delete' ] |
| 436 | ] ); |
| 437 | } |
| 438 | catch ( Exception $e ) { |
| 439 | Meow_MWAI_Logging::error( 'REST API initialization failed: ' . $e->getMessage() ); |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | public function rest_start_session() { |
| 444 | try { |
| 445 | $sessionId = $this->core->get_session_id(); |
| 446 | $restNonce = $this->core->get_nonce( true ); |
| 447 | |
| 448 | $response = [ |
| 449 | 'success' => true, |
| 450 | 'sessionId' => $sessionId, |
| 451 | 'restNonce' => $restNonce |
| 452 | ]; |
| 453 | |
| 454 | // If in test mode and we have a new token, it will be added by create_rest_response |
| 455 | // But we also want to ensure the restNonce matches the test token if available |
| 456 | if ( get_option( 'mwai_token_test_mode' ) ) { |
| 457 | $token_data = get_option( 'mwai_test_token_data' ); |
| 458 | if ( $token_data && isset( $token_data['token'] ) ) { |
| 459 | $response['restNonce'] = $token_data['token']; |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | return $this->create_rest_response( $response, 200 ); |
| 464 | } |
| 465 | catch ( Exception $e ) { |
| 466 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 467 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 468 | } |
| 469 | } |
| 470 | |
| 471 | public function rest_settings_list() { |
| 472 | return $this->create_rest_response( [ |
| 473 | 'success' => true, |
| 474 | 'options' => $this->core->get_all_options() |
| 475 | ], 200 ); |
| 476 | } |
| 477 | |
| 478 | public function rest_helpers_cron_events( $request ) { |
| 479 | try { |
| 480 | // Only show AI Engine cron events (those starting with mwai_) |
| 481 | $cron_events = []; |
| 482 | $crons = _get_cron_array(); |
| 483 | |
| 484 | // Get transient data for last run status (we'll store this when crons run) |
| 485 | $last_run_data = get_transient( 'mwai_cron_last_run' ) ?: []; |
| 486 | |
| 487 | // Get all scheduled events and filter for AI Engine ones |
| 488 | foreach ( $crons as $timestamp => $cron ) { |
| 489 | foreach ( $cron as $hook => $details ) { |
| 490 | // Only process AI Engine hooks (starting with mwai_) |
| 491 | if ( strpos( $hook, 'mwai_' ) !== 0 ) { |
| 492 | continue; |
| 493 | } |
| 494 | |
| 495 | $schedule_key = array_keys( $details )[0]; |
| 496 | $schedule_info = $details[$schedule_key]; |
| 497 | |
| 498 | // Get schedule display name |
| 499 | $schedule = $schedule_info['schedule']; |
| 500 | $schedules = wp_get_schedules(); |
| 501 | $schedule_display = isset( $schedules[$schedule]['display'] ) ? |
| 502 | $schedules[$schedule]['display'] : $schedule; |
| 503 | |
| 504 | $event_info = [ |
| 505 | 'hook' => $hook, |
| 506 | 'name' => $this->get_cron_display_name( $hook ), |
| 507 | 'description' => $this->get_cron_description( $hook ), |
| 508 | 'next_run' => $timestamp, |
| 509 | 'next_run_human' => '', |
| 510 | 'last_run' => isset( $last_run_data[$hook]['time'] ) ? $last_run_data[$hook]['time'] : null, |
| 511 | 'last_run_human' => isset( $last_run_data[$hook]['time'] ) ? |
| 512 | human_time_diff( $last_run_data[$hook]['time'], time() ) . ' ago' : |
| 513 | 'Never', |
| 514 | 'last_status' => isset( $last_run_data[$hook]['status'] ) ? $last_run_data[$hook]['status'] : 'unknown', |
| 515 | 'schedule' => $schedule_display, |
| 516 | 'is_running' => false, |
| 517 | 'is_scheduled' => true |
| 518 | ]; |
| 519 | |
| 520 | // Calculate next run time properly |
| 521 | // If we have a last run time and schedule interval, calculate the actual next run |
| 522 | if ( isset( $last_run_data[$hook]['time'] ) && isset( $schedules[$schedule]['interval'] ) ) { |
| 523 | $interval = $schedules[$schedule]['interval']; |
| 524 | $last_run = $last_run_data[$hook]['time']; |
| 525 | $expected_next_run = $last_run + $interval; |
| 526 | |
| 527 | // If the scheduled timestamp is in the past but we ran recently, |
| 528 | // the next run should be based on the last actual run |
| 529 | if ( $timestamp < time() && |
| 530 | $last_run > ( time() - $interval ) ) { |
| 531 | // Cron ran recently, calculate next run from last run time |
| 532 | $event_info['next_run'] = $expected_next_run; |
| 533 | $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $expected_next_run ); |
| 534 | } |
| 535 | else if ( $timestamp < time() ) { |
| 536 | // Genuinely overdue |
| 537 | $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp ); |
| 538 | } |
| 539 | else { |
| 540 | // Future scheduled time |
| 541 | $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp ); |
| 542 | } |
| 543 | } |
| 544 | else { |
| 545 | // No last run data, use the scheduled timestamp but be conservative about "overdue" |
| 546 | if ( $timestamp < time() ) { |
| 547 | // Only show as overdue if it's significantly past due (more than the schedule interval) |
| 548 | // to avoid false positives for crons that might be running but not tracked |
| 549 | $time_past_due = time() - $timestamp; |
| 550 | $interval = isset( $schedules[$schedule]['interval'] ) ? $schedules[$schedule]['interval'] : 3600; // Default 1 hour |
| 551 | |
| 552 | if ( $time_past_due > $interval ) { |
| 553 | $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp ); |
| 554 | } |
| 555 | else { |
| 556 | $event_info['next_run_human'] = 'Due to run'; |
| 557 | } |
| 558 | } |
| 559 | else { |
| 560 | $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp ); |
| 561 | } |
| 562 | } |
| 563 | |
| 564 | // Check if currently running (via transient) |
| 565 | $running_transient = get_transient( 'mwai_cron_running_' . $hook ); |
| 566 | if ( $running_transient ) { |
| 567 | $event_info['is_running'] = true; |
| 568 | } |
| 569 | |
| 570 | $cron_events[] = $event_info; |
| 571 | } |
| 572 | } |
| 573 | |
| 574 | return $this->create_rest_response( [ 'success' => true, 'events' => $cron_events ], 200 ); |
| 575 | } |
| 576 | catch ( Exception $e ) { |
| 577 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 578 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 579 | } |
| 580 | } |
| 581 | |
| 582 | public function rest_helpers_run_cron( $request ) { |
| 583 | try { |
| 584 | $params = $request->get_json_params(); |
| 585 | $hook = isset( $params['hook'] ) ? $params['hook'] : null; |
| 586 | |
| 587 | if ( empty( $hook ) ) { |
| 588 | return $this->create_rest_response( [ 'success' => false, 'message' => 'No cron hook provided' ], 400 ); |
| 589 | } |
| 590 | |
| 591 | // Only allow running AI Engine crons (starting with mwai_) |
| 592 | if ( strpos( $hook, 'mwai_' ) !== 0 ) { |
| 593 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid cron hook' ], 400 ); |
| 594 | } |
| 595 | |
| 596 | // Prevent running the Tasks Runner hooks directly - they should only run via cron |
| 597 | if ( $hook === 'mwai_tasks_internal_run' || $hook === 'mwai_tasks_internal_dev_run' ) { |
| 598 | return $this->create_rest_response( [ |
| 599 | 'success' => false, |
| 600 | 'message' => 'The Tasks Runner cannot be triggered manually. It runs automatically based on its schedule.' |
| 601 | ], 403 ); |
| 602 | } |
| 603 | |
| 604 | // Check if the hook exists |
| 605 | if ( !has_action( $hook ) ) { |
| 606 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Cron hook not found' ], 404 ); |
| 607 | } |
| 608 | |
| 609 | // Run the cron action |
| 610 | do_action( $hook ); |
| 611 | |
| 612 | return $this->create_rest_response( [ |
| 613 | 'success' => true, |
| 614 | 'message' => 'Cron executed successfully', |
| 615 | 'hook' => $hook |
| 616 | ], 200 ); |
| 617 | } |
| 618 | catch ( Exception $e ) { |
| 619 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 620 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 621 | } |
| 622 | } |
| 623 | |
| 624 | private function get_cron_display_name( $hook ) { |
| 625 | $names = [ |
| 626 | 'mwai_tasks_internal_run' => 'Tasks Runner', |
| 627 | 'mwai_tasks_internal_dev_run' => 'Tasks Runner (Dev)', |
| 628 | 'mwai_cleanup_oauth' => 'OAuth Cleanup', |
| 629 | 'mwai_files_cleanup' => 'Files Cleanup', |
| 630 | 'mwai_discussions' => 'Discussions Cleanup' |
| 631 | ]; |
| 632 | return isset( $names[$hook] ) ? $names[$hook] : $hook; |
| 633 | } |
| 634 | |
| 635 | private function get_cron_description( $hook ) { |
| 636 | $descriptions = [ |
| 637 | 'mwai_tasks_internal_run' => 'Processes background tasks and queued operations.', |
| 638 | 'mwai_tasks_internal_dev_run' => 'Processes tasks in development mode (every 5 seconds).', |
| 639 | 'mwai_cleanup_oauth' => 'Cleans up expired OAuth tokens and sessions.', |
| 640 | 'mwai_files_cleanup' => 'Removes expired files based on expiration dates.', |
| 641 | 'mwai_discussions' => 'Maintains chat discussions database and removes old entries.' |
| 642 | ]; |
| 643 | return isset( $descriptions[$hook] ) ? $descriptions[$hook] : ''; |
| 644 | } |
| 645 | |
| 646 | public function rest_settings_update( $request ) { |
| 647 | try { |
| 648 | $params = $request->get_json_params(); |
| 649 | $value = $params['options']; |
| 650 | $options = $this->core->update_options( $value ); |
| 651 | $success = !!$options; |
| 652 | $message = __( $success ? 'OK' : 'Could not update options.', 'ai-engine' ); |
| 653 | return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 ); |
| 654 | } |
| 655 | catch ( Exception $e ) { |
| 656 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 657 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 658 | } |
| 659 | } |
| 660 | |
| 661 | public function rest_settings_reset() { |
| 662 | try { |
| 663 | $options = $this->core->reset_options(); |
| 664 | $success = !!$options; |
| 665 | $message = __( $success ? 'OK' : 'Could not reset options.', 'ai-engine' ); |
| 666 | return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 ); |
| 667 | } |
| 668 | catch ( Exception $e ) { |
| 669 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 670 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 671 | } |
| 672 | } |
| 673 | |
| 674 | public function rest_ai_models( $request ) { |
| 675 | try { |
| 676 | $params = $request->get_json_params(); |
| 677 | $envId = $params['envId']; |
| 678 | $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId ); |
| 679 | $models = $engine->retrieve_models(); |
| 680 | return $this->create_rest_response( [ 'success' => true, 'models' => $models ], 200 ); |
| 681 | } |
| 682 | catch ( Exception $e ) { |
| 683 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 684 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 685 | } |
| 686 | } |
| 687 | |
| 688 | public function rest_ai_test_connection( $request ) { |
| 689 | try { |
| 690 | $params = $request->get_json_params(); |
| 691 | $envId = $params['env_id']; |
| 692 | |
| 693 | // Get the environment details |
| 694 | $env = null; |
| 695 | $envs = $this->core->get_option( 'ai_envs' ); |
| 696 | foreach ( $envs as $e ) { |
| 697 | if ( $e['id'] === $envId ) { |
| 698 | $env = $e; |
| 699 | break; |
| 700 | } |
| 701 | } |
| 702 | |
| 703 | if ( !$env ) { |
| 704 | throw new Exception( __( 'Environment not found.', 'ai-engine' ) ); |
| 705 | } |
| 706 | |
| 707 | // Get the engine and test connection |
| 708 | $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId ); |
| 709 | $result = $engine->connection_check(); |
| 710 | |
| 711 | // Format the response based on provider |
| 712 | $response = [ |
| 713 | 'success' => true, |
| 714 | 'provider' => $env['type'], |
| 715 | 'name' => $env['name'], |
| 716 | 'data' => $result |
| 717 | ]; |
| 718 | |
| 719 | return $this->create_rest_response( $response, 200 ); |
| 720 | } |
| 721 | catch ( Exception $e ) { |
| 722 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 723 | return $this->create_rest_response( [ |
| 724 | 'success' => false, |
| 725 | 'error' => $message, |
| 726 | 'provider' => isset( $env ) ? $env['type'] : 'unknown' |
| 727 | ], 200 ); // Return 200 even on error for consistent modal display |
| 728 | } |
| 729 | } |
| 730 | |
| 731 | public function rest_ai_completions( $request ) { |
| 732 | try { |
| 733 | // can_access_features is Editor-and-up (and filterable), so the payload is not |
| 734 | // trusted: inject_params() honours a client-supplied apiKey for every query type, |
| 735 | // which would run the site's completions against a key of the caller's choosing. |
| 736 | $params = Meow_MWAI_Core::sanitize_rest_params( $request->get_json_params() ); |
| 737 | $message = $this->retrieve_message( $params ); |
| 738 | $query = new Meow_MWAI_Query_Text( $message ); |
| 739 | $query->inject_params( $params ); |
| 740 | |
| 741 | // Handle streaming |
| 742 | $stream = $params['stream'] ?? false; |
| 743 | $streamCallback = null; |
| 744 | if ( $stream ) { |
| 745 | $streamCallback = function ( $reply ) use ( $query ) { |
| 746 | //$raw = _wp_specialchars( $reply, ENT_NOQUOTES, 'UTF-8', true ); |
| 747 | $raw = $reply; |
| 748 | $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ], $query ); |
| 749 | if ( ob_get_level() > 0 ) { |
| 750 | ob_flush(); |
| 751 | } |
| 752 | flush(); |
| 753 | }; |
| 754 | if ( headers_sent( $filename, $linenum ) ) { |
| 755 | throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." ); |
| 756 | } |
| 757 | header( 'Cache-Control: no-cache' ); |
| 758 | header( 'Content-Type: text/event-stream' ); |
| 759 | header( 'X-Accel-Buffering: no' ); // This is useful to disable buffering in nginx through headers. |
| 760 | ob_implicit_flush( true ); |
| 761 | if ( ob_get_level() > 0 ) { |
| 762 | ob_end_flush(); |
| 763 | } |
| 764 | // Finish the request even if the client disconnects, so usage/credits are |
| 765 | // still recorded after the stream completes. Otherwise an abort mid-stream |
| 766 | // kills the script before accounting runs. See chat_submit for details. |
| 767 | ignore_user_abort( true ); |
| 768 | } |
| 769 | |
| 770 | // Process Reply |
| 771 | $reply = $this->core->run_query( $query, $streamCallback ); |
| 772 | $restRes = [ |
| 773 | 'success' => true, |
| 774 | 'data' => $reply->result, |
| 775 | 'usage' => $reply->usage |
| 776 | ]; |
| 777 | if ( $stream ) { |
| 778 | $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $restRes ) ], $query ); |
| 779 | die(); |
| 780 | } |
| 781 | return $this->create_rest_response( $restRes, 200 ); |
| 782 | } |
| 783 | catch ( Exception $e ) { |
| 784 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 785 | if ( $stream ) { |
| 786 | $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query ); |
| 787 | } |
| 788 | else { |
| 789 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 790 | } |
| 791 | } |
| 792 | } |
| 793 | |
| 794 | public function rest_ai_images( $request ) { |
| 795 | try { |
| 796 | // Same reasoning as rest_ai_completions(): Editor-reachable route, so the |
| 797 | // client-controlled apiKey override has to leave the array. |
| 798 | $params = Meow_MWAI_Core::sanitize_rest_params( $request->get_json_params() ); |
| 799 | $message = $this->retrieve_message( $params ); |
| 800 | $query = new Meow_MWAI_Query_Image( $message ); |
| 801 | $query->inject_params( $params ); |
| 802 | $reply = $this->core->run_query( $query ); |
| 803 | return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 ); |
| 804 | } |
| 805 | catch ( Exception $e ) { |
| 806 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 807 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 808 | } |
| 809 | } |
| 810 | |
| 811 | public function rest_ai_image_edit( $request ) { |
| 812 | try { |
| 813 | // Check if this is a multipart request with files |
| 814 | $files = $request->get_file_params(); |
| 815 | $params = null; |
| 816 | |
| 817 | // Debug logging |
| 818 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 819 | error_log( '[AI Engine Queries] Image Edit Request - Method: ' . $request->get_method() ); |
| 820 | $content_type = $request->get_content_type(); |
| 821 | if ( is_array( $content_type ) ) { |
| 822 | error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type['value'] ); |
| 823 | } |
| 824 | else { |
| 825 | error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type ); |
| 826 | } |
| 827 | error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes (' . count( $files ) . ')' : 'no' ) ); |
| 828 | } |
| 829 | |
| 830 | if ( !empty( $files ) ) { |
| 831 | // Handle multipart form data - get all params including POST data |
| 832 | $params = $request->get_params(); |
| 833 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 834 | error_log( '[AI Engine Queries] Image Edit Request - Using form data params' ); |
| 835 | } |
| 836 | } |
| 837 | else { |
| 838 | // Try to get body params first (for form data without files) |
| 839 | $body_params = $request->get_body_params(); |
| 840 | if ( !empty( $body_params ) ) { |
| 841 | $params = $body_params; |
| 842 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 843 | error_log( '[AI Engine Queries] Image Edit Request - Using body params' ); |
| 844 | } |
| 845 | } |
| 846 | else { |
| 847 | // Handle JSON request |
| 848 | $params = $request->get_json_params(); |
| 849 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 850 | error_log( '[AI Engine Queries] Image Edit Request - Using JSON params' ); |
| 851 | } |
| 852 | } |
| 853 | } |
| 854 | |
| 855 | // Ensure params is always an array, and strip the client-controlled keys. Done |
| 856 | // here rather than at each read above so both the multipart and the JSON branch |
| 857 | // are covered, and before the debug log so a blocked key is never written to it. |
| 858 | $params = Meow_MWAI_Core::sanitize_rest_params( $params ); |
| 859 | |
| 860 | // Debug logging |
| 861 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 862 | error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes' : 'no' ) ); |
| 863 | error_log( '[AI Engine Queries] Image Edit Request - Params: ' . json_encode( $params ) ); |
| 864 | } |
| 865 | |
| 866 | $message = $this->retrieve_message( $params ); |
| 867 | $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0; |
| 868 | $query = new Meow_MWAI_Query_EditImage( $message ); |
| 869 | |
| 870 | // The inject_params method will handle setting the file from mediaId |
| 871 | $query->inject_params( $params ); |
| 872 | |
| 873 | // Handle mask file if provided |
| 874 | if ( !empty( $files['mask'] ) ) { |
| 875 | $mask_file = $files['mask']; |
| 876 | if ( $mask_file['error'] === UPLOAD_ERR_OK ) { |
| 877 | $mask_data = file_get_contents( $mask_file['tmp_name'] ); |
| 878 | $query->set_mask( Meow_MWAI_Query_DroppedFile::from_data( $mask_data, 'analysis', $mask_file['type'] ) ); |
| 879 | } |
| 880 | } |
| 881 | |
| 882 | $reply = $this->core->run_query( $query ); |
| 883 | return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 ); |
| 884 | } |
| 885 | catch ( Exception $e ) { |
| 886 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 887 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 888 | } |
| 889 | } |
| 890 | |
| 891 | public function rest_ai_magic_wand( $request ) { |
| 892 | try { |
| 893 | $params = $request->get_json_params(); |
| 894 | $action = isset( $params['action'] ) ? $params['action'] : null; |
| 895 | $data = isset( $params['data'] ) ? $params['data'] : null; |
| 896 | if ( empty( $data ) || empty( $action ) ) { |
| 897 | return $this->create_rest_response( [ 'success' => false, 'message' => 'An action and some data are required.' ], 500 ); |
| 898 | } |
| 899 | $data = apply_filters( 'mwai_magic_wand_' . $action, '', $data ); |
| 900 | return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 ); |
| 901 | } |
| 902 | catch ( Exception $e ) { |
| 903 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 904 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 905 | } |
| 906 | } |
| 907 | |
| 908 | public function rest_ai_copilot( $request ) { |
| 909 | try { |
| 910 | $params = $request->get_json_params(); |
| 911 | $action = sanitize_text_field( $params['action'] ); |
| 912 | $message = $this->retrieve_message( $params, true ); |
| 913 | $context = sanitize_text_field( $params['context'] ); |
| 914 | $postId = !empty( $params['postId'] ) ? intval( $params['postId'] ) : null; |
| 915 | if ( empty( $action ) || empty( $message ) ) { |
| 916 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Copilot needs an action and a prompt.' ], 500 ); |
| 917 | } |
| 918 | |
| 919 | global $mwai; |
| 920 | $result = null; |
| 921 | $params = [ 'scope' => 'copilot' ]; |
| 922 | |
| 923 | if ( $action === 'text' ) { |
| 924 | $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."; |
| 925 | if ( !empty( $model ) ) { |
| 926 | $params['model'] = $model; |
| 927 | } |
| 928 | $result = $mwai->simpleTextQuery( $prompt, $params ); |
| 929 | } |
| 930 | else if ( $action === 'image' ) { |
| 931 | $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."; |
| 932 | |
| 933 | // Create the image |
| 934 | $simplifiedPrompt = $mwai->simpleTextQuery( $prompt, $params ); |
| 935 | $media = $mwai->imageQueryForMediaLibrary( $simplifiedPrompt, $params, $postId ); |
| 936 | $result = [ 'media' => $media ]; |
| 937 | } |
| 938 | return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 ); |
| 939 | } |
| 940 | catch ( Exception $e ) { |
| 941 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 942 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 943 | } |
| 944 | } |
| 945 | |
| 946 | public function rest_helpers_update_title( $request ) { |
| 947 | try { |
| 948 | $params = $request->get_json_params(); |
| 949 | $title = sanitize_text_field( $params['title'] ); |
| 950 | $postId = intval( $params['postId'] ); |
| 951 | $post = get_post( $postId ); |
| 952 | if ( !$post ) { |
| 953 | throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) ); |
| 954 | } |
| 955 | $post->post_title = $title; |
| 956 | //$post->post_name = sanitize_title( $title ); |
| 957 | wp_update_post( $post ); |
| 958 | return $this->create_rest_response( [ 'success' => true, 'message' => 'Title updated.' ], 200 ); |
| 959 | } |
| 960 | catch ( Exception $e ) { |
| 961 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 962 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 963 | } |
| 964 | } |
| 965 | |
| 966 | public function rest_helpers_update_excerpt( $request ) { |
| 967 | try { |
| 968 | $params = $request->get_json_params(); |
| 969 | $excerpt = sanitize_text_field( $params['excerpt'] ); |
| 970 | $postId = intval( $params['postId'] ); |
| 971 | $post = get_post( $postId ); |
| 972 | if ( !$post ) { |
| 973 | throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) ); |
| 974 | } |
| 975 | $post->post_excerpt = $excerpt; |
| 976 | wp_update_post( $post ); |
| 977 | return $this->create_rest_response( [ 'success' => true, 'message' => 'Excerpt updated.' ], 200 ); |
| 978 | } |
| 979 | catch ( Exception $e ) { |
| 980 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 981 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 982 | } |
| 983 | } |
| 984 | |
| 985 | public function rest_helpers_create_post( $request ) { |
| 986 | try { |
| 987 | $params = $request->get_json_params(); |
| 988 | $title = sanitize_text_field( $params['title'] ); |
| 989 | $content = sanitize_textarea_field( $params['content'] ); |
| 990 | $excerpt = sanitize_text_field( $params['excerpt'] ); |
| 991 | $postType = sanitize_text_field( $params['postType'] ); |
| 992 | $post = new stdClass(); |
| 993 | $post->post_title = $title; |
| 994 | $post->post_excerpt = $excerpt; |
| 995 | $post->post_content = $content; |
| 996 | $post->post_status = 'draft'; |
| 997 | $post->post_type = isset( $postType ) ? $postType : 'post'; |
| 998 | // TODO: Let's try to avoid using Markdown to create the Post |
| 999 | // Instead, we should create Gutenberg Blocks, or simple HTML. |
| 1000 | // Then, we can get rid of the library for Markdown. |
| 1001 | $post->post_content = $this->core->markdown_to_html( $post->post_content ); |
| 1002 | $postId = wp_insert_post( $post ); |
| 1003 | return $this->create_rest_response( [ 'success' => true, 'postId' => $postId ], 200 ); |
| 1004 | } |
| 1005 | catch ( Exception $e ) { |
| 1006 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1007 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1008 | } |
| 1009 | } |
| 1010 | |
| 1011 | public function rest_helpers_create_images( $request ) { |
| 1012 | try { |
| 1013 | $params = $request->get_json_params(); |
| 1014 | $title = sanitize_text_field( $params['title'] ); |
| 1015 | $caption = sanitize_text_field( $params['caption'] ); |
| 1016 | $alt = sanitize_text_field( $params['alt'] ); |
| 1017 | $description = sanitize_text_field( $params['description'] ); |
| 1018 | $url = $params['url']; |
| 1019 | $filename = sanitize_text_field( $params['filename'] ); |
| 1020 | |
| 1021 | // Prepare AI metadata |
| 1022 | $ai_metadata = []; |
| 1023 | if ( !empty( $params['model'] ) ) { |
| 1024 | $ai_metadata['model'] = $params['model']; |
| 1025 | } |
| 1026 | if ( !empty( $params['latency'] ) ) { |
| 1027 | $ai_metadata['latency'] = $params['latency']; |
| 1028 | } |
| 1029 | if ( !empty( $params['env_id'] ) ) { |
| 1030 | $ai_metadata['env_id'] = $params['env_id']; |
| 1031 | } |
| 1032 | |
| 1033 | // Debug logging |
| 1034 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 1035 | error_log( '[AI Engine] create_image metadata: ' . json_encode( $ai_metadata ) ); |
| 1036 | } |
| 1037 | |
| 1038 | // Create as mwai_image post type (draft image) |
| 1039 | $attachmentId = $this->core->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, null, 'inherit', 'mwai_image', $ai_metadata ); |
| 1040 | |
| 1041 | // Add to user's draft media |
| 1042 | $user_id = get_current_user_id(); |
| 1043 | $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true ); |
| 1044 | if ( !is_array( $draft_media ) ) { |
| 1045 | $draft_media = []; |
| 1046 | } |
| 1047 | $draft_media[] = [ |
| 1048 | 'attachment_id' => $attachmentId, |
| 1049 | 'type' => 'image', |
| 1050 | 'created_at' => time() |
| 1051 | ]; |
| 1052 | update_user_meta( $user_id, 'mwai_draft_media', $draft_media ); |
| 1053 | |
| 1054 | return $this->create_rest_response( [ 'success' => true, 'attachmentId' => $attachmentId ], 200 ); |
| 1055 | } |
| 1056 | catch ( Exception $e ) { |
| 1057 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1058 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1059 | } |
| 1060 | } |
| 1061 | |
| 1062 | public function rest_helpers_generate_image_meta( $request ) { |
| 1063 | try { |
| 1064 | global $mwai; |
| 1065 | $params = $request->get_json_params(); |
| 1066 | $attachment_id = isset( $params['attachmentId'] ) ? intval( $params['attachmentId'] ) : null; |
| 1067 | |
| 1068 | if ( empty( $attachment_id ) ) { |
| 1069 | throw new Exception( __( 'The attachment ID is required.', 'ai-engine' ) ); |
| 1070 | } |
| 1071 | |
| 1072 | // Get the file path from the attachment ID |
| 1073 | $file_path = get_attached_file( $attachment_id ); |
| 1074 | if ( empty( $file_path ) || !file_exists( $file_path ) ) { |
| 1075 | throw new Exception( __( 'Could not find the attachment file.', 'ai-engine' ) ); |
| 1076 | } |
| 1077 | |
| 1078 | $prompt = 'Describe this image and suggest a short title and description. ' |
| 1079 | . 'Also suggest an SEO-friendly filename (lowercase, ASCII characters only, with hyphens instead of spaces). ' |
| 1080 | . 'Return a JSON with the keys: title, description, filename.'; |
| 1081 | |
| 1082 | // Use file path instead of URL to avoid network issues |
| 1083 | $result = $mwai->simpleVisionQuery( $prompt, null, $file_path, [ 'scope' => 'admin-tools' ] ); |
| 1084 | $result = preg_replace( '/^```json\s*/', '', $result ); |
| 1085 | $result = preg_replace( '/\s*```$/', '', $result ); |
| 1086 | if ( is_string( $result ) ) { |
| 1087 | $data = json_decode( $result, true ); |
| 1088 | } |
| 1089 | else { |
| 1090 | $data = $result; |
| 1091 | } |
| 1092 | if ( !is_array( $data ) ) { |
| 1093 | $data = []; |
| 1094 | } |
| 1095 | $data = array_merge( [ 'title' => '', 'description' => '', 'filename' => '' ], $data ); |
| 1096 | return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 ); |
| 1097 | } |
| 1098 | catch ( Exception $e ) { |
| 1099 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1100 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1101 | } |
| 1102 | } |
| 1103 | |
| 1104 | public function rest_helpers_update_media_metadata( $request ) { |
| 1105 | try { |
| 1106 | $params = $request->get_json_params(); |
| 1107 | $attachment_id = intval( $params['attachmentId'] ); |
| 1108 | $title = sanitize_text_field( $params['title'] ?? '' ); |
| 1109 | $description = sanitize_text_field( $params['description'] ?? '' ); |
| 1110 | $caption = sanitize_text_field( $params['caption'] ?? '' ); |
| 1111 | $alt = sanitize_text_field( $params['alt'] ?? '' ); |
| 1112 | $filename = sanitize_file_name( $params['filename'] ?? '' ); |
| 1113 | |
| 1114 | if ( !$attachment_id ) { |
| 1115 | throw new Exception( __( 'Attachment ID is required.', 'ai-engine' ) ); |
| 1116 | } |
| 1117 | |
| 1118 | // Generate slug from filename (without extension) |
| 1119 | $slug = ''; |
| 1120 | if ( !empty( $filename ) ) { |
| 1121 | $slug = pathinfo( $filename, PATHINFO_FILENAME ); |
| 1122 | } |
| 1123 | |
| 1124 | // Update post title, content (description), caption, and slug |
| 1125 | $update_data = [ |
| 1126 | 'ID' => $attachment_id, |
| 1127 | 'post_title' => $title, |
| 1128 | 'post_content' => $description, |
| 1129 | 'post_excerpt' => $caption |
| 1130 | ]; |
| 1131 | |
| 1132 | if ( !empty( $slug ) ) { |
| 1133 | $update_data['post_name'] = $slug; |
| 1134 | } |
| 1135 | |
| 1136 | wp_update_post( $update_data ); |
| 1137 | |
| 1138 | // Update alt text |
| 1139 | if ( !empty( $alt ) ) { |
| 1140 | update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt ); |
| 1141 | } |
| 1142 | |
| 1143 | // Update filename if provided |
| 1144 | $new_url = null; |
| 1145 | if ( !empty( $filename ) ) { |
| 1146 | $file_path = get_attached_file( $attachment_id ); |
| 1147 | if ( $file_path ) { |
| 1148 | // Security: Validate file extension to prevent arbitrary file upload attacks |
| 1149 | $original_ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) ); |
| 1150 | $new_ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); |
| 1151 | |
| 1152 | // Allowlist of safe media extensions (no executable types) |
| 1153 | $allowed_extensions = [ |
| 1154 | 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif', |
| 1155 | 'mp4', 'webm', 'ogg', 'mov', 'avi', 'wmv', 'flv', 'm4v', |
| 1156 | 'mp3', 'wav', 'flac', 'aac', 'm4a', 'wma', |
| 1157 | 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'rtf' |
| 1158 | ]; |
| 1159 | |
| 1160 | // Extension must be in allowlist AND match original extension |
| 1161 | if ( !in_array( $new_ext, $allowed_extensions, true ) ) { |
| 1162 | throw new Exception( __( 'Invalid file extension. Only media file extensions are allowed.', 'ai-engine' ) ); |
| 1163 | } |
| 1164 | if ( $new_ext !== $original_ext ) { |
| 1165 | throw new Exception( __( 'File extension must match the original file type.', 'ai-engine' ) ); |
| 1166 | } |
| 1167 | |
| 1168 | $path_parts = pathinfo( $file_path ); |
| 1169 | $new_file_path = $path_parts['dirname'] . '/' . $filename; |
| 1170 | if ( rename( $file_path, $new_file_path ) ) { |
| 1171 | update_attached_file( $attachment_id, $new_file_path ); |
| 1172 | // Build new URL from file path for custom post types |
| 1173 | $upload_dir = wp_upload_dir(); |
| 1174 | $new_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $new_file_path ); |
| 1175 | } |
| 1176 | } |
| 1177 | } |
| 1178 | |
| 1179 | $response = [ 'success' => true ]; |
| 1180 | if ( $new_url ) { |
| 1181 | $response['url'] = $new_url; |
| 1182 | } |
| 1183 | |
| 1184 | return $this->create_rest_response( $response, 200 ); |
| 1185 | } |
| 1186 | catch ( Exception $e ) { |
| 1187 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1188 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1189 | } |
| 1190 | } |
| 1191 | |
| 1192 | public function rest_openai_files_get() { |
| 1193 | try { |
| 1194 | $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null; |
| 1195 | $purposeFilter = isset( $_GET['purpose'] ) ? $_GET['purpose'] : null; |
| 1196 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1197 | $files = $openai->list_files( $purposeFilter ); |
| 1198 | return $this->create_rest_response( [ 'success' => true, 'files' => $files ], 200 ); |
| 1199 | } |
| 1200 | catch ( Exception $e ) { |
| 1201 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1202 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1203 | } |
| 1204 | } |
| 1205 | |
| 1206 | // TODO: Remove all the rest_openai_*finetune* handlers below after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06). |
| 1207 | public function rest_openai_deleted_finetunes_get() { |
| 1208 | try { |
| 1209 | $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null; |
| 1210 | $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false; |
| 1211 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1212 | $finetunes = $openai->list_deleted_finetunes( $legacy ); |
| 1213 | return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 ); |
| 1214 | } |
| 1215 | catch ( Exception $e ) { |
| 1216 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1217 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1218 | } |
| 1219 | } |
| 1220 | |
| 1221 | public function rest_openai_finetunes_get() { |
| 1222 | try { |
| 1223 | $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null; |
| 1224 | $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false; |
| 1225 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1226 | $finetunes = $openai->list_finetunes( $legacy ); |
| 1227 | return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 ); |
| 1228 | } |
| 1229 | catch ( Exception $e ) { |
| 1230 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1231 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1232 | } |
| 1233 | } |
| 1234 | |
| 1235 | public function rest_openai_files_upload( $request ) { |
| 1236 | try { |
| 1237 | $params = $request->get_json_params(); |
| 1238 | $envId = $params['envId']; |
| 1239 | ; |
| 1240 | $filename = sanitize_text_field( $params['filename'] ); |
| 1241 | $data = $params['data']; |
| 1242 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1243 | $file = $openai->upload_file( $filename, $data ); |
| 1244 | return $this->create_rest_response( [ 'success' => true, 'file' => $file ], 200 ); |
| 1245 | } |
| 1246 | catch ( Exception $e ) { |
| 1247 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1248 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1249 | } |
| 1250 | } |
| 1251 | |
| 1252 | public function rest_openai_files_delete( $request ) { |
| 1253 | try { |
| 1254 | $params = $request->get_json_params(); |
| 1255 | $envId = $params['envId']; |
| 1256 | ; |
| 1257 | $fileId = $params['fileId']; |
| 1258 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1259 | $openai->delete_file( $fileId ); |
| 1260 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 1261 | } |
| 1262 | catch ( Exception $e ) { |
| 1263 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1264 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1265 | } |
| 1266 | } |
| 1267 | |
| 1268 | public function rest_openai_finetunes_cancel( $request ) { |
| 1269 | try { |
| 1270 | $params = $request->get_json_params(); |
| 1271 | $envId = $params['envId']; |
| 1272 | ; |
| 1273 | $finetuneId = $params['finetuneId']; |
| 1274 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1275 | $openai->cancel_finetune( $finetuneId ); |
| 1276 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 1277 | } |
| 1278 | catch ( Exception $e ) { |
| 1279 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1280 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1281 | } |
| 1282 | } |
| 1283 | |
| 1284 | public function rest_openai_finetunes_delete( $request ) { |
| 1285 | try { |
| 1286 | $params = $request->get_json_params(); |
| 1287 | $envId = $params['envId']; |
| 1288 | ; |
| 1289 | $modelId = $params['modelId']; |
| 1290 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1291 | $openai->delete_finetune( $modelId ); |
| 1292 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 1293 | } |
| 1294 | catch ( Exception $e ) { |
| 1295 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1296 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1297 | } |
| 1298 | } |
| 1299 | |
| 1300 | public function rest_openai_files_download( $request ) { |
| 1301 | try { |
| 1302 | $params = $request->get_json_params(); |
| 1303 | $envId = $params['envId']; |
| 1304 | ; |
| 1305 | $fileId = $params['fileId']; |
| 1306 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1307 | $filename = $openai->download_file( $fileId ); |
| 1308 | $data = file_get_contents( $filename ); |
| 1309 | return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 ); |
| 1310 | } |
| 1311 | catch ( Exception $e ) { |
| 1312 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1313 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1314 | } |
| 1315 | } |
| 1316 | |
| 1317 | public function rest_openai_files_finetune( $request ) { |
| 1318 | try { |
| 1319 | $params = $request->get_json_params(); |
| 1320 | $envId = $params['envId']; |
| 1321 | ; |
| 1322 | $fileId = $params['fileId']; |
| 1323 | $model = $params['model']; |
| 1324 | $suffix = $params['suffix']; |
| 1325 | $hyperparams = [ |
| 1326 | 'nEpochs' => isset( $params['nEpochs'] ) ? $params['nEpochs'] : null, |
| 1327 | 'batchSize' => isset( $params['batchSize'] ) ? $params['batchSize'] : null, |
| 1328 | ]; |
| 1329 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1330 | $finetune = $openai->run_finetune( $fileId, $model, $suffix, $hyperparams ); |
| 1331 | return $this->create_rest_response( [ 'success' => true, 'finetune' => $finetune ], 200 ); |
| 1332 | } |
| 1333 | catch ( Exception $e ) { |
| 1334 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1335 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1336 | } |
| 1337 | } |
| 1338 | |
| 1339 | /** |
| 1340 | * Term joins mirroring the Sync settings, shared by the Push All count and the |
| 1341 | * Push All id list so the two never disagree. |
| 1342 | * |
| 1343 | * Push All must select exactly what Sync would maintain. Otherwise it seeds the |
| 1344 | * index with posts the lifecycle hooks never update or remove: a site syncing |
| 1345 | * only 'fr' was pushing every language. Semantics match |
| 1346 | * MeowPro_MWAI_Embeddings: an empty list means no filtering, a non-empty one |
| 1347 | * requires at least one match. The joins go before the WHERE clause, so their |
| 1348 | * arguments come first in the prepare() call. |
| 1349 | */ |
| 1350 | private function sync_term_joins( $params, &$joinArgs ) { |
| 1351 | global $wpdb; |
| 1352 | $joinArgs = []; |
| 1353 | $joins = ''; |
| 1354 | $csv = function ( $key ) use ( $params ) { |
| 1355 | if ( empty( $params[$key] ) ) { |
| 1356 | return []; |
| 1357 | } |
| 1358 | return array_values( array_filter( array_map( 'trim', explode( ',', $params[$key] ) ) ) ); |
| 1359 | }; |
| 1360 | $termJoin = function ( $alias, $taxonomy, $slugs ) use ( $wpdb, &$joins, &$joinArgs ) { |
| 1361 | $placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); |
| 1362 | $joins .= " INNER JOIN {$wpdb->term_relationships} tr_{$alias}" |
| 1363 | . " ON tr_{$alias}.object_id = p.ID" |
| 1364 | . " INNER JOIN {$wpdb->term_taxonomy} tt_{$alias}" |
| 1365 | . " ON tt_{$alias}.term_taxonomy_id = tr_{$alias}.term_taxonomy_id" |
| 1366 | . " AND tt_{$alias}.taxonomy = '{$taxonomy}'" |
| 1367 | . " INNER JOIN {$wpdb->terms} t_{$alias}" |
| 1368 | . " ON t_{$alias}.term_id = tt_{$alias}.term_id AND t_{$alias}.slug IN ({$placeholders})"; |
| 1369 | $joinArgs = array_merge( $joinArgs, $slugs ); |
| 1370 | }; |
| 1371 | $postCategories = $csv( 'postCategories' ); |
| 1372 | if ( !empty( $postCategories ) ) { |
| 1373 | $termJoin( 'cat', 'category', $postCategories ); |
| 1374 | } |
| 1375 | // Polylang stores the language as a 'language' term whose slug is the code, |
| 1376 | // which is what pll_get_post_language() returns. Without Polylang there is |
| 1377 | // nothing to filter on, exactly like is_post_language_synced(). |
| 1378 | $postLanguages = $csv( 'postLanguages' ); |
| 1379 | if ( !empty( $postLanguages ) && taxonomy_exists( 'language' ) ) { |
| 1380 | $termJoin( 'lang', 'language', $postLanguages ); |
| 1381 | } |
| 1382 | return $joins; |
| 1383 | } |
| 1384 | |
| 1385 | public function rest_helpers_count_posts( $request ) { |
| 1386 | try { |
| 1387 | global $wpdb; |
| 1388 | $params = $request->get_query_params(); |
| 1389 | $postType = $params['postType']; |
| 1390 | $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ]; |
| 1391 | $joinArgs = []; |
| 1392 | $joins = $this->sync_term_joins( $params, $joinArgs ); |
| 1393 | $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) ); |
| 1394 | $ignored_ids = $wpdb->get_col( |
| 1395 | "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_mwai_embedding_ignore'" |
| 1396 | ); |
| 1397 | $exclude_sql = ''; |
| 1398 | if ( !empty( $ignored_ids ) ) { |
| 1399 | $ignored_ids = array_map( 'intval', $ignored_ids ); |
| 1400 | $exclude_sql = ' AND p.ID NOT IN (' . implode( ',', $ignored_ids ) . ')'; |
| 1401 | } |
| 1402 | $mimeFilter = ''; |
| 1403 | if ( $postType === 'attachment' ) { |
| 1404 | $mimeFilter = " AND p.post_mime_type LIKE 'image/%'"; |
| 1405 | } |
| 1406 | // COUNT(DISTINCT) because a post matching several joined terms would repeat. |
| 1407 | $query = "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p" . $joins . " |
| 1408 | WHERE p.post_type = %s |
| 1409 | AND p.post_status IN ($statusPlaceholders)" . $exclude_sql . $mimeFilter; |
| 1410 | $prepareArgs = array_merge( $joinArgs, [ $postType ], $postStatus ); |
| 1411 | $count = (int) $wpdb->get_var( $wpdb->prepare( $query, ...$prepareArgs ) ); |
| 1412 | return $this->create_rest_response( [ 'success' => true, 'count' => $count ], 200 ); |
| 1413 | } |
| 1414 | catch ( Exception $e ) { |
| 1415 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1416 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1417 | } |
| 1418 | } |
| 1419 | |
| 1420 | public function rest_helpers_posts_ids( $request ) { |
| 1421 | try { |
| 1422 | global $wpdb; |
| 1423 | $params = $request->get_query_params(); |
| 1424 | $postType = $params['postType']; |
| 1425 | $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ]; |
| 1426 | $joinArgs = []; |
| 1427 | $joins = $this->sync_term_joins( $params, $joinArgs ); |
| 1428 | |
| 1429 | // Use direct SQL query instead of get_posts to avoid memory issues with large sites |
| 1430 | $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) ); |
| 1431 | $ignored_ids = $wpdb->get_col( |
| 1432 | "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_mwai_embedding_ignore'" |
| 1433 | ); |
| 1434 | $exclude_sql = ''; |
| 1435 | if ( !empty( $ignored_ids ) ) { |
| 1436 | $ignored_ids = array_map( 'intval', $ignored_ids ); |
| 1437 | $exclude_sql = ' AND p.ID NOT IN (' . implode( ',', $ignored_ids ) . ')'; |
| 1438 | } |
| 1439 | $mimeFilter = ''; |
| 1440 | if ( $postType === 'attachment' ) { |
| 1441 | $mimeFilter = " AND p.post_mime_type LIKE 'image/%'"; |
| 1442 | } |
| 1443 | // DISTINCT because a post matching several of the joined terms would repeat. |
| 1444 | $query = "SELECT DISTINCT p.ID FROM {$wpdb->posts} p" . $joins . " |
| 1445 | WHERE p.post_type = %s |
| 1446 | AND p.post_status IN ($statusPlaceholders)" . $exclude_sql . $mimeFilter . ' |
| 1447 | ORDER BY p.ID ASC'; |
| 1448 | |
| 1449 | $prepareArgs = array_merge( $joinArgs, [ $postType ], $postStatus ); |
| 1450 | $postIds = $wpdb->get_col( $wpdb->prepare( $query, ...$prepareArgs ) ); |
| 1451 | $postIds = array_map( 'intval', $postIds ); |
| 1452 | |
| 1453 | return $this->create_rest_response( [ 'success' => true, 'postIds' => $postIds ], 200 ); |
| 1454 | } |
| 1455 | catch ( Exception $e ) { |
| 1456 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1457 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1458 | } |
| 1459 | } |
| 1460 | |
| 1461 | public function rest_helpers_post_content( $request ) { |
| 1462 | try { |
| 1463 | $params = $request->get_query_params(); |
| 1464 | $offset = (int) $params['offset']; |
| 1465 | $postType = $params['postType']; |
| 1466 | $postStatus = isset( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ]; |
| 1467 | $postId = (int) $params['postId']; |
| 1468 | |
| 1469 | $post = null; |
| 1470 | if ( !empty( $postId ) ) { |
| 1471 | $post = get_post( $postId ); |
| 1472 | if ( $post->post_status !== 'publish' && $post->post_status !== 'future' |
| 1473 | && $post->post_status !== 'draft' && $post->post_status !== 'private' ) { |
| 1474 | $post = null; |
| 1475 | } |
| 1476 | } |
| 1477 | else { |
| 1478 | $posts = get_posts( [ |
| 1479 | 'posts_per_page' => 1, |
| 1480 | 'post_type' => $postType, |
| 1481 | 'offset' => $offset, |
| 1482 | 'post_status' => $postStatus, |
| 1483 | ] ); |
| 1484 | $post = count( $posts ) === 0 ? null : $posts[0]; |
| 1485 | } |
| 1486 | if ( !$post ) { |
| 1487 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Post not found' ], 404 ); |
| 1488 | } |
| 1489 | $cleanPost = $this->core->get_post( $post ); |
| 1490 | return $this->create_rest_response( [ 'success' => true, 'content' => $cleanPost['content'], |
| 1491 | 'checksum' => $cleanPost['checksum'], 'language' => $cleanPost['language'], 'excerpt' => $cleanPost['excerpt'], |
| 1492 | 'postId' => $cleanPost['postId'], 'title' => $cleanPost['title'], 'url' => $cleanPost['url'] ], 200 ); |
| 1493 | } |
| 1494 | catch ( Exception $e ) { |
| 1495 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1496 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1497 | } |
| 1498 | } |
| 1499 | |
| 1500 | // Batch check which posts have content (for Push All optimization) |
| 1501 | public function rest_helpers_check_posts_content( $request ) { |
| 1502 | try { |
| 1503 | $params = $request->get_json_params(); |
| 1504 | $postIds = isset( $params['postIds'] ) ? $params['postIds'] : []; |
| 1505 | |
| 1506 | if ( empty( $postIds ) || !is_array( $postIds ) ) { |
| 1507 | return $this->create_rest_response( [ |
| 1508 | 'success' => false, |
| 1509 | 'message' => 'postIds array is required' |
| 1510 | ], 400 ); |
| 1511 | } |
| 1512 | |
| 1513 | // Sanitize post IDs |
| 1514 | $postIds = array_map( 'intval', $postIds ); |
| 1515 | |
| 1516 | // Check content using the mwai_pre_post_content filter to support page builders, |
| 1517 | // ACF, and other plugins that store content outside of post_content |
| 1518 | $postsWithContent = []; |
| 1519 | |
| 1520 | foreach ( $postIds as $postId ) { |
| 1521 | $post = get_post( $postId ); |
| 1522 | if ( !$post ) { |
| 1523 | continue; |
| 1524 | } |
| 1525 | // Apply the same filter used by get_post_content() in core.php |
| 1526 | $content = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId ); |
| 1527 | $content = trim( strip_tags( $content ) ); |
| 1528 | if ( !empty( $content ) ) { |
| 1529 | $postsWithContent[] = $postId; |
| 1530 | } |
| 1531 | } |
| 1532 | |
| 1533 | return $this->create_rest_response( [ |
| 1534 | 'success' => true, |
| 1535 | 'postsWithContent' => $postsWithContent |
| 1536 | ], 200 ); |
| 1537 | } |
| 1538 | catch ( Exception $e ) { |
| 1539 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1540 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1541 | } |
| 1542 | } |
| 1543 | |
| 1544 | public function rest_helpers_run_tasks( $request ) { |
| 1545 | try { |
| 1546 | // Prevent concurrent execution with a transient lock |
| 1547 | $lock_key = 'mwai_rest_run_tasks_lock'; |
| 1548 | if ( get_transient( $lock_key ) ) { |
| 1549 | // Log excessive calls for debugging |
| 1550 | if ( $this->core->get_option( 'dev_mode' ) ) { |
| 1551 | error_log( '[AI Engine] WARNING: rest_helpers_run_tasks called while already running' ); |
| 1552 | } |
| 1553 | return $this->create_rest_response( [ |
| 1554 | 'success' => false, |
| 1555 | 'message' => 'Tasks are already running. Please wait.' |
| 1556 | ], 429 ); // 429 Too Many Requests |
| 1557 | } |
| 1558 | |
| 1559 | // Set lock for 30 seconds |
| 1560 | set_transient( $lock_key, true, 30 ); |
| 1561 | |
| 1562 | // Log task execution start |
| 1563 | if ( $this->core->get_option( 'dev_mode' ) ) { |
| 1564 | error_log( '[AI Engine] rest_helpers_run_tasks triggered via REST API' ); |
| 1565 | } |
| 1566 | |
| 1567 | try { |
| 1568 | do_action( 'mwai_tasks_run' ); |
| 1569 | delete_transient( $lock_key ); |
| 1570 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 1571 | } |
| 1572 | catch ( Exception $e ) { |
| 1573 | delete_transient( $lock_key ); |
| 1574 | throw $e; |
| 1575 | } |
| 1576 | } |
| 1577 | catch ( Exception $e ) { |
| 1578 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1579 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1580 | } |
| 1581 | } |
| 1582 | |
| 1583 | public function rest_helpers_optimize_database( $request ) { |
| 1584 | try { |
| 1585 | global $wpdb; |
| 1586 | $results = []; |
| 1587 | |
| 1588 | // Add indexes to optimize query performance |
| 1589 | $indexes = [ |
| 1590 | // mwai_logs indexes |
| 1591 | [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time', 'columns' => 'time' ], |
| 1592 | [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_userId', 'columns' => 'userId' ], |
| 1593 | [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_envId', 'columns' => 'envId' ], |
| 1594 | [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_refId', 'columns' => 'refId' ], |
| 1595 | [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time_model', 'columns' => 'time, model' ], |
| 1596 | |
| 1597 | // mwai_logmeta indexes |
| 1598 | [ 'table' => 'mwai_logmeta', 'name' => 'idx_mwai_logmeta_log_id', 'columns' => 'log_id' ], |
| 1599 | |
| 1600 | // mwai_vectors indexes |
| 1601 | [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_envId_status_dbId', 'columns' => 'envId, status, dbId' ], |
| 1602 | [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_refId', 'columns' => 'refId' ], |
| 1603 | [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_status', 'columns' => 'status' ], |
| 1604 | [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_updated', 'columns' => 'updated' ], |
| 1605 | |
| 1606 | // mwai_files indexes |
| 1607 | [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_expires', 'columns' => 'expires' ], |
| 1608 | [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_userId', 'columns' => 'userId' ], |
| 1609 | [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_purpose', 'columns' => 'purpose' ], |
| 1610 | |
| 1611 | // mwai_filemeta indexes |
| 1612 | [ 'table' => 'mwai_filemeta', 'name' => 'idx_mwai_filemeta_file_id', 'columns' => 'file_id' ], |
| 1613 | |
| 1614 | // mwai_chats indexes |
| 1615 | [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_botId', 'columns' => 'chatId, botId' ], |
| 1616 | [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_userId', 'columns' => 'chatId, userId' ], |
| 1617 | [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_updated', 'columns' => 'updated' ], |
| 1618 | ]; |
| 1619 | |
| 1620 | // Add indexes |
| 1621 | foreach ( $indexes as $index ) { |
| 1622 | $table = $wpdb->prefix . $index['table']; |
| 1623 | $index_name = $index['name']; |
| 1624 | $columns = $index['columns']; |
| 1625 | |
| 1626 | // Check if index already exists |
| 1627 | $existing = $wpdb->get_var( $wpdb->prepare( |
| 1628 | 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS |
| 1629 | WHERE table_schema = %s AND table_name = %s AND index_name = %s', |
| 1630 | DB_NAME, |
| 1631 | $table, |
| 1632 | $index_name |
| 1633 | ) ); |
| 1634 | |
| 1635 | if ( !$existing ) { |
| 1636 | $wpdb->query( "ALTER TABLE `$table` ADD INDEX `$index_name` ($columns)" ); |
| 1637 | $results[] = "Added index $index_name on $table"; |
| 1638 | } |
| 1639 | } |
| 1640 | |
| 1641 | // Clean up old logs (older than 3 months) |
| 1642 | $three_months_ago = date( 'Y-m-d H:i:s', strtotime( '-3 months' ) ); |
| 1643 | |
| 1644 | // Delete old logs |
| 1645 | $deleted_logs = $wpdb->query( $wpdb->prepare( |
| 1646 | "DELETE FROM {$wpdb->prefix}mwai_logs WHERE time < %s", |
| 1647 | $three_months_ago |
| 1648 | ) ); |
| 1649 | $results[] = "Deleted $deleted_logs old log entries"; |
| 1650 | |
| 1651 | // Delete orphaned logmeta |
| 1652 | $deleted_logmeta = $wpdb->query( |
| 1653 | "DELETE lm FROM {$wpdb->prefix}mwai_logmeta lm |
| 1654 | LEFT JOIN {$wpdb->prefix}mwai_logs l ON lm.log_id = l.id |
| 1655 | WHERE l.id IS NULL" |
| 1656 | ); |
| 1657 | $results[] = "Deleted $deleted_logmeta orphaned logmeta entries"; |
| 1658 | |
| 1659 | // Delete old chats (older than 3 months) |
| 1660 | $deleted_chats = $wpdb->query( $wpdb->prepare( |
| 1661 | "DELETE FROM {$wpdb->prefix}mwai_chats WHERE updated < %s", |
| 1662 | $three_months_ago |
| 1663 | ) ); |
| 1664 | $results[] = "Deleted $deleted_chats old chat discussions"; |
| 1665 | |
| 1666 | // Optimize tables |
| 1667 | $tables = [ 'mwai_logs', 'mwai_logmeta', 'mwai_vectors', 'mwai_files', 'mwai_filemeta', 'mwai_chats' ]; |
| 1668 | foreach ( $tables as $table ) { |
| 1669 | $wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}$table" ); |
| 1670 | } |
| 1671 | $results[] = 'Optimized all AI Engine tables'; |
| 1672 | |
| 1673 | $message = implode( "\n", $results ); |
| 1674 | return $this->create_rest_response( [ 'success' => true, 'message' => $message ], 200 ); |
| 1675 | } |
| 1676 | catch ( Exception $e ) { |
| 1677 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1678 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1679 | } |
| 1680 | } |
| 1681 | |
| 1682 | public function rest_system_templates_get( $request ) { |
| 1683 | try { |
| 1684 | $params = $request->get_query_params(); |
| 1685 | $category = $params['category']; |
| 1686 | $templates = []; |
| 1687 | $templates_option = get_option( 'mwai_templates', [] ); |
| 1688 | if ( !is_array( $templates_option ) ) { |
| 1689 | update_option( 'mwai_templates', [] ); |
| 1690 | $templates_option = []; |
| 1691 | } |
| 1692 | |
| 1693 | // Migration: DALL-E was removed (deprecated by OpenAI). Move templates to the image fallback. |
| 1694 | // TODO: Remove after 2027-04 (1 year after the shutdown on 2026-05-12). |
| 1695 | $deprecated = [ 'dall-e', 'dall-e-2', 'dall-e-3', 'dall-e-3-hd' ]; |
| 1696 | $migrated = false; |
| 1697 | foreach ( $templates_option as &$group ) { |
| 1698 | if ( !empty( $group['templates'] ) && is_array( $group['templates'] ) ) { |
| 1699 | foreach ( $group['templates'] as &$template ) { |
| 1700 | if ( isset( $template['model'] ) && in_array( $template['model'], $deprecated, true ) ) { |
| 1701 | $template['model'] = MWAI_FALLBACK_MODEL_IMAGES; |
| 1702 | $migrated = true; |
| 1703 | } |
| 1704 | } |
| 1705 | } |
| 1706 | } |
| 1707 | unset( $group, $template ); |
| 1708 | if ( $migrated ) { |
| 1709 | update_option( 'mwai_templates', $templates_option ); |
| 1710 | } |
| 1711 | |
| 1712 | $categories = array_column( $templates_option, 'category' ); |
| 1713 | $index = array_search( $category, $categories ); |
| 1714 | $templates = []; |
| 1715 | if ( $index !== false ) { |
| 1716 | $templates = $templates_option[$index]['templates']; |
| 1717 | } |
| 1718 | return $this->create_rest_response( [ 'success' => true, 'templates' => $templates ], 200 ); |
| 1719 | } |
| 1720 | catch ( Exception $e ) { |
| 1721 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1722 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1723 | } |
| 1724 | } |
| 1725 | |
| 1726 | public function rest_system_templates_save( $request ) { |
| 1727 | try { |
| 1728 | $params = $request->get_json_params(); |
| 1729 | $category = $params['category']; |
| 1730 | $templates = $params['templates']; |
| 1731 | $templates_option = get_option( 'mwai_templates', [] ); |
| 1732 | $categories = array_column( $templates_option, 'category' ); |
| 1733 | $index = array_search( $category, $categories ); |
| 1734 | if ( $index !== false && $index >= 0 ) { |
| 1735 | $templates_option[$index]['templates'] = $templates; |
| 1736 | } |
| 1737 | else { |
| 1738 | $group = [ 'category' => $category, 'templates' => $templates ]; |
| 1739 | $templates_option[] = $group; |
| 1740 | } |
| 1741 | |
| 1742 | update_option( 'mwai_templates', $templates_option ); |
| 1743 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 1744 | } |
| 1745 | catch ( Exception $e ) { |
| 1746 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1747 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1748 | } |
| 1749 | } |
| 1750 | |
| 1751 | public function rest_system_logs_list( $request ) { |
| 1752 | try { |
| 1753 | $params = $request->get_json_params(); |
| 1754 | $offset = $params['offset']; |
| 1755 | $limit = $params['limit']; |
| 1756 | $filters = $params['filters']; |
| 1757 | $sort = isset( $params['sort'] ) ? $params['sort'] : null; |
| 1758 | $logs = apply_filters( 'mwai_stats_logs_list', [], $offset, $limit, $filters, $sort ); |
| 1759 | return $this->create_rest_response( [ 'success' => true, 'total' => $logs['total'], 'logs' => $logs['rows'] ], 200 ); |
| 1760 | } |
| 1761 | catch ( Exception $e ) { |
| 1762 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1763 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1764 | } |
| 1765 | } |
| 1766 | |
| 1767 | public function rest_system_logs_delete( $request ) { |
| 1768 | try { |
| 1769 | $params = $request->get_json_params(); |
| 1770 | $logIds = $params['logIds']; |
| 1771 | $success = apply_filters( 'mwai_stats_logs_delete', true, $logIds ); |
| 1772 | return $this->create_rest_response( [ 'success' => $success ], 200 ); |
| 1773 | } |
| 1774 | catch ( Exception $e ) { |
| 1775 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1776 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1777 | } |
| 1778 | } |
| 1779 | |
| 1780 | public function rest_system_logs_meta_get( $request ) { |
| 1781 | try { |
| 1782 | $params = $request->get_json_params(); |
| 1783 | $logId = $params['logId']; |
| 1784 | $metaKeys = $params['metaKeys']; |
| 1785 | $data = apply_filters( 'mwai_stats_logs_meta', [], $logId, $metaKeys ); |
| 1786 | return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 ); |
| 1787 | } |
| 1788 | catch ( Exception $e ) { |
| 1789 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1790 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1791 | } |
| 1792 | } |
| 1793 | |
| 1794 | public function rest_system_logs_activity( $request ) { |
| 1795 | try { |
| 1796 | $params = $request->get_json_params(); |
| 1797 | $hours = isset( $params['hours'] ) ? intval( $params['hours'] ) : 24; |
| 1798 | $data = apply_filters( 'mwai_stats_logs_activity', [], $hours ); |
| 1799 | return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 ); |
| 1800 | } |
| 1801 | catch ( Exception $e ) { |
| 1802 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1803 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1804 | } |
| 1805 | } |
| 1806 | |
| 1807 | public function rest_system_logs_activity_daily( $request ) { |
| 1808 | try { |
| 1809 | $params = $request->get_json_params(); |
| 1810 | $days = isset( $params['days'] ) ? intval( $params['days'] ) : 31; |
| 1811 | $byModel = isset( $params['byModel'] ) ? (bool) $params['byModel'] : false; |
| 1812 | $feature = isset( $params['feature'] ) ? sanitize_text_field( $params['feature'] ) : null; |
| 1813 | |
| 1814 | if ( $byModel ) { |
| 1815 | $data = apply_filters( 'mwai_stats_logs_activity_daily_by_model', [], $days ); |
| 1816 | } |
| 1817 | else { |
| 1818 | $data = apply_filters( 'mwai_stats_logs_activity_daily', [], $days, $feature ); |
| 1819 | } |
| 1820 | |
| 1821 | return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 ); |
| 1822 | } |
| 1823 | catch ( Exception $e ) { |
| 1824 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1825 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1826 | } |
| 1827 | } |
| 1828 | |
| 1829 | public function rest_ai_moderate( $request ) { |
| 1830 | try { |
| 1831 | $params = $request->get_json_params(); |
| 1832 | $envId = $params['envId']; |
| 1833 | $text = $params['text']; |
| 1834 | if ( !$text ) { |
| 1835 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Text not found.' ], 404 ); |
| 1836 | } |
| 1837 | $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId ); |
| 1838 | $results = $openai->moderate( $text ); |
| 1839 | return $this->create_rest_response( [ 'success' => true, 'results' => $results ], 200 ); |
| 1840 | } |
| 1841 | catch ( Exception $e ) { |
| 1842 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1843 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1844 | } |
| 1845 | } |
| 1846 | |
| 1847 | public function rest_ai_transcribe_audio( $request ) { |
| 1848 | try { |
| 1849 | global $mwai; |
| 1850 | // A client-supplied path is deliberately ignored. It used to reach |
| 1851 | // file_get_contents() with only a stream-wrapper blocklist in front of it, so an |
| 1852 | // absolute path or ../ went straight through and the bytes were forwarded to the |
| 1853 | // configured transcription endpoint. On multisite that let a subsite administrator |
| 1854 | // read the network wp-config.php and exfiltrate the auth salts off-host. This is |
| 1855 | // the same class as CVE-2024-38791, which the image handler below already dropped |
| 1856 | // its path for. mediaId stays: it resolves through get_attached_file() on a real |
| 1857 | // attachment instead of an arbitrary string. |
| 1858 | // |
| 1859 | // Dropping the local $path alone was not enough (the 3.6.4 fix, bypassed): $params |
| 1860 | // is forwarded to simpleTranscribeAudio(), whose inject_params() runs before the |
| 1861 | // "URL or path required" guard and repopulates $query->path from $params['path'], |
| 1862 | // which the engine then reads. The key has to leave the array itself. |
| 1863 | $params = Meow_MWAI_Core::sanitize_rest_params( $request->get_json_params() ); |
| 1864 | $url = !empty( $params['url'] ) ? $params['url'] : null; |
| 1865 | $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0; |
| 1866 | $path = null; |
| 1867 | |
| 1868 | // If mediaId is provided, get the file path |
| 1869 | if ( !$path && $mediaId > 0 ) { |
| 1870 | $path = get_attached_file( $mediaId ); |
| 1871 | if ( empty( $path ) ) { |
| 1872 | throw new Exception( __( 'The media file cannot be found.', 'ai-engine' ) ); |
| 1873 | } |
| 1874 | } |
| 1875 | |
| 1876 | // Set the scope for admin tools |
| 1877 | if ( !isset( $params['scope'] ) ) { |
| 1878 | $params['scope'] = 'admin-tools'; |
| 1879 | } |
| 1880 | |
| 1881 | $result = $mwai->simpleTranscribeAudio( $url, $path, $params ); |
| 1882 | return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 ); |
| 1883 | } |
| 1884 | catch ( Exception $e ) { |
| 1885 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1886 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1887 | } |
| 1888 | } |
| 1889 | |
| 1890 | public function rest_ai_transcribe_image( $request ) { |
| 1891 | try { |
| 1892 | global $mwai; |
| 1893 | $params = $request->get_json_params(); |
| 1894 | $message = $this->retrieve_message( $params ); |
| 1895 | $url = !empty( $params['url'] ) ? $params['url'] : null; |
| 1896 | // This could lead to a security issue, so let's avoid using path directly. |
| 1897 | //$path = !empty( $params['path'] ) ? $params['path'] : null; |
| 1898 | $result = $mwai->simpleVisionQuery( $message, $url ); |
| 1899 | return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 ); |
| 1900 | } |
| 1901 | catch ( Exception $e ) { |
| 1902 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1903 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1904 | } |
| 1905 | } |
| 1906 | |
| 1907 | public function rest_ai_json( $request ) { |
| 1908 | try { |
| 1909 | global $mwai; |
| 1910 | $params = $request->get_json_params(); |
| 1911 | $message = $this->retrieve_message( $params ); |
| 1912 | $result = $mwai->simpleJsonQuery( $message ); |
| 1913 | return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 ); |
| 1914 | } |
| 1915 | catch ( Exception $e ) { |
| 1916 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1917 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1918 | } |
| 1919 | } |
| 1920 | |
| 1921 | public function rest_mcp_functions( $request ) { |
| 1922 | try { |
| 1923 | // Get all registered MCP tools. Handlers key entries by tool name, so the |
| 1924 | // array is associative; array_values() forces a JSON array (not an object) |
| 1925 | // for the client, which groups them with functions.reduce(). |
| 1926 | $tools = array_values( apply_filters( 'mwai_mcp_tools', [] ) ); |
| 1927 | |
| 1928 | // Format the response |
| 1929 | $response = [ |
| 1930 | 'success' => true, |
| 1931 | 'count' => count( $tools ), |
| 1932 | 'functions' => $tools |
| 1933 | ]; |
| 1934 | |
| 1935 | return $this->create_rest_response( $response, 200 ); |
| 1936 | } |
| 1937 | catch ( Exception $e ) { |
| 1938 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 1939 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 1940 | } |
| 1941 | } |
| 1942 | |
| 1943 | /** |
| 1944 | * Loopback test that mimics the way Anthropic's claude.ai connector reaches |
| 1945 | * the site, so admins can detect a hosting-layer block without waiting until |
| 1946 | * they try to connect from claude.ai. Two stages, because WAFs filter them |
| 1947 | * independently and real tickets showed both patterns: |
| 1948 | * |
| 1949 | * 1. GET on the OAuth discovery path with the python-httpx User-Agent |
| 1950 | * (WP Engine's WAF blocks this one). |
| 1951 | * 2. POST on /wp-json/mcp/v1/http, python-httpx vs neutral User-Agent |
| 1952 | * (Bluehost's mod_security lets discovery GETs through but 403s the |
| 1953 | * POSTs, so OAuth client registration and every MCP call fail while |
| 1954 | * stage 1 looks fine). Both POSTs are unauthenticated, so WordPress |
| 1955 | * itself answers them identically: any python-only difference in |
| 1956 | * status or content type is the firewall talking. |
| 1957 | */ |
| 1958 | public function rest_mcp_self_test( $request ) { |
| 1959 | try { |
| 1960 | $resource_url = rest_url( 'mcp/v1/http' ); |
| 1961 | $probe_url = home_url( '/.well-known/oauth-protected-resource' . wp_parse_url( $resource_url, PHP_URL_PATH ) ); |
| 1962 | $reference_url = rest_url( 'mcp/v1/.well-known/oauth-protected-resource' ); |
| 1963 | |
| 1964 | $args = [ |
| 1965 | 'timeout' => 10, |
| 1966 | 'redirection' => 3, |
| 1967 | 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ), |
| 1968 | 'user-agent' => 'python-httpx/0.28.1', |
| 1969 | 'headers' => [ |
| 1970 | 'Accept' => 'application/json', |
| 1971 | ], |
| 1972 | ]; |
| 1973 | |
| 1974 | $probe_response = wp_remote_get( $probe_url, $args ); |
| 1975 | $reference_args = $args; |
| 1976 | $reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0'; |
| 1977 | $reference_response = wp_remote_get( $reference_url, $reference_args ); |
| 1978 | |
| 1979 | $build = function ( $url, $response ) { |
| 1980 | if ( is_wp_error( $response ) ) { |
| 1981 | return [ |
| 1982 | 'url' => $url, |
| 1983 | 'reachable' => false, |
| 1984 | 'error' => $response->get_error_message(), |
| 1985 | 'status' => null, |
| 1986 | 'content_type' => null, |
| 1987 | ]; |
| 1988 | } |
| 1989 | return [ |
| 1990 | 'url' => $url, |
| 1991 | 'reachable' => true, |
| 1992 | 'status' => (int) wp_remote_retrieve_response_code( $response ), |
| 1993 | 'content_type' => wp_remote_retrieve_header( $response, 'content-type' ), |
| 1994 | ]; |
| 1995 | }; |
| 1996 | |
| 1997 | $probe = $build( $probe_url, $probe_response ); |
| 1998 | $reference = $build( $reference_url, $reference_response ); |
| 1999 | |
| 2000 | // Stage 2: the POST pair. An unauthenticated initialize is harmless and |
| 2001 | // never reaches a tool; we only care whether the firewall lets it in. |
| 2002 | $post_body = wp_json_encode( [ |
| 2003 | 'jsonrpc' => '2.0', |
| 2004 | 'id' => 1, |
| 2005 | 'method' => 'initialize', |
| 2006 | 'params' => [ |
| 2007 | 'protocolVersion' => '2025-03-26', |
| 2008 | 'capabilities' => new stdClass(), |
| 2009 | 'clientInfo' => [ 'name' => 'ai-engine-self-test', 'version' => '1.0' ], |
| 2010 | ], |
| 2011 | ] ); |
| 2012 | $post_args = [ |
| 2013 | 'timeout' => 10, |
| 2014 | 'redirection' => 3, |
| 2015 | 'sslverify' => apply_filters( 'mwai_mcp_self_test_sslverify', true ), |
| 2016 | 'user-agent' => 'python-httpx/0.28.1', |
| 2017 | 'headers' => [ |
| 2018 | 'Accept' => 'application/json, text/event-stream', |
| 2019 | 'Content-Type' => 'application/json', |
| 2020 | ], |
| 2021 | 'body' => $post_body, |
| 2022 | ]; |
| 2023 | $post_probe = $build( $resource_url, wp_remote_post( $resource_url, $post_args ) ); |
| 2024 | $post_reference_args = $post_args; |
| 2025 | $post_reference_args['user-agent'] = 'AI-Engine-Self-Test/1.0'; |
| 2026 | $post_reference = $build( $resource_url, wp_remote_post( $resource_url, $post_reference_args ) ); |
| 2027 | |
| 2028 | // Stage 3: the name the real connector sends. Claude.ai identifies itself as |
| 2029 | // Claude-User, and Cloudflare's AI bot blocking (Security > Bots) matches that |
| 2030 | // name alongside ClaudeBot and GPTBot. A site can pass every python-httpx probe |
| 2031 | // above and still 403 the actual connector, so probe the crawler name too, |
| 2032 | // against the URL the reference has already proven answers fine. |
| 2033 | $ai_args = $reference_args; |
| 2034 | $ai_args['user-agent'] = 'Claude-User/1.0'; |
| 2035 | $ai_probe = $build( $reference_url, wp_remote_get( $reference_url, $ai_args ) ); |
| 2036 | $ai_blocked = $reference['reachable'] && ( |
| 2037 | !$ai_probe['reachable'] |
| 2038 | || ( $ai_probe['status'] !== $reference['status'] |
| 2039 | && in_array( $ai_probe['status'], [ 403, 406, 418, 429 ], true ) ) |
| 2040 | ); |
| 2041 | |
| 2042 | // We run on the server, so we can answer directly what an HTTP probe can only infer: |
| 2043 | // is there a second .htaccess inside .well-known? Apache does not apply the rewrite |
| 2044 | // rules of the root file to a directory carrying its own .htaccess, so a perfectly |
| 2045 | // correct rule in the main file sits there doing nothing, and the user has no way to |
| 2046 | // see why. Hosts create that folder for SSL certificate renewals. |
| 2047 | $nested_htaccess = false; |
| 2048 | $wellknown_htaccess = ABSPATH . '.well-known/.htaccess'; |
| 2049 | if ( @is_dir( ABSPATH . '.well-known' ) && @is_file( $wellknown_htaccess ) ) { |
| 2050 | $nested_htaccess = $wellknown_htaccess; |
| 2051 | } |
| 2052 | |
| 2053 | $is_json = function ( $probe ) { |
| 2054 | return $probe['content_type'] && strpos( $probe['content_type'], 'json' ) !== false; |
| 2055 | }; |
| 2056 | // The firewall reveals itself by treating the python UA differently from |
| 2057 | // the neutral one: different status, or a block page instead of the JSON |
| 2058 | // that WordPress returns to both unauthenticated POSTs. |
| 2059 | $post_blocked = ( $post_reference['reachable'] && ( |
| 2060 | ( !$post_probe['reachable'] ) |
| 2061 | || ( $post_probe['status'] !== $post_reference['status'] && in_array( $post_probe['status'], [ 403, 406, 418, 429 ], true ) ) |
| 2062 | || ( $is_json( $post_reference ) && !$is_json( $post_probe ) ) |
| 2063 | ) ); |
| 2064 | |
| 2065 | // A 404 on discovery has two very different causes, and they need opposite fixes. |
| 2066 | // Either the hosting layer never lets the path reach WordPress, or a page cache |
| 2067 | // stored a 404 from a moment when it legitimately was one (the plugin inactive, |
| 2068 | // mid-update) and now serves that hit forever. Repeating the request with a |
| 2069 | // throwaway query string separates them: same path, different cache key. If the |
| 2070 | // busted request answers properly, WordPress was always fine and the cache is the |
| 2071 | // problem. Seen on LiteSpeed, which returns x-litespeed-cache: hit on the stale |
| 2072 | // 404 while honouring our no-cache headers on the fresh response. |
| 2073 | $cache_probe = null; |
| 2074 | if ( $probe['reachable'] && $probe['status'] === 404 ) { |
| 2075 | $busted_url = add_query_arg( 'mwai_cb', (string) time(), $probe_url ); |
| 2076 | $cache_probe = $build( $busted_url, wp_remote_get( $busted_url, $args ) ); |
| 2077 | } |
| 2078 | $stale_404_cached = $cache_probe && $cache_probe['reachable'] && $cache_probe['status'] === 200; |
| 2079 | |
| 2080 | $verdict = 'unknown'; |
| 2081 | $message = ''; |
| 2082 | if ( $stale_404_cached ) { |
| 2083 | $verdict = 'wellknown_cached_404'; |
| 2084 | $message = 'Your OAuth discovery path returns 404, but the same URL with a query string added returns the correct response. That means WordPress is answering fine and a cache is serving an old 404 in front of it, which is why connecting works sometimes and not others. Fix: purge your page cache and your CDN, then exclude /.well-known/ from caching. In LiteSpeed Cache that is Cache > Excludes > Do Not Cache URIs. Without the exclusion it will come back the next time a 404 gets cached.'; |
| 2085 | } |
| 2086 | else if ( $probe['reachable'] && $probe['status'] === 403 ) { |
| 2087 | $verdict = 'waf_blocks_python_ua'; |
| 2088 | $message = 'Your host returned 403 to a User-Agent containing "python" on the OAuth discovery path. Claude.ai uses python-httpx as its outbound HTTP client, so its connector will fail with "Couldn\'t reach the MCP server". This is a common default on WP Engine. Fix: add a Cloudflare Transform Rule that rewrites the User-Agent for /.well-known/oauth-* and /wp-json/mcp/v1/* paths before the request reaches your origin. See https://meowapps.com/fix-mcp-wordpress-connection for the full recipe.'; |
| 2089 | } |
| 2090 | else if ( $probe['reachable'] && $probe['status'] === 404 ) { |
| 2091 | $verdict = 'wellknown_blocked'; |
| 2092 | $verdict = $nested_htaccess ? 'wellknown_blocked_nested_htaccess' : 'wellknown_blocked'; |
| 2093 | $message = 'Your host returned 404 for the host-root /.well-known/oauth-protected-resource path, so the request never reaches WordPress. '; |
| 2094 | if ( $nested_htaccess ) { |
| 2095 | $message .= 'We found why: there is a second .htaccess inside your .well-known folder, at ' . esc_html( $nested_htaccess ) . '. ' |
| 2096 | . 'Apache stops applying the rules from your main .htaccess to a folder that has its own, so any rewrite you add to the main file is ignored for these paths, however correct it is. ' |
| 2097 | . 'Your host created that folder for SSL certificate renewals. Fix: rename that file to htaccess-old. Renewals keep working, because those are real files that nothing rewrites.'; |
| 2098 | } |
| 2099 | else { |
| 2100 | $message .= 'This usually means your hosting layer (.htaccess, nginx config, or a security plugin) intercepts /.well-known/* paths before WordPress sees them. Adjust rewrites so the path reaches index.php. ' |
| 2101 | . 'We checked and there is no .htaccess inside your .well-known folder, so the interception is happening in your server or CDN configuration rather than in a file you can edit. Your host can fix it with one sentence: let /.well-known/* fall through to WordPress.'; |
| 2102 | } |
| 2103 | } |
| 2104 | else if ( !$probe['reachable'] ) { |
| 2105 | $verdict = 'unreachable'; |
| 2106 | $message = 'The loopback request could not reach the site at all (' . esc_html( $probe['error'] ) . '). Check that the site is publicly resolvable and that the server can reach itself over HTTPS.'; |
| 2107 | } |
| 2108 | else if ( $probe['status'] === 200 && $post_blocked ) { |
| 2109 | $verdict = 'waf_blocks_python_post'; |
| 2110 | $message = 'OAuth discovery works, but your host blocks POST requests from the python-httpx User-Agent on /wp-json/mcp/v1/*. Claude.ai\'s connector will pass discovery and then fail at client registration ("Couldn\'t register with your sign-in service") or on the first MCP call. This pattern is common with mod_security on shared hosts (seen on Bluehost). Fix: ask your host to whitelist POST requests on the /wp-json/mcp/v1/ path, or add a Cloudflare Transform Rule that rewrites the User-Agent for /.well-known/oauth-* and /wp-json/mcp/v1/* before the request reaches your origin. See https://meowapps.com/fix-mcp-wordpress-connection for the full recipe.'; |
| 2111 | } |
| 2112 | else if ( $probe['status'] === 200 ) { |
| 2113 | $verdict = 'ok'; |
| 2114 | $message = 'Your site accepts the python-httpx and Claude-User User-Agents on both the OAuth discovery path (GET) and the MCP endpoint (POST). Claude.ai\'s connector should be able to reach it. ' |
| 2115 | . 'One caveat: these checks run from your server to itself, and many hosts resolve their own domain straight to the origin, so a block that lives at your CDN can pass here and still refuse the real connector. ' |
| 2116 | . 'If connecting still fails, run the same requests from an external machine, and check whether your CDN blocks AI crawlers by name (in Cloudflare: Security > Bots). Blocking AI crawlers also blocks your own connector.'; |
| 2117 | } |
| 2118 | else { |
| 2119 | $verdict = 'unexpected_status'; |
| 2120 | $message = 'Got HTTP ' . $probe['status'] . ' from the loopback probe. Expected 200. Investigate the response in your CDN/origin logs.'; |
| 2121 | } |
| 2122 | |
| 2123 | // A site that answers on both apex and www advertises only one of them in its OAuth |
| 2124 | // metadata, because everything there is built from home_url(). A user who types the |
| 2125 | // other spelling gets told the resource lives on a different origin than the one they |
| 2126 | // entered, which RFC 9728 requires the client to check, and strict clients stop there |
| 2127 | // with no error the user can see. Cheap to detect, impossible to guess from outside. |
| 2128 | $canonical_host = wp_parse_url( home_url(), PHP_URL_HOST ); |
| 2129 | $other_host = $canonical_host && strpos( $canonical_host, 'www.' ) === 0 |
| 2130 | ? substr( $canonical_host, 4 ) : 'www.' . $canonical_host; |
| 2131 | $other_probe = null; |
| 2132 | if ( $canonical_host ) { |
| 2133 | $other_url = str_replace( '//' . $canonical_host, '//' . $other_host, $reference_url ); |
| 2134 | // Do NOT follow redirects here. A site that correctly sends www to its canonical |
| 2135 | // host would otherwise answer 200 at the end of the redirect and look exactly like |
| 2136 | // a site serving both, which is the opposite of the problem we are looking for. |
| 2137 | // Only a direct 200 on the other spelling means both hosts really serve the site. |
| 2138 | $other_args = $reference_args; |
| 2139 | $other_args['redirection'] = 0; |
| 2140 | $other_probe = $build( $other_url, wp_remote_get( $other_url, $other_args ) ); |
| 2141 | } |
| 2142 | if ( $other_probe && $other_probe['reachable'] && $other_probe['status'] === 200 ) { |
| 2143 | $message .= ' Also worth knowing: your site answers on both ' . esc_html( $canonical_host ) |
| 2144 | . ' and ' . esc_html( $other_host ) . ', but its OAuth metadata only ever advertises ' |
| 2145 | . esc_html( $canonical_host ) . ', because that is your WordPress address. ' |
| 2146 | . 'Connect your client using exactly that spelling, otherwise it is told the server lives somewhere else and may refuse without explaining why.'; |
| 2147 | } |
| 2148 | |
| 2149 | // Reported on top of whatever else is wrong, because it is a separate blocker with a |
| 2150 | // separate fix: a site can have perfect discovery and still refuse the connector. |
| 2151 | if ( $ai_blocked ) { |
| 2152 | $ai_message = 'Your site answers the OAuth discovery URL normally to a neutral User-Agent but returns HTTP ' |
| 2153 | . ( $ai_probe['reachable'] ? $ai_probe['status'] : 'no response' ) |
| 2154 | . ' to "Claude-User". That is the name Claude.ai\'s connector sends, so it will be refused even once everything else works. ' |
| 2155 | . 'The usual cause is Cloudflare\'s AI bot blocking (Security > Bots), which matches Claude-User, ClaudeBot and GPTBot. ' |
| 2156 | . 'Fix: turn that off, or add a skip rule covering /.well-known/oauth-* and /wp-json/mcp/v1/*. ' |
| 2157 | . 'Note that blocking AI crawlers also blocks your own connector.'; |
| 2158 | if ( $verdict === 'ok' ) { |
| 2159 | $verdict = 'waf_blocks_ai_ua'; |
| 2160 | $message = $ai_message; |
| 2161 | } |
| 2162 | else { |
| 2163 | $message .= ' ' . $ai_message; |
| 2164 | } |
| 2165 | } |
| 2166 | |
| 2167 | return $this->create_rest_response( [ |
| 2168 | 'success' => true, |
| 2169 | 'verdict' => $verdict, |
| 2170 | 'message' => $message, |
| 2171 | 'probe' => $probe, |
| 2172 | 'reference' => $reference, |
| 2173 | 'post_probe' => $post_probe, |
| 2174 | 'post_reference' => $post_reference, |
| 2175 | 'ai_probe' => $ai_probe, |
| 2176 | 'nested_htaccess' => $nested_htaccess, |
| 2177 | 'cache_probe' => $cache_probe, |
| 2178 | ], 200 ); |
| 2179 | } |
| 2180 | catch ( Exception $e ) { |
| 2181 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 2182 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 2183 | } |
| 2184 | } |
| 2185 | |
| 2186 | /** |
| 2187 | * Top MCP tools by call count over the last N days. Powers the |
| 2188 | * "Top Tools" widget in the MCP Logs view of the Insights screen. |
| 2189 | * Returns rows shaped as { tool, count, success_count, error_count }. |
| 2190 | */ |
| 2191 | public function rest_mcp_top_tools( $request ) { |
| 2192 | try { |
| 2193 | $params = $request->get_json_params(); |
| 2194 | $days = isset( $params['days'] ) ? max( 1, intval( $params['days'] ) ) : 7; |
| 2195 | $limit = isset( $params['limit'] ) ? max( 1, min( 50, intval( $params['limit'] ) ) ) : 10; |
| 2196 | |
| 2197 | global $wpdb; |
| 2198 | $table = $wpdb->prefix . 'mwai_logs'; |
| 2199 | $rows = $wpdb->get_results( |
| 2200 | $wpdb->prepare( |
| 2201 | "SELECT scope AS tool, |
| 2202 | COUNT(*) AS count, |
| 2203 | SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS success_count, |
| 2204 | SUM(CASE WHEN stats LIKE %s THEN 1 ELSE 0 END) AS error_count |
| 2205 | FROM $table |
| 2206 | WHERE feature = 'mcp_tool' |
| 2207 | AND time >= DATE_SUB(NOW(), INTERVAL %d DAY) |
| 2208 | GROUP BY scope |
| 2209 | ORDER BY count DESC |
| 2210 | LIMIT %d", |
| 2211 | '%"status":"success"%', |
| 2212 | '%"status":"error"%', |
| 2213 | $days, |
| 2214 | $limit |
| 2215 | ), |
| 2216 | ARRAY_A |
| 2217 | ); |
| 2218 | |
| 2219 | foreach ( $rows as &$row ) { |
| 2220 | $row['count'] = (int) $row['count']; |
| 2221 | $row['success_count'] = (int) $row['success_count']; |
| 2222 | $row['error_count'] = (int) $row['error_count']; |
| 2223 | } |
| 2224 | unset( $row ); |
| 2225 | |
| 2226 | return $this->create_rest_response( [ 'success' => true, 'tools' => $rows ?: [] ], 200 ); |
| 2227 | } |
| 2228 | catch ( Exception $e ) { |
| 2229 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 2230 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 2231 | } |
| 2232 | } |
| 2233 | |
| 2234 | public function rest_helpers_post_types() { |
| 2235 | try { |
| 2236 | $postTypes = $this->core->get_post_types(); |
| 2237 | return $this->create_rest_response( [ 'success' => true, 'postTypes' => $postTypes ], 200 ); |
| 2238 | } |
| 2239 | catch ( Exception $e ) { |
| 2240 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 2241 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 2242 | } |
| 2243 | } |
| 2244 | |
| 2245 | public function rest_settings_themes( $request ) { |
| 2246 | try { |
| 2247 | $method = $request->get_method(); |
| 2248 | if ( $method === 'GET' ) { |
| 2249 | $themes = $this->core->get_themes(); |
| 2250 | return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 ); |
| 2251 | } |
| 2252 | else if ( $method === 'POST' ) { |
| 2253 | $params = $request->get_json_params(); |
| 2254 | $themes = $params['themes']; |
| 2255 | $themes = $this->core->update_themes( $themes ); |
| 2256 | return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 ); |
| 2257 | } |
| 2258 | } |
| 2259 | catch ( Exception $e ) { |
| 2260 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 2261 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 2262 | } |
| 2263 | } |
| 2264 | |
| 2265 | public function rest_settings_chatbots( $request ) { |
| 2266 | try { |
| 2267 | $method = $request->get_method(); |
| 2268 | if ( $method === 'GET' ) { |
| 2269 | $chatbots = $this->core->get_chatbots(); |
| 2270 | return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 ); |
| 2271 | } |
| 2272 | else if ( $method === 'POST' ) { |
| 2273 | $params = $request->get_json_params(); |
| 2274 | $chatbots = $params['chatbots']; |
| 2275 | $chatbots = $this->core->update_chatbots( $chatbots ); |
| 2276 | return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 ); |
| 2277 | } |
| 2278 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Method not allowed' ], 405 ); |
| 2279 | } |
| 2280 | catch ( Exception $e ) { |
| 2281 | $message = apply_filters( 'mwai_ai_exception', $e->getMessage() ); |
| 2282 | return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 ); |
| 2283 | } |
| 2284 | } |
| 2285 | |
| 2286 | #region Logs |
| 2287 | |
| 2288 | public function rest_get_logs() { |
| 2289 | $logs = Meow_MWAI_Logging::get(); |
| 2290 | return $this->create_rest_response( [ 'success' => true, 'data' => $logs ], 200 ); |
| 2291 | } |
| 2292 | |
| 2293 | public function rest_clear_logs() { |
| 2294 | Meow_MWAI_Logging::clear(); |
| 2295 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 2296 | } |
| 2297 | |
| 2298 | #endregion |
| 2299 | |
| 2300 | #region Forms |
| 2301 | |
| 2302 | public function rest_forms_list( $request ) { |
| 2303 | try { |
| 2304 | $args = [ |
| 2305 | 'post_type' => 'mwai_form', |
| 2306 | 'posts_per_page' => 100, |
| 2307 | 'post_status' => 'any', |
| 2308 | 'orderby' => 'date', |
| 2309 | 'order' => 'DESC' |
| 2310 | ]; |
| 2311 | |
| 2312 | $posts = get_posts( $args ); |
| 2313 | $forms = array_map( function ( $post ) { |
| 2314 | return [ |
| 2315 | 'id' => $post->ID, |
| 2316 | 'title' => $post->post_title, |
| 2317 | 'status' => $post->post_status |
| 2318 | ]; |
| 2319 | }, $posts ); |
| 2320 | |
| 2321 | return $this->create_rest_response( [ 'success' => true, 'forms' => $forms ], 200 ); |
| 2322 | } |
| 2323 | catch ( Exception $e ) { |
| 2324 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2325 | } |
| 2326 | } |
| 2327 | |
| 2328 | public function rest_forms_get( $request ) { |
| 2329 | try { |
| 2330 | $id = intval( $request->get_param( 'id' ) ); |
| 2331 | if ( !$id ) { |
| 2332 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 ); |
| 2333 | } |
| 2334 | |
| 2335 | $post = get_post( $id ); |
| 2336 | if ( !$post || $post->post_type !== 'mwai_form' ) { |
| 2337 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 ); |
| 2338 | } |
| 2339 | |
| 2340 | $form = [ |
| 2341 | 'id' => $post->ID, |
| 2342 | 'title' => [ |
| 2343 | 'raw' => $post->post_title, |
| 2344 | 'rendered' => $post->post_title |
| 2345 | ], |
| 2346 | 'content' => [ |
| 2347 | 'raw' => $post->post_content, |
| 2348 | 'rendered' => $post->post_content |
| 2349 | ], |
| 2350 | 'status' => $post->post_status |
| 2351 | ]; |
| 2352 | |
| 2353 | return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 ); |
| 2354 | } |
| 2355 | catch ( Exception $e ) { |
| 2356 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2357 | } |
| 2358 | } |
| 2359 | |
| 2360 | public function rest_forms_create( $request ) { |
| 2361 | try { |
| 2362 | $params = $request->get_json_params(); |
| 2363 | $title = isset( $params['title'] ) ? $params['title'] : 'Untitled Form'; |
| 2364 | |
| 2365 | // wp_insert_post expects slashed data - it calls wp_unslash() internally, which would |
| 2366 | // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and corrupt |
| 2367 | // the stored blocks. |
| 2368 | $post_data = wp_slash( [ |
| 2369 | 'post_title' => $title, |
| 2370 | 'post_content' => '', |
| 2371 | 'post_status' => 'draft', |
| 2372 | 'post_type' => 'mwai_form' |
| 2373 | ] ); |
| 2374 | |
| 2375 | $post_id = wp_insert_post( $post_data ); |
| 2376 | |
| 2377 | if ( is_wp_error( $post_id ) ) { |
| 2378 | return $this->create_rest_response( [ 'success' => false, 'message' => $post_id->get_error_message() ], 500 ); |
| 2379 | } |
| 2380 | |
| 2381 | $post = get_post( $post_id ); |
| 2382 | $form = [ |
| 2383 | 'id' => $post->ID, |
| 2384 | 'title' => [ |
| 2385 | 'raw' => $post->post_title, |
| 2386 | 'rendered' => $post->post_title |
| 2387 | ], |
| 2388 | 'status' => $post->post_status |
| 2389 | ]; |
| 2390 | |
| 2391 | return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 ); |
| 2392 | } |
| 2393 | catch ( Exception $e ) { |
| 2394 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2395 | } |
| 2396 | } |
| 2397 | |
| 2398 | public function rest_forms_update( $request ) { |
| 2399 | try { |
| 2400 | $params = $request->get_json_params(); |
| 2401 | $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0; |
| 2402 | |
| 2403 | if ( !$id ) { |
| 2404 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 ); |
| 2405 | } |
| 2406 | |
| 2407 | $post = get_post( $id ); |
| 2408 | if ( !$post || $post->post_type !== 'mwai_form' ) { |
| 2409 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 ); |
| 2410 | } |
| 2411 | |
| 2412 | $post_data = [ 'ID' => $id ]; |
| 2413 | |
| 2414 | if ( isset( $params['title'] ) ) { |
| 2415 | $post_data['post_title'] = $params['title']; |
| 2416 | } |
| 2417 | |
| 2418 | if ( isset( $params['content'] ) ) { |
| 2419 | $post_data['post_content'] = $params['content']; |
| 2420 | } |
| 2421 | |
| 2422 | if ( isset( $params['status'] ) ) { |
| 2423 | $post_data['post_status'] = $params['status']; |
| 2424 | } |
| 2425 | |
| 2426 | // wp_update_post expects slashed data - it calls wp_unslash() internally, which would |
| 2427 | // otherwise strip backslashes from block-comment JSON escapes (e.g. \n → n) and break |
| 2428 | // Gutenberg blocks on reload. |
| 2429 | $result = wp_update_post( wp_slash( $post_data ) ); |
| 2430 | |
| 2431 | if ( is_wp_error( $result ) ) { |
| 2432 | return $this->create_rest_response( [ 'success' => false, 'message' => $result->get_error_message() ], 500 ); |
| 2433 | } |
| 2434 | |
| 2435 | $post = get_post( $id ); |
| 2436 | $form = [ |
| 2437 | 'id' => $post->ID, |
| 2438 | 'title' => [ |
| 2439 | 'raw' => $post->post_title, |
| 2440 | 'rendered' => $post->post_title |
| 2441 | ], |
| 2442 | 'content' => [ |
| 2443 | 'raw' => $post->post_content, |
| 2444 | 'rendered' => $post->post_content |
| 2445 | ], |
| 2446 | 'status' => $post->post_status |
| 2447 | ]; |
| 2448 | |
| 2449 | return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 ); |
| 2450 | } |
| 2451 | catch ( Exception $e ) { |
| 2452 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2453 | } |
| 2454 | } |
| 2455 | |
| 2456 | public function rest_forms_delete( $request ) { |
| 2457 | try { |
| 2458 | $params = $request->get_json_params(); |
| 2459 | $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0; |
| 2460 | |
| 2461 | if ( !$id ) { |
| 2462 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 ); |
| 2463 | } |
| 2464 | |
| 2465 | $post = get_post( $id ); |
| 2466 | if ( !$post || $post->post_type !== 'mwai_form' ) { |
| 2467 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 ); |
| 2468 | } |
| 2469 | |
| 2470 | $result = wp_delete_post( $id, true ); |
| 2471 | |
| 2472 | if ( !$result ) { |
| 2473 | return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to delete form' ], 500 ); |
| 2474 | } |
| 2475 | |
| 2476 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 2477 | } |
| 2478 | catch ( Exception $e ) { |
| 2479 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2480 | } |
| 2481 | } |
| 2482 | |
| 2483 | #endregion |
| 2484 | |
| 2485 | #region Video Generation Helpers |
| 2486 | |
| 2487 | public function rest_helpers_create_video( $request ) { |
| 2488 | try { |
| 2489 | $params = $request->get_json_params(); |
| 2490 | $prompt = sanitize_text_field( $params['prompt'] ); |
| 2491 | $model = sanitize_text_field( $params['model'] ?? 'sora-2' ); |
| 2492 | $size = sanitize_text_field( $params['size'] ?? '720x1280' ); |
| 2493 | $seconds = absint( $params['seconds'] ?? 4 ); |
| 2494 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 2495 | |
| 2496 | // Check if envId is provided (defaults not supported for videos yet) |
| 2497 | if ( empty( $envId ) ) { |
| 2498 | throw new Exception( 'Please select a specific environment and model in the Video Generator. Default environments are not yet supported for video generation.' ); |
| 2499 | } |
| 2500 | |
| 2501 | // Get API key from environment |
| 2502 | $env = $this->core->get_ai_env( $envId ); |
| 2503 | $api_key = $env['apikey'] ?? ''; |
| 2504 | |
| 2505 | if ( empty( $api_key ) ) { |
| 2506 | throw new Exception( 'OpenAI API key not found.' ); |
| 2507 | } |
| 2508 | |
| 2509 | // Prepare multipart boundary |
| 2510 | $boundary = wp_generate_password( 24, false ); |
| 2511 | $body = ''; |
| 2512 | |
| 2513 | // Add model |
| 2514 | $body .= "--{$boundary}\r\n"; |
| 2515 | $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n"; |
| 2516 | $body .= "{$model}\r\n"; |
| 2517 | |
| 2518 | // Add prompt |
| 2519 | $body .= "--{$boundary}\r\n"; |
| 2520 | $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n"; |
| 2521 | $body .= "{$prompt}\r\n"; |
| 2522 | |
| 2523 | // Add size |
| 2524 | $body .= "--{$boundary}\r\n"; |
| 2525 | $body .= "Content-Disposition: form-data; name=\"size\"\r\n\r\n"; |
| 2526 | $body .= "{$size}\r\n"; |
| 2527 | |
| 2528 | // Add seconds |
| 2529 | $body .= "--{$boundary}\r\n"; |
| 2530 | $body .= "Content-Disposition: form-data; name=\"seconds\"\r\n\r\n"; |
| 2531 | $body .= "{$seconds}\r\n"; |
| 2532 | |
| 2533 | $body .= "--{$boundary}--\r\n"; |
| 2534 | |
| 2535 | // Call OpenAI API to create video |
| 2536 | $response = wp_remote_post( 'https://api.openai.com/v1/videos', [ |
| 2537 | 'headers' => [ |
| 2538 | 'Authorization' => 'Bearer ' . $api_key, |
| 2539 | 'Content-Type' => 'multipart/form-data; boundary=' . $boundary |
| 2540 | ], |
| 2541 | 'body' => $body, |
| 2542 | 'timeout' => 30 |
| 2543 | ] ); |
| 2544 | |
| 2545 | if ( is_wp_error( $response ) ) { |
| 2546 | throw new Exception( $response->get_error_message() ); |
| 2547 | } |
| 2548 | |
| 2549 | $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); |
| 2550 | |
| 2551 | if ( isset( $response_body['error'] ) ) { |
| 2552 | throw new Exception( $response_body['error']['message'] ?? 'Unknown error' ); |
| 2553 | } |
| 2554 | |
| 2555 | // Record usage (price is calculated per second) |
| 2556 | $usage = $this->core->record_videos_usage( $model, $size, $seconds ); |
| 2557 | |
| 2558 | // Log to Query Logs (Statistics) |
| 2559 | try { |
| 2560 | if ( class_exists( 'MeowPro_MWAI_Stats' ) && class_exists( 'MeowPro_MWAI_Statistics' ) ) { |
| 2561 | $statsObject = new MeowPro_MWAI_Stats(); |
| 2562 | $statsObject->session = $params['session'] ?? null; |
| 2563 | $statsObject->scope = 'admin-tools'; |
| 2564 | $statsObject->feature = 'video-generator'; |
| 2565 | $statsObject->model = $model; |
| 2566 | $statsObject->envId = $envId; |
| 2567 | $statsObject->units = $seconds; |
| 2568 | $statsObject->type = 'seconds'; |
| 2569 | $statsObject->price = $usage['price'] ?? 0; |
| 2570 | $statsObject->accuracy = $usage['accuracy'] ?? 'full'; |
| 2571 | |
| 2572 | $statistics = new MeowPro_MWAI_Statistics(); |
| 2573 | $statistics->commit_stats( $statsObject ); |
| 2574 | } |
| 2575 | } |
| 2576 | catch ( Exception $statsError ) { |
| 2577 | // Log the error but don't fail the video creation |
| 2578 | error_log( '[AI Engine Video] Failed to log statistics: ' . $statsError->getMessage() ); |
| 2579 | } |
| 2580 | |
| 2581 | // Store metadata for later retrieval when video completes |
| 2582 | if ( isset( $response_body['id'] ) ) { |
| 2583 | set_transient( 'mwai_video_metadata_' . $response_body['id'], [ |
| 2584 | 'model' => $model, |
| 2585 | 'env_id' => $envId, |
| 2586 | 'created_at' => time() |
| 2587 | ], 7 * DAY_IN_SECONDS ); |
| 2588 | } |
| 2589 | |
| 2590 | return $this->create_rest_response( [ 'success' => true, 'video' => $response_body, 'usage' => $usage ], 200 ); |
| 2591 | } |
| 2592 | catch ( Exception $e ) { |
| 2593 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2594 | } |
| 2595 | } |
| 2596 | |
| 2597 | public function rest_helpers_video_status( $request ) { |
| 2598 | try { |
| 2599 | $params = $request->get_json_params(); |
| 2600 | $video_ids = $params['videoIds'] ?? []; |
| 2601 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 2602 | |
| 2603 | if ( empty( $video_ids ) ) { |
| 2604 | return $this->create_rest_response( [ 'success' => true, 'videos' => [] ], 200 ); |
| 2605 | } |
| 2606 | |
| 2607 | // Get API key from environment |
| 2608 | $env = $this->core->get_ai_env( $envId ); |
| 2609 | $api_key = $env['apikey'] ?? ''; |
| 2610 | |
| 2611 | if ( empty( $api_key ) ) { |
| 2612 | throw new Exception( 'OpenAI API key not found.' ); |
| 2613 | } |
| 2614 | |
| 2615 | $videos = []; |
| 2616 | foreach ( $video_ids as $video_id ) { |
| 2617 | $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id, [ |
| 2618 | 'headers' => [ |
| 2619 | 'Authorization' => 'Bearer ' . $api_key |
| 2620 | ], |
| 2621 | 'timeout' => 15 |
| 2622 | ] ); |
| 2623 | |
| 2624 | if ( !is_wp_error( $response ) ) { |
| 2625 | $body = json_decode( wp_remote_retrieve_body( $response ), true ); |
| 2626 | if ( !isset( $body['error'] ) ) { |
| 2627 | // If video is completed and we haven't saved it yet, download and save to media library |
| 2628 | if ( $body['status'] === 'completed' && empty( get_transient( 'mwai_video_saved_' . $video_id ) ) ) { |
| 2629 | // Retrieve metadata that was stored when video was created |
| 2630 | $metadata = get_transient( 'mwai_video_metadata_' . $video_id ); |
| 2631 | $ai_metadata = []; |
| 2632 | if ( $metadata ) { |
| 2633 | $ai_metadata = [ |
| 2634 | 'model' => $metadata['model'] ?? null, |
| 2635 | 'env_id' => $metadata['env_id'] ?? null, |
| 2636 | 'latency' => isset( $metadata['created_at'] ) ? ( time() - $metadata['created_at'] ) : null |
| 2637 | ]; |
| 2638 | } |
| 2639 | |
| 2640 | $attachment_id = $this->download_and_save_video( $video_id, $api_key, '', '', $ai_metadata ); |
| 2641 | if ( $attachment_id ) { |
| 2642 | $body['attachment_id'] = $attachment_id; |
| 2643 | // Build URL from file path for custom post types |
| 2644 | $file_path = get_attached_file( $attachment_id ); |
| 2645 | $upload_dir = wp_upload_dir(); |
| 2646 | $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path ); |
| 2647 | // Mark as saved so we don't download again |
| 2648 | set_transient( 'mwai_video_saved_' . $video_id, $attachment_id, DAY_IN_SECONDS ); |
| 2649 | // Clean up metadata transient |
| 2650 | delete_transient( 'mwai_video_metadata_' . $video_id ); |
| 2651 | } |
| 2652 | } |
| 2653 | // Check if we already have this video saved |
| 2654 | else if ( $body['status'] === 'completed' ) { |
| 2655 | $attachment_id = get_transient( 'mwai_video_saved_' . $video_id ); |
| 2656 | if ( $attachment_id ) { |
| 2657 | $body['attachment_id'] = $attachment_id; |
| 2658 | // Build URL from file path for custom post types |
| 2659 | $file_path = get_attached_file( $attachment_id ); |
| 2660 | $upload_dir = wp_upload_dir(); |
| 2661 | $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path ); |
| 2662 | } |
| 2663 | } |
| 2664 | $videos[] = $body; |
| 2665 | } |
| 2666 | else { |
| 2667 | // Include error information in the response |
| 2668 | error_log( 'AI Engine: Video generation failed for ID ' . $video_id . ': ' . json_encode( $body['error'] ) ); |
| 2669 | $videos[] = [ |
| 2670 | 'id' => $video_id, |
| 2671 | 'status' => 'failed', |
| 2672 | 'error' => $body['error'] |
| 2673 | ]; |
| 2674 | } |
| 2675 | } |
| 2676 | else { |
| 2677 | // WP HTTP error |
| 2678 | error_log( 'AI Engine: Failed to check video status for ID ' . $video_id . ': ' . $response->get_error_message() ); |
| 2679 | $videos[] = [ |
| 2680 | 'id' => $video_id, |
| 2681 | 'status' => 'failed', |
| 2682 | 'error' => [ 'message' => $response->get_error_message() ] |
| 2683 | ]; |
| 2684 | } |
| 2685 | } |
| 2686 | |
| 2687 | return $this->create_rest_response( [ 'success' => true, 'videos' => $videos ], 200 ); |
| 2688 | } |
| 2689 | catch ( Exception $e ) { |
| 2690 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2691 | } |
| 2692 | } |
| 2693 | |
| 2694 | public function rest_helpers_download_video( $request ) { |
| 2695 | try { |
| 2696 | $params = $request->get_json_params(); |
| 2697 | $video_id = sanitize_text_field( $params['videoId'] ); |
| 2698 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 2699 | |
| 2700 | // Get API key from environment |
| 2701 | $env = $this->core->get_ai_env( $envId ); |
| 2702 | $api_key = $env['apikey'] ?? ''; |
| 2703 | |
| 2704 | if ( empty( $api_key ) ) { |
| 2705 | throw new Exception( 'OpenAI API key not found.' ); |
| 2706 | } |
| 2707 | |
| 2708 | $temp_file = wp_tempnam( $video_id . '.mp4' ); |
| 2709 | |
| 2710 | $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [ |
| 2711 | 'headers' => [ |
| 2712 | 'Authorization' => 'Bearer ' . $api_key |
| 2713 | ], |
| 2714 | 'timeout' => 120, |
| 2715 | 'stream' => true, |
| 2716 | 'filename' => $temp_file |
| 2717 | ] ); |
| 2718 | |
| 2719 | if ( is_wp_error( $response ) ) { |
| 2720 | throw new Exception( $response->get_error_message() ); |
| 2721 | } |
| 2722 | |
| 2723 | $file_data = file_get_contents( $temp_file ); |
| 2724 | $base64 = base64_encode( $file_data ); |
| 2725 | |
| 2726 | unlink( $temp_file ); |
| 2727 | |
| 2728 | return $this->create_rest_response( [ |
| 2729 | 'success' => true, |
| 2730 | 'data' => $base64, |
| 2731 | 'mimeType' => 'video/mp4' |
| 2732 | ], 200 ); |
| 2733 | } |
| 2734 | catch ( Exception $e ) { |
| 2735 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2736 | } |
| 2737 | } |
| 2738 | |
| 2739 | public function rest_helpers_delete_video( $request ) { |
| 2740 | try { |
| 2741 | $params = $request->get_json_params(); |
| 2742 | $video_id = sanitize_text_field( $params['videoId'] ); |
| 2743 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 2744 | |
| 2745 | // Get API key from environment |
| 2746 | $env = $this->core->get_ai_env( $envId ); |
| 2747 | $api_key = $env['apikey'] ?? ''; |
| 2748 | |
| 2749 | if ( empty( $api_key ) ) { |
| 2750 | throw new Exception( 'OpenAI API key not found.' ); |
| 2751 | } |
| 2752 | |
| 2753 | $response = wp_remote_request( 'https://api.openai.com/v1/videos/' . $video_id, [ |
| 2754 | 'method' => 'DELETE', |
| 2755 | 'headers' => [ |
| 2756 | 'Authorization' => 'Bearer ' . $api_key |
| 2757 | ], |
| 2758 | 'timeout' => 15 |
| 2759 | ] ); |
| 2760 | |
| 2761 | if ( is_wp_error( $response ) ) { |
| 2762 | throw new Exception( $response->get_error_message() ); |
| 2763 | } |
| 2764 | |
| 2765 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 2766 | } |
| 2767 | catch ( Exception $e ) { |
| 2768 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2769 | } |
| 2770 | } |
| 2771 | |
| 2772 | private function download_and_save_video( $video_id, $api_key, $title = '', $description = '', $ai_metadata = [] ) { |
| 2773 | try { |
| 2774 | // Download video content |
| 2775 | $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [ |
| 2776 | 'headers' => [ |
| 2777 | 'Authorization' => 'Bearer ' . $api_key |
| 2778 | ], |
| 2779 | 'timeout' => 120 |
| 2780 | ] ); |
| 2781 | |
| 2782 | if ( is_wp_error( $response ) ) { |
| 2783 | error_log( 'Error downloading video: ' . $response->get_error_message() ); |
| 2784 | return false; |
| 2785 | } |
| 2786 | |
| 2787 | $video_data = wp_remote_retrieve_body( $response ); |
| 2788 | if ( empty( $video_data ) ) { |
| 2789 | error_log( 'Empty video data received' ); |
| 2790 | return false; |
| 2791 | } |
| 2792 | |
| 2793 | // Generate filename |
| 2794 | $filename = $video_id . '.mp4'; |
| 2795 | $upload_dir = wp_upload_dir(); |
| 2796 | $file_path = $upload_dir['path'] . '/' . $filename; |
| 2797 | |
| 2798 | // Save to file |
| 2799 | file_put_contents( $file_path, $video_data ); |
| 2800 | |
| 2801 | // Prepare attachment data - use mwai_video post type (draft video) |
| 2802 | $attachment = [ |
| 2803 | 'post_mime_type' => 'video/mp4', |
| 2804 | 'post_title' => !empty( $title ) ? $title : 'AI Generated Video', |
| 2805 | 'post_content' => $description, |
| 2806 | 'post_status' => 'inherit', |
| 2807 | 'post_type' => 'mwai_video' |
| 2808 | ]; |
| 2809 | |
| 2810 | // Use wp_insert_post instead of wp_insert_attachment to allow custom post types |
| 2811 | $attachment_id = wp_insert_post( $attachment ); |
| 2812 | |
| 2813 | // Set the attached file manually since we're not using wp_insert_attachment |
| 2814 | update_attached_file( $attachment_id, $file_path ); |
| 2815 | |
| 2816 | if ( is_wp_error( $attachment_id ) ) { |
| 2817 | error_log( 'Error creating attachment: ' . $attachment_id->get_error_message() ); |
| 2818 | return false; |
| 2819 | } |
| 2820 | |
| 2821 | // Generate attachment metadata |
| 2822 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 2823 | $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path ); |
| 2824 | wp_update_attachment_metadata( $attachment_id, $attach_data ); |
| 2825 | |
| 2826 | // Store AI-related metadata |
| 2827 | if ( !empty( $ai_metadata['model'] ) ) { |
| 2828 | update_post_meta( $attachment_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) ); |
| 2829 | } |
| 2830 | if ( !empty( $ai_metadata['latency'] ) ) { |
| 2831 | update_post_meta( $attachment_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) ); |
| 2832 | } |
| 2833 | if ( !empty( $ai_metadata['env_id'] ) ) { |
| 2834 | update_post_meta( $attachment_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) ); |
| 2835 | } |
| 2836 | |
| 2837 | // Add to user's draft media |
| 2838 | $user_id = get_current_user_id(); |
| 2839 | $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true ); |
| 2840 | if ( !is_array( $draft_media ) ) { |
| 2841 | $draft_media = []; |
| 2842 | } |
| 2843 | $draft_media[] = [ |
| 2844 | 'attachment_id' => $attachment_id, |
| 2845 | 'type' => 'video', |
| 2846 | 'openai_id' => $video_id, |
| 2847 | 'created_at' => time() |
| 2848 | ]; |
| 2849 | update_user_meta( $user_id, 'mwai_draft_media', $draft_media ); |
| 2850 | |
| 2851 | return $attachment_id; |
| 2852 | } |
| 2853 | catch ( Exception $e ) { |
| 2854 | error_log( 'Exception in download_and_save_video: ' . $e->getMessage() ); |
| 2855 | return false; |
| 2856 | } |
| 2857 | } |
| 2858 | |
| 2859 | public function rest_helpers_save_video_to_library( $request ) { |
| 2860 | try { |
| 2861 | $params = $request->get_json_params(); |
| 2862 | $video_id = sanitize_text_field( $params['videoId'] ); |
| 2863 | $title = sanitize_text_field( $params['title'] ); |
| 2864 | $description = sanitize_text_field( $params['description'] ); |
| 2865 | $filename = sanitize_file_name( $params['filename'] ); |
| 2866 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 2867 | |
| 2868 | // Ensure filename has .mp4 extension |
| 2869 | if ( !preg_match( '/\.mp4$/i', $filename ) ) { |
| 2870 | $filename .= '.mp4'; |
| 2871 | } |
| 2872 | |
| 2873 | // Get API key from environment |
| 2874 | $env = $this->core->get_ai_env( $envId ); |
| 2875 | $api_key = $env['apikey'] ?? ''; |
| 2876 | |
| 2877 | if ( empty( $api_key ) ) { |
| 2878 | throw new Exception( 'OpenAI API key not found.' ); |
| 2879 | } |
| 2880 | |
| 2881 | // Download video content |
| 2882 | $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [ |
| 2883 | 'headers' => [ |
| 2884 | 'Authorization' => 'Bearer ' . $api_key |
| 2885 | ], |
| 2886 | 'timeout' => 120 |
| 2887 | ] ); |
| 2888 | |
| 2889 | if ( is_wp_error( $response ) ) { |
| 2890 | throw new Exception( $response->get_error_message() ); |
| 2891 | } |
| 2892 | |
| 2893 | $video_data = wp_remote_retrieve_body( $response ); |
| 2894 | |
| 2895 | // Upload to WordPress media library |
| 2896 | $upload_dir = wp_upload_dir(); |
| 2897 | $file_path = $upload_dir['path'] . '/' . $filename; |
| 2898 | |
| 2899 | file_put_contents( $file_path, $video_data ); |
| 2900 | |
| 2901 | $attachment = [ |
| 2902 | 'post_mime_type' => 'video/mp4', |
| 2903 | 'post_title' => $title, |
| 2904 | 'post_content' => $description, |
| 2905 | 'post_status' => 'inherit' |
| 2906 | ]; |
| 2907 | |
| 2908 | $attach_id = wp_insert_attachment( $attachment, $file_path ); |
| 2909 | |
| 2910 | require_once( ABSPATH . 'wp-admin/includes/image.php' ); |
| 2911 | $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path ); |
| 2912 | wp_update_attachment_metadata( $attach_id, $attach_data ); |
| 2913 | |
| 2914 | return $this->create_rest_response( [ |
| 2915 | 'success' => true, |
| 2916 | 'attachmentId' => $attach_id |
| 2917 | ], 200 ); |
| 2918 | } |
| 2919 | catch ( Exception $e ) { |
| 2920 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2921 | } |
| 2922 | } |
| 2923 | |
| 2924 | public function rest_helpers_delete_video_from_library( $request ) { |
| 2925 | try { |
| 2926 | $params = $request->get_json_params(); |
| 2927 | $attachment_id = absint( $params['attachmentId'] ); |
| 2928 | |
| 2929 | if ( empty( $attachment_id ) ) { |
| 2930 | throw new Exception( 'Attachment ID is required.' ); |
| 2931 | } |
| 2932 | |
| 2933 | $deleted = wp_delete_attachment( $attachment_id, true ); |
| 2934 | |
| 2935 | if ( !$deleted ) { |
| 2936 | throw new Exception( 'Failed to delete attachment.' ); |
| 2937 | } |
| 2938 | |
| 2939 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 2940 | } |
| 2941 | catch ( Exception $e ) { |
| 2942 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 2943 | } |
| 2944 | } |
| 2945 | |
| 2946 | public function rest_helpers_list_draft_media( $request ) { |
| 2947 | try { |
| 2948 | $type = $request->get_param( 'type' ); // 'image', 'video', or null for all |
| 2949 | $user_id = get_current_user_id(); |
| 2950 | $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true ); |
| 2951 | |
| 2952 | if ( !is_array( $draft_media ) ) { |
| 2953 | return $this->create_rest_response( [ 'success' => true, 'media' => [] ], 200 ); |
| 2954 | } |
| 2955 | |
| 2956 | $media_items = []; |
| 2957 | foreach ( $draft_media as $item ) { |
| 2958 | // Filter by type if specified |
| 2959 | if ( $type && $item['type'] !== $type ) { |
| 2960 | continue; |
| 2961 | } |
| 2962 | |
| 2963 | $attachment_id = $item['attachment_id']; |
| 2964 | $attachment = get_post( $attachment_id ); |
| 2965 | |
| 2966 | if ( $attachment ) { |
| 2967 | // For custom post types (mwai_image, mwai_video), build URL from file path |
| 2968 | $file_path = get_attached_file( $attachment_id ); |
| 2969 | $upload_dir = wp_upload_dir(); |
| 2970 | $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path ); |
| 2971 | |
| 2972 | $model = get_post_meta( $attachment_id, 'mwai_model', true ); |
| 2973 | $generation_time = get_post_meta( $attachment_id, 'mwai_latency', true ); |
| 2974 | $env_id = get_post_meta( $attachment_id, 'mwai_env_id', true ); |
| 2975 | |
| 2976 | // Debug logging |
| 2977 | if ( $this->core->get_option( 'queries_debug_mode' ) ) { |
| 2978 | 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 ) ); |
| 2979 | } |
| 2980 | |
| 2981 | $media_items[] = [ |
| 2982 | 'attachment_id' => $attachment_id, |
| 2983 | 'type' => $item['type'], |
| 2984 | 'openai_id' => $item['openai_id'] ?? null, |
| 2985 | 'url' => $url, |
| 2986 | 'title' => $attachment->post_title, |
| 2987 | 'description' => $attachment->post_content, |
| 2988 | 'filename' => basename( $file_path ), |
| 2989 | 'created_at' => $item['created_at'], |
| 2990 | 'model' => $model, |
| 2991 | 'generation_time' => $generation_time, |
| 2992 | 'env_id' => $env_id |
| 2993 | ]; |
| 2994 | } |
| 2995 | } |
| 2996 | |
| 2997 | return $this->create_rest_response( [ 'success' => true, 'media' => $media_items ], 200 ); |
| 2998 | } |
| 2999 | catch ( Exception $e ) { |
| 3000 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 3001 | } |
| 3002 | } |
| 3003 | |
| 3004 | public function rest_helpers_approve_media( $request ) { |
| 3005 | try { |
| 3006 | $params = $request->get_json_params(); |
| 3007 | $attachment_id = absint( $params['attachmentId'] ); |
| 3008 | $openai_id = sanitize_text_field( $params['openaiId'] ?? '' ); |
| 3009 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 3010 | |
| 3011 | if ( empty( $attachment_id ) ) { |
| 3012 | throw new Exception( 'Attachment ID is required.' ); |
| 3013 | } |
| 3014 | |
| 3015 | // Convert from mwai_image/mwai_video to attachment post type |
| 3016 | wp_update_post( [ |
| 3017 | 'ID' => $attachment_id, |
| 3018 | 'post_type' => 'attachment', |
| 3019 | 'post_status' => 'inherit' |
| 3020 | ] ); |
| 3021 | |
| 3022 | // Remove from draft media list |
| 3023 | $user_id = get_current_user_id(); |
| 3024 | $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true ); |
| 3025 | if ( is_array( $draft_media ) ) { |
| 3026 | $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) { |
| 3027 | return $item['attachment_id'] !== $attachment_id; |
| 3028 | } ); |
| 3029 | update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) ); |
| 3030 | } |
| 3031 | |
| 3032 | // Delete video from OpenAI if applicable |
| 3033 | if ( !empty( $openai_id ) ) { |
| 3034 | $env = $this->core->get_ai_env( $envId ); |
| 3035 | $api_key = $env['apikey'] ?? ''; |
| 3036 | |
| 3037 | if ( !empty( $api_key ) ) { |
| 3038 | wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [ |
| 3039 | 'method' => 'DELETE', |
| 3040 | 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ], |
| 3041 | 'timeout' => 15 |
| 3042 | ] ); |
| 3043 | } |
| 3044 | } |
| 3045 | |
| 3046 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 3047 | } |
| 3048 | catch ( Exception $e ) { |
| 3049 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 3050 | } |
| 3051 | } |
| 3052 | |
| 3053 | public function rest_helpers_reject_media( $request ) { |
| 3054 | try { |
| 3055 | $params = $request->get_json_params(); |
| 3056 | $attachment_id = absint( $params['attachmentId'] ); |
| 3057 | $openai_id = sanitize_text_field( $params['openaiId'] ?? '' ); |
| 3058 | $envId = sanitize_text_field( $params['envId'] ?? '' ); |
| 3059 | |
| 3060 | if ( empty( $attachment_id ) ) { |
| 3061 | throw new Exception( 'Attachment ID is required.' ); |
| 3062 | } |
| 3063 | |
| 3064 | // Convert from mwai_image/mwai_video to attachment post type first |
| 3065 | // This ensures wp_delete_attachment properly deletes the physical file |
| 3066 | wp_update_post( [ |
| 3067 | 'ID' => $attachment_id, |
| 3068 | 'post_type' => 'attachment' |
| 3069 | ] ); |
| 3070 | |
| 3071 | // Delete attachment from WordPress (now that it's a proper attachment, files will be deleted) |
| 3072 | wp_delete_attachment( $attachment_id, true ); |
| 3073 | |
| 3074 | // Remove from draft media list |
| 3075 | $user_id = get_current_user_id(); |
| 3076 | $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true ); |
| 3077 | if ( is_array( $draft_media ) ) { |
| 3078 | $draft_media = array_filter( $draft_media, function ( $item ) use ( $attachment_id ) { |
| 3079 | return $item['attachment_id'] !== $attachment_id; |
| 3080 | } ); |
| 3081 | update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) ); |
| 3082 | } |
| 3083 | |
| 3084 | // Delete video from OpenAI if applicable |
| 3085 | if ( !empty( $openai_id ) ) { |
| 3086 | $env = $this->core->get_ai_env( $envId ); |
| 3087 | $api_key = $env['apikey'] ?? ''; |
| 3088 | |
| 3089 | if ( !empty( $api_key ) ) { |
| 3090 | wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [ |
| 3091 | 'method' => 'DELETE', |
| 3092 | 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ], |
| 3093 | 'timeout' => 15 |
| 3094 | ] ); |
| 3095 | } |
| 3096 | } |
| 3097 | |
| 3098 | return $this->create_rest_response( [ 'success' => true ], 200 ); |
| 3099 | } |
| 3100 | catch ( Exception $e ) { |
| 3101 | return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 3102 | } |
| 3103 | } |
| 3104 | |
| 3105 | #endregion |
| 3106 | } |
| 3107 |