settings = AICP_Settings::get_instance(); } public function init() { add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { // Validate consumer credentials endpoint register_rest_route($this->namespace, '/validate', array( 'methods' => 'POST', 'callback' => array($this, 'validate_credentials'), 'permission_callback' => array($this, 'check_hmac_signature'), )); // Get site info for ManagerApp register_rest_route($this->namespace, '/site-info', array( 'methods' => 'GET', 'callback' => array($this, 'get_site_info'), 'permission_callback' => array($this, 'check_hmac_signature'), )); // Publish blog post from AI Content Plan register_rest_route($this->namespace, '/publish', array( 'methods' => 'POST', 'callback' => array($this, 'publish_post'), 'permission_callback' => array($this, 'check_hmac_signature'), )); // Update existing blog post from AI Content Plan register_rest_route($this->namespace, '/update', array( 'methods' => 'POST', 'callback' => array($this, 'update_post'), 'permission_callback' => array($this, 'check_hmac_signature'), )); // Search posts and products register_rest_route($this->namespace, '/search', array( 'methods' => 'GET', 'callback' => array($this, 'search_content'), 'permission_callback' => array($this, 'check_hmac_signature'), )); // Upload media from external URLs register_rest_route($this->namespace, '/upload-media', array( 'methods' => 'POST', 'callback' => array($this, 'upload_media'), 'permission_callback' => array($this, 'check_hmac_signature'), )); // Fetch full content details by IDs register_rest_route($this->namespace, '/content', array( 'methods' => 'POST', 'callback' => array($this, 'get_content_by_ids'), 'permission_callback' => array($this, 'check_hmac_signature'), )); } /** * HMAC signature verification for incoming requests */ public function check_hmac_signature($request) { $consumer_key = $request->get_header('X-AICP-Key'); $timestamp = $request->get_header('X-AICP-Timestamp'); $signature = $request->get_header('X-AICP-Signature'); if (empty($consumer_key) || empty($timestamp) || empty($signature)) { return new WP_Error('missing_auth', 'Missing authentication headers', array('status' => 401)); } // Validate timestamp (within 5 minutes) $current_time = time(); if (abs($current_time - intval($timestamp)) > 300) { return new WP_Error('expired_request', 'Request timestamp expired', array('status' => 401)); } // Verify consumer key matches $stored_key = $this->settings->get_consumer_key(); if ($consumer_key !== $stored_key) { return new WP_Error('invalid_key', 'Invalid consumer key', array('status' => 401)); } // Compute expected signature $secret = $this->settings->get_consumer_secret(); $message = $consumer_key . ':' . $timestamp; $expected_signature = base64_encode(hash_hmac('sha256', $message, $secret, true)); if (!hash_equals($expected_signature, $signature)) { return new WP_Error('invalid_signature', 'Invalid signature', array('status' => 401)); } return true; } /** * Validate credentials and return site info */ public function validate_credentials($request) { return rest_ensure_response(array( 'valid' => true, 'site_url' => get_site_url(), 'site_name' => get_bloginfo('name'), 'wordpress_version' => get_bloginfo('version'), 'plugin_version' => AICP_BRIDGE_VERSION, )); } /** * Get site info (categories, authors) for configuration */ public function get_site_info($request) { // Get categories $categories = get_categories(array('hide_empty' => false)); $category_list = array_map(function($cat) { return array( 'id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug, ); }, $categories); // Get authors $authors = get_users(array('role__in' => array('administrator', 'editor', 'author'))); $author_list = array_map(function($user) { return array( 'id' => $user->ID, 'name' => $user->display_name, 'email' => $user->user_email, ); }, $authors); // Check WooCommerce status $woocommerce_active = class_exists('WooCommerce'); $product_categories = array(); if ($woocommerce_active) { $product_cats = get_terms(array('taxonomy' => 'product_cat', 'hide_empty' => false)); if (!is_wp_error($product_cats)) { $product_categories = array_map(function($cat) { return array( 'id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug, ); }, $product_cats); } } return rest_ensure_response(array( 'site_url' => get_site_url(), 'site_name' => get_bloginfo('name'), 'categories' => $category_list, 'authors' => $author_list, 'woocommerce_active' => $woocommerce_active, 'product_categories' => $product_categories, )); } /** * Validate post_status against allowed values, defaulting to 'draft' * * @param string $status The status to validate * @return string A valid post status */ private function validate_post_status($status) { if (empty($status) || !in_array($status, $this->allowed_post_statuses, true)) { return 'draft'; } return $status; } /** * Publish a blog post from AI Content Plan */ public function publish_post($request) { $params = $request->get_json_params(); // Validate required fields if (empty($params['title']) || empty($params['content'])) { return new WP_Error('missing_fields', 'Title and content are required', array('status' => 400)); } // Get publishing defaults $defaults = $this->settings->get_publishing_defaults(); // Determine and validate post status $raw_status = isset($params['status']) ? $params['status'] : $defaults['post_status']; $post_status = $this->validate_post_status($raw_status); // Prepare post data $post_data = array( 'post_title' => sanitize_text_field($params['title']), 'post_content' => wp_kses_post($params['content']), 'post_status' => $post_status, 'post_author' => isset($params['author_id']) ? intval($params['author_id']) : $defaults['default_author_id'], 'post_category' => isset($params['category_ids']) ? array_map('intval', $params['category_ids']) : ($defaults['default_category_id'] ? array($defaults['default_category_id']) : array()), ); // Add excerpt if provided if (!empty($params['excerpt'])) { $post_data['post_excerpt'] = sanitize_textarea_field($params['excerpt']); } // Add slug if provided if (!empty($params['slug'])) { $post_data['post_name'] = sanitize_title($params['slug']); } // Insert the post $post_id = wp_insert_post($post_data, true); if (is_wp_error($post_id)) { return new WP_Error('insert_failed', $post_id->get_error_message(), array('status' => 500)); } // Handle featured image (default to true if not explicitly set) $set_featured = isset($defaults['set_featured_image']) ? $defaults['set_featured_image'] : true; if (!empty($params['featured_image_url']) && $set_featured) { $this->set_featured_image($post_id, $params['featured_image_url'], $params['featured_image_id'] ?? null, $params['featured_image_title'] ?? null); } // Add meta data if (!empty($params['meta_description'])) { update_post_meta($post_id, '_yoast_wpseo_metadesc', sanitize_textarea_field($params['meta_description'])); update_post_meta($post_id, '_aioseop_description', sanitize_textarea_field($params['meta_description'])); } // Mark as created by AI Content Plan update_post_meta($post_id, '_aicp_created', true); update_post_meta($post_id, '_aicp_blog_post_id', sanitize_text_field($params['blog_post_id'] ?? '')); return rest_ensure_response(array( 'success' => true, 'post_id' => $post_id, 'post_url' => get_permalink($post_id), 'edit_url' => get_edit_post_link($post_id, 'raw'), )); } /** * Update an existing blog post from AI Content Plan */ public function update_post($request) { $params = $request->get_json_params(); // Validate required fields if (empty($params['post_id'])) { return new WP_Error('missing_post_id', 'post_id is required', array('status' => 400)); } $post_id = intval($params['post_id']); // Check if the post exists $existing_post = get_post($post_id); if (!$existing_post) { return new WP_Error('post_not_found', 'Post not found', array('status' => 404)); } // Build update data — only include fields that were sent $post_data = array( 'ID' => $post_id, ); if (!empty($params['title'])) { $post_data['post_title'] = sanitize_text_field($params['title']); } if (isset($params['content'])) { $post_data['post_content'] = wp_kses_post($params['content']); } if (!empty($params['excerpt'])) { $post_data['post_excerpt'] = sanitize_textarea_field($params['excerpt']); } if (!empty($params['slug'])) { $post_data['post_name'] = sanitize_title($params['slug']); } if (!empty($params['status'])) { $post_data['post_status'] = $this->validate_post_status($params['status']); } if (!empty($params['category_ids'])) { $post_data['post_category'] = array_map('intval', $params['category_ids']); } if (!empty($params['author_id'])) { $post_data['post_author'] = intval($params['author_id']); } // Update the post $result = wp_update_post($post_data, true); if (is_wp_error($result)) { return new WP_Error('update_failed', $result->get_error_message(), array('status' => 500)); } // Update featured image if provided (default to true if not explicitly set) $defaults = $this->settings->get_publishing_defaults(); $set_featured = isset($defaults['set_featured_image']) ? $defaults['set_featured_image'] : true; if (!empty($params['featured_image_url']) && $set_featured) { $this->set_featured_image($post_id, $params['featured_image_url'], $params['featured_image_id'] ?? null, $params['featured_image_title'] ?? null); } // Update meta data if (!empty($params['meta_description'])) { update_post_meta($post_id, '_yoast_wpseo_metadesc', sanitize_textarea_field($params['meta_description'])); update_post_meta($post_id, '_aioseop_description', sanitize_textarea_field($params['meta_description'])); } // Mark as updated by AI Content Plan update_post_meta($post_id, '_aicp_updated_at', current_time('mysql')); return rest_ensure_response(array( 'success' => true, 'post_id' => $post_id, 'post_url' => get_permalink($post_id), 'edit_url' => get_edit_post_link($post_id, 'raw'), )); } /** * Search posts and WooCommerce products */ public function search_content($request) { $query = sanitize_text_field($request->get_param('query') ?? ''); $type = sanitize_text_field($request->get_param('type') ?? 'all'); $per_page = intval($request->get_param('per_page') ?? 10); $per_page = max(1, min($per_page, 50)); $posts = array(); $products = array(); // Search posts if ($type === 'all' || $type === 'posts') { $post_args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => $per_page, 's' => $query, ); $found_posts = get_posts($post_args); foreach ($found_posts as $post) { $posts[] = array( 'id' => $post->ID, 'title' => $post->post_title, 'url' => get_permalink($post->ID), 'excerpt' => wp_trim_words($post->post_excerpt ?: $post->post_content, 30, '...'), 'date' => $post->post_date, 'type' => 'post', ); } } // Search products (if WooCommerce is active) if (($type === 'all' || $type === 'products') && class_exists('WooCommerce')) { $product_args = array( 's' => $query, 'limit' => $per_page, 'status' => 'publish', ); $found_products = wc_get_products($product_args); foreach ($found_products as $product) { $image_id = $product->get_image_id(); $image_url = $image_id ? wp_get_attachment_image_url($image_id, 'thumbnail') : null; $products[] = array( 'id' => $product->get_id(), 'title' => $product->get_name(), 'url' => $product->get_permalink(), 'excerpt' => wp_trim_words($product->get_short_description(), 30, '...'), 'price' => $product->get_price(), 'regularPrice' => $product->get_regular_price(), 'salePrice' => $product->get_sale_price(), 'imageUrl' => $image_url, 'type' => 'product', ); } } return rest_ensure_response(array( 'posts' => $posts, 'products' => $products, )); } /** * Fetch full content details by IDs */ public function get_content_by_ids($request) { $params = $request->get_json_params(); $post_ids = isset($params['post_ids']) ? array_map('intval', $params['post_ids']) : array(); $product_ids = isset($params['product_ids']) ? array_map('intval', $params['product_ids']) : array(); $posts = array(); $products = array(); // Fetch posts foreach ($post_ids as $post_id) { $post = get_post($post_id); if (!$post || $post->post_status !== 'publish') continue; $categories = wp_get_post_categories($post_id, array('fields' => 'names')); $tags = wp_get_post_tags($post_id, array('fields' => 'names')); $posts[] = array( 'id' => $post->ID, 'title' => $post->post_title, 'content' => $post->post_content, 'excerpt' => $post->post_excerpt, 'url' => get_permalink($post->ID), 'categories' => $categories ?: array(), 'tags' => $tags ?: array(), ); } // Fetch products (if WooCommerce is active) if (!empty($product_ids) && class_exists('WooCommerce')) { foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if (!$product || $product->get_status() !== 'publish') continue; $image_id = $product->get_image_id(); $image_url = $image_id ? wp_get_attachment_image_url($image_id, 'full') : null; $cat_ids = $product->get_category_ids(); $categories = array(); foreach ($cat_ids as $cat_id) { $term = get_term($cat_id, 'product_cat'); if ($term && !is_wp_error($term)) { $categories[] = $term->name; } } $products[] = array( 'id' => $product->get_id(), 'title' => $product->get_name(), 'description' => $product->get_description(), 'shortDescription' => $product->get_short_description(), 'url' => $product->get_permalink(), 'price' => $product->get_price(), 'regularPrice' => $product->get_regular_price(), 'salePrice' => $product->get_sale_price(), 'sku' => $product->get_sku(), 'imageUrl' => $image_url, 'categories' => $categories, ); } } return rest_ensure_response(array( 'posts' => $posts, 'products' => $products, )); } /** * Upload media from external URLs to WordPress media library */ public function upload_media($request) { $params = $request->get_json_params(); if (empty($params['images']) || !is_array($params['images'])) { return new WP_Error('missing_images', 'images array is required', array('status' => 400)); } require_once(ABSPATH . 'wp-admin/includes/media.php'); require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); $media = array(); foreach ($params['images'] as $image) { if (empty($image['url'])) { continue; } $image_id = !empty($image['image_id']) ? $image['image_id'] : null; $title = !empty($image['title']) ? sanitize_text_field($image['title']) : null; // Dedup: if this image was already uploaded, return the existing WP URL if ($image_id) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Required for image deduplication $existing = get_posts(array( 'post_type' => 'attachment', 'meta_key' => '_aicp_source_image_id', 'meta_value' => $image_id, 'posts_per_page' => 1, 'fields' => 'ids', )); if (!empty($existing)) { $media[] = array( 'media_id' => $existing[0], 'url' => wp_get_attachment_url($existing[0]), ); continue; } } $media_id = media_sideload_image($image['url'], 0, $title, 'id'); if (is_wp_error($media_id)) { $media[] = array( 'error' => $media_id->get_error_message(), 'url' => $image['url'], ); } else { // Set the attachment title to the original filename (without extension) if ($title) { wp_update_post(array( 'ID' => $media_id, 'post_title' => $title, )); } // Store source image ID for deduplication on future republishes if ($image_id) { update_post_meta($media_id, '_aicp_source_image_id', $image_id); } update_post_meta($media_id, '_aicp_uploaded_by', 'aicontentplan.com'); $media[] = array( 'media_id' => $media_id, 'url' => wp_get_attachment_url($media_id), ); } } return rest_ensure_response(array( 'media' => $media, )); } /** * Set featured image from URL. * Skips re-upload if the current thumbnail was already sideloaded from the same URL. */ private function set_featured_image($post_id, $image_url, $image_id = null, $title = null) { // Dedup: skip re-upload if current thumbnail came from the same source image $current_thumbnail_id = get_post_thumbnail_id($post_id); if ($current_thumbnail_id && $image_id) { $existing_image_id = get_post_meta($current_thumbnail_id, '_aicp_source_image_id', true); if ($existing_image_id === $image_id) { return; // Same image, skip re-upload } } require_once(ABSPATH . 'wp-admin/includes/media.php'); require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); $attachment_id = media_sideload_image($image_url, $post_id, $title, 'id'); if (is_wp_error($attachment_id)) { return; } // Set the attachment title to the original filename (without extension) if ($title) { wp_update_post(array( 'ID' => $attachment_id, 'post_title' => sanitize_text_field($title), )); } set_post_thumbnail($post_id, $attachment_id); // Store source image ID for deduplication on future updates if ($image_id) { update_post_meta($attachment_id, '_aicp_source_image_id', $image_id); } update_post_meta($attachment_id, '_aicp_uploaded_by', 'aicontentplan.com'); } }