diff --git a/composer.json b/composer.json index 0651cc5e238..e2926b9cebe 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "WooCommerce blocks for the Gutenberg editor.", "homepage": "https://woocommerce.com/", "type": "wordpress-plugin", - "version": "11.5.1", + "version": "11.5.2", "keywords": [ "gutenberg", "woocommerce", diff --git a/docs/internal-developers/testing/releases/1152.md b/docs/internal-developers/testing/releases/1152.md new file mode 100644 index 00000000000..b0ee1965800 --- /dev/null +++ b/docs/internal-developers/testing/releases/1152.md @@ -0,0 +1,19 @@ +# Testing notes and ZIP for release 11.5.2 + +Zip file for testing: [woocommerce-gutenberg-products-block.zip](https://github.com/woocommerce/woocommerce-blocks/files/13300708/woocommerce-gutenberg-products-block.zip) + +## WooCommerce Core + +### Bug Fixes + +#### Rename the Centered Header Menu with Search to Centered Header Menu. [11637](https://github.com/woocommerce/woocommerce-blocks/pull/11637) + +1. Create a new page or post. +2. Confirm that the `Centered Header Menu` pattern appears under the WooCommerce category dropdown and is NOT named `Centered Header Menu with Search`. +3. Insert in and confirm everything appears as expected. + +#### Fix decoding issue and pattern. [11681](https://github.com/woocommerce/woocommerce-blocks/pull/11681) + +1. Ensure you are on fresh install (it should not have the `patterns_ai_data` post created). +2. Insert the `Featured Products: Fresh & Tasty` pattern and make sure it has all the default text content, nothing is empty. +3. Insert a few other WooCommerce Blocks patterns and check all of them have all the text content as well. diff --git a/docs/internal-developers/testing/releases/README.md b/docs/internal-developers/testing/releases/README.md index 2f7a91fae67..bee4b2a8908 100644 --- a/docs/internal-developers/testing/releases/README.md +++ b/docs/internal-developers/testing/releases/README.md @@ -187,3 +187,5 @@ Every release includes specific testing instructions for new features and bug fi - [11.4.3](./1143.md) - [11.5.0](./1150.md) - [11.5.1](./1151.md) + - [11.5.2](./1152.md) + diff --git a/package-lock.json b/package-lock.json index 708613e751b..ec5a77d1fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@woocommerce/block-library", - "version": "11.5.1", + "version": "11.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@woocommerce/block-library", - "version": "11.5.1", + "version": "11.5.2", "hasInstallScript": true, "license": "GPL-3.0+", "dependencies": { diff --git a/package.json b/package.json index 140ed141f95..9319a71f52c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@woocommerce/block-library", "title": "WooCommerce Blocks", "author": "Automattic", - "version": "11.5.1", + "version": "11.5.2", "description": "WooCommerce blocks for the Gutenberg editor.", "homepage": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/", "keywords": [ diff --git a/patterns/featured-products-fresh-and-tasty.php b/patterns/featured-products-fresh-and-tasty.php index 60b814a6f2a..818893c0ddb 100644 --- a/patterns/featured-products-fresh-and-tasty.php +++ b/patterns/featured-products-fresh-and-tasty.php @@ -18,6 +18,7 @@ $first_description = $content['descriptions'][0]['default'] ?? ''; $second_description = $content['descriptions'][1]['default'] ?? ''; $third_description = $content['descriptions'][2]['default'] ?? ''; +$fourth_description = $content['descriptions'][3]['default'] ?? ''; ?> @@ -129,7 +130,7 @@
-

+

diff --git a/patterns/header-centered-search-pattern.php b/patterns/header-centered-pattern.php similarity index 96% rename from patterns/header-centered-search-pattern.php rename to patterns/header-centered-pattern.php index eaebf5fbee9..517bc1672d4 100644 --- a/patterns/header-centered-search-pattern.php +++ b/patterns/header-centered-pattern.php @@ -1,7 +1,7 @@ array( + 'name' => __( 'Patterns AI Data', 'woo-gutenberg-products-block' ), + 'singular_name' => __( 'Patterns AI Data', 'woo-gutenberg-products-block' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + ) + ); + $default_headers = array( 'title' => 'Title', 'slug' => 'Slug', @@ -250,7 +267,7 @@ public function schedule_on_option_update( $option, $value ) { * @param array $options Array of bulk item update data. */ public function schedule_on_plugin_update( $upgrader_object, $options ) { - if ( 'update' === $options['action'] && 'plugin' === $options['type'] ) { + if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) { foreach ( $options['plugins'] as $plugin ) { if ( str_contains( $plugin, 'woocommerce-gutenberg-products-block.php' ) || str_contains( $plugin, 'woocommerce.php' ) ) { $business_description = get_option( 'woo_ai_describe_store_description' ); diff --git a/src/Package.php b/src/Package.php index 192c675fc7c..568cb3d5eef 100644 --- a/src/Package.php +++ b/src/Package.php @@ -109,7 +109,7 @@ public static function container( $reset = false ) { NewPackage::class, function ( $container ) { // leave for automated version bumping. - $version = '11.5.1'; + $version = '11.5.2'; return new NewPackage( $version, dirname( __DIR__ ), diff --git a/src/Patterns/PatternUpdater.php b/src/Patterns/PatternUpdater.php index 67568e8ba5a..affbd20cbeb 100644 --- a/src/Patterns/PatternUpdater.php +++ b/src/Patterns/PatternUpdater.php @@ -10,11 +10,6 @@ */ class PatternUpdater { - /** - * The patterns content option name. - */ - const WC_BLOCKS_PATTERNS_CONTENT = 'wc_blocks_patterns_content'; - /** * Creates the patterns content for the given vertical. * @@ -42,13 +37,15 @@ public function generate_content( $ai_connection, $token, $images, $business_des return new WP_Error( 'failed_to_set_pattern_content', __( 'Failed to set the pattern content.', 'woo-gutenberg-products-block' ) ); } - if ( get_option( self::WC_BLOCKS_PATTERNS_CONTENT ) === $patterns_with_images_and_content ) { + $patterns_ai_data_post = PatternsHelper::get_patterns_ai_data_post(); + + if ( isset( $patterns_ai_data_post->post_content ) && json_decode( $patterns_ai_data_post->post_content ) === $patterns_with_images_and_content ) { return true; } - $updated_content = update_option( self::WC_BLOCKS_PATTERNS_CONTENT, $patterns_with_images_and_content ); + $updated_content = PatternsHelper::upsert_patterns_ai_data_post( $patterns_with_images_and_content ); - if ( ! $updated_content ) { + if ( is_wp_error( $updated_content ) ) { return new WP_Error( 'failed_to_update_patterns_content', __( 'Failed to update patterns content.', 'woo-gutenberg-products-block' ) ); } diff --git a/src/Patterns/PatternsHelper.php b/src/Patterns/PatternsHelper.php index 2ac827f030d..591db624374 100644 --- a/src/Patterns/PatternsHelper.php +++ b/src/Patterns/PatternsHelper.php @@ -74,6 +74,48 @@ public static function get_image_url( array $images, int $index, string $default return $image; } + /** + * Returns the post that has the generated data by the AI for the patterns. + * + * @return WP_Post|null + */ + public static function get_patterns_ai_data_post() { + $arg = array( + 'post_type' => 'patterns_ai_data', + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'cache_results' => true, + ); + + $query = new \WP_Query( $arg ); + + $posts = $query->get_posts(); + return isset( $posts[0] ) ? $posts[0] : null; + } + + /** + * Upsert the patterns AI data. + * + * @param array $patterns_dictionary The patterns dictionary. + * + * @return WP_Error|null + */ + public static function upsert_patterns_ai_data_post( $patterns_dictionary ) { + $patterns_ai_data_post = self::get_patterns_ai_data_post(); + + if ( isset( $patterns_ai_data_post ) ) { + $patterns_ai_data_post->post_content = wp_json_encode( $patterns_dictionary ); + return wp_update_post( $patterns_ai_data_post, true ); + } else { + $patterns_ai_data_post = array( + 'post_title' => 'Patterns AI Data', + 'post_content' => wp_json_encode( $patterns_dictionary ), + 'post_status' => 'publish', + 'post_type' => 'patterns_ai_data', + ); + return wp_insert_post( $patterns_ai_data_post, true ); + } + } /** * Get the Patterns Dictionary. @@ -83,9 +125,11 @@ public static function get_image_url( array $images, int $index, string $default * @return mixed|WP_Error|null */ private static function get_patterns_dictionary( $pattern_slug = null ) { - $patterns_dictionary = get_option( PatternUpdater::WC_BLOCKS_PATTERNS_CONTENT ); - if ( ! empty( $patterns_dictionary ) ) { + $patterns_ai_data_post = self::get_patterns_ai_data_post(); + + if ( isset( $patterns_ai_data_post ) ) { + $patterns_dictionary = json_decode( $patterns_ai_data_post->post_content, true ); if ( empty( $pattern_slug ) ) { return $patterns_dictionary; } @@ -103,7 +147,7 @@ private static function get_patterns_dictionary( $pattern_slug = null ) { return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woo-gutenberg-products-block' ) ); } - $patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) ); + $patterns_dictionary = json_decode( $patterns_dictionary_file, true ); if ( ! empty( $pattern_slug ) ) { foreach ( $patterns_dictionary as $pattern_dictionary ) { diff --git a/src/Patterns/ProductUpdater.php b/src/Patterns/ProductUpdater.php index 1184fc75d68..ee13c515e49 100644 --- a/src/Patterns/ProductUpdater.php +++ b/src/Patterns/ProductUpdater.php @@ -17,7 +17,7 @@ class ProductUpdater { * @param array $images The array of images. * @param string $business_description The business description. * - * @return bool|WP_Error True if the content was generated successfully, WP_Error otherwise. + * @return array|WP_Error The generated content for the products. An error if the content could not be generated. */ public function generate_content( $ai_connection, $token, $images, $business_description ) { if ( empty( $business_description ) ) { @@ -28,16 +28,48 @@ public function generate_content( $ai_connection, $token, $images, $business_des if ( $last_business_description === $business_description ) { if ( is_string( $business_description ) && is_string( $last_business_description ) ) { - return true; + return array( + 'product_content' => array(), + ); } else { return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woo-gutenberg-products-block' ) ); } } + $ai_selected_products_images = $this->get_images_information( $images ); + $products_information_list = $this->assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images ); + + $response = $this->generate_product_content( $ai_connection, $token, $products_information_list ); + + if ( is_wp_error( $response ) ) { + $error_msg = $response; + } elseif ( empty( $response ) || ! isset( $response['completion'] ) ) { + $error_msg = new \WP_Error( 'missing_completion_key', __( 'The response from the AI service is empty or missing the completion key.', 'woo-gutenberg-products-block' ) ); + } + + if ( isset( $error_msg ) ) { + return $error_msg; + } + + $product_content = json_decode( $response['completion'], true ); + + return array( + 'product_content' => $product_content, + ); + } + + /** + * Return all dummy products that were not modified by the store owner. + * + * @return array|WP_Error An array with the dummy products that need to have their content updated by AI. + */ + public function fetch_dummy_products_to_update() { $real_products = $this->fetch_product_ids(); if ( is_array( $real_products ) && count( $real_products ) > 0 ) { - return true; + return array( + 'product_content' => array(), + ); } $dummy_products = $this->fetch_product_ids( 'dummy' ); @@ -82,62 +114,7 @@ function ( $product ) { } } - if ( empty( $dummy_products_to_update ) ) { - return true; - } - - $ai_selected_products_images = $this->get_images_information( $images ); - $products_information_list = $this->assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images ); - - $response = $this->generate_product_content( $ai_connection, $token, $products_information_list ); - - if ( is_wp_error( $response ) ) { - $error_msg = $response; - } elseif ( empty( $response ) || ! isset( $response['completion'] ) ) { - $error_msg = new \WP_Error( 'missing_completion_key', __( 'The response from the AI service is empty or missing the completion key.', 'woo-gutenberg-products-block' ) ); - } - - if ( isset( $error_msg ) ) { - $this->update_dummy_products( $dummy_products_to_update, $products_information_list ); - - return $error_msg; - } - - $product_content = json_decode( $response['completion'], true ); - - if ( is_null( $product_content ) ) { - $this->update_dummy_products( $dummy_products_to_update, $products_information_list ); - - return new \WP_Error( 'invalid_json', __( 'The response from the AI service is not a valid JSON.', 'woo-gutenberg-products-block' ) ); - } - - // This is required to allow the usage of the media_sideload_image function outside the context of /wp-admin/. - // See https://developer.wordpress.org/reference/functions/media_sideload_image/ for more details. - require_once ABSPATH . 'wp-admin/includes/media.php'; - require_once ABSPATH . 'wp-admin/includes/file.php'; - require_once ABSPATH . 'wp-admin/includes/image.php'; - - $this->update_dummy_products( $dummy_products_to_update, $product_content ); - - return true; - } - - /** - * Update the dummy products with the content from the information list. - * - * @param array $dummy_products_to_update The dummy products to update. - * @param array $products_information_list The products information list. - */ - public function update_dummy_products( $dummy_products_to_update, $products_information_list ) { - $i = 0; - foreach ( $dummy_products_to_update as $dummy_product ) { - if ( ! isset( $products_information_list[ $i ] ) ) { - continue; - } - - $this->update_product_content( $dummy_product, $products_information_list[ $i ] ); - ++$i; - } + return $dummy_products_to_update; } /** @@ -273,11 +250,15 @@ public function update_product_content( $product, $ai_generated_product_content if ( ! isset( $ai_generated_product_content['image']['src'] ) || ! isset( $ai_generated_product_content['image']['alt'] ) || ! isset( $ai_generated_product_content['title'] ) || ! isset( $ai_generated_product_content['description'] ) ) { return; } + + require_once ABSPATH . 'wp-admin/includes/media.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + // Since the media_sideload_image function is expensive and can take longer to complete // the process of downloading the external image and uploading it to the media library, - // here we are increasing the time limit and the memory limit to avoid any issues. + // here we are increasing the time limit to avoid any issues. set_time_limit( 60 ); - wp_raise_memory_limit(); $product_image_id = media_sideload_image( $ai_generated_product_content['image']['src'], $product->get_id(), $ai_generated_product_content['image']['alt'], 'id' ); diff --git a/src/StoreApi/Routes/V1/AI/BusinessDescription.php b/src/StoreApi/Routes/V1/AI/BusinessDescription.php new file mode 100644 index 00000000000..f377eff9a6f --- /dev/null +++ b/src/StoreApi/Routes/V1/AI/BusinessDescription.php @@ -0,0 +1,89 @@ + \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ Middleware::class, 'is_authorized' ], + 'args' => [ + 'business_description' => [ + 'description' => __( 'The business description for a given store.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + ], + ], + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } + + /** + * Update the last business description. + * + * @param \WP_REST_Request $request Request object. + * + * @return bool|string|\WP_Error|\WP_REST_Response + */ + protected function get_route_post_response( \WP_REST_Request $request ) { + + $business_description = $request->get_param( 'business_description' ); + + if ( ! $business_description ) { + return $this->error_to_response( + new \WP_Error( + 'invalid_business_description', + __( 'Invalid business description.', 'woo-gutenberg-products-block' ) + ) + ); + } + + update_option( 'last_business_description_with_ai_content_generated', $business_description ); + + return rest_ensure_response( + array( + 'ai_content_generated' => true, + ) + ); + } + +} diff --git a/src/StoreApi/Routes/V1/AI/Images.php b/src/StoreApi/Routes/V1/AI/Images.php new file mode 100644 index 00000000000..f6070e2c1b6 --- /dev/null +++ b/src/StoreApi/Routes/V1/AI/Images.php @@ -0,0 +1,118 @@ + \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ Middleware::class, 'is_authorized' ], + 'args' => [ + 'business_description' => [ + 'description' => __( 'The business description for a given store.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + ], + ], + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } + + /** + * Generate Images from Pexels + * + * @param \WP_REST_Request $request Request object. + * + * @return bool|string|\WP_Error|\WP_REST_Response + */ + protected function get_route_post_response( \WP_REST_Request $request ) { + + $business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) ); + + if ( empty( $business_description ) ) { + $business_description = get_option( 'woo_ai_describe_store_description' ); + } + + $last_business_description = get_option( 'last_business_description_with_ai_content_generated' ); + + if ( $last_business_description === $business_description ) { + return rest_ensure_response( + $this->prepare_item_for_response( + [ + 'ai_content_generated' => true, + 'images' => array(), + ], + $request + ) + ); + } + + $ai_connection = new Connection(); + + $site_id = $ai_connection->get_site_id(); + + if ( is_wp_error( $site_id ) ) { + return $this->error_to_response( $site_id ); + } + + $token = $ai_connection->get_jwt_token( $site_id ); + + if ( is_wp_error( $token ) ) { + return $this->error_to_response( $token ); + } + + $images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description ); + + if ( is_wp_error( $images ) ) { + return $this->error_to_response( $images ); + } + + return rest_ensure_response( + $this->prepare_item_for_response( + [ + 'ai_content_generated' => true, + 'images' => $images, + ], + $request + ) + ); + } +} diff --git a/src/StoreApi/Routes/V1/AI/Middleware.php b/src/StoreApi/Routes/V1/AI/Middleware.php new file mode 100644 index 00000000000..797e2e00216 --- /dev/null +++ b/src/StoreApi/Routes/V1/AI/Middleware.php @@ -0,0 +1,50 @@ +getErrorCode(), + $error->getMessage(), + array( 'status' => $error->getCode() ) + ); + } + + $allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' ); + + if ( ! $allow_ai_connection ) { + try { + throw new RouteException( 'ai_connection_not_allowed', __( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woo-gutenberg-products-block' ), 403 ); + } catch ( RouteException $error ) { + return new \WP_Error( + $error->getErrorCode(), + $error->getMessage(), + array( 'status' => $error->getCode() ) + ); + } + } + + return true; + } +} diff --git a/src/StoreApi/Routes/V1/AI/Patterns.php b/src/StoreApi/Routes/V1/AI/Patterns.php new file mode 100644 index 00000000000..120e6479885 --- /dev/null +++ b/src/StoreApi/Routes/V1/AI/Patterns.php @@ -0,0 +1,94 @@ + \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ Middleware::class, 'is_authorized' ], + 'args' => [ + 'business_description' => [ + 'description' => __( 'The business description for a given store.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + ], + 'images' => [ + 'description' => __( 'The images for a given store.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + ], + ], + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } + + /** + * Update patterns with the content and images powered by AI. + * + * @param \WP_REST_Request $request Request object. + * + * @return bool|string|\WP_Error|\WP_REST_Response + */ + protected function get_route_post_response( \WP_REST_Request $request ) { + $business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) ); + + $ai_connection = new Connection(); + + $site_id = $ai_connection->get_site_id(); + + if ( is_wp_error( $site_id ) ) { + return $this->error_to_response( $site_id ); + } + + $token = $ai_connection->get_jwt_token( $site_id ); + + $images = $request['images']; + + try { + ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description ); + return rest_ensure_response( array( 'ai_content_generated' => true ) ); + } catch ( \WP_Error $e ) { + return $this->error_to_response( $e ); + } + } +} diff --git a/src/StoreApi/Routes/V1/AI/Product.php b/src/StoreApi/Routes/V1/AI/Product.php new file mode 100644 index 00000000000..cf99f28af8c --- /dev/null +++ b/src/StoreApi/Routes/V1/AI/Product.php @@ -0,0 +1,111 @@ + \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ Middleware::class, 'is_authorized' ], + 'args' => [ + 'index' => [ + 'description' => __( 'The business description for a given store.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + ], + 'products_information' => [ + 'description' => __( 'Data generated by AI for updating dummy products.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + ], + ], + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } + + /** + * Update product with the content and image powered by AI. + * + * @param \WP_REST_Request $request Request object. + * + * @return bool|string|\WP_Error|\WP_REST_Response + */ + protected function get_route_post_response( \WP_REST_Request $request ) { + $product_updater = new ProductUpdater(); + $dummy_products = $product_updater->fetch_dummy_products_to_update(); + + if ( empty( $dummy_products ) ) { + return rest_ensure_response( + array( + 'ai_content_generated' => true, + ) + ); + } + + $index = $request['index']; + if ( ! is_numeric( $index ) ) { + return rest_ensure_response( + array( + 'ai_content_generated' => false, + ) + ); + } + + $products_information = $request['products_information'] ?? array(); + + if ( ! isset( $dummy_products[ $index ] ) ) { + return rest_ensure_response( + array( + 'ai_content_generated' => false, + ) + ); + } + + $product_updater->update_product_content( $dummy_products[ $index ], $products_information ); + + return rest_ensure_response( + array( + 'ai_content_generated' => true, + ) + ); + } + +} diff --git a/src/StoreApi/Routes/V1/Patterns.php b/src/StoreApi/Routes/V1/AI/Products.php similarity index 51% rename from src/StoreApi/Routes/V1/Patterns.php rename to src/StoreApi/Routes/V1/AI/Products.php index a378244ab09..72024429a2d 100644 --- a/src/StoreApi/Routes/V1/Patterns.php +++ b/src/StoreApi/Routes/V1/AI/Products.php @@ -1,30 +1,30 @@ \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_response' ], - 'permission_callback' => [ $this, 'is_authorized' ], + 'permission_callback' => [ Middleware::class, 'is_authorized' ], 'args' => [ 'business_description' => [ 'description' => __( 'The business description for a given store.', 'woo-gutenberg-products-block' ), 'type' => 'string', ], + 'images' => [ + 'description' => __( 'The images for a given store.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + ], ], ], 'schema' => [ $this->schema, 'get_public_item_schema' ], @@ -59,30 +63,7 @@ public function get_args() { } /** - * Permission callback. - * - * @throws RouteException If the user is not allowed to make this request. - * - * @return true|\WP_Error - */ - public function is_authorized() { - try { - if ( ! current_user_can( 'manage_options' ) ) { - throw new RouteException( 'woocommerce_rest_invalid_user', __( 'You are not allowed to make this request. Please make sure you are logged in.', 'woo-gutenberg-products-block' ), 403 ); - } - } catch ( RouteException $error ) { - return new \WP_Error( - $error->getErrorCode(), - $error->getMessage(), - array( 'status' => $error->getCode() ) - ); - } - - return true; - } - - /** - * Ensure the content and images in patterns are powered by AI. + * Generate the content for the products. * * @param \WP_REST_Request $request Request object. * @@ -115,6 +96,7 @@ protected function get_route_post_response( \WP_REST_Request $request ) { $this->prepare_item_for_response( [ 'ai_content_generated' => true, + 'product_content' => null, ], $request ) @@ -126,46 +108,34 @@ protected function get_route_post_response( \WP_REST_Request $request ) { $site_id = $ai_connection->get_site_id(); if ( is_wp_error( $site_id ) ) { - return $site_id; + return $this->error_to_response( $site_id ); } $token = $ai_connection->get_jwt_token( $site_id ); if ( is_wp_error( $token ) ) { - return $token; + return $this->error_to_response( $token ); } - $images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description ); - - if ( is_wp_error( $images ) ) { - $response = $this->error_to_response( $images ); - } else { - $populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description ); - - if ( is_wp_error( $populate_patterns ) ) { - $response = $this->error_to_response( $populate_patterns ); - } - - $populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description ); + $images = $request['images']; - if ( is_wp_error( $populate_products ) ) { - $response = $this->error_to_response( $populate_products ); - } + $populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description ); - if ( true === $populate_patterns && true === $populate_products ) { - update_option( 'last_business_description_with_ai_content_generated', $business_description ); - } + if ( is_wp_error( $populate_products ) ) { + return $this->error_to_response( $populate_products ); } - if ( ! isset( $response ) ) { - $response = $this->prepare_item_for_response( - [ - 'ai_content_generated' => true, - ], - $request - ); + if ( ! isset( $populate_products['product_content'] ) ) { + return $this->error_to_response( new \WP_Error( 'product_content_not_found', __( 'Product content not found.', 'woo-gutenberg-products-block' ) ) ); } - return rest_ensure_response( $response ); + $product_content = $populate_products['product_content']; + + $item = array( + 'ai_content_generated' => true, + 'product_content' => $product_content, + ); + + return rest_ensure_response( $item ); } } diff --git a/src/StoreApi/RoutesController.php b/src/StoreApi/RoutesController.php index 79ba9c973c0..aace314ca39 100644 --- a/src/StoreApi/RoutesController.php +++ b/src/StoreApi/RoutesController.php @@ -1,7 +1,6 @@ schema_controller = $schema_controller; $this->routes = [ - 'v1' => [ + 'v1' => [ Routes\V1\Batch::IDENTIFIER => Routes\V1\Batch::class, Routes\V1\Cart::IDENTIFIER => Routes\V1\Cart::class, Routes\V1\CartAddItem::IDENTIFIER => Routes\V1\CartAddItem::class, @@ -48,7 +47,6 @@ public function __construct( SchemaController $schema_controller ) { Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class, Routes\V1\CheckoutOrder::IDENTIFIER => Routes\V1\CheckoutOrder::class, Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class, - Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class, Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class, Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class, Routes\V1\ProductAttributeTerms::IDENTIFIER => Routes\V1\ProductAttributeTerms::class, @@ -61,6 +59,14 @@ public function __construct( SchemaController $schema_controller ) { Routes\V1\ProductsById::IDENTIFIER => Routes\V1\ProductsById::class, Routes\V1\ProductsBySlug::IDENTIFIER => Routes\V1\ProductsBySlug::class, ], + // @todo Migrate internal AI routes to WooCommerce Core codebase. + 'private' => [ + Routes\V1\AI\Images::IDENTIFIER => Routes\V1\AI\Images::class, + Routes\V1\AI\Patterns::IDENTIFIER => Routes\V1\AI\Patterns::class, + Routes\V1\AI\Product::IDENTIFIER => Routes\V1\AI\Product::class, + Routes\V1\AI\Products::IDENTIFIER => Routes\V1\AI\Products::class, + Routes\V1\AI\BusinessDescription::IDENTIFIER => Routes\V1\AI\BusinessDescription::class, + ], ]; } @@ -70,6 +76,7 @@ public function __construct( SchemaController $schema_controller ) { public function register_all_routes() { $this->register_routes( 'v1', 'wc/store' ); $this->register_routes( 'v1', 'wc/store/v1' ); + $this->register_routes( 'private', 'wc/private' ); } /** diff --git a/src/StoreApi/SchemaController.php b/src/StoreApi/SchemaController.php index 55e38cb18c6..bc1a284ca7e 100644 --- a/src/StoreApi/SchemaController.php +++ b/src/StoreApi/SchemaController.php @@ -49,12 +49,16 @@ public function __construct( ExtendSchema $extend ) { Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class, Schemas\V1\OrderFeeSchema::IDENTIFIER => Schemas\V1\OrderFeeSchema::class, Schemas\V1\OrderSchema::IDENTIFIER => Schemas\V1\OrderSchema::class, - Schemas\V1\PatternsSchema::IDENTIFIER => Schemas\V1\PatternsSchema::class, Schemas\V1\ProductSchema::IDENTIFIER => Schemas\V1\ProductSchema::class, Schemas\V1\ProductAttributeSchema::IDENTIFIER => Schemas\V1\ProductAttributeSchema::class, Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class, Schemas\V1\ProductCollectionDataSchema::IDENTIFIER => Schemas\V1\ProductCollectionDataSchema::class, Schemas\V1\ProductReviewSchema::IDENTIFIER => Schemas\V1\ProductReviewSchema::class, + Schemas\V1\AI\ImagesSchema::IDENTIFIER => Schemas\V1\AI\ImagesSchema::class, + Schemas\V1\AI\PatternsSchema::IDENTIFIER => Schemas\V1\AI\PatternsSchema::class, + Schemas\V1\AI\ProductSchema::IDENTIFIER => Schemas\V1\AI\ProductSchema::class, + Schemas\V1\AI\ProductsSchema::IDENTIFIER => Schemas\V1\AI\ProductsSchema::class, + Schemas\V1\AI\BusinessDescriptionSchema::IDENTIFIER => Schemas\V1\AI\BusinessDescriptionSchema::class, ], ]; } diff --git a/src/StoreApi/Schemas/V1/AI/BusinessDescriptionSchema.php b/src/StoreApi/Schemas/V1/AI/BusinessDescriptionSchema.php new file mode 100644 index 00000000000..b628fe9c33a --- /dev/null +++ b/src/StoreApi/Schemas/V1/AI/BusinessDescriptionSchema.php @@ -0,0 +1,47 @@ + true, + ]; + } +} diff --git a/src/StoreApi/Schemas/V1/AI/ImagesSchema.php b/src/StoreApi/Schemas/V1/AI/ImagesSchema.php new file mode 100644 index 00000000000..317e4c028f6 --- /dev/null +++ b/src/StoreApi/Schemas/V1/AI/ImagesSchema.php @@ -0,0 +1,45 @@ + true, + ]; + } +} diff --git a/src/StoreApi/Schemas/V1/AI/ProductsSchema.php b/src/StoreApi/Schemas/V1/AI/ProductsSchema.php new file mode 100644 index 00000000000..114df7807af --- /dev/null +++ b/src/StoreApi/Schemas/V1/AI/ProductsSchema.php @@ -0,0 +1,48 @@ + $item['ai_content_generated'], + 'product_content' => $item['product_content'], + ]; + } +} diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index fa2218f122e..8ad020bfda1 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Blocks * Plugin URI: https://github.com/woocommerce/woocommerce-gutenberg-products-block * Description: WooCommerce blocks for the Gutenberg editor. - * Version: 11.5.1 + * Version: 11.5.2 * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woo-gutenberg-products-block