\d+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_post_by_content_id'), 'permission_callback' => array($this, 'check_permission'), 'args' => array( 'content_id' => array( 'required' => true, 'type' => 'integer', 'description' => 'IGNY8 content ID' ) ) )); // Get post by IGNY8 task_id register_rest_route('igny8/v1', '/post-by-task-id/(?P\d+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_post_by_task_id'), 'permission_callback' => array($this, 'check_permission'), 'args' => array( 'task_id' => array( 'required' => true, 'type' => 'integer', 'description' => 'IGNY8 task ID' ) ) )); // Get post status by content_id or post_id register_rest_route('igny8/v1', '/post-status/(?P\d+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_post_status'), 'permission_callback' => array($this, 'check_permission'), 'args' => array( 'id' => array( 'required' => true, 'type' => 'integer', 'description' => 'WordPress post ID or IGNY8 content ID (tries both)' ) ) )); // Site metadata - post types, taxonomies and counts (unified response format) register_rest_route('igny8/v1', '/site-metadata/', array( 'methods' => 'GET', // We perform permission checks inside callback to ensure unified response format 'callback' => array($this, 'get_site_metadata'), 'permission_callback' => '__return_true', )); // Plugin status endpoint - returns connection status and API key info register_rest_route('igny8/v1', '/status', array( 'methods' => 'GET', 'callback' => array($this, 'get_status'), 'permission_callback' => '__return_true', // Public endpoint for health checks )); // Manual publish endpoint - for triggering WordPress publish from IGNY8 register_rest_route('igny8/v1', '/publish-content/', array( 'methods' => 'POST', 'callback' => array($this, 'publish_content_to_wordpress'), 'permission_callback' => array($this, 'check_permission'), 'args' => array( 'content_id' => array( 'required' => true, 'type' => 'integer', 'description' => 'IGNY8 content ID' ), 'task_id' => array( 'required' => false, 'type' => 'integer', 'description' => 'IGNY8 task ID' ) ) )); } /** * Check API permission - uses API key only * * @param WP_REST_Request $request Request object * @return bool|WP_Error */ public function check_permission($request) { // Check if authenticated with IGNY8 via API key $api = new Igny8API(); // Accept explicit X-IGNY8-API-KEY header for incoming requests $header_api_key = $request->get_header('x-igny8-api-key'); if ($header_api_key) { $stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); if ($stored_api_key && hash_equals($stored_api_key, $header_api_key)) { return true; } } // Check Authorization Bearer header $auth_header = $request->get_header('Authorization'); if ($auth_header) { $stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); if ($stored_api_key && strpos($auth_header, 'Bearer ' . $stored_api_key) !== false) { return true; } } // Allow if API key is configured (for internal use) if ($api->is_authenticated()) { return true; } return new WP_Error( 'rest_forbidden', __('IGNY8 API key not authenticated', 'igny8-bridge'), array('status' => 401) ); } /** * Get post by content_id * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function get_post_by_content_id($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) ); } $content_id = intval($request['content_id']); // Find post by content_id meta $posts = get_posts(array( 'meta_key' => '_igny8_content_id', 'meta_value' => $content_id, 'post_type' => 'any', 'posts_per_page' => 1, 'post_status' => 'any' )); if (empty($posts)) { return new WP_Error( 'rest_not_found', __('Post not found for this content ID', 'igny8-bridge'), array('status' => 404) ); } $post = $posts[0]; return rest_ensure_response(array( 'success' => true, 'data' => array( 'post_id' => $post->ID, 'title' => $post->post_title, 'status' => $post->post_status, 'wordpress_status' => $post->post_status, 'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status), 'url' => get_permalink($post->ID), 'post_type' => $post->post_type, 'content_id' => $content_id, 'task_id' => get_post_meta($post->ID, '_igny8_task_id', true), 'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true) ) )); } /** * Get post by task_id * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function get_post_by_task_id($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) ); } $task_id = intval($request['task_id']); // Find post by task_id meta $posts = get_posts(array( 'meta_key' => '_igny8_task_id', 'meta_value' => $task_id, 'post_type' => 'any', 'posts_per_page' => 1, 'post_status' => 'any' )); if (empty($posts)) { return new WP_Error( 'rest_not_found', __('Post not found for this task ID', 'igny8-bridge'), array('status' => 404) ); } $post = $posts[0]; return rest_ensure_response(array( 'success' => true, 'data' => array( 'post_id' => $post->ID, 'title' => $post->post_title, 'status' => $post->post_status, 'wordpress_status' => $post->post_status, 'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status), 'url' => get_permalink($post->ID), 'post_type' => $post->post_type, 'task_id' => $task_id, 'content_id' => get_post_meta($post->ID, '_igny8_content_id', true), 'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true) ) )); } /** * Get post status by post ID or content_id * Accepts either WordPress post_id or IGNY8 content_id * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function get_post_status($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) ); } $id = intval($request['id']); $post = null; $lookup_method = null; // First try as WordPress post ID if (post_type_exists('post') || post_type_exists('page')) { $post = get_post($id); if ($post) { $lookup_method = 'wordpress_post_id'; } } // If not found, try as IGNY8 content_id if (!$post) { $posts = get_posts(array( 'meta_key' => '_igny8_content_id', 'meta_value' => $id, 'post_type' => 'any', 'posts_per_page' => 1, 'post_status' => 'any' )); if (!empty($posts)) { $post = $posts[0]; $lookup_method = 'igny8_content_id'; } } if (!$post) { return rest_ensure_response(array( 'success' => false, 'message' => 'Post not found', 'searched_id' => $id )); } return rest_ensure_response(array( 'success' => true, 'data' => array( 'post_id' => $post->ID, 'post_status' => $post->post_status, 'post_title' => $post->post_title, 'post_type' => $post->post_type, 'post_modified' => $post->post_modified, 'post_url' => get_permalink($post->ID), 'wordpress_status' => $post->post_status, 'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status), 'content_id' => get_post_meta($post->ID, '_igny8_content_id', true), 'task_id' => get_post_meta($post->ID, '_igny8_task_id', true), 'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true), 'lookup_method' => $lookup_method ) )); } /** * Get post status by content_id (DEPRECATED - use get_post_status instead) * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function get_post_status_by_content_id($request) { // Redirect to new unified method return $this->get_post_status($request); } /** * Helper: generate a request_id (UUIDv4 if available) * * @return string */ private function generate_request_id() { if (function_exists('wp_generate_uuid4')) { return wp_generate_uuid4(); } // Fallback: uniqid with more entropy return uniqid('', true); } /** * Helper: Build unified API response and return WP_REST_Response * * @param bool $success * @param mixed $data * @param string|null $message * @param string|null $error * @param array|null $errors * @param int $status * @return WP_REST_Response */ private function build_unified_response($success, $data = null, $message = null, $error = null, $errors = null, $status = 200) { $payload = array( 'success' => (bool) $success, 'data' => $data, 'message' => $message, 'request_id' => $this->generate_request_id() ); if (!$success) { $payload['error'] = $error ?: 'Unknown error'; if (!empty($errors)) { $payload['errors'] = $errors; } } $response = rest_ensure_response($payload); $response->set_status($status); return $response; } /** * GET /status - Returns plugin connection status and API key info * * @param WP_REST_Request $request * @return WP_REST_Response */ public function get_status($request) { $api = new Igny8API(); $api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); $connection_enabled = igny8_is_connection_enabled(); $data = array( 'connected' => !empty($api_key) && $api->is_authenticated(), 'has_api_key' => !empty($api_key), 'communication_enabled' => $connection_enabled, 'plugin_version' => defined('IGNY8_BRIDGE_VERSION') ? IGNY8_BRIDGE_VERSION : '1.0.0', 'wordpress_version' => get_bloginfo('version'), 'last_health_check' => get_option('igny8_last_api_health_check', 0), 'health' => (!empty($api_key) && $connection_enabled) ? 'healthy' : 'not_configured' ); return $this->build_unified_response(true, $data, 'Plugin status retrieved', null, null, 200); } /** * GET /site-metadata/ - returns post types, taxonomies and counts in unified format * * @param WP_REST_Request $request * @return WP_REST_Response */ public function get_site_metadata($request) { // Use transient cache to avoid expensive counts on large sites $cache_key = 'igny8_site_metadata_v1'; $cached = get_transient($cache_key); if ($cached !== false) { return $this->build_unified_response(true, $cached, 'Site metadata (cached)', null, null, 200); } // Perform permission check and return unified error if not allowed $perm = $this->check_permission($request); if (is_wp_error($perm)) { $status = 403; $error_data = $perm->get_error_data(); if (is_array($error_data) && isset($error_data['status'])) { $status = intval($error_data['status']); } return $this->build_unified_response(false, null, null, $perm->get_error_message(), null, $status); } // Collect post types (public) $post_types_objects = get_post_types(array('public' => true), 'objects'); $post_types = array(); foreach ($post_types_objects as $slug => $obj) { // Get total count across statuses $count_obj = wp_count_posts($slug); $total = 0; if (is_object($count_obj)) { foreach (get_object_vars($count_obj) as $val) { $total += intval($val); } } $post_types[$slug] = array( 'label' => $obj->labels->singular_name ?? $obj->label, 'count' => $total ); } // Collect taxonomies (public) $taxonomy_objects = get_taxonomies(array('public' => true), 'objects'); $taxonomies = array(); foreach ($taxonomy_objects as $slug => $obj) { // Use wp_count_terms when available $term_count = 0; if (function_exists('wp_count_terms')) { $term_count = intval(wp_count_terms($slug)); } else { $terms = get_terms(array('taxonomy' => $slug, 'hide_empty' => false, 'fields' => 'ids')); $term_count = is_array($terms) ? count($terms) : 0; } $taxonomies[$slug] = array( 'label' => $obj->labels->name ?? $obj->label, 'count' => $term_count ); } $data = array( 'post_types' => $post_types, 'taxonomies' => $taxonomies, 'generated_at' => time(), 'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(), 'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1) ); // Cache for 5 minutes set_transient($cache_key, $data, 300); return $this->build_unified_response(true, $data, 'Site metadata retrieved', null, null, 200); } /** * Publish content to WordPress * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function publish_content_to_wordpress($request) { // Check connection if (!igny8_is_connection_enabled()) { return $this->build_unified_response( false, null, 'IGNY8 connection is disabled', 'connection_disabled', null, 403 ); } // Get content data from POST body (IGNY8 backend already sends everything) $content_data = $request->get_json_params(); // Extract IDs for validation $content_id = isset($content_data['content_id']) ? $content_data['content_id'] : null; $task_id = isset($content_data['task_id']) ? $content_data['task_id'] : null; // ALWAYS log incoming data for debugging error_log('========== IGNY8 PUBLISH REQUEST =========='); error_log('Content ID: ' . $content_id); error_log('Task ID: ' . $task_id); error_log('Title: ' . (isset($content_data['title']) ? $content_data['title'] : 'MISSING')); error_log('Content HTML: ' . (isset($content_data['content_html']) ? strlen($content_data['content_html']) . ' chars' : 'MISSING')); error_log('Categories: ' . (isset($content_data['categories']) ? json_encode($content_data['categories']) : 'MISSING')); error_log('Tags: ' . (isset($content_data['tags']) ? json_encode($content_data['tags']) : 'MISSING')); error_log('Featured Image: ' . (isset($content_data['featured_image_url']) ? $content_data['featured_image_url'] : 'MISSING')); error_log('Gallery Images: ' . (isset($content_data['gallery_images']) ? count($content_data['gallery_images']) . ' images' : 'MISSING')); error_log('SEO Title: ' . (isset($content_data['seo_title']) ? 'YES' : 'NO')); error_log('SEO Description: ' . (isset($content_data['seo_description']) ? 'YES' : 'NO')); error_log('Primary Keyword: ' . (isset($content_data['primary_keyword']) ? $content_data['primary_keyword'] : 'MISSING')); error_log('==========================================='); // Validate required fields if (empty($content_id)) { return $this->build_unified_response( false, null, 'Missing content_id in request', 'missing_content_id', null, 400 ); } if (empty($content_data['title'])) { return $this->build_unified_response( false, null, 'Missing title in request', 'missing_title', null, 400 ); } if (empty($content_data['content_html'])) { return $this->build_unified_response( false, null, 'Missing content_html in request', 'missing_content_html', null, 400 ); } // Debug logging if (defined('IGNY8_DEBUG') && IGNY8_DEBUG) { error_log('IGNY8 Publish Request - Content ID: ' . $content_id); error_log('IGNY8 Publish Request - Title: ' . $content_data['title']); error_log('IGNY8 Publish Request - Content HTML length: ' . strlen($content_data['content_html'])); } // Check if content already exists $existing_posts = get_posts(array( 'meta_key' => '_igny8_content_id', 'meta_value' => $content_id, 'post_type' => 'any', 'posts_per_page' => 1 )); if (!empty($existing_posts)) { return $this->build_unified_response( false, array('post_id' => $existing_posts[0]->ID), 'Content already exists as WordPress post', 'content_exists', null, 409 ); } // Create WordPress post $post_id = igny8_create_wordpress_post_from_task($content_data); if (is_wp_error($post_id)) { return $this->build_unified_response( false, null, 'Failed to create WordPress post: ' . $post_id->get_error_message(), 'post_creation_failed', null, 500 ); } // Return success response return $this->build_unified_response( true, array( 'post_id' => $post_id, 'post_url' => get_permalink($post_id), 'post_status' => get_post_status($post_id), 'content_id' => $content_id, 'task_id' => $task_id ), 'Content successfully published to WordPress', null, null, 201 ); } } // Initialize REST API new Igny8RestAPI();