dependencies = $dependencies; $this->product_creator = $product_creator; $this->product_ai_generator = $product_ai_generator; } /** * Initialize action hooks. * * @return void */ public function init() { add_action( 'admin_post_apsw_prepare_product_draft_input', array( $this, 'handle_prepare_input' ) ); add_action( 'admin_post_apsw_create_manual_product_draft', array( $this, 'handle_create_manual_product_draft' ) ); add_action( 'admin_post_apsw_generate_ai_product_data', array( $this, 'handle_generate_ai_product_data' ) ); add_action( 'admin_post_apsw_create_ai_product_draft', array( $this, 'handle_create_ai_product_draft' ) ); add_action( 'admin_post_apsw_generate_and_create_product_draft', array( $this, 'handle_generate_and_create_product_draft' ) ); add_action( 'admin_post_apsw_review_prompt_action', array( $this, 'handle_review_prompt_action' ) ); } /** * Return allowed product context options. * * @return array */ public function get_allowed_contexts() { return array( 'general' => __( 'General product', 'ai-product-studio-for-woocommerce' ), 'clothing' => __( 'Clothing', 'ai-product-studio-for-woocommerce' ), 'shoes' => __( 'Shoes', 'ai-product-studio-for-woocommerce' ), 'book' => __( 'Book', 'ai-product-studio-for-woocommerce' ), 'cosmetics' => __( 'Cosmetics', 'ai-product-studio-for-woocommerce' ), 'food' => __( 'Food', 'ai-product-studio-for-woocommerce' ), 'digital' => __( 'Digital product', 'ai-product-studio-for-woocommerce' ), 'handmade' => __( 'Handmade product', 'ai-product-studio-for-woocommerce' ), ); } /** * Return status preference options. * * @return array */ public function get_status_options() { return array( 'draft' => __( 'Review before publishing', 'ai-product-studio-for-woocommerce' ), 'private' => __( 'Private', 'ai-product-studio-for-woocommerce' ), ); } /** * Return product kind options. * * @return array */ public function get_product_kind_options() { return array( 'physical' => __( 'Physical product', 'ai-product-studio-for-woocommerce' ), 'virtual' => __( 'Virtual product', 'ai-product-studio-for-woocommerce' ), ); } /** * Return WordPress.org review URL. * * @return string */ public function get_review_url() { return self::REVIEW_URL; } /** * Determine whether the review prompt should show for a user. * * @param int $user_id User ID. * @return bool */ public function should_show_review_prompt( $user_id = 0 ) { $user_id = $user_id ? absint( $user_id ) : get_current_user_id(); if ( $user_id <= 0 ) { return false; } $first_success = absint( get_user_meta( $user_id, self::REVIEW_PROMPT_FIRST_SUCCESS_META, true ) ); if ( $first_success <= 0 ) { return false; } if ( 'yes' === get_user_meta( $user_id, self::REVIEW_PROMPT_DISMISSED_META, true ) ) { return false; } $later_until = absint( get_user_meta( $user_id, self::REVIEW_PROMPT_LATER_UNTIL_META, true ) ); if ( $later_until > time() ) { return false; } return true; } /** * Save review prompt dismissal state. * * @return void */ public function handle_review_prompt_action() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_review_prompt_action' ); $choice = isset( $_POST['apsw_review_prompt_choice'] ) ? sanitize_key( wp_unslash( $_POST['apsw_review_prompt_choice'] ) ) : ''; $user_id = get_current_user_id(); if ( 'later' === $choice ) { update_user_meta( $user_id, self::REVIEW_PROMPT_LATER_UNTIL_META, time() + self::REVIEW_PROMPT_LATER_SECONDS ); } elseif ( 'dismiss' === $choice ) { update_user_meta( $user_id, self::REVIEW_PROMPT_DISMISSED_META, 'yes' ); } $this->redirect_to_create_page(); } /** * Handle submitted draft preparation input. * * @return void */ public function handle_prepare_input() { if ( ! $this->current_user_can_submit() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_prepare_product_draft_input' ); if ( ! $this->dependencies->is_woocommerce_active() ) { $this->redirect_with_notice( 'error', __( 'WooCommerce must be active before preparing product input.', 'ai-product-studio-for-woocommerce' ) ); } $draft_input = array(); if ( isset( $_POST['apsw_draft_input'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized recursively before use below. $draft_input = wp_unslash( $_POST['apsw_draft_input'] ); } $input = is_array( $draft_input ) ? map_deep( $draft_input, 'sanitize_textarea_field' ) : array(); $prepared = $this->sanitize_input( $input ); $valid = $this->validate_input( $prepared ); if ( is_wp_error( $valid ) ) { $this->redirect_with_notice( 'error', $valid->get_error_message() ); } set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $this->redirect_with_notice( 'success', __( 'Product input prepared successfully.', 'ai-product-studio-for-woocommerce' ) ); } /** * Handle manual WooCommerce draft creation from prepared input. * * @return void */ public function handle_create_manual_product_draft() { if ( ! $this->current_user_can_submit() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_create_manual_product_draft' ); if ( ! $this->dependencies->is_woocommerce_active() ) { $this->set_creation_notice( array( 'status' => 'error', 'message' => __( 'Product could not be created. Please check the prepared input and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'WooCommerce is not active.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $prepared = $this->get_last_prepared_input( get_current_user_id() ); if ( empty( $prepared ) ) { $this->set_creation_notice( array( 'status' => 'error', 'message' => __( 'Product could not be created. Please check the prepared input and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'No prepared input was found. Please prepare product input first.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $existing_product_id = isset( $prepared['created_product_id'] ) ? absint( $prepared['created_product_id'] ) : 0; if ( $existing_product_id > 0 && 'product' === get_post_type( $existing_product_id ) ) { $this->set_creation_notice( array( 'status' => 'warning', 'message' => __( 'This prepared input has already created a product.', 'ai-product-studio-for-woocommerce' ), 'product_id' => $existing_product_id, 'edit_url' => (string) get_edit_post_link( $existing_product_id, 'raw' ), 'warnings' => array(), ) ); $this->redirect_to_create_page(); } $result = $this->product_creator->create_manual_draft_from_input( $prepared, $this->get_allowed_contexts() ); if ( ! empty( $result['success'] ) && ! empty( $result['product_id'] ) ) { $prepared['created_product_id'] = absint( $result['product_id'] ); set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $this->set_creation_notice( array( 'status' => 'success', 'message' => __( 'Product created successfully.', 'ai-product-studio-for-woocommerce' ), 'product_id' => absint( $result['product_id'] ), 'edit_url' => isset( $result['edit_url'] ) ? esc_url_raw( $result['edit_url'] ) : '', 'warnings' => isset( $result['warnings'] ) && is_array( $result['warnings'] ) ? $result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } $this->set_creation_notice( array( 'status' => 'error', 'message' => __( 'Product could not be created. Please check the prepared input and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => isset( $result['warnings'] ) && is_array( $result['warnings'] ) ? $result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } /** * Handle AI product data generation from prepared input. * * @return void */ public function handle_generate_ai_product_data() { if ( ! $this->current_user_can_submit() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_generate_ai_product_data' ); $prepared = $this->get_last_prepared_input( get_current_user_id() ); if ( empty( $prepared ) ) { $this->set_ai_generation_notice( array( 'status' => 'error', 'message' => __( 'AI product data generation failed. Please check your Clugins connection and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'No prepared input was found. Please prepare product input first.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $result = $this->product_ai_generator->generate_from_prepared_input( $prepared, $this->get_allowed_contexts() ); if ( ! empty( $result['success'] ) && ! empty( $result['data'] ) && is_array( $result['data'] ) ) { $prepared['ai_generated_product_data'] = $result['data']; $prepared['ai_generated_at'] = time(); $prepared['ai_generation_provider'] = sanitize_key( isset( $result['provider'] ) ? $result['provider'] : '' ); $prepared['ai_generation_model'] = sanitize_text_field( isset( $result['model'] ) ? $result['model'] : '' ); set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $this->set_ai_generation_notice( array( 'status' => 'success', 'message' => __( 'AI product data generated successfully. Review the preview before creating a product.', 'ai-product-studio-for-woocommerce' ), 'warnings' => isset( $result['warnings'] ) && is_array( $result['warnings'] ) ? $result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } $this->set_ai_generation_notice( array( 'status' => 'error', 'message' => $this->get_ai_generation_failure_message( $result ), 'warnings' => isset( $result['warnings'] ) && is_array( $result['warnings'] ) ? $result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } /** * Handle AI-based draft product creation from AI preview data. * * @return void */ public function handle_create_ai_product_draft() { if ( ! $this->current_user_can_submit() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_create_ai_product_draft' ); if ( ! $this->dependencies->is_woocommerce_active() ) { $this->set_ai_creation_notice( array( 'status' => 'error', 'message' => __( 'AI product could not be created. Please review the AI product data and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'WooCommerce is not active.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $prepared = $this->get_last_prepared_input( get_current_user_id() ); if ( empty( $prepared ) ) { $this->set_ai_creation_notice( array( 'status' => 'error', 'message' => __( 'AI product could not be created. Please review the AI product data and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'No prepared input was found. Please prepare product input first.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $existing_ai_product_id = isset( $prepared['ai_created_product_id'] ) ? absint( $prepared['ai_created_product_id'] ) : 0; if ( $existing_ai_product_id > 0 && 'product' === get_post_type( $existing_ai_product_id ) ) { $this->set_ai_creation_notice( array( 'status' => 'warning', 'message' => __( 'A product has already been created from this AI product data.', 'ai-product-studio-for-woocommerce' ), 'product_id' => $existing_ai_product_id, 'edit_url' => (string) get_edit_post_link( $existing_ai_product_id, 'raw' ), 'warnings' => array(), ) ); $this->redirect_to_create_page(); } if ( empty( $prepared['ai_generated_product_data'] ) || ! is_array( $prepared['ai_generated_product_data'] ) ) { $this->set_ai_creation_notice( array( 'status' => 'error', 'message' => __( 'AI product could not be created. Please review the AI product data and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'AI product data is missing. Please generate AI product data first.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $result = $this->product_creator->create_ai_draft_from_prepared_input( $prepared, $this->get_allowed_contexts() ); if ( ! empty( $result['success'] ) && ! empty( $result['product_id'] ) ) { $product_id = absint( $result['product_id'] ); $post = get_post( $product_id ); if ( ! $post || 'product' !== $post->post_type ) { $this->set_ai_creation_notice( array( 'status' => 'error', 'message' => __( 'The product was saved, but it could not be verified in WooCommerce products.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array(), ) ); $this->redirect_to_create_page(); } $prepared['ai_created_product_id'] = $product_id; set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $this->record_successful_product_generation(); $this->set_ai_creation_notice( array( 'status' => 'success', 'message' => __( 'AI product created successfully.', 'ai-product-studio-for-woocommerce' ), 'product_id' => $product_id, 'edit_url' => isset( $result['edit_url'] ) ? esc_url_raw( $result['edit_url'] ) : '', 'warnings' => isset( $result['warnings'] ) && is_array( $result['warnings'] ) ? $result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } $this->set_ai_creation_notice( array( 'status' => 'error', 'message' => __( 'AI product could not be created. Please review the AI product data and try again.', 'ai-product-studio-for-woocommerce' ), 'warnings' => isset( $result['warnings'] ) && is_array( $result['warnings'] ) ? $result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } /** * Handle the one-click AI generation + WooCommerce draft creation flow. * * @return void */ public function handle_generate_and_create_product_draft() { if ( ! $this->current_user_can_submit() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_generate_and_create_product_draft' ); if ( ! $this->dependencies->is_woocommerce_active() ) { $this->set_generate_create_notice( array( 'status' => 'error', 'message' => __( 'WooCommerce must be active before creating products. No product was created.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array( __( 'WooCommerce is not active.', 'ai-product-studio-for-woocommerce' ) ), ) ); $this->redirect_to_create_page(); } $draft_input = array(); if ( isset( $_POST['apsw_draft_input'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized recursively before use below. $draft_input = wp_unslash( $_POST['apsw_draft_input'] ); } $input = is_array( $draft_input ) ? map_deep( $draft_input, 'sanitize_textarea_field' ) : array(); if ( isset( $_POST['product_image_ids'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized as IDs or text before use below. $product_image_ids = wp_unslash( $_POST['product_image_ids'] ); if ( is_array( $product_image_ids ) ) { $product_image_ids = map_deep( $product_image_ids, 'absint' ); } $input['product_image_ids'] = is_array( $product_image_ids ) ? implode( ',', array_map( 'absint', $product_image_ids ) ) : sanitize_text_field( $product_image_ids ); } $prepared = $this->prepare_input_from_payload( $input ); if ( is_wp_error( $prepared ) ) { $this->set_generate_create_notice( array( 'status' => 'error', 'message' => $prepared->get_error_message(), 'warnings' => array(), ) ); $this->redirect_to_create_page(); } if ( self::GENERATION_MODE_BULK_IMAGE === $prepared['generation_mode'] ) { $this->handle_bulk_image_product_generation( $prepared ); } $request_hash = $this->get_one_click_request_hash( $prepared ); $duplicate_data = get_transient( $this->get_one_click_duplicate_key( get_current_user_id(), $request_hash ) ); if ( is_array( $duplicate_data ) ) { $duplicate_product_id = isset( $duplicate_data['product_id'] ) ? absint( $duplicate_data['product_id'] ) : 0; if ( $duplicate_product_id > 0 && 'product' === get_post_type( $duplicate_product_id ) ) { $this->set_generate_create_notice( array( 'status' => 'warning', 'message' => __( 'This exact request was submitted recently, so no duplicate product was created. You can open the existing product below or change the input before trying again.', 'ai-product-studio-for-woocommerce' ), 'product_id' => $duplicate_product_id, 'product_title' => $this->sanitize_product_title_for_display( get_the_title( $duplicate_product_id ) ), 'product_status' => get_post_status( $duplicate_product_id ), 'edit_url' => admin_url( 'post.php?post=' . $duplicate_product_id . '&action=edit' ), 'products_list_url' => admin_url( 'edit.php?post_type=product' ), 'warnings' => array(), 'ai_summary' => isset( $duplicate_data['ai_summary'] ) && is_array( $duplicate_data['ai_summary'] ) ? $duplicate_data['ai_summary'] : array(), ) ); $this->redirect_to_create_page(); } } set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $ai_result = $this->product_ai_generator->generate_from_prepared_input( $prepared, $this->get_allowed_contexts() ); if ( empty( $ai_result['success'] ) || empty( $ai_result['data'] ) || ! is_array( $ai_result['data'] ) ) { $error_code = isset( $ai_result['error_code'] ) ? sanitize_key( $ai_result['error_code'] ) : ''; $message = 'json_parse_failed' === $error_code ? __( 'The AI response could not be parsed as valid product data. No product was created.', 'ai-product-studio-for-woocommerce' ) : $this->get_ai_generation_failure_message( $ai_result ); $this->set_generate_create_notice( array( 'status' => 'error', 'message' => $message, 'warnings' => isset( $ai_result['warnings'] ) && is_array( $ai_result['warnings'] ) ? $ai_result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } $prepared['ai_generated_product_data'] = $ai_result['data']; $prepared['ai_generated_at'] = time(); $prepared['ai_generation_provider'] = sanitize_key( isset( $ai_result['provider'] ) ? $ai_result['provider'] : '' ); $prepared['ai_generation_model'] = sanitize_text_field( isset( $ai_result['model'] ) ? $ai_result['model'] : '' ); set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $creation_result = $this->product_creator->create_ai_draft_from_prepared_input( $prepared, $this->get_allowed_contexts() ); if ( empty( $creation_result['success'] ) || empty( $creation_result['product_id'] ) ) { $this->set_generate_create_notice( array( 'status' => 'error', 'message' => __( 'AI data was generated, but the WooCommerce product could not be created.', 'ai-product-studio-for-woocommerce' ), 'warnings' => isset( $creation_result['warnings'] ) && is_array( $creation_result['warnings'] ) ? $creation_result['warnings'] : array(), ) ); $this->redirect_to_create_page(); } $product_id = absint( $creation_result['product_id'] ); $post = get_post( $product_id ); if ( ! $post || 'product' !== $post->post_type ) { $this->set_generate_create_notice( array( 'status' => 'error', 'message' => __( 'The product was saved, but it could not be verified in WooCommerce products.', 'ai-product-studio-for-woocommerce' ), 'warnings' => array(), ) ); $this->redirect_to_create_page(); } update_post_meta( $product_id, '_apsw_created_phase', 'ai_draft_phase_4d_one_click' ); update_post_meta( $product_id, '_apsw_one_click_workflow', 'yes' ); $this->record_successful_product_generation(); $prepared['ai_created_product_id'] = $product_id; set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $ai_summary = $this->get_ai_summary( $prepared['ai_generated_product_data'] ); $notice = array( 'status' => 'success', 'message' => __( 'Product created successfully.', 'ai-product-studio-for-woocommerce' ), 'product_id' => $product_id, 'product_title' => $this->sanitize_product_title_for_display( get_the_title( $product_id ) ), 'product_status' => get_post_status( $product_id ), 'edit_url' => admin_url( 'post.php?post=' . $product_id . '&action=edit' ), 'products_list_url' => admin_url( 'edit.php?post_type=product' ), 'warnings' => array_merge( isset( $ai_result['warnings'] ) && is_array( $ai_result['warnings'] ) ? $ai_result['warnings'] : array(), isset( $creation_result['warnings'] ) && is_array( $creation_result['warnings'] ) ? $creation_result['warnings'] : array() ), 'ai_summary' => $ai_summary, ); set_transient( $this->get_one_click_duplicate_key( get_current_user_id(), $request_hash ), array( 'product_id' => $product_id, 'ai_summary' => $ai_summary, ), 2 * MINUTE_IN_SECONDS ); $this->set_generate_create_notice( $notice ); $this->redirect_to_create_page(); } /** * Handle Bulk Image to Product as local per-image generation requests. * * @param array $prepared Prepared bulk input. * @return void */ private function handle_bulk_image_product_generation( array $prepared ) { $image_ids = isset( $prepared['product_image_ids'] ) && is_array( $prepared['product_image_ids'] ) ? array_values( array_unique( array_filter( array_map( 'absint', $prepared['product_image_ids'] ) ) ) ) : array(); $created_products = array(); $failures = array(); $warnings = array(); $total_images = count( $image_ids ); set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); foreach ( $image_ids as $index => $attachment_id ) { $image_label = $this->get_attachment_label( $attachment_id, $index ); $single_prepared = $prepared; $single_prepared['generation_mode'] = self::GENERATION_MODE_IMAGE_TO_PRODUCT; $single_prepared['product_image_ids'] = array( $attachment_id ); $single_prepared['product_main_image_id'] = $attachment_id; $single_prepared['product_gallery_image_ids'] = array(); $single_prepared['rough_notes'] = ''; $single_prepared['price'] = ''; $single_prepared['sale_price'] = ''; $single_prepared['sku'] = ''; $single_prepared['stock_quantity'] = ''; $single_prepared['prepared_timestamp'] = absint( $prepared['prepared_timestamp'] ) + $index; $ai_result = $this->product_ai_generator->generate_from_prepared_input( $single_prepared, $this->get_allowed_contexts() ); if ( empty( $ai_result['success'] ) || empty( $ai_result['data'] ) || ! is_array( $ai_result['data'] ) ) { $failures[] = sprintf( /* translators: 1: image label, 2: error message */ __( '%1$s: %2$s', 'ai-product-studio-for-woocommerce' ), $image_label, $this->get_ai_generation_failure_message( $ai_result ) ); continue; } $single_prepared['ai_generated_product_data'] = $ai_result['data']; $single_prepared['ai_generated_at'] = time(); $single_prepared['ai_generation_provider'] = sanitize_key( isset( $ai_result['provider'] ) ? $ai_result['provider'] : '' ); $single_prepared['ai_generation_model'] = sanitize_text_field( isset( $ai_result['model'] ) ? $ai_result['model'] : '' ); $creation_result = $this->product_creator->create_ai_draft_from_prepared_input( $single_prepared, $this->get_allowed_contexts() ); if ( empty( $creation_result['success'] ) || empty( $creation_result['product_id'] ) ) { $failures[] = sprintf( /* translators: 1: image label, 2: error message */ __( '%1$s: %2$s', 'ai-product-studio-for-woocommerce' ), $image_label, __( 'AI data was generated, but the WooCommerce product could not be created.', 'ai-product-studio-for-woocommerce' ) ); continue; } $product_id = absint( $creation_result['product_id'] ); $post = get_post( $product_id ); if ( ! $post || 'product' !== $post->post_type ) { $failures[] = sprintf( /* translators: 1: image label, 2: error message */ __( '%1$s: %2$s', 'ai-product-studio-for-woocommerce' ), $image_label, __( 'The product was saved, but it could not be verified in WooCommerce products.', 'ai-product-studio-for-woocommerce' ) ); continue; } update_post_meta( $product_id, '_apsw_created_phase', 'ai_draft_phase_4g_bulk_image' ); update_post_meta( $product_id, '_apsw_bulk_image_workflow', 'yes' ); update_post_meta( $product_id, '_apsw_bulk_source_image_id', $attachment_id ); $created_products[] = array( 'product_id' => $product_id, 'product_title' => $this->sanitize_product_title_for_display( get_the_title( $product_id ) ), 'product_status' => get_post_status( $product_id ), 'edit_url' => admin_url( 'post.php?post=' . $product_id . '&action=edit' ), 'image_id' => $attachment_id, 'image_label' => $image_label, ); if ( isset( $ai_result['warnings'] ) && is_array( $ai_result['warnings'] ) ) { $warnings = array_merge( $warnings, array_map( 'sanitize_text_field', $ai_result['warnings'] ) ); } if ( isset( $creation_result['warnings'] ) && is_array( $creation_result['warnings'] ) ) { $warnings = array_merge( $warnings, array_map( 'sanitize_text_field', $creation_result['warnings'] ) ); } } if ( ! empty( $created_products ) ) { $this->record_successful_product_generation(); } $created_count = count( $created_products ); $failure_count = count( $failures ); $status = $created_count > 0 && 0 === $failure_count ? 'success' : ( $created_count > 0 ? 'warning' : 'error' ); $message = sprintf( /* translators: 1: total selected images, 2: products created, 3: failed images */ __( 'Bulk Image to Product finished: %1$d selected, %2$d products created, %3$d failed.', 'ai-product-studio-for-woocommerce' ), $total_images, $created_count, $failure_count ); $notice_warnings = array_values( array_unique( array_filter( array_merge( $warnings, $failures ) ) ) ); $first_product = ! empty( $created_products[0] ) ? $created_products[0] : array(); $prepared['bulk_created_product_ids'] = wp_list_pluck( $created_products, 'product_id' ); set_transient( $this->get_transient_key( get_current_user_id() ), $prepared, 30 * MINUTE_IN_SECONDS ); $this->set_generate_create_notice( array( 'status' => $status, 'message' => $message, 'product_id' => isset( $first_product['product_id'] ) ? absint( $first_product['product_id'] ) : 0, 'product_title' => isset( $first_product['product_title'] ) ? sanitize_text_field( $first_product['product_title'] ) : '', 'product_status' => isset( $first_product['product_status'] ) ? sanitize_key( $first_product['product_status'] ) : '', 'edit_url' => isset( $first_product['edit_url'] ) ? esc_url_raw( $first_product['edit_url'] ) : '', 'products_list_url' => admin_url( 'edit.php?post_type=product' ), 'warnings' => $notice_warnings, 'bulk_results' => $created_products, ) ); $this->redirect_to_create_page(); } /** * Check submission capability. * * @return bool */ private function current_user_can_submit() { if ( $this->dependencies->is_woocommerce_active() && current_user_can( 'manage_woocommerce' ) ) { return true; } return current_user_can( 'manage_options' ); } /** * Sanitize payload. * * @param array $input Raw payload. * @return array */ public function sanitize_input( array $input ) { $generation_mode = $this->normalize_generation_mode( isset( $input['generation_mode'] ) ? $input['generation_mode'] : '' ); $image_ids_raw = isset( $input['product_image_ids'] ) ? sanitize_text_field( $input['product_image_ids'] ) : ''; $main_image_id = isset( $input['product_main_image_id'] ) ? absint( $input['product_main_image_id'] ) : 0; $gallery_ids_raw = isset( $input['product_gallery_image_ids'] ) ? sanitize_text_field( $input['product_gallery_image_ids'] ) : ''; $product_category_id = isset( $input['product_category_id'] ) ? absint( $input['product_category_id'] ) : 0; $product_kind = isset( $input['product_kind'] ) ? sanitize_key( $input['product_kind'] ) : 'physical'; $is_downloadable = ! empty( $input['is_downloadable'] ); if ( 'digital' === $product_kind ) { $product_kind = 'virtual'; } if ( 'downloadable' === $product_kind ) { $product_kind = 'virtual'; $is_downloadable = true; } if ( 'virtual' !== $product_kind ) { $is_downloadable = false; } $rough_notes = isset( $input['rough_notes'] ) ? sanitize_textarea_field( $input['rough_notes'] ) : ''; $price_raw = isset( $input['price'] ) ? sanitize_text_field( $input['price'] ) : ''; $sale_raw = isset( $input['sale_price'] ) ? sanitize_text_field( $input['sale_price'] ) : ''; $sku = isset( $input['sku'] ) ? sanitize_text_field( $input['sku'] ) : ''; $stock_raw = isset( $input['stock_quantity'] ) ? sanitize_text_field( $input['stock_quantity'] ) : ''; $image_ids = array_filter( array_map( 'absint', array_map( 'trim', explode( ',', $image_ids_raw ) ) ) ); $image_ids = array_values( array_unique( $image_ids ) ); $gallery_image_ids = array_filter( array_map( 'absint', array_map( 'trim', explode( ',', $gallery_ids_raw ) ) ) ); $gallery_image_ids = array_values( array_unique( $gallery_image_ids ) ); if ( $main_image_id > 0 || ! empty( $gallery_image_ids ) ) { $image_ids = array_values( array_filter( array_merge( array( $main_image_id ), $gallery_image_ids ) ) ); } elseif ( ! empty( $image_ids ) ) { $main_image_id = absint( $image_ids[0] ); $gallery_image_ids = array_slice( $image_ids, 1 ); } if ( self::GENERATION_MODE_BULK_IMAGE === $generation_mode ) { if ( empty( $image_ids ) && ! empty( $gallery_image_ids ) ) { $image_ids = $gallery_image_ids; } $main_image_id = 0; $gallery_image_ids = $image_ids; $rough_notes = ''; $price_raw = ''; $sale_raw = ''; $sku = ''; $stock_raw = ''; } elseif ( in_array( $generation_mode, array( self::GENERATION_MODE_IMAGE_TO_PRODUCT, self::GENERATION_MODE_ADVANCED_PRODUCT_GENERATION ), true ) ) { $gallery_image_ids = array(); $image_ids = $main_image_id > 0 ? array( $main_image_id ) : array(); if ( self::GENERATION_MODE_IMAGE_TO_PRODUCT === $generation_mode ) { $rough_notes = ''; } } $category_label = ''; if ( $product_category_id > 0 ) { $term = get_term( $product_category_id, 'product_cat' ); if ( $term && ! is_wp_error( $term ) ) { $category_label = sanitize_text_field( $term->name ); } } return array( 'generation_mode' => $generation_mode, 'product_image_ids' => $image_ids, 'product_main_image_id' => $main_image_id, 'product_gallery_image_ids' => $gallery_image_ids, 'product_category_id' => $product_category_id, 'product_category_label' => $category_label, 'product_kind' => $product_kind, 'is_downloadable' => $is_downloadable ? 1 : 0, 'rough_notes' => $rough_notes, 'price' => $this->sanitize_decimal_value( $price_raw ), 'sale_price' => $this->sanitize_decimal_value( $sale_raw ), 'sku' => $sku, 'stock_quantity' => '' === trim( $stock_raw ) ? '' : absint( $stock_raw ), 'status' => 'draft', 'prepared_timestamp' => time(), ); } /** * Validate sanitized payload. * * @param array $prepared Sanitized payload. * @return true|WP_Error */ public function validate_input( array $prepared ) { $product_kinds = $this->get_product_kind_options(); $generation_mode = $this->normalize_generation_mode( isset( $prepared['generation_mode'] ) ? $prepared['generation_mode'] : '' ); $is_image_mode = self::GENERATION_MODE_IMAGE_TO_PRODUCT === $generation_mode; $is_advanced_mode = self::GENERATION_MODE_ADVANCED_PRODUCT_GENERATION === $generation_mode; $is_bulk_mode = self::GENERATION_MODE_BULK_IMAGE === $generation_mode; if ( ! $is_image_mode && ! $is_bulk_mode && '' === trim( $prepared['rough_notes'] ) ) { return new WP_Error( 'apsw_empty_notes', __( 'Rough notes are required.', 'ai-product-studio-for-woocommerce' ) ); } if ( $is_advanced_mode && ! $this->has_meaningful_advanced_notes( isset( $prepared['rough_notes'] ) ? $prepared['rough_notes'] : '' ) ) { return new WP_Error( 'apsw_short_advanced_notes', __( 'Advanced Product needs a little more product detail. Add a few notes about the item before generating.', 'ai-product-studio-for-woocommerce' ) ); } if ( ! array_key_exists( $prepared['product_kind'], $product_kinds ) ) { return new WP_Error( 'apsw_invalid_product_kind', __( 'Please select a valid product kind.', 'ai-product-studio-for-woocommerce' ) ); } if ( ! empty( $prepared['product_category_id'] ) && ! $this->is_valid_product_category( $prepared['product_category_id'] ) ) { return new WP_Error( 'apsw_invalid_product_category', __( 'Please select a valid WooCommerce category.', 'ai-product-studio-for-woocommerce' ) ); } foreach ( $prepared['product_image_ids'] as $attachment_id ) { if ( ! $this->is_valid_image_attachment( $attachment_id ) ) { return new WP_Error( 'apsw_invalid_image', __( 'One or more selected attachments are not valid images.', 'ai-product-studio-for-woocommerce' ) ); } } if ( $is_image_mode || $is_advanced_mode ) { $image_validation = $this->validate_generation_image_attachment( isset( $prepared['product_main_image_id'] ) ? $prepared['product_main_image_id'] : 0 ); if ( is_wp_error( $image_validation ) ) { return $image_validation; } } if ( $is_bulk_mode ) { $bulk_validation = $this->validate_bulk_image_attachments( isset( $prepared['product_image_ids'] ) && is_array( $prepared['product_image_ids'] ) ? $prepared['product_image_ids'] : array() ); if ( is_wp_error( $bulk_validation ) ) { return $bulk_validation; } } if ( ! $is_image_mode && ! $is_advanced_mode && ! $is_bulk_mode && '' === $prepared['price'] ) { return new WP_Error( 'apsw_empty_price', __( 'Price is required.', 'ai-product-studio-for-woocommerce' ) ); } if ( '' !== $prepared['price'] && ! is_numeric( $prepared['price'] ) ) { return new WP_Error( 'apsw_invalid_price', __( 'Price must be a valid number.', 'ai-product-studio-for-woocommerce' ) ); } if ( '' !== $prepared['sale_price'] && ! is_numeric( $prepared['sale_price'] ) ) { return new WP_Error( 'apsw_invalid_sale_price', __( 'Sale price must be a valid number.', 'ai-product-studio-for-woocommerce' ) ); } return true; } /** * Sanitize and validate raw draft input using the standard workflow rules. * * @param array $input Raw payload. * @return array|WP_Error */ public function prepare_input_from_payload( array $input ) { $prepared = $this->sanitize_input( $input ); $valid = $this->validate_input( $prepared ); if ( is_wp_error( $valid ) ) { return $valid; } return $prepared; } /** * Get user-specific transient key. * * @param int $user_id User ID. * @return string */ public function get_transient_key( $user_id ) { return 'apsw_last_product_draft_input_' . absint( $user_id ); } /** * Retrieve last prepared input for a user. * * @param int $user_id User ID. * @return array */ public function get_last_prepared_input( $user_id = 0 ) { $user_id = $user_id ? absint( $user_id ) : get_current_user_id(); $data = get_transient( $this->get_transient_key( $user_id ) ); return is_array( $data ) ? $data : array(); } /** * Get user-specific transient key for creation notices. * * @param int $user_id User ID. * @return string */ public function get_creation_notice_transient_key( $user_id ) { return 'apsw_last_product_create_notice_' . absint( $user_id ); } /** * Save creation notice for the current user. * * @param array $notice Notice payload. * @return void */ public function set_creation_notice( array $notice ) { set_transient( $this->get_creation_notice_transient_key( get_current_user_id() ), $notice, 5 * MINUTE_IN_SECONDS ); } /** * Get and clear the latest creation notice for a user. * * @param int $user_id User ID. * @return array */ public function consume_creation_notice( $user_id = 0 ) { $user_id = $user_id ? absint( $user_id ) : get_current_user_id(); $key = $this->get_creation_notice_transient_key( $user_id ); $notice = get_transient( $key ); if ( false !== $notice ) { delete_transient( $key ); } return is_array( $notice ) ? $notice : array(); } /** * Get user-specific transient key for AI generation notices. * * @param int $user_id User ID. * @return string */ public function get_ai_notice_transient_key( $user_id ) { return 'apsw_last_ai_generation_notice_' . absint( $user_id ); } /** * Save AI generation notice for current user. * * @param array $notice Notice payload. * @return void */ public function set_ai_generation_notice( array $notice ) { set_transient( $this->get_ai_notice_transient_key( get_current_user_id() ), $notice, 5 * MINUTE_IN_SECONDS ); } /** * Get and clear latest AI generation notice for a user. * * @param int $user_id User ID. * @return array */ public function consume_ai_generation_notice( $user_id = 0 ) { $user_id = $user_id ? absint( $user_id ) : get_current_user_id(); $key = $this->get_ai_notice_transient_key( $user_id ); $notice = get_transient( $key ); if ( false !== $notice ) { delete_transient( $key ); } return is_array( $notice ) ? $notice : array(); } /** * Get user-specific transient key for AI draft creation notices. * * @param int $user_id User ID. * @return string */ public function get_ai_creation_notice_transient_key( $user_id ) { return 'apsw_last_ai_creation_notice_' . absint( $user_id ); } /** * Save AI draft creation notice for current user. * * @param array $notice Notice payload. * @return void */ public function set_ai_creation_notice( array $notice ) { set_transient( $this->get_ai_creation_notice_transient_key( get_current_user_id() ), $notice, 5 * MINUTE_IN_SECONDS ); } /** * Get and clear latest AI draft creation notice for a user. * * @param int $user_id User ID. * @return array */ public function consume_ai_creation_notice( $user_id = 0 ) { $user_id = $user_id ? absint( $user_id ) : get_current_user_id(); $key = $this->get_ai_creation_notice_transient_key( $user_id ); $notice = get_transient( $key ); if ( false !== $notice ) { delete_transient( $key ); } return is_array( $notice ) ? $notice : array(); } /** * Get user-specific transient key for one-click result notices. * * @param int $user_id User ID. * @return string */ public function get_generate_create_notice_transient_key( $user_id ) { return 'apsw_last_generate_create_notice_' . absint( $user_id ); } /** * Save one-click result notice for current user. * * @param array $notice Notice payload. * @return void */ public function set_generate_create_notice( array $notice ) { set_transient( $this->get_generate_create_notice_transient_key( get_current_user_id() ), $notice, 5 * MINUTE_IN_SECONDS ); } /** * Get and clear latest one-click result notice for a user. * * @param int $user_id User ID. * @return array */ public function consume_generate_create_notice( $user_id = 0 ) { $user_id = $user_id ? absint( $user_id ) : get_current_user_id(); $key = $this->get_generate_create_notice_transient_key( $user_id ); $notice = get_transient( $key ); if ( false !== $notice ) { delete_transient( $key ); } return is_array( $notice ) ? $notice : array(); } /** * Redirect to Create Product page with notice query args. * * @param string $status Success/error. * @param string $message Notice text. * @return void */ private function redirect_with_notice( $status, $message ) { $url = add_query_arg( array( 'page' => 'apsw-create-product', 'apsw_prepare_status' => sanitize_key( $status ), 'apsw_prepare_message' => sanitize_text_field( $message ), ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** * Redirect back to Create Product Draft page. * * @return void */ private function redirect_to_create_page() { $url = add_query_arg( array( 'page' => 'apsw-create-product', ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** * Mark the current user eligible for the review prompt after a verified product creation. * * @return void */ private function record_successful_product_generation() { $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return; } if ( absint( get_user_meta( $user_id, self::REVIEW_PROMPT_FIRST_SUCCESS_META, true ) ) > 0 ) { return; } update_user_meta( $user_id, self::REVIEW_PROMPT_FIRST_SUCCESS_META, time() ); } /** * Ensure selected attachment is an image. * * @param int $attachment_id Attachment ID. * @return bool */ private function is_valid_image_attachment( $attachment_id ) { $attachment_id = absint( $attachment_id ); if ( $attachment_id <= 0 ) { return false; } return 'attachment' === get_post_type( $attachment_id ) && wp_attachment_is_image( $attachment_id ); } /** * Normalize supported plugin generation modes. * * @param string $mode Raw mode. * @return string */ private function normalize_generation_mode( $mode ) { $mode = sanitize_key( (string) $mode ); if ( self::GENERATION_MODE_IMAGE_TO_PRODUCT === $mode ) { return self::GENERATION_MODE_IMAGE_TO_PRODUCT; } if ( self::GENERATION_MODE_ADVANCED_PRODUCT_GENERATION === $mode ) { return self::GENERATION_MODE_ADVANCED_PRODUCT_GENERATION; } if ( self::GENERATION_MODE_BULK_IMAGE === $mode ) { return self::GENERATION_MODE_BULK_IMAGE; } return self::GENERATION_MODE_TEXT_TO_PRODUCT; } /** * Check whether Advanced Product notes have enough usable detail. * * @param string $notes Product notes. * @return bool */ private function has_meaningful_advanced_notes( $notes ) { $notes = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $notes ) ) ); return strlen( $notes ) >= self::ADVANCED_NOTES_MIN_LENGTH; } /** * Validate selected main image for Image to Product. * * @param int $attachment_id Attachment ID. * @return true|WP_Error */ private function validate_generation_image_attachment( $attachment_id ) { $attachment_id = absint( $attachment_id ); if ( ! $this->is_valid_image_attachment( $attachment_id ) ) { return new WP_Error( 'apsw_invalid_generation_image', __( 'Please select a main product image before generating from an image.', 'ai-product-studio-for-woocommerce' ) ); } $mime_type = strtolower( (string) get_post_mime_type( $attachment_id ) ); if ( ! in_array( $mime_type, self::ALLOWED_IMAGE_MIME_TYPES, true ) ) { return new WP_Error( 'apsw_invalid_generation_image_type', __( 'Please select a JPG, PNG, or WebP image.', 'ai-product-studio-for-woocommerce' ) ); } $file_path = get_attached_file( $attachment_id, true ); if ( ! is_string( $file_path ) || '' === $file_path || ! file_exists( $file_path ) || ! is_readable( $file_path ) ) { return new WP_Error( 'apsw_invalid_generation_image_file', __( 'The selected image could not be read. Please choose another image.', 'ai-product-studio-for-woocommerce' ) ); } if ( ! $this->is_file_in_uploads_directory( $file_path ) ) { return new WP_Error( 'apsw_invalid_generation_image_file', __( 'The selected image could not be read. Please choose another image.', 'ai-product-studio-for-woocommerce' ) ); } $file_size = filesize( $file_path ); if ( false === $file_size || $file_size <= 0 ) { return new WP_Error( 'apsw_invalid_generation_image_file', __( 'The selected image could not be read. Please choose another image.', 'ai-product-studio-for-woocommerce' ) ); } if ( $file_size > self::MAX_IMAGE_BYTES ) { return new WP_Error( 'apsw_generation_image_too_large', __( 'Please select an image that is 8 MB or smaller.', 'ai-product-studio-for-woocommerce' ) ); } return true; } /** * Check whether a local attachment file is inside the uploads directory. * * @param string $file_path Local file path. * @return bool */ private function is_file_in_uploads_directory( $file_path ) { $uploads = wp_get_upload_dir(); if ( empty( $uploads['basedir'] ) || ! is_string( $uploads['basedir'] ) ) { return false; } $real_file_path = realpath( $file_path ); $real_uploads = realpath( $uploads['basedir'] ); if ( false === $real_file_path || false === $real_uploads ) { return false; } $real_file_path = wp_normalize_path( $real_file_path ); $real_uploads = trailingslashit( wp_normalize_path( $real_uploads ) ); return 0 === strpos( $real_file_path, $real_uploads ); } /** * Validate selected images for Bulk Image to Product. * * @param array $attachment_ids Attachment IDs. * @return true|WP_Error */ private function validate_bulk_image_attachments( array $attachment_ids ) { $attachment_ids = array_values( array_unique( array_filter( array_map( 'absint', $attachment_ids ) ) ) ); if ( empty( $attachment_ids ) ) { return new WP_Error( 'apsw_empty_bulk_images', __( 'Please select at least one image before generating products from images.', 'ai-product-studio-for-woocommerce' ) ); } if ( count( $attachment_ids ) > self::BULK_IMAGE_LIMIT ) { return new WP_Error( 'apsw_too_many_bulk_images', sprintf( /* translators: %d: maximum number of images */ __( 'Please select %d images or fewer for Bulk Image to Product.', 'ai-product-studio-for-woocommerce' ), self::BULK_IMAGE_LIMIT ) ); } foreach ( $attachment_ids as $attachment_id ) { $image_validation = $this->validate_generation_image_attachment( $attachment_id ); if ( is_wp_error( $image_validation ) ) { return $image_validation; } } return true; } /** * Ensure selected product category exists. * * @param int $term_id Term ID. * @return bool */ private function is_valid_product_category( $term_id ) { $term_id = absint( $term_id ); if ( $term_id <= 0 || ! taxonomy_exists( 'product_cat' ) ) { return false; } $term = get_term( $term_id, 'product_cat' ); return $term && ! is_wp_error( $term ) && 'product_cat' === $term->taxonomy; } /** * Build a short-lived duplicate prevention hash for one-click submissions. * * @param array $prepared Prepared input payload. * @return string */ private function get_one_click_request_hash( array $prepared ) { $hash_input = $prepared; unset( $hash_input['prepared_timestamp'] ); return md5( wp_json_encode( $hash_input ) ); } /** * Get transient key for exact duplicate one-click requests. * * @param int $user_id User ID. * @param string $request_hash Request hash. * @return string */ private function get_one_click_duplicate_key( $user_id, $request_hash ) { return 'apsw_one_click_' . absint( $user_id ) . '_' . sanitize_key( $request_hash ); } /** * Build compact AI data summary for the result card. * * @param array $ai_data Sanitized AI data. * @return array */ private function get_ai_summary( array $ai_data ) { return array( 'product_title' => sanitize_text_field( isset( $ai_data['product_title'] ) ? $ai_data['product_title'] : '' ), 'missing_info_notes' => isset( $ai_data['missing_info_notes'] ) && is_array( $ai_data['missing_info_notes'] ) ? array_map( 'sanitize_text_field', $ai_data['missing_info_notes'] ) : array(), ); } /** * Build a concise label for a selected image. * * @param int $attachment_id Attachment ID. * @param int $index Zero-based image index. * @return string */ private function get_attachment_label( $attachment_id, $index ) { $title = get_the_title( absint( $attachment_id ) ); $title = sanitize_text_field( $title ); if ( '' !== $title ) { return sprintf( /* translators: 1: image number, 2: image title */ __( 'Image %1$d (%2$s)', 'ai-product-studio-for-woocommerce' ), absint( $index ) + 1, $title ); } return sprintf( /* translators: %d: image number */ __( 'Image %d', 'ai-product-studio-for-woocommerce' ), absint( $index ) + 1 ); } /** * Return a safe generation failure message. * * @param array $ai_result AI result payload. * @return string */ private function get_ai_generation_failure_message( array $ai_result ) { $error_code = isset( $ai_result['error_code'] ) ? sanitize_key( $ai_result['error_code'] ) : ''; switch ( $error_code ) { case 'missing_access_key': return __( 'Clugins Access Key is missing. Please add your Access Key in Settings.', 'ai-product-studio-for-woocommerce' ); case 'invalid_access_key': case 'unauthorized': case 'forbidden': case 'site_not_allowed': case 'site_key_mismatch': case 'access_key_site_required': return __( 'Your Clugins connection could not be verified. Please reconnect your site in Settings.', 'ai-product-studio-for-woocommerce' ); case 'insufficient_credits': case 'usage_limit_exceeded': return __( 'Your current Clugins plan limit has been reached for this request. Please review your plan or try again later.', 'ai-product-studio-for-woocommerce' ); case 'unsupported_operation': return __( 'This generation type is not available yet. Please update the Clugins service or contact support.', 'ai-product-studio-for-woocommerce' ); case 'invalid_request': case 'invalid_payload': case 'invalid_image_payload': case 'image_required': case 'image_too_large': case 'unsupported_image_type': return __( 'The request could not be processed. Please check your input and try again.', 'ai-product-studio-for-woocommerce' ); case 'service_unavailable': return __( 'Clugins is temporarily unavailable. Please try again shortly.', 'ai-product-studio-for-woocommerce' ); } return __( 'AI product data generation failed.', 'ai-product-studio-for-woocommerce' ); } /** * Sanitize decimal values using WooCommerce format when available. * * @param string $value Raw decimal input. * @return string */ private function sanitize_decimal_value( $value ) { $value = trim( (string) $value ); if ( '' === $value ) { return ''; } if ( function_exists( 'wc_format_decimal' ) ) { return wc_format_decimal( $value, wc_get_price_decimals(), false ); } return preg_replace( '/[^0-9.\-]/', '', $value ); } /** * Decode title entities and sanitize before showing result text. * * @param string $title Product title. * @return string */ private function sanitize_product_title_for_display( $title ) { $title = wp_specialchars_decode( (string) $title, ENT_QUOTES ); return sanitize_text_field( $title ); } }