'GET', 'callback' => array( __CLASS__, 'rest_export_content' ), 'permission_callback' => array( __CLASS__, 'check_export_permissions' ), 'args' => array( 'post_type' => array( 'default' => 'all', 'sanitize_callback' => 'sanitize_text_field', ), 'status' => array( 'default' => 'publish', 'sanitize_callback' => 'sanitize_text_field', ), 'ids' => array( 'default' => array(), 'sanitize_callback' => array( __CLASS__, 'sanitize_ids_array' ), ), ), ) ); } /** * Check permissions for export endpoint. * * @param WP_REST_Request $request Request object. * @return bool True if user has permission. */ public static function check_export_permissions( $request ) { // Check for API key in header $api_key = $request->get_header( 'X-AEO-API-Key' ); $stored_key = get_option( 'ai_answer_toolkit_api_key' ); if ( ! empty( $api_key ) && ! empty( $stored_key ) && hash_equals( $stored_key, $api_key ) ) { return true; } // Check for admin capability if ( current_user_can( 'manage_options' ) ) { return true; } // Public read-only access for published content $status = $request->get_param( 'status' ); if ( $status === 'publish' ) { return true; } return false; } /** * Sanitize IDs array. * * @param mixed $value Value to sanitize. * @return array Sanitized array of IDs. */ public static function sanitize_ids_array( $value ) { if ( ! is_array( $value ) ) { $value = explode( ',', $value ); } return array_map( 'absint', $value ); } /** * REST API callback for content export. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public static function rest_export_content( $request ) { $post_type = $request->get_param( 'post_type' ); $status = $request->get_param( 'status' ); $ids = $request->get_param( 'ids' ); $args = array( 'post_type' => $post_type, 'post_status' => $status, 'ids' => $ids, ); $data = self::get_bulk_export_data( $args ); return new WP_REST_Response( array( 'success' => true, 'count' => count( $data ), 'posts' => $data, ), 200 ); } /** * Get export data for a single post. * * @param int $post_id Post ID. * @return array|null Export data or null if post not found. */ public static function get_post_export_data( $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { return null; } // Get final summary $summary = AI_Answer_Toolkit_Summary::get_final_summary( $post_id ); $summary_type = AI_Answer_Toolkit_Summary::get_summary_type( $post_id ); // Get FAQ items $faq_items = self::extract_faq_items( $post ); // Get service metadata if applicable $service_metadata = null; if ( $post->post_type === 'service' ) { $service_metadata = array( 'areaServed' => get_post_meta( $post_id, '_aitk_service_area', true ), 'serviceType' => get_post_meta( $post_id, '_aitk_service_type', true ), ); } // Get cleaned content $content = self::get_cleaned_content( $post ); // Build export data $export_data = array( 'id' => $post_id, 'title' => get_the_title( $post_id ), 'summary' => $summary, 'summary_type' => $summary_type, 'faq' => $faq_items, 'service_metadata' => $service_metadata, 'content' => $content, 'url' => get_permalink( $post_id ), 'post_type' => $post->post_type, 'published_date' => get_the_date( 'c', $post_id ), 'modified_date' => get_the_modified_date( 'c', $post_id ), ); // Allow filtering return apply_filters( 'ai_answer_toolkit_export_post', $export_data, $post ); } /** * Get bulk export data. * * @param array $args Query arguments. * @return array Array of export data. */ public static function get_bulk_export_data( $args ) { $defaults = array( 'post_type' => 'all', 'post_status' => 'publish', 'ids' => array(), 'per_page' => 100, 'paged' => 1, ); $args = wp_parse_args( $args, $defaults ); // Build query args $query_args = array( 'post_status' => $args['post_status'], 'posts_per_page' => $args['per_page'], 'paged' => $args['paged'], 'orderby' => 'date', 'order' => 'DESC', ); // Handle post type if ( $args['post_type'] === 'all' ) { $available = self::get_available_post_types(); $query_args['post_type'] = array_keys( $available ); } else { $query_args['post_type'] = $args['post_type']; } // Handle specific IDs if ( ! empty( $args['ids'] ) ) { $query_args['post__in'] = $args['ids']; $query_args['posts_per_page'] = -1; } $query = new WP_Query( $query_args ); $export_data = array(); if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); $post_data = self::get_post_export_data( get_the_ID() ); if ( $post_data ) { $export_data[] = $post_data; } } wp_reset_postdata(); } // Allow filtering return apply_filters( 'ai_answer_toolkit_export_bulk', $export_data, $args ); } /** * Extract FAQ items from post. * * @param WP_Post $post Post object. * @return array FAQ items. */ private static function extract_faq_items( $post ) { $content = get_post_field( 'post_content', $post->ID ); if ( empty( $content ) ) { return array(); } $blocks = parse_blocks( $content ); if ( empty( $blocks ) ) { return array(); } return self::parse_faq_blocks( $blocks ); } /** * Parse FAQ blocks recursively. * * @param array $blocks Gutenberg blocks. * @return array FAQ items. */ private static function parse_faq_blocks( $blocks ) { $faq_items = array(); for ( $i = 0; $i < count( $blocks ); $i++ ) { $block = $blocks[ $i ]; if ( empty( $block['blockName'] ) ) { continue; } if ( $block['blockName'] === 'core/heading' ) { $question = wp_strip_all_tags( $block['innerHTML'] ); if ( ! empty( $question ) && substr( trim( $question ), -1 ) === '?' ) { if ( isset( $blocks[ $i + 1 ] ) && $blocks[ $i + 1 ]['blockName'] === 'core/paragraph' ) { $answer = wp_strip_all_tags( $blocks[ $i + 1 ]['innerHTML'] ); if ( ! empty( $answer ) ) { $faq_items[] = array( 'question' => trim( $question ), 'answer' => trim( $answer ), ); } } } } if ( ! empty( $block['innerBlocks'] ) ) { $inner_faq = self::parse_faq_blocks( $block['innerBlocks'] ); if ( ! empty( $inner_faq ) ) { $faq_items = array_merge( $faq_items, $inner_faq ); } } } return $faq_items; } /** * Get cleaned content. * * @param WP_Post $post Post object. * @return string Cleaned content. */ private static function get_cleaned_content( $post ) { $content = $post->post_content; // Remove shortcodes $content = strip_shortcodes( $content ); // Remove HTML tags $content = wp_strip_all_tags( $content ); // Remove extra whitespace $content = preg_replace( '/\s+/', ' ', $content ); // Trim $content = trim( $content ); return $content; } /** * Get available post types. * * @return array Post types. */ private static function get_available_post_types() { $post_types = get_post_types( array( 'public' => true ), 'objects' ); $available = array(); foreach ( $post_types as $post_type ) { if ( $post_type->name === 'attachment' ) { continue; } $available[ $post_type->name ] = $post_type->labels->name; } return $available; } /** * Export content to JSON file. * * @param array $args Export arguments. * @return array Result with file path and URL. */ public static function export_to_file( $args = array() ) { $data = self::get_bulk_export_data( $args ); // Create export directory $upload_dir = wp_upload_dir(); $export_dir = $upload_dir['basedir'] . '/aeo-export'; if ( ! file_exists( $export_dir ) ) { wp_mkdir_p( $export_dir ); } // Generate filename $filename = 'aeo-export-' . gmdate( 'Y-m-d-His' ) . '.json'; $filepath = $export_dir . '/' . $filename; // Write JSON file $json = wp_json_encode( array( 'exported_at' => gmdate( 'c' ), 'site_url' => home_url(), 'count' => count( $data ), 'posts' => $data, ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); $result = file_put_contents( $filepath, $json ); if ( $result === false ) { return array( 'success' => false, 'message' => __( 'Failed to write export file.', 'ai-answer-toolkit' ), ); } // Update last export timestamp update_option( 'ai_answer_toolkit_last_export', time() ); update_option( 'ai_answer_toolkit_last_export_file', $filename ); return array( 'success' => true, 'filepath' => $filepath, 'filename' => $filename, 'url' => $upload_dir['baseurl'] . '/aeo-export/' . $filename, 'size' => filesize( $filepath ), 'count' => count( $data ), ); } /** * Trigger webhook on post save. * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ public static function maybe_trigger_webhook( $post_id, $post ) { // Check if webhook is enabled $webhook_enabled = get_option( 'ai_answer_toolkit_webhook_enabled', false ); if ( ! $webhook_enabled ) { return; } // Skip autosaves and revisions if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( wp_is_post_revision( $post_id ) ) { return; } // Get webhook URL $webhook_url = get_option( 'ai_answer_toolkit_webhook_url' ); if ( empty( $webhook_url ) ) { return; } // Get export data $export_data = self::get_post_export_data( $post_id ); if ( ! $export_data ) { return; } // Send webhook self::send_webhook( $webhook_url, $export_data ); } /** * Send webhook to external endpoint. * * @param string $url Webhook URL. * @param array $data Export data. * @return array|WP_Error Response or error. */ public static function send_webhook( $url, $data ) { $api_key = get_option( 'ai_answer_toolkit_webhook_api_key' ); $headers = array( 'Content-Type' => 'application/json', ); if ( ! empty( $api_key ) ) { $headers['Authorization'] = 'Bearer ' . $api_key; } $response = wp_remote_post( $url, array( 'headers' => $headers, 'body' => wp_json_encode( $data ), 'timeout' => 15, ) ); do_action( 'ai_answer_toolkit_export_after_send', $response, $url, $data ); return $response; } /** * Get last export info. * * @return array Export info. */ public static function get_last_export_info() { $timestamp = get_option( 'ai_answer_toolkit_last_export' ); $filename = get_option( 'ai_answer_toolkit_last_export_file' ); if ( ! $timestamp ) { return array( 'has_export' => false, ); } $upload_dir = wp_upload_dir(); $filepath = $upload_dir['basedir'] . '/aeo-export/' . $filename; $filesize = file_exists( $filepath ) ? filesize( $filepath ) : 0; return array( 'has_export' => true, 'timestamp' => $timestamp, 'date' => gmdate( 'Y-m-d H:i:s', $timestamp ), 'filename' => $filename, 'size' => $filesize, 'size_human' => size_format( $filesize ), 'url' => $upload_dir['baseurl'] . '/aeo-export/' . $filename, ); } } AI_Answer_Toolkit_Export::init();