'POST', 'callback' => array($this, 'handle_webhook'), 'permission_callback' => array($this, 'verify_webhook_secret'), 'args' => array( 'event' => array( 'required' => true, 'type' => 'string', 'description' => 'Event type' ), 'data' => array( 'required' => true, 'type' => 'object', 'description' => 'Event data' ) ) )); } /** * Verify webhook shared secret * * @param WP_REST_Request $request Request object * @return bool|WP_Error */ public function verify_webhook_secret($request) { // First check if connection is enabled if (!igny8_is_connection_enabled()) { return new WP_Error( 'rest_forbidden', __('IGNY8 connection is disabled', 'igny8-bridge'), array('status' => 403) ); } // Get shared secret from settings $shared_secret = igny8_get_webhook_secret(); if (empty($shared_secret)) { return new WP_Error( 'rest_forbidden', __('Webhook secret not configured', 'igny8-bridge'), array('status' => 403) ); } // Check X-IGNY8-Signature header $signature = $request->get_header('X-IGNY8-Signature'); if (empty($signature)) { return new WP_Error( 'rest_forbidden', __('Missing webhook signature', 'igny8-bridge'), array('status' => 401) ); } // Verify signature $body = $request->get_body(); $expected_signature = hash_hmac('sha256', $body, $shared_secret); if (!hash_equals($expected_signature, $signature)) { igny8_log_webhook_activity(array( 'event' => 'authentication_failed', 'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'), 'error' => 'Invalid signature' )); return new WP_Error( 'rest_forbidden', __('Invalid webhook signature', 'igny8-bridge'), array('status' => 401) ); } return true; } /** * Handle incoming webhook * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function handle_webhook($request) { // Double-check connection is enabled if (!igny8_is_connection_enabled()) { return new WP_Error( 'rest_forbidden', __('IGNY8 connection is disabled', 'igny8-bridge'), array('status' => 403) ); } $event = $request->get_param('event'); $data = $request->get_param('data'); if (empty($event) || empty($data)) { return new WP_Error( 'rest_invalid_param', __('Missing event or data parameter', 'igny8-bridge'), array('status' => 400) ); } // Log webhook receipt $log_id = igny8_log_webhook_activity(array( 'event' => $event, 'data' => $data, 'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'), 'user_agent' => $request->get_header('User-Agent'), 'status' => 'received' )); // Route to appropriate handler $result = null; switch ($event) { case 'task_published': case 'task_completed': $result = $this->handle_task_published($data); break; case 'link_recommendation': case 'insert_link': $result = $this->handle_link_recommendation($data); break; case 'optimizer_request': case 'optimizer_job_completed': $result = $this->handle_optimizer_request($data); break; default: $result = array( 'success' => false, 'error' => 'Unknown event type: ' . $event ); } // Update log with result if ($log_id) { igny8_update_webhook_log($log_id, array( 'status' => $result['success'] ? 'processed' : 'failed', 'response' => $result, 'processed_at' => current_time('mysql') )); } return rest_ensure_response($result); } /** * Handle task published event * * @param array $data Event data * @return array Result */ private function handle_task_published($data) { if (!igny8_is_connection_enabled()) { return array('success' => false, 'error' => 'Connection disabled'); } if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('writer')) { return array('success' => false, 'error' => 'Writer module disabled'); } $task_id = $data['task_id'] ?? null; if (!$task_id) { return array('success' => false, 'error' => 'Missing task_id'); } // Check if post already exists $existing_posts = get_posts(array( 'meta_key' => '_igny8_task_id', 'meta_value' => $task_id, 'post_type' => 'any', 'posts_per_page' => 1 )); if (!empty($existing_posts)) { // Post already exists, just update status if needed $post_id = $existing_posts[0]->ID; $status = $data['status'] ?? 'publish'; if ($status === 'publish' || $status === 'completed') { wp_update_post(array( 'ID' => $post_id, 'post_status' => 'publish' )); } return array( 'success' => true, 'message' => 'Post updated', 'post_id' => $post_id ); } // Fetch full task data and create post $api = new Igny8API(); $task_response = $api->get("/writer/tasks/{$task_id}/"); if (!$task_response['success']) { return array( 'success' => false, 'error' => 'Failed to fetch task: ' . ($task_response['error'] ?? 'Unknown error') ); } $task = $task_response['data']; $enabled_post_types = igny8_get_enabled_post_types(); $content_data = array( 'task_id' => $task['id'], 'title' => $task['title'] ?? 'Untitled', 'content' => $task['content'] ?? '', 'status' => $task['status'] ?? 'draft', 'cluster_id' => $task['cluster_id'] ?? null, 'sector_id' => $task['sector_id'] ?? null, 'keyword_ids' => $task['keyword_ids'] ?? array(), 'content_type' => $task['content_type'] ?? 'post', 'categories' => $task['categories'] ?? array(), 'tags' => $task['tags'] ?? array(), 'featured_image' => $task['featured_image'] ?? null, 'gallery_images' => $task['gallery_images'] ?? array(), 'meta_title' => $task['meta_title'] ?? null, 'meta_description' => $task['meta_description'] ?? null ); $post_id = igny8_create_wordpress_post_from_task($content_data, $enabled_post_types); if (is_wp_error($post_id)) { return array( 'success' => false, 'error' => $post_id->get_error_message() ); } return array( 'success' => true, 'message' => 'Post created', 'post_id' => $post_id ); } /** * Handle link recommendation event * * @param array $data Event data * @return array Result */ private function handle_link_recommendation($data) { if (!igny8_is_connection_enabled()) { return array('success' => false, 'error' => 'Connection disabled'); } if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { return array('success' => false, 'error' => 'Linker module disabled'); } $post_id = $data['post_id'] ?? null; $target_url = $data['target_url'] ?? null; $anchor = $data['anchor'] ?? $data['anchor_text'] ?? null; if (!$post_id || !$target_url || !$anchor) { return array( 'success' => false, 'error' => 'Missing required parameters: post_id, target_url, anchor' ); } // Queue link insertion $queued = igny8_queue_link_insertion(array( 'post_id' => intval($post_id), 'target_url' => esc_url_raw($target_url), 'anchor' => sanitize_text_field($anchor), 'source' => 'igny8_linker', 'priority' => $data['priority'] ?? 'normal', 'created_at' => current_time('mysql') )); if ($queued) { return array( 'success' => true, 'message' => 'Link queued for insertion', 'queue_id' => $queued ); } return array( 'success' => false, 'error' => 'Failed to queue link insertion' ); } /** * Handle optimizer request event * * @param array $data Event data * @return array Result */ private function handle_optimizer_request($data) { if (!igny8_is_connection_enabled()) { return array('success' => false, 'error' => 'Connection disabled'); } if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('optimizer')) { return array('success' => false, 'error' => 'Optimizer module disabled'); } $post_id = $data['post_id'] ?? null; $job_id = $data['job_id'] ?? null; $status = $data['status'] ?? null; $score_changes = $data['score_changes'] ?? null; $recommendations = $data['recommendations'] ?? null; if (!$post_id) { return array('success' => false, 'error' => 'Missing post_id'); } // Update optimizer status if job_id provided if ($job_id) { update_post_meta($post_id, '_igny8_optimizer_job_id', $job_id); } if ($status) { update_post_meta($post_id, '_igny8_optimizer_status', $status); } if ($score_changes) { update_post_meta($post_id, '_igny8_optimizer_score_changes', $score_changes); } if ($recommendations) { update_post_meta($post_id, '_igny8_optimizer_recommendations', $recommendations); } return array( 'success' => true, 'message' => 'Optimizer data updated', 'post_id' => $post_id ); } } // Initialize webhooks new Igny8Webhooks();