dependencies = $dependencies; $this->settings = $settings; } /** * Create manual WooCommerce draft product from prepared input. * * @param array $input Prepared input payload. * @param array $contexts Allowed context labels. * @return array */ public function create_manual_draft_from_input( array $input, array $contexts ) { $result = $this->get_default_result( __( 'Product could not be created. Please check the input and try again.', 'ai-product-studio-for-woocommerce' ) ); if ( ! $this->is_woocommerce_ready() ) { return $result; } $validated = $this->validate_prepared_input( $input, $contexts ); if ( is_wp_error( $validated ) ) { return $result; } $category_label = '' !== $validated['product_category_label'] ? $validated['product_category_label'] : __( 'Product', 'ai-product-studio-for-woocommerce' ); $title = sprintf( /* translators: 1: context label, 2: date and time */ __( 'AI Product - %1$s - %2$s', 'ai-product-studio-for-woocommerce' ), $category_label, wp_date( 'Y-m-d H:i' ) ); $product = new WC_Product_Simple(); $product->set_name( $this->sanitize_product_title( $title ) ); $product->set_status( 'draft' ); $product->set_virtual( 'virtual' === $validated['product_kind'] ); $product->set_downloadable( ! empty( $validated['is_downloadable'] ) ); $product->set_short_description( __( 'Review and update the short description before publishing this product.', 'ai-product-studio-for-woocommerce' ) ); $product->set_description( wp_kses_post( wpautop( __( 'Original rough notes provided by the store owner:', 'ai-product-studio-for-woocommerce' ) . "\n\n" . $validated['rough_notes'] ) ) ); $this->apply_price_data( $product, $validated, $result['warnings'] ); $this->apply_sku_data( $product, $validated, $result['warnings'] ); $this->apply_stock_data( $product, $validated ); $this->apply_image_data( $product, $validated['product_image_ids'], $validated['product_main_image_id'], $validated['product_gallery_image_ids'] ); $product->update_meta_data( '_apsw_created_by_plugin', 'yes' ); $product->update_meta_data( '_apsw_created_phase', 'manual_draft_phase_4a' ); $product->update_meta_data( '_apsw_product_kind', $validated['product_kind'] ); $product->update_meta_data( '_apsw_selected_product_category_id', $validated['product_category_id'] ); $product->update_meta_data( '_apsw_selected_product_category_label', $validated['product_category_label'] ); $product->update_meta_data( '_apsw_original_rough_notes', $validated['rough_notes'] ); $product->update_meta_data( '_apsw_prepared_input_created_at', $validated['prepared_timestamp'] > 0 ? $validated['prepared_timestamp'] : time() ); $save_result = $this->save_product_result( $product, $result, __( 'Product created successfully.', 'ai-product-studio-for-woocommerce' ) ); if ( ! empty( $save_result['success'] ) ) { $this->apply_product_category( $save_result['product_id'], $validated['product_category_id'], $save_result['warnings'] ); } return $save_result; } /** * Create AI draft product from prepared input + generated data. * * @param array $input Prepared input payload. * @param array $contexts Allowed context labels. * @return array */ public function create_ai_draft_from_prepared_input( array $input, array $contexts ) { $result = $this->get_default_result( __( 'Product could not be created. Please review the generated product data and try again.', 'ai-product-studio-for-woocommerce' ) ); if ( ! $this->is_woocommerce_ready() ) { return $result; } $validated = $this->validate_prepared_input( $input, $contexts ); if ( is_wp_error( $validated ) ) { return $result; } $ai_payload = $this->sanitize_ai_data( isset( $input['ai_generated_product_data'] ) && is_array( $input['ai_generated_product_data'] ) ? $input['ai_generated_product_data'] : array() ); $ai_data = isset( $ai_payload['data'] ) && is_array( $ai_payload['data'] ) ? $ai_payload['data'] : array(); if ( empty( $ai_data ) ) { return $result; } if ( ! empty( $ai_payload['warnings'] ) ) { $result['warnings'] = array_merge( $result['warnings'], $ai_payload['warnings'] ); } if ( empty( $ai_data['generation_mode'] ) && ! empty( $validated['generation_mode'] ) ) { $ai_data['generation_mode'] = $this->sanitize_generation_mode( $validated['generation_mode'] ); } $category_label = '' !== $validated['product_category_label'] ? $validated['product_category_label'] : __( 'Product', 'ai-product-studio-for-woocommerce' ); $title = '' !== $ai_data['title'] ? $ai_data['title'] : sprintf( /* translators: 1: context label, 2: date and time */ __( 'AI Product - %1$s - %2$s', 'ai-product-studio-for-woocommerce' ), $category_label, wp_date( 'Y-m-d H:i' ) ); $short_description = '' !== $ai_data['short_description'] ? $ai_data['short_description'] : __( 'AI-generated short description was not available.', 'ai-product-studio-for-woocommerce' ); $long_description = '' !== $ai_data['long_description'] ? $ai_data['long_description'] : __( 'AI-generated long description was not available.', 'ai-product-studio-for-woocommerce' ); $product = new WC_Product_Simple(); $product->set_name( $this->sanitize_product_title( $title ) ); $product->set_status( 'draft' ); $product->set_virtual( 'virtual' === $validated['product_kind'] ); $product->set_downloadable( ! empty( $validated['is_downloadable'] ) ); $product->set_short_description( wp_kses_post( $short_description ) ); $product->set_description( wp_kses_post( $long_description ) ); $this->apply_price_data( $product, $validated, $result['warnings'] ); $this->apply_sku_data( $product, $validated, $result['warnings'] ); $this->apply_stock_data( $product, $validated ); $this->apply_image_data( $product, $validated['product_image_ids'], $validated['product_main_image_id'], $validated['product_gallery_image_ids'] ); if ( $this->is_generation_preference_enabled( APSW_Settings::FIELD_APPLY_SHIPPING_SPECS ) ) { $this->apply_shipping_data( $product, $ai_data['shipping'] ); } if ( $this->is_generation_preference_enabled( APSW_Settings::FIELD_APPLY_ATTRIBUTES ) ) { $this->apply_product_attributes( $product, $ai_data['attributes'] ); } if ( $this->is_generation_preference_enabled( APSW_Settings::FIELD_STORE_SEO_META ) ) { $this->apply_seo_meta( $product, $ai_data['seo'] ); } $product->update_meta_data( '_apsw_created_by_plugin', 'yes' ); $product->update_meta_data( '_apsw_created_phase', 'ai_draft_phase_4c' ); $product->update_meta_data( '_apsw_product_kind', $validated['product_kind'] ); $product->update_meta_data( '_apsw_selected_product_category_id', $validated['product_category_id'] ); $product->update_meta_data( '_apsw_selected_product_category_label', $validated['product_category_label'] ); $product->update_meta_data( '_apsw_original_rough_notes', $validated['rough_notes'] ); $product->update_meta_data( '_apsw_ai_generation_provider', sanitize_key( isset( $input['ai_generation_provider'] ) ? $input['ai_generation_provider'] : '' ) ); $product->update_meta_data( '_apsw_ai_generation_model', sanitize_text_field( isset( $input['ai_generation_model'] ) ? $input['ai_generation_model'] : '' ) ); $product->update_meta_data( '_apsw_ai_generated_at', absint( isset( $input['ai_generated_at'] ) ? $input['ai_generated_at'] : 0 ) ); $product->update_meta_data( '_apsw_prepared_input_created_at', $validated['prepared_timestamp'] > 0 ? $validated['prepared_timestamp'] : time() ); $product->update_meta_data( '_apsw_created_from_ai_preview', 'yes' ); $product->update_meta_data( '_apsw_ai_generated_data', wp_json_encode( $ai_data ) ); if ( ! empty( $validated['batch_create_item'] ) ) { $product->update_meta_data( '_apsw_batch_create_item', 'yes' ); } $this->apply_rich_product_meta( $product, $ai_data ); $save_result = $this->save_product_result( $product, $result, __( 'Product created successfully.', 'ai-product-studio-for-woocommerce' ) ); if ( empty( $save_result['success'] ) ) { return $save_result; } $product_id = $save_result['product_id']; $this->apply_product_category( $product_id, $validated['product_category_id'], $save_result['warnings'] ); if ( $this->is_generation_preference_enabled( APSW_Settings::FIELD_APPLY_TAGS ) ) { $this->apply_product_tags( $product_id, $ai_data['tags'], $save_result['warnings'] ); } if ( $this->is_generation_preference_enabled( APSW_Settings::FIELD_APPLY_IMAGE_ALT_TEXT ) ) { $this->apply_image_alt_text( $validated['product_image_ids'], $validated['product_main_image_id'], $validated['product_gallery_image_ids'], $ai_data['image_alt_text'] ); } return $save_result; } /** * Move an incomplete product to trash after a failed workflow. * * @param int $product_id Product ID. * @return bool */ public function trash_incomplete_product( $product_id ) { $product_id = absint( $product_id ); if ( $product_id <= 0 || 'product' !== get_post_type( $product_id ) ) { return false; } $trashed = wp_trash_post( $product_id ); if ( false === $trashed || is_wp_error( $trashed ) ) { return false; } return true; } /** * Build default result payload. * * @param string $message Default message. * @return array */ private function get_default_result( $message ) { return array( 'success' => false, 'product_id' => 0, 'edit_url' => '', 'warnings' => array(), 'message' => $message, ); } /** * Verify WooCommerce product creation is available. * * @return bool */ private function is_woocommerce_ready() { return $this->dependencies->is_woocommerce_active() && class_exists( 'WC_Product_Simple' ); } /** * Save product and map result structure. * * @param WC_Product_Simple $product Product object. * @param array $result Result payload. * @param string $message Success message. * @return array */ private function save_product_result( WC_Product_Simple $product, array $result, $message ) { try { $product_id = $product->save(); } catch ( Exception $exception ) { return $result; } if ( ! $product_id ) { return $result; } $result['success'] = true; $result['product_id'] = absint( $product_id ); $result['edit_url'] = (string) get_edit_post_link( $product_id, 'raw' ); $result['message'] = $message; return $result; } /** * Validate prepared input payload before product creation. * * @param array $input Prepared payload. * @param array $contexts Allowed contexts. * @return array|WP_Error */ private function validate_prepared_input( array $input, array $contexts ) { $defaults = array( 'generation_mode' => 'text_to_product', 'product_image_ids' => array(), 'product_main_image_id' => 0, 'product_gallery_image_ids' => array(), 'product_category_id' => 0, 'product_category_label' => '', 'product_kind' => 'physical', 'is_downloadable' => 0, 'rough_notes' => '', 'price' => '', 'sale_price' => '', 'sku' => '', 'stock_quantity' => '', 'status' => 'draft', 'prepared_timestamp' => 0, 'batch_create_item' => 0, ); $data = wp_parse_args( $input, $defaults ); $data['generation_mode'] = $this->sanitize_generation_mode( $data['generation_mode'] ); $data['product_category_id'] = absint( $data['product_category_id'] ); $data['product_category_label'] = sanitize_text_field( $data['product_category_label'] ); $data['product_kind'] = sanitize_key( $data['product_kind'] ); $data['is_downloadable'] = ! empty( $data['is_downloadable'] ) ? 1 : 0; if ( 'digital' === $data['product_kind'] ) { $data['product_kind'] = 'virtual'; } if ( 'downloadable' === $data['product_kind'] ) { $data['product_kind'] = 'virtual'; $data['is_downloadable'] = 1; } if ( 'virtual' !== $data['product_kind'] ) { $data['is_downloadable'] = 0; } $data['rough_notes'] = sanitize_textarea_field( $data['rough_notes'] ); $data['price'] = $this->sanitize_decimal( $data['price'] ); $data['sale_price'] = $this->sanitize_decimal( $data['sale_price'] ); $data['sku'] = sanitize_text_field( $data['sku'] ); $data['status'] = 'draft'; $data['stock_quantity'] = '' === (string) $data['stock_quantity'] ? '' : absint( $data['stock_quantity'] ); $data['prepared_timestamp'] = absint( $data['prepared_timestamp'] ); $data['batch_create_item'] = ! empty( $data['batch_create_item'] ) ? 1 : 0; $is_image_mode = 'image_to_product' === $data['generation_mode']; $is_advanced_mode = 'advanced_product_generation' === $data['generation_mode']; $image_payload_mode = $is_image_mode || $is_advanced_mode; $is_batch_item = ! empty( $data['batch_create_item'] ); if ( ! $is_image_mode && '' === trim( $data['rough_notes'] ) ) { return new WP_Error( 'apsw_empty_notes', 'Missing rough notes.' ); } if ( $is_advanced_mode && ! $this->has_meaningful_advanced_notes( $data['rough_notes'] ) ) { return new WP_Error( 'apsw_empty_notes', 'Advanced Product needs more product detail.' ); } if ( ( ! $image_payload_mode && ! $is_batch_item && '' === $data['price'] ) || ( '' !== $data['price'] && ! is_numeric( $data['price'] ) ) ) { return new WP_Error( 'apsw_invalid_price', 'Missing or invalid price.' ); } if ( '' !== $data['sale_price'] && ! is_numeric( $data['sale_price'] ) ) { return new WP_Error( 'apsw_invalid_sale_price', 'Invalid sale price.' ); } if ( ! in_array( $data['product_kind'], array( 'physical', 'virtual' ), true ) ) { return new WP_Error( 'apsw_invalid_product_kind', 'Invalid product kind.' ); } if ( $data['product_category_id'] > 0 ) { $term = get_term( $data['product_category_id'], 'product_cat' ); if ( ! $term || is_wp_error( $term ) || 'product_cat' !== $term->taxonomy ) { return new WP_Error( 'apsw_invalid_product_category', 'Invalid product category.' ); } $data['product_category_label'] = sanitize_text_field( $term->name ); } $main_image_id = absint( $data['product_main_image_id'] ); $gallery_images = is_array( $data['product_gallery_image_ids'] ) ? $data['product_gallery_image_ids'] : array(); $gallery_images = array_values( array_unique( array_filter( array_map( 'absint', $gallery_images ) ) ) ); $images = is_array( $data['product_image_ids'] ) ? $data['product_image_ids'] : array(); $images = array_values( array_unique( array_filter( array_map( 'absint', $images ) ) ) ); if ( $main_image_id > 0 || ! empty( $gallery_images ) ) { $images = array_values( array_filter( array_merge( array( $main_image_id ), $gallery_images ) ) ); } elseif ( ! empty( $images ) ) { $main_image_id = absint( $images[0] ); $gallery_images = array_slice( $images, 1 ); } if ( $image_payload_mode ) { $gallery_images = array(); $images = $main_image_id > 0 ? array( $main_image_id ) : array(); } foreach ( $images as $attachment_id ) { if ( 'attachment' !== get_post_type( $attachment_id ) || ! wp_attachment_is_image( $attachment_id ) ) { return new WP_Error( 'apsw_invalid_image', 'Invalid image attachment selected.' ); } } if ( $image_payload_mode && $main_image_id <= 0 ) { return new WP_Error( 'apsw_invalid_image', 'Missing main product image.' ); } $data['product_image_ids'] = $images; $data['product_main_image_id'] = $main_image_id; $data['product_gallery_image_ids'] = $gallery_images; return $data; } /** * Sanitize AI generated data payload. * * @param array $ai_data Raw AI data. * @return array */ private function sanitize_ai_data( array $ai_data ) { $allowed_html = array( 'p' => array(), 'br' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(), 'strong' => array(), 'em' => array(), 'b' => array(), 'i' => array(), ); $data = array( 'title' => sanitize_text_field( isset( $ai_data['product_title'] ) ? $ai_data['product_title'] : ( isset( $ai_data['title'] ) ? $ai_data['title'] : '' ) ), 'short_description' => wp_kses( isset( $ai_data['short_description'] ) ? $ai_data['short_description'] : '', $allowed_html ), 'long_description' => wp_kses( isset( $ai_data['long_description'] ) ? $ai_data['long_description'] : '', $allowed_html ), 'missing_info_notes' => array(), 'tags' => $this->sanitize_generated_tags( $this->get_first_present_ai_value( $ai_data, array( 'tags', 'product_tags' ) ) ), 'attributes' => $this->sanitize_generated_attributes( $this->get_first_present_ai_value( $ai_data, array( 'attributes', 'product_attributes' ) ) ), 'shipping' => $this->sanitize_generated_shipping( $this->get_first_present_ai_value( $ai_data, array( 'shipping', 'shipping_specs' ) ) ), 'image_alt_text' => $this->sanitize_generated_image_alt_text( $this->get_first_present_ai_value( $ai_data, array( 'image_alt_text', 'alt_text', 'images_alt_text', 'image_alt_texts' ) ) ), 'seo' => $this->sanitize_generated_seo_meta( $this->get_first_present_ai_value( $ai_data, array( 'seo', 'seo_meta' ) ) ), 'suggested_category' => $this->sanitize_suggested_category( $this->get_first_present_ai_value( $ai_data, array( 'suggested_category', 'category_suggestion' ) ) ), 'generation_mode' => $this->sanitize_generation_mode( $this->get_first_present_ai_value( $ai_data, array( 'generation_mode', 'mode', 'source_mode', 'operation_type' ) ) ), ); $warnings = array(); if ( isset( $ai_data['missing_info_notes'] ) && is_array( $ai_data['missing_info_notes'] ) ) { foreach ( array_slice( $ai_data['missing_info_notes'], 0, 20 ) as $note ) { $note = sanitize_text_field( (string) $note ); if ( '' !== $note ) { $data['missing_info_notes'][] = $note; } } } return array( 'data' => $data, 'warnings' => array_values( array_unique( $warnings ) ), ); } /** * Return the first present AI value from possible aliases. * * @param array $ai_data Raw AI data. * @param array $aliases Candidate keys. * @return mixed|null */ private function get_first_present_ai_value( array $ai_data, array $aliases ) { foreach ( $aliases as $alias ) { if ( ! array_key_exists( $alias, $ai_data ) ) { continue; } if ( '' === $ai_data[ $alias ] || array() === $ai_data[ $alias ] ) { continue; } return $ai_data[ $alias ]; } return null; } /** * Sanitize generated tag names. * * @param mixed $value Raw tag value. * @return array */ private function sanitize_generated_tags( $value ) { return $this->sanitize_text_list( $value, 30 ); } /** * Sanitize generated product-level attributes. * * @param mixed $value Raw attributes value. * @return array */ private function sanitize_generated_attributes( $value ) { if ( ! is_array( $value ) ) { return array(); } $attributes = array(); $items = $this->is_list_array( $value ) ? $value : $this->assoc_attributes_to_list( $value ); foreach ( array_slice( $items, 0, 30 ) as $item ) { $attribute = $this->sanitize_generated_attribute_item( $item ); if ( empty( $attribute['name'] ) || empty( $attribute['values'] ) ) { continue; } $key = sanitize_title( $attribute['name'] ); if ( '' === $key ) { continue; } if ( isset( $attributes[ $key ] ) ) { $attributes[ $key ]['values'] = array_values( array_unique( array_merge( $attributes[ $key ]['values'], $attribute['values'] ) ) ); continue; } $attributes[ $key ] = $attribute; } return array_values( $attributes ); } /** * Convert associative attribute maps to list-shaped attribute items. * * @param array $attributes Raw associative attributes. * @return array */ private function assoc_attributes_to_list( array $attributes ) { $list = array(); foreach ( $attributes as $name => $values ) { if ( is_int( $name ) && is_array( $values ) ) { $list[] = $values; continue; } $list[] = array( 'name' => $name, 'values' => $values, ); } return $list; } /** * Sanitize one generated attribute item. * * @param mixed $item Raw attribute item. * @return array */ private function sanitize_generated_attribute_item( $item ) { if ( ! is_array( $item ) ) { return array( 'name' => '', 'values' => array(), ); } $name = ''; foreach ( array( 'name', 'label', 'title', 'attribute' ) as $name_key ) { if ( isset( $item[ $name_key ] ) && ! is_array( $item[ $name_key ] ) ) { $name = sanitize_text_field( (string) $item[ $name_key ] ); break; } } $values = null; foreach ( array( 'values', 'value', 'options', 'terms' ) as $value_key ) { if ( array_key_exists( $value_key, $item ) ) { $values = $item[ $value_key ]; break; } } return array( 'name' => $this->sanitize_attribute_name( $name ), 'values' => $this->sanitize_text_list( $values, 50 ), ); } /** * Sanitize a custom product attribute name. * * @param string $name Raw attribute name. * @return string */ private function sanitize_attribute_name( $name ) { $name = sanitize_text_field( wp_strip_all_tags( (string) $name ) ); $name = trim( preg_replace( '/\s+/', ' ', $name ) ); return is_string( $name ) ? substr( $name, 0, 100 ) : ''; } /** * Sanitize generated shipping measurements. * * @param mixed $value Raw shipping value. * @return array */ private function sanitize_generated_shipping( $value ) { if ( ! is_array( $value ) ) { return array(); } $shipping = $value; if ( isset( $value['dimensions'] ) && is_array( $value['dimensions'] ) ) { $shipping = array_merge( $value, $value['dimensions'] ); } $data = array(); foreach ( array( 'weight', 'length', 'width', 'height' ) as $field ) { if ( ! array_key_exists( $field, $shipping ) ) { continue; } $decimal = $this->sanitize_measurement_decimal( $shipping[ $field ] ); if ( '' !== $decimal ) { $data[ $field ] = $decimal; } } return $data; } /** * Sanitize image alt text in string, ordered array, or attachment-ID map form. * * @param mixed $value Raw alt text value. * @return array */ private function sanitize_generated_image_alt_text( $value ) { $alt_text = array( 'main' => '', 'ordered' => array(), 'by_id' => array(), ); if ( ! is_array( $value ) ) { $alt_text['main'] = sanitize_text_field( (string) $value ); return $alt_text; } $is_ordered_list = $this->is_list_array( $value ); foreach ( array_slice( $value, 0, 50, true ) as $key => $item ) { if ( 'gallery' === $key && is_array( $item ) ) { $alt_text['ordered'] = array_merge( $alt_text['ordered'], $this->sanitize_text_list( $item, 50 ) ); continue; } if ( is_array( $item ) ) { $item_alt = $this->extract_alt_text_from_array( $item ); $item_id = $this->extract_attachment_id_from_array( $item ); } else { $item_alt = sanitize_text_field( (string) $item ); $item_id = ! $is_ordered_list && is_numeric( $key ) ? absint( $key ) : 0; } if ( '' === $item_alt ) { continue; } if ( $item_id > 0 ) { $alt_text['by_id'][ $item_id ] = $item_alt; continue; } if ( in_array( $key, array( 'main', 'featured', 'primary' ), true ) ) { $alt_text['main'] = $item_alt; continue; } $alt_text['ordered'][] = $item_alt; } $alt_text['ordered'] = array_values( array_filter( $alt_text['ordered'] ) ); return $alt_text; } /** * Extract alt text from an array item. * * @param array $item Raw alt item. * @return string */ private function extract_alt_text_from_array( array $item ) { foreach ( array( 'alt_text', 'image_alt_text', 'alt', 'text', 'value' ) as $key ) { if ( isset( $item[ $key ] ) && ! is_array( $item[ $key ] ) ) { return sanitize_text_field( (string) $item[ $key ] ); } } return ''; } /** * Extract attachment ID from an array item. * * @param array $item Raw alt item. * @return int */ private function extract_attachment_id_from_array( array $item ) { foreach ( array( 'id', 'image_id', 'attachment_id' ) as $key ) { if ( isset( $item[ $key ] ) ) { return absint( $item[ $key ] ); } } return 0; } /** * Sanitize generated SEO metadata. * * @param mixed $value Raw SEO value. * @return array */ private function sanitize_generated_seo_meta( $value ) { if ( ! is_array( $value ) ) { return array(); } return array( 'title' => $this->sanitize_text_from_aliases( $value, array( 'title', 'seo_title', 'meta_title' ) ), 'description' => $this->sanitize_textarea_from_aliases( $value, array( 'description', 'seo_description', 'meta_description' ) ), 'focus_keyword' => $this->sanitize_text_from_aliases( $value, array( 'focus_keyword', 'keyword', 'primary_keyword' ) ), ); } /** * Sanitize suggested category metadata. * * @param mixed $value Raw category suggestion. * @return string */ private function sanitize_suggested_category( $value ) { if ( is_array( $value ) ) { $value = $this->sanitize_text_from_aliases( $value, array( 'name', 'label', 'category', 'suggestion' ) ); } return sanitize_text_field( (string) $value ); } /** * Sanitize generation mode metadata. * * @param mixed $value Raw mode value. * @return string */ private function sanitize_generation_mode( $value ) { if ( null === $value || '' === $value || is_array( $value ) ) { return ''; } $mode = sanitize_key( str_replace( '-', '_', (string) $value ) ); $map = array( 'text' => 'text_to_product', 'text_to_product' => 'text_to_product', 'image' => 'image_to_product', 'image_to_product' => 'image_to_product', 'advanced' => 'advanced_product_generation', 'advanced_product_generation' => 'advanced_product_generation', 'bulk' => 'bulk_image', 'bulk_image' => 'bulk_image', 'batch' => 'batch', ); return isset( $map[ $mode ] ) ? $map[ $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 ) >= 20; } /** * Sanitize a generic text list from arrays or comma-separated strings. * * @param mixed $value Raw value. * @param int $limit Maximum items. * @return array */ private function sanitize_text_list( $value, $limit = 50 ) { if ( is_string( $value ) ) { $value = explode( ',', $value ); } if ( ! is_array( $value ) ) { return array(); } $items = array(); foreach ( array_slice( $value, 0, absint( $limit ) ) as $item ) { if ( is_array( $item ) ) { $item = $this->sanitize_text_from_aliases( $item, array( 'name', 'label', 'title', 'value', 'text' ) ); } if ( is_array( $item ) ) { continue; } $item = sanitize_text_field( wp_strip_all_tags( (string) $item ) ); $item = trim( preg_replace( '/\s+/', ' ', $item ) ); if ( '' !== $item ) { $items[] = $item; } } return array_values( array_unique( $items ) ); } /** * Sanitize the first text value from aliases. * * @param array $data Raw data. * @param array $aliases Candidate keys. * @return string */ private function sanitize_text_from_aliases( array $data, array $aliases ) { foreach ( $aliases as $alias ) { if ( isset( $data[ $alias ] ) && ! is_array( $data[ $alias ] ) ) { return sanitize_text_field( (string) $data[ $alias ] ); } } return ''; } /** * Sanitize the first textarea value from aliases. * * @param array $data Raw data. * @param array $aliases Candidate keys. * @return string */ private function sanitize_textarea_from_aliases( array $data, array $aliases ) { foreach ( $aliases as $alias ) { if ( isset( $data[ $alias ] ) && ! is_array( $data[ $alias ] ) ) { return sanitize_textarea_field( (string) $data[ $alias ] ); } } return ''; } /** * Detect list-shaped arrays without requiring newer PHP helpers. * * @param array $value Raw array. * @return bool */ private function is_list_array( array $value ) { if ( array() === $value ) { return true; } return array_keys( $value ) === range( 0, count( $value ) - 1 ); } /** * Apply regular/sale/current price values. * * @param WC_Product_Simple $product Product object. * @param array $data Prepared data. * @param array $warnings Warning messages. * @return void */ private function apply_price_data( WC_Product_Simple $product, array $data, array &$warnings ) { $regular = $data['price']; $sale = $data['sale_price']; if ( '' !== $regular ) { $product->set_regular_price( $regular ); } if ( '' !== $sale && '' !== $regular ) { if ( (float) $sale > (float) $regular ) { $warnings[] = __( 'Sale price was skipped because it is greater than regular price.', 'ai-product-studio-for-woocommerce' ); $product->set_price( $regular ); return; } $product->set_sale_price( $sale ); $product->set_price( $sale ); return; } if ( '' !== $regular ) { $product->set_price( $regular ); } } /** * Apply SKU with duplicate-safe fallback. * * @param WC_Product_Simple $product Product object. * @param array $data Prepared data. * @param array $warnings Warning messages. * @return void */ private function apply_sku_data( WC_Product_Simple $product, array $data, array &$warnings ) { if ( '' === $data['sku'] ) { return; } try { $product->set_sku( $data['sku'] ); } catch ( Exception $exception ) { $warnings[] = __( 'Product was created, but SKU was skipped because it already exists.', 'ai-product-studio-for-woocommerce' ); } } /** * Apply stock values. * * @param WC_Product_Simple $product Product object. * @param array $data Prepared data. * @return void */ private function apply_stock_data( WC_Product_Simple $product, array $data ) { if ( '' === $data['stock_quantity'] ) { return; } $quantity = absint( $data['stock_quantity'] ); $product->set_manage_stock( true ); $product->set_stock_quantity( $quantity ); $product->set_stock_status( $quantity > 0 ? 'instock' : 'outofstock' ); } /** * Apply generated shipping measurements. * * @param WC_Product_Simple $product Product object. * @param array $shipping Sanitized shipping data. * @return void */ private function apply_shipping_data( WC_Product_Simple $product, array $shipping ) { if ( isset( $shipping['weight'] ) ) { $product->set_weight( $shipping['weight'] ); } if ( isset( $shipping['length'] ) ) { $product->set_length( $shipping['length'] ); } if ( isset( $shipping['width'] ) ) { $product->set_width( $shipping['width'] ); } if ( isset( $shipping['height'] ) ) { $product->set_height( $shipping['height'] ); } } /** * Apply generated product-level custom attributes. * * @param WC_Product_Simple $product Product object. * @param array $attributes Sanitized attributes. * @return void */ private function apply_product_attributes( WC_Product_Simple $product, array $attributes ) { if ( empty( $attributes ) || ! class_exists( 'WC_Product_Attribute' ) ) { return; } $product_attributes = array(); $position = 0; foreach ( $attributes as $attribute ) { if ( empty( $attribute['name'] ) || empty( $attribute['values'] ) || ! is_array( $attribute['values'] ) ) { continue; } $attribute_object = new WC_Product_Attribute(); $attribute_object->set_id( 0 ); $attribute_object->set_name( $attribute['name'] ); $attribute_object->set_options( $attribute['values'] ); $attribute_object->set_position( $position ); $attribute_object->set_visible( true ); $attribute_object->set_variation( false ); $product_attributes[ sanitize_title( $attribute['name'] ) ] = $attribute_object; ++$position; } if ( ! empty( $product_attributes ) ) { $product->set_attributes( $product_attributes ); } } /** * Apply featured and gallery images. * * @param WC_Product_Simple $product Product object. * @param array $attachment_ids Legacy combined image attachment IDs. * @param int $main_image_id Featured image attachment ID. * @param array $gallery_image_ids Gallery attachment IDs. * @return void */ private function apply_image_data( WC_Product_Simple $product, array $attachment_ids, $main_image_id = 0, array $gallery_image_ids = array() ) { $attachment_ids = array_values( array_filter( array_map( 'absint', $attachment_ids ) ) ); $main_image_id = absint( $main_image_id ); $gallery_image_ids = array_values( array_filter( array_map( 'absint', $gallery_image_ids ) ) ); if ( 0 === $main_image_id && empty( $gallery_image_ids ) && ! empty( $attachment_ids ) ) { $main_image_id = absint( $attachment_ids[0] ); $gallery_image_ids = array_slice( $attachment_ids, 1 ); } if ( 0 === $main_image_id && empty( $gallery_image_ids ) ) { return; } if ( $main_image_id > 0 ) { $product->set_image_id( $main_image_id ); } if ( ! empty( $gallery_image_ids ) ) { $product->set_gallery_image_ids( $gallery_image_ids ); } } /** * Assign selected WooCommerce product category. * * @param int $product_id Product ID. * @param int $category_id Product category term ID. * @param array $warnings Warnings list. * @return void */ private function apply_product_category( $product_id, $category_id, array &$warnings ) { $category_id = absint( $category_id ); if ( $category_id <= 0 ) { return; } $term = get_term( $category_id, 'product_cat' ); if ( ! $term || is_wp_error( $term ) || 'product_cat' !== $term->taxonomy ) { $warnings[] = __( 'Selected category could not be assigned because it is no longer available.', 'ai-product-studio-for-woocommerce' ); return; } $term_result = wp_set_object_terms( absint( $product_id ), array( $category_id ), 'product_cat', false ); if ( is_wp_error( $term_result ) ) { $warnings[] = __( 'Selected category could not be assigned.', 'ai-product-studio-for-woocommerce' ); } } /** * Assign generated product tags. * * @param int $product_id Product ID. * @param array $tags Sanitized tag names. * @param array $warnings Warnings list. * @return void */ private function apply_product_tags( $product_id, array $tags, array &$warnings ) { $product_id = absint( $product_id ); $tags = array_values( array_filter( array_map( 'sanitize_text_field', $tags ) ) ); if ( $product_id <= 0 || empty( $tags ) || ! taxonomy_exists( 'product_tag' ) ) { return; } $term_result = wp_set_object_terms( $product_id, $tags, 'product_tag', false ); if ( is_wp_error( $term_result ) ) { $warnings[] = __( 'Generated product tags could not be assigned.', 'ai-product-studio-for-woocommerce' ); } } /** * Update selected image attachment alt text when generated text is present. * * @param array $attachment_ids Combined attachment IDs. * @param int $main_image_id Main image attachment ID. * @param array $gallery_image_ids Gallery image attachment IDs. * @param array $alt_text Sanitized alt text data. * @return void */ private function apply_image_alt_text( array $attachment_ids, $main_image_id, array $gallery_image_ids, array $alt_text ) { $attachment_ids = array_values( array_filter( array_map( 'absint', $attachment_ids ) ) ); $main_image_id = absint( $main_image_id ); $gallery_image_ids = array_values( array_filter( array_map( 'absint', $gallery_image_ids ) ) ); if ( $main_image_id > 0 || ! empty( $gallery_image_ids ) ) { $attachment_ids = array_values( array_unique( array_filter( array_merge( array( $main_image_id ), $gallery_image_ids ) ) ) ); } if ( empty( $attachment_ids ) ) { return; } foreach ( $attachment_ids as $index => $attachment_id ) { $text = ''; if ( isset( $alt_text['by_id'][ $attachment_id ] ) ) { $text = $alt_text['by_id'][ $attachment_id ]; } elseif ( 0 === $index && ! empty( $alt_text['main'] ) ) { $text = $alt_text['main']; } elseif ( isset( $alt_text['ordered'][ $index ] ) ) { $text = $alt_text['ordered'][ $index ]; } $text = sanitize_text_field( (string) $text ); if ( '' === $text || 'attachment' !== get_post_type( $attachment_id ) || ! wp_attachment_is_image( $attachment_id ) ) { continue; } update_post_meta( $attachment_id, '_wp_attachment_image_alt', $text ); } } /** * Save generated SEO metadata using plugin-scoped meta keys. * * @param WC_Product_Simple $product Product object. * @param array $seo Sanitized SEO metadata. * @return void */ private function apply_seo_meta( WC_Product_Simple $product, array $seo ) { if ( ! empty( $seo['title'] ) ) { $product->update_meta_data( '_apsw_seo_title', $seo['title'] ); } if ( ! empty( $seo['description'] ) ) { $product->update_meta_data( '_apsw_seo_description', $seo['description'] ); } if ( ! empty( $seo['focus_keyword'] ) ) { $product->update_meta_data( '_apsw_focus_keyword', $seo['focus_keyword'] ); } } /** * Save generated plugin-scoped rich metadata. * * @param WC_Product_Simple $product Product object. * @param array $ai_data Sanitized AI data. * @return void */ private function apply_rich_product_meta( WC_Product_Simple $product, array $ai_data ) { if ( ! empty( $ai_data['suggested_category'] ) ) { $product->update_meta_data( '_apsw_suggested_category', $ai_data['suggested_category'] ); } if ( ! empty( $ai_data['generation_mode'] ) ) { $product->update_meta_data( '_apsw_generation_mode', $ai_data['generation_mode'] ); } } /** * Sanitize decimal using WooCommerce formatter when available. * * @param string $value Raw value. * @return string */ private function sanitize_decimal( $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 ); } /** * Sanitize generated shipping measurement decimals. * * @param mixed $value Raw value. * @return string */ private function sanitize_measurement_decimal( $value ) { if ( is_array( $value ) ) { return ''; } $value = trim( (string) $value ); if ( '' === $value ) { return ''; } if ( function_exists( 'wc_format_decimal' ) ) { $value = wc_format_decimal( $value, false, false ); } else { $value = preg_replace( '/[^0-9.\-]/', '', $value ); } if ( ! is_numeric( $value ) || (float) $value < 0 ) { return ''; } return (string) $value; } /** * Decode common title entities and sanitize before saving to WooCommerce. * * @param string $title Product title. * @return string */ private function sanitize_product_title( $title ) { $title = wp_specialchars_decode( (string) $title, ENT_QUOTES ); return sanitize_text_field( $title ); } /** * Check whether a generation preference is enabled. * * @param string $key Setting key. * @return bool */ private function is_generation_preference_enabled( $key ) { return $this->settings->is_generation_option_enabled( $key ); } }