From 0e20c86c41d953f81f510530dbbf7f9a9e7d07d3 Mon Sep 17 00:00:00 2001 From: Woo Date: Tue, 4 Jun 2024 10:17:56 +0000 Subject: [PATCH] Updates to 4.3.2 --- changelog.txt | 6 + ...ss-wc-min-max-quantities-admin-notices.php | 6 +- includes/api/class-wc-mmq-rest-api.php | 323 +++++++++++------- ...ss-wc-min-max-quantities-compatibility.php | 23 +- ...in-max-quantities-paypal-compatibility.php | 45 +++ ...in-max-quantities-stripe-compatibility.php | 59 ++++ ...x-quantities-wc-payments-compatibility.php | 46 +++ languages/woocommerce-min-max-quantities.pot | 63 ++-- woocommerce-min-max-quantities.php | 182 +++++++++- 9 files changed, 593 insertions(+), 160 deletions(-) create mode 100644 includes/compatibility/modules/class-wc-min-max-quantities-paypal-compatibility.php create mode 100644 includes/compatibility/modules/class-wc-min-max-quantities-stripe-compatibility.php create mode 100644 includes/compatibility/modules/class-wc-min-max-quantities-wc-payments-compatibility.php diff --git a/changelog.txt b/changelog.txt index 6302ab2..9227f9a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,11 @@ *** Woo Min/Max Quantities Changelog *** +2024.06.04 - version 4.3.2 +* Tweak - Updated REST API validation to prevent incompatible quantity rules from being set. +* Fix - Hide express checkout buttons on Single product page when Min/Max quantity can't be verified. +* Fix - Resolved a fatal error triggered when trying to get the 'Group of' value of orphaned variations via REST API. +* Fix - Improved validation of values when getting maintenance and dismissed notices from the DB. + 2024.05.15 - version 4.3.1 * Fix - Resolved an issue in the REST API that returned a cached 'Group of' value. * Fix - Resolved a fatal error triggered by the REST API when trying to set a 'Group of' value with an empty Min/Max Quantity. diff --git a/includes/admin/class-wc-min-max-quantities-admin-notices.php b/includes/admin/class-wc-min-max-quantities-admin-notices.php index 8379235..605263b 100644 --- a/includes/admin/class-wc-min-max-quantities-admin-notices.php +++ b/includes/admin/class-wc-min-max-quantities-admin-notices.php @@ -15,7 +15,7 @@ * Admin notices handling. * * @class WC_Min_Max_Quantities_Admin_Notices - * @version 4.2.3 + * @version 4.3.2 */ class WC_Min_Max_Quantities_Admin_Notices { @@ -61,9 +61,9 @@ public static function init() { $GLOBALS[ 'sw_store' ][ 'notices_unique' ] = array(); } - self::$maintenance_notices = get_option( 'wc_mmq_maintenance_notices', array() ); + self::$maintenance_notices = is_array( get_option( 'wc_mmq_maintenance_notices' ) ) ? get_option( 'wc_mmq_maintenance_notices' ) : array(); self::$dismissed_notices = get_user_meta( get_current_user_id(), 'wc_mmq_dismissed_notices', true ); - self::$dismissed_notices = empty( self::$dismissed_notices ) ? array() : self::$dismissed_notices; + self::$dismissed_notices = is_array( self::$dismissed_notices ) ? self::$dismissed_notices : array(); // Show meta box notices. add_action( 'admin_notices', array( __CLASS__, 'output_notices' ) ); diff --git a/includes/api/class-wc-mmq-rest-api.php b/includes/api/class-wc-mmq-rest-api.php index 34ff1eb..6221b1f 100644 --- a/includes/api/class-wc-mmq-rest-api.php +++ b/includes/api/class-wc-mmq-rest-api.php @@ -15,7 +15,7 @@ * Add custom REST API fields. * * @class WC_MMQ_REST_API - * @version 4.3.1 + * @version 4.3.2 */ class WC_MMQ_REST_API { @@ -42,6 +42,10 @@ public static function init() { // Filter responses from the variations endpoint. add_action( 'rest_api_init', array( __CLASS__, 'filter_variation_fields' ), 0 ); + + // Validates and sets product fields based on PUT/POST REST API requests. + add_filter( 'woocommerce_rest_pre_insert_product_object', array( __CLASS__, 'handle_product_update' ), 10, 2 ); + } /** @@ -53,7 +57,7 @@ public static function filter_variation_fields() { add_filter( 'woocommerce_rest_prepare_product_variation_object', array( __CLASS__, 'filter_product_variation_response' ), 10, 2 ); // Modify PUT requests for product variations. - add_filter( 'woocommerce_rest_pre_insert_product_variation_object', array( __CLASS__, 'set_variation_quantity_rules' ), 10, 2 ); + add_filter( 'woocommerce_rest_pre_insert_product_variation_object', array( __CLASS__, 'handle_product_variation_update' ), 10, 2 ); // Add Min/Max Quantities fields to variations schema. add_filter( 'woocommerce_rest_product_variation_schema', array( __CLASS__, 'filter_variation_schema' ) ); @@ -87,6 +91,7 @@ public static function filter_variation_schema( $schema ) { * * @param WP_REST_Response $response * @param WC_Data $product + * * @return WP_REST_Response */ public static function filter_product_variation_response( $response, $product ) { @@ -111,77 +116,126 @@ public static function filter_product_variation_response( $response, $product ) * @since 4.3.0 * * @param WC_Product_Variation $variation - * @param WP_REST_Response $response + * @param WP_REST_Request $request * + * @return WC_Product_Variation */ - public static function set_variation_quantity_rules( $variation, $request ) { + public static function handle_product_variation_update( $variation, $request ) { if ( ! is_a( $variation, 'WC_Product_Variation' ) ) { return $variation; } + self::rest_validate_product_variation_quantity_rules( $variation, $request ); + + return self::rest_set_product_variation_fields( $variation, $request ); + } + + /** + * Validates quantity rules in REST API PUT/POST requests. + * + * @since 4.3.2 + * + * @throws WC_REST_Exception When invalid quantity rules for variations. + * + * @param WC_Product_Variation $variation + * @param WP_REST_Request $request + * + * @return WC_Product_Variation + */ + public static function rest_validate_product_variation_quantity_rules( $variation, $request ) { + + // Check if no validation is needed. + if ( ! isset( $request[ 'group_of_quantity' ] ) && + ! isset( $request[ 'min_quantity' ] ) && + ! isset( $request[ 'max_quantity' ] ) && + ! isset( $request[ 'combine_variations' ] ) + ) { + return $variation; + } + + // Validate variation props. + if ( isset( $request[ 'combine_variations' ] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_type_allow_combinations', __( 'The Allow Combinations option can only be set for Variable Products.', 'woocommerce-min-max-quantities' ) , 400 ); + } + + if ( isset( $request[ 'min_quantity' ] ) ) { + $min_quantity = (int) $request[ 'min_quantity' ]; + } else { + $min_quantity = '' !== $variation->get_meta( 'variation_minimum_allowed_quantity' ) + ? (int) $variation->get_meta( 'variation_minimum_allowed_quantity' ) + : ''; + } + + if ( isset( $request[ 'group_of_quantity' ] ) ) { + $group_of_rule = (int) $request[ 'group_of_quantity' ]; + } else { + $group_of_rule = (int) $variation->get_meta( 'variation_group_of_quantity' ); + } + + if ( isset( $request[ 'max_quantity' ] ) ) { + $max_quantity = $request[ 'max_quantity' ]; + } else { + $max_quantity = $variation->get_meta( 'variation_maximum_allowed_quantity' ); + } + + $max_quantity = $max_quantity ? (int) $max_quantity : ''; + + if ( '' !== $max_quantity && '' !== $min_quantity && $max_quantity < $min_quantity ) { + /* translators: Minimum quantity */ + throw new WC_REST_Exception( 'woocommerce_rest_invalid_variation_min_quantity', sprintf( __( 'The minimum quantity must be less than %d, which is the Maximum Quantity.', 'woocommerce-min-max-quantities' ), $max_quantity ), 400 ); + } + + if ( $group_of_rule && '' !== $min_quantity && ( ( 0 !== $min_quantity % $group_of_rule ) || 0 === $min_quantity ) ) { + /* translators: Group of quantity */ + throw new WC_REST_Exception( 'woocommerce_rest_invalid_variation_min_quantity', sprintf( __( 'The minimum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); + } + + if ( $group_of_rule && '' !== $max_quantity && ( 0 !== $max_quantity % $group_of_rule ) ) { + /* translators: Group of quantity */ + throw new WC_REST_Exception( 'woocommerce_rest_invalid_variation_max_quantity', sprintf( __( 'The maximum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); + } + } + + /** + * Updates product variation meta data based on quantity rules from REST request. + * + * @since 4.3.2 + * + * @param WC_Product_Variation $variation + * @param WP_REST_Request $request + * + * @return WC_Product_Variation + */ + public static function rest_set_product_variation_fields( $variation, $request ) { + if ( isset( $request[ 'variation_quantity_rules' ] ) ) { $variation->update_meta_data( 'min_max_rules', wc_clean( $request[ 'variation_quantity_rules' ] ) ); - $variation->save(); } if ( isset( $request[ 'group_of_quantity' ] ) ) { $variation->update_meta_data( 'variation_group_of_quantity', (int) wc_clean( $request[ 'group_of_quantity' ] ) ); - $variation->save(); // Increments the transient version to invalidate cache. WC_Cache_Helper::get_transient_version( 'wc_min_max_group_quantity', true ); } if ( isset( $request[ 'min_quantity' ] ) ) { - - $group_of_rule = $variation->get_meta( 'variation_group_of_quantity', true ); - $min_quantity = (int) wc_clean( $request[ 'min_quantity' ] ); - - if ( $group_of_rule && ( 0 !== $min_quantity % $group_of_rule ) ) { - /* translators: Group of quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_variation_min_quantity', sprintf( __( 'The minimum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); - } - - $variation->update_meta_data( 'variation_minimum_allowed_quantity', $min_quantity ); - $variation->save(); + $variation->update_meta_data( 'variation_minimum_allowed_quantity', (int) wc_clean( $request[ 'min_quantity' ] ) ); } if ( isset( $request[ 'max_quantity' ] ) ) { - - $group_of_rule = (int) $variation->get_meta( 'variation_group_of_quantity', true ); - $min_quantity = (int) $variation->get_meta( 'variation_minimum_allowed_quantity', true ); - $max_quantity = '' !== wc_clean( $request[ 'max_quantity' ] ) ? (int) wc_clean( $request[ 'max_quantity' ] ) : ''; - if ( '' !== $max_quantity ) { - if ( $min_quantity && $max_quantity < $min_quantity ) { - /* translators: Minimum quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_variation_max_quantity', sprintf( __( 'The maximum quantity must be greater than %d, which is the Minimum Quantity.', 'woocommerce-min-max-quantities' ), $min_quantity ), 400 ); - } - - if ( $group_of_rule && ( 0 !== $max_quantity % $group_of_rule ) ) { - /* translators: Group of quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_variation_max_quantity', sprintf( __( 'The maximum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); - } - } - $variation->update_meta_data( 'variation_maximum_allowed_quantity', $max_quantity ); - $variation->save(); } if ( isset( $request[ 'exclude_order_quantity_value_rules' ] ) ) { $variation->update_meta_data( 'variation_minmax_cart_exclude', wc_clean( $request[ 'exclude_order_quantity_value_rules' ] ) ); - $variation->save(); } if ( isset( $request[ 'exclude_category_quantity_rules' ] ) ) { $variation->update_meta_data( 'variation_minmax_category_group_of_exclude', wc_clean( $request[ 'exclude_category_quantity_rules' ] ) ); - $variation->save(); - } - - if ( isset( $request[ 'combine_variations' ] ) ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_type_allow_combinations', __( 'The Allow Combinations option can only be set for Variable Products.', 'woocommerce-min-max-quantities' ) , 400 ); } return $variation; @@ -251,9 +305,6 @@ public static function register_product_fields() { if ( in_array( 'get', $field_supports ) ) { $args[ 'get_callback' ] = array( __CLASS__, 'get_product_field_value' ); } - if ( in_array( 'update', $field_supports ) ) { - $args[ 'update_callback' ] = array( __CLASS__, 'update_product_field_value' ); - } register_rest_field( 'product', $field_name, $args ); } @@ -338,106 +389,136 @@ public static function get_product_field_value( $response, $field_name, $request } /** - * Updates values for MMQ product fields. + * Validates and sets product fields based on PUT/POST REST API requests. + * + * @since 4.3.2 + * + * @param WC_Product $product + * @param WP_REST_Request $request * - * @param mixed $value - * @param mixed $response - * @param string $field_name - * @return boolean */ - public static function update_product_field_value( $field_value, $response, $field_name ) { + public static function handle_product_update( $product, $request ) { - $product_id = false; - - if ( $response instanceof WP_Post ) { - $product_id = absint( $response->ID ); - $product = wc_get_product( $product_id ); - $product_type = $product->get_type(); - } elseif ( $response instanceof WC_Product ) { - $product_id = $response->get_id(); - $product = $response; - $product_type = $response->get_type(); + if ( ! is_a( $product, 'WC_Product' ) ) { + return $product; } - // Only possible to set fields of 'bundle' type products. - if ( $product_id ) { - - // Set group of value. - if ( 'group_of_quantity' === $field_name ) { + self::rest_validate_product_quantity_rules($product, $request ); - $product->update_meta_data( 'group_of_quantity', (int) wc_clean( $field_value ) ); - $product->save(); - - // Increments the transient version to invalidate cache. - WC_Cache_Helper::get_transient_version( 'wc_min_max_group_quantity', true ); + return self::rest_set_product_fields( $product, $request ); + } - // Set minimum quantity. - } elseif ( 'min_quantity' === $field_name ) { + /** + * Updates product meta data based on quantity rules from REST request. + * + * @since 4.3.2 + * + * @param WC_Product $product + * @param WP_REST_Request $request + * + */ + public static function rest_set_product_fields( $product, $request ) { - $mmq_instance = WC_Min_Max_Quantities::get_instance(); - $group_of_rule = $product->get_meta( 'group_of_quantity', true ) ? $product->get_meta( 'group_of_quantity', true ) : $mmq_instance->get_group_of_quantity_for_product( $product ); - $max_quantity = $product->get_meta( 'maximum_allowed_quantity', true ); - $min_quantity = (int) wc_clean( $field_value ); + // Set group of value. + if ( isset( $request[ 'group_of_quantity' ] ) ) { + $product->update_meta_data( 'group_of_quantity', (int) wc_clean( $request[ 'group_of_quantity' ] ) ); - if ( '' !== $max_quantity && $max_quantity < $min_quantity ) { - /* translators: Minimum quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_max_quantity', sprintf( __( 'The minimum quantity must be less than %d, which is the Maximum Quantity.', 'woocommerce-min-max-quantities' ), $max_quantity ), 400 ); - } + // Increments the transient version to invalidate cache. + WC_Cache_Helper::get_transient_version( 'wc_min_max_group_quantity', true ); + } - if ( $group_of_rule && ( ( 0 !== $min_quantity % $group_of_rule ) || 0 === $min_quantity ) ) { - /* translators: Group of quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_min_quantity', sprintf( __( 'The minimum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); - } + // Set minimum quantity. + if ( isset( $request[ 'min_quantity' ] ) ) { + $product->update_meta_data( 'minimum_allowed_quantity', (int) wc_clean( $request[ 'min_quantity' ] ) ); + } - $product->update_meta_data( 'minimum_allowed_quantity', $min_quantity ); - $product->save(); + // Set maximum quantity. + if ( isset( $request[ 'max_quantity' ] ) ) { + $product->update_meta_data( 'maximum_allowed_quantity', wc_clean( $request[ 'max_quantity' ] ) ); + } - // Set maximum quantity. - } elseif ( 'max_quantity' === $field_name ) { + // Set Exclude from > Order rules. + if ( isset( $request[ 'exclude_order_quantity_value_rules' ] ) ) { + $product->update_meta_data( 'minmax_cart_exclude', wc_clean( $request[ 'exclude_order_quantity_value_rules' ] ) ); + } - $mmq_instance = WC_Min_Max_Quantities::get_instance(); - $group_of_rule = $product->get_meta( 'group_of_quantity', true ) ? $product->get_meta( 'group_of_quantity', true ) : $mmq_instance->get_group_of_quantity_for_product( $product ); - $min_quantity = $product->get_meta( 'minimum_allowed_quantity', true ); - $max_quantity = wc_clean( $field_value ); + // Set Exclude from > Category rules. + if ( isset( $request[ 'exclude_category_quantity_rules' ] ) ) { + $product->update_meta_data( 'minmax_category_group_of_exclude', wc_clean( $request[ 'exclude_category_quantity_rules' ] ) ); + } - if ( $min_quantity && $max_quantity && $max_quantity < $min_quantity ) { - /* translators: Minimum quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_max_quantity', sprintf( __( 'The maximum quantity must be greater than %d, which is the Minimum Quantity.', 'woocommerce-min-max-quantities' ), $min_quantity ), 400 ); - } + // Set Exclude from > Category rules. + if ( isset( $request[ 'combine_variations' ] ) ) { + $product->update_meta_data( 'allow_combination', wc_clean( $request[ 'combine_variations' ] ) ); + } - if ( $group_of_rule && '' !== $max_quantity && ( 0 !== $max_quantity % $group_of_rule ) ) { - /* translators: Group of quantity */ - throw new WC_REST_Exception( 'woocommerce_rest_invalid_max_quantity', sprintf( __( 'The maximum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); - } + return $product; + } - $product->update_meta_data( 'maximum_allowed_quantity', wc_clean( $field_value ) ); - $product->save(); + /** + * Validates quantity rules in REST API PUT/POST requests. + * + * @since 4.3.2 + * + * @throws WC_REST_Exception When invalid quantity rules. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * + */ + public static function rest_validate_product_quantity_rules( $product, $request ) { + + // Check if no validation is needed. + if ( ! isset( $request[ 'group_of_quantity' ] ) && + ! isset( $request[ 'min_quantity' ] ) && + ! isset( $request[ 'max_quantity' ] ) && + ! isset( $request[ 'combine_variations' ] ) + ) { + return $product; + } - // Set Exclude from > Order rules. - } elseif ( 'exclude_order_quantity_value_rules' === $field_name ) { + // Validate product props. + if ( isset( $request[ 'combine_variations' ] ) ) { + if ( ! $product->is_type( 'variable' ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_type_allow_combinations', __( 'The Allow Combinations option can only be set for Variable Products.', 'woocommerce-min-max-quantities' ) , 400 ); + } + } - $product->update_meta_data( 'minmax_cart_exclude', wc_clean( $field_value ) ); - $product->save(); + if ( isset( $request[ 'min_quantity' ] ) ) { + $min_quantity = (int) $request[ 'min_quantity' ]; + } else { + $min_quantity = '' !== $product->get_meta( 'minimum_allowed_quantity' ) ? (int) $product->get_meta( 'minimum_allowed_quantity' ) : ''; + } - // Set Exclude from > Category rules. - } elseif ( 'exclude_category_quantity_rules' === $field_name ) { + if ( isset( $request[ 'group_of_quantity' ] ) ) { + $group_of_rule = (int) $request[ 'group_of_quantity' ]; + } else { + $mmq_instance = WC_Min_Max_Quantities::get_instance(); + $group_of_rule = $mmq_instance->get_group_of_quantity_for_product( $product ); + } - $product->update_meta_data( 'minmax_category_group_of_exclude', wc_clean( $field_value ) ); - $product->save(); + if ( isset( $request[ 'max_quantity' ] ) ) { + $max_quantity = $request[ 'max_quantity' ]; + } else { + $max_quantity = $product->get_meta( 'maximum_allowed_quantity' ); + } - // Set Exclude from > Category rules. - } elseif ( 'combine_variations' === $field_name ) { + $max_quantity = $max_quantity ? (int) $max_quantity : ''; - if ( 'variable' !== $product_type ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_type_allow_combinations', __( 'The Allow Combinations option can only be set for Variable Products.', 'woocommerce-min-max-quantities' ) , 400 ); - } + if ( '' !== $max_quantity && '' !== $min_quantity && $max_quantity < $min_quantity ) { + /* translators: Minimum quantity */ + throw new WC_REST_Exception( 'woocommerce_rest_invalid_min_quantity', sprintf( __( 'The minimum quantity must be less than %d, which is the Maximum Quantity.', 'woocommerce-min-max-quantities' ), $max_quantity ), 400 ); + } - $product->update_meta_data( 'allow_combination', wc_clean( $field_value ) ); - $product->save(); - } + if ( $group_of_rule && '' !== $min_quantity && ( ( 0 !== $min_quantity % $group_of_rule ) || 0 === $min_quantity ) ) { + /* translators: Group of quantity */ + throw new WC_REST_Exception( 'woocommerce_rest_invalid_min_quantity', sprintf( __( 'The minimum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); } - return true; + if ( $group_of_rule && '' !== $max_quantity && ( 0 !== $max_quantity % $group_of_rule ) ) { + /* translators: Group of quantity */ + throw new WC_REST_Exception( 'woocommerce_rest_invalid_max_quantity', sprintf( __( 'The maximum quantity must be a multiple of %d.', 'woocommerce-min-max-quantities' ), $group_of_rule ), 400 ); + } } /** @@ -452,7 +533,6 @@ public static function update_product_field_value( $field_value, $response, $fie private static function get_product_field( $key, $product ) { $product_type = $product->get_type(); - $product_id = $product->get_id(); switch ( $key ) { @@ -463,6 +543,11 @@ private static function get_product_field( $key, $product ) { $value = (int) $product->get_meta( 'variation_group_of_quantity', true ); } else { $parent_product = wc_get_product( $product->get_parent_id() ); + + if ( ! is_a( $parent_product, 'WC_Product' ) ) { + return ''; + } + $mmq_instance = WC_Min_Max_Quantities::get_instance(); $value = $mmq_instance->get_group_of_quantity_for_product( $parent_product ); } diff --git a/includes/compatibility/class-wc-min-max-quantities-compatibility.php b/includes/compatibility/class-wc-min-max-quantities-compatibility.php index 7aa4836..0a1c68a 100644 --- a/includes/compatibility/class-wc-min-max-quantities-compatibility.php +++ b/includes/compatibility/class-wc-min-max-quantities-compatibility.php @@ -4,6 +4,7 @@ * * @package Woo Min/Max Quantities * @since 4.0.4 + * @version 4.3.2 */ // Exit if accessed directly. @@ -81,8 +82,9 @@ protected function __construct() { // Define dependencies. $this->required = array( - 'pao' => '3.0.14', - 'blocks' => '7.2.0' + 'pao' => '3.0.14', + 'blocks' => '7.2.0', + 'wc_stripe' => '4.1.0' // when wc_stripe_hide_payment_request_on_product_page filter was made usable. ); // Initialize. @@ -123,6 +125,8 @@ public function is_module_loaded( $name ) { /** * Load compatibility classes. + * + * @version 4.3.2 * * @return void */ @@ -140,6 +144,21 @@ public function module_includes() { $module_paths[ 'product_addons' ] = WC_MMQ_ABSPATH . 'includes/compatibility/modules/class-wc-min-max-quantities-addons.php'; } + // Smart button on Product page: PayPal compatibility. + if ( class_exists( '\WooCommerce\PayPalCommerce\PluginModule' ) ) { + $module_paths[ 'paypal' ] = WC_MMQ_ABSPATH . '/includes/compatibility/modules/class-wc-min-max-quantities-paypal-compatibility.php'; + } + + // Express checkout on Product page: Stripe compatibility. + if ( class_exists( 'WC_Stripe' ) && defined( 'WC_STRIPE_VERSION' ) && version_compare( WC_STRIPE_VERSION, $this->required[ 'wc_stripe' ] ) >= 0 ) { + $module_paths[ 'wc_stripe' ] = WC_MMQ_ABSPATH . '/includes/compatibility/modules/class-wc-min-max-quantities-stripe-compatibility.php'; + } + + // Express checkout on Product page: WooPayments compatibility. + if ( class_exists( 'WC_Payments' ) ) { + $module_paths[ 'wcpay' ] = WC_MMQ_ABSPATH . '/includes/compatibility/modules/class-wc-min-max-quantities-wc-payments-compatibility.php'; + } + /** * 'woocommerce_mmq_compatibility_modules' filter. * diff --git a/includes/compatibility/modules/class-wc-min-max-quantities-paypal-compatibility.php b/includes/compatibility/modules/class-wc-min-max-quantities-paypal-compatibility.php new file mode 100644 index 0000000..1f57f70 --- /dev/null +++ b/includes/compatibility/modules/class-wc-min-max-quantities-paypal-compatibility.php @@ -0,0 +1,45 @@ +can_display_express_checkout( $product ); + } +} + +WC_Min_Max_Quantities_PayPal_Compatibility::init(); diff --git a/includes/compatibility/modules/class-wc-min-max-quantities-stripe-compatibility.php b/includes/compatibility/modules/class-wc-min-max-quantities-stripe-compatibility.php new file mode 100644 index 0000000..21b20aa --- /dev/null +++ b/includes/compatibility/modules/class-wc-min-max-quantities-stripe-compatibility.php @@ -0,0 +1,59 @@ +ID ) ) { + return $hide; + } + + $product = wc_get_product( $post->ID ); + if ( $product && is_a( $product, 'WC_Product' ) ) { + $mmq_instance = WC_Min_Max_Quantities::get_instance(); + $hide = ! $mmq_instance->can_display_express_checkout( $product ); // the filter needs true for hiding the button. + return $hide; + } + + return $hide; + } +} + +WC_Min_Max_Quantities_Stripe_Compatibility::init(); diff --git a/includes/compatibility/modules/class-wc-min-max-quantities-wc-payments-compatibility.php b/includes/compatibility/modules/class-wc-min-max-quantities-wc-payments-compatibility.php new file mode 100644 index 0000000..8aadbac --- /dev/null +++ b/includes/compatibility/modules/class-wc-min-max-quantities-wc-payments-compatibility.php @@ -0,0 +1,46 @@ +can_display_express_checkout( $product ); + } +} + +WC_Min_Max_Quantities_WC_Payments_Compatibility::init(); diff --git a/languages/woocommerce-min-max-quantities.pot b/languages/woocommerce-min-max-quantities.pot index 81708b1..bbdf743 100644 --- a/languages/woocommerce-min-max-quantities.pot +++ b/languages/woocommerce-min-max-quantities.pot @@ -2,14 +2,14 @@ # This file is distributed under the GNU General Public License v3.0. msgid "" msgstr "" -"Project-Id-Version: Woo Min/Max Quantities 4.3.1\n" +"Project-Id-Version: Woo Min/Max Quantities 4.3.2\n" "Report-Msgid-Bugs-To: https://woocommerce.com/my-account/create-a-ticket/\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2024-05-15T12:22:54+00:00\n" +"POT-Creation-Date: 2024-06-04T09:20:25+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.10.0\n" "language-team: LANGUAGE \n" @@ -244,82 +244,77 @@ msgstr "" msgid "Support" msgstr "" -#. translators: Group of quantity -#: includes/api/class-wc-mmq-rest-api.php:143 -#: includes/api/class-wc-mmq-rest-api.php:389 -msgid "The minimum quantity must be a multiple of %d." +#: includes/api/class-wc-mmq-rest-api.php:159 +#: includes/api/class-wc-mmq-rest-api.php:483 +msgid "The Allow Combinations option can only be set for Variable Products." msgstr "" #. translators: Minimum quantity -#: includes/api/class-wc-mmq-rest-api.php:160 -#: includes/api/class-wc-mmq-rest-api.php:405 -msgid "The maximum quantity must be greater than %d, which is the Minimum Quantity." +#: includes/api/class-wc-mmq-rest-api.php:186 +#: includes/api/class-wc-mmq-rest-api.php:510 +msgid "The minimum quantity must be less than %d, which is the Maximum Quantity." msgstr "" #. translators: Group of quantity -#: includes/api/class-wc-mmq-rest-api.php:165 -#: includes/api/class-wc-mmq-rest-api.php:410 -msgid "The maximum quantity must be a multiple of %d." +#: includes/api/class-wc-mmq-rest-api.php:191 +#: includes/api/class-wc-mmq-rest-api.php:515 +msgid "The minimum quantity must be a multiple of %d." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:184 -#: includes/api/class-wc-mmq-rest-api.php:432 -msgid "The Allow Combinations option can only be set for Variable Products." +#. translators: Group of quantity +#: includes/api/class-wc-mmq-rest-api.php:196 +#: includes/api/class-wc-mmq-rest-api.php:520 +msgid "The maximum quantity must be a multiple of %d." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:199 +#: includes/api/class-wc-mmq-rest-api.php:253 msgid "Enable this option to set quantity rules for a specific variation." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:205 +#: includes/api/class-wc-mmq-rest-api.php:259 msgid "Require variations to be purchased in multiples of this value." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:210 +#: includes/api/class-wc-mmq-rest-api.php:264 msgid "Minimum required variation quantity." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:215 +#: includes/api/class-wc-mmq-rest-api.php:269 msgid "Maximum allowed variation quantity." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:220 +#: includes/api/class-wc-mmq-rest-api.php:274 msgid "Exclude variation from order quantity and value rules." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:226 +#: includes/api/class-wc-mmq-rest-api.php:280 msgid "Exclude variation from category quantity rules." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:271 +#: includes/api/class-wc-mmq-rest-api.php:322 msgid "Require products to be purchased in multiples of this value." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:276 +#: includes/api/class-wc-mmq-rest-api.php:327 msgid "Minimum required product quantity." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:281 +#: includes/api/class-wc-mmq-rest-api.php:332 msgid "Maximum allowed product quantity." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:286 +#: includes/api/class-wc-mmq-rest-api.php:337 msgid "Exclude product from order quantity and value rules." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:292 +#: includes/api/class-wc-mmq-rest-api.php:343 msgid "Exclude product from category quantity rules." msgstr "" -#: includes/api/class-wc-mmq-rest-api.php:298 +#: includes/api/class-wc-mmq-rest-api.php:349 msgid "Enable this option to combine the quantities of all purchased variations when checking quantity rules." msgstr "" -#. translators: Minimum quantity -#: includes/api/class-wc-mmq-rest-api.php:384 -msgid "The minimum quantity must be less than %d, which is the Maximum Quantity." -msgstr "" - #: includes/class-wc-min-max-quantities-blocks.php:59 #: includes/class-wc-min-max-quantities-blocks.php:138 msgid "Sell in groups of" @@ -365,8 +360,8 @@ msgstr "" msgid "Enter a maximum quantity customers can buy in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods." msgstr "" -#: includes/compatibility/class-wc-min-max-quantities-compatibility.php:65 -#: includes/compatibility/class-wc-min-max-quantities-compatibility.php:74 +#: includes/compatibility/class-wc-min-max-quantities-compatibility.php:66 +#: includes/compatibility/class-wc-min-max-quantities-compatibility.php:75 msgid "Foul!" msgstr "" diff --git a/woocommerce-min-max-quantities.php b/woocommerce-min-max-quantities.php index 194abb6..3642994 100644 --- a/woocommerce-min-max-quantities.php +++ b/woocommerce-min-max-quantities.php @@ -3,7 +3,7 @@ * Plugin Name: Woo Min/Max Quantities * Plugin URI: https://woocommerce.com/products/minmax-quantities/ * Description: Define minimum/maximum allowed quantities for products, variations and orders. - * Version: 4.3.1 + * Version: 4.3.2 * Author: Woo * Author URI: https://woocommerce.com * Requires at least: 4.4 @@ -26,7 +26,7 @@ if ( ! class_exists( 'WC_Min_Max_Quantities' ) ) : - define( 'WC_MIN_MAX_QUANTITIES', '4.3.1' ); // WRCS: DEFINED_VERSION. + define( 'WC_MIN_MAX_QUANTITIES', '4.3.2' ); // WRCS: DEFINED_VERSION. /** * Min Max Quantities class. @@ -1398,6 +1398,8 @@ public function available_variation( $data, $product, $variation ) { * Get group_of_quantity setting for a product. * * @param WC_Product $product Product object. + * + * Doesn't handle variations on variable products. * * @return int */ @@ -1573,6 +1575,182 @@ public static function adjust_max_quantity( $max_quantity, $group_of_quantity, $ return absint( $max_quantity ); } + + /** + * Check if the product always conforms to MMQ rules. + * + * That is, there are either no rules set, or the min/max rules are set and equal. + * + * @param WC_Product $product Product object. + * + * @return bool + */ + private function can_skip_product_rules_validation( $product ) { + + if ( ! $product ) { + return false; + } + + $product_min_qty = absint( $product->get_meta( 'minimum_allowed_quantity' ) ); + $product_max_qty = absint( $product->get_meta( 'maximum_allowed_quantity' ) ); + $group_of_qty = $this->get_group_of_quantity_for_product( $product ); + + // If no min/max rules are set, there's nothing to check -> can skip validation. + if ( 1 >= $product_min_qty && 0 === $product_max_qty && 1 >= $group_of_qty ) { + return true; + } + + // If min and max are set and equal, the product has qty locked -> can skip validation. + if ( 0 < $product_min_qty && 0 < $product_max_qty && $product_min_qty === $product_max_qty ) { + return true; + } + + return false; + + } + + /** + * Check if the variable product supports express checkout. + * + * Simplified version that is a bit lighter on resources: If there are any variations with min/max rules enabled, return false. + * Variations can have different prices, too. But that's handled outside of this plugin. + + * + * @param WC_Product $product Product object. + * + * @return bool + */ + private function variable_product_supports_express_checkout( $product ) { + + if ( ! $product ) { + return false; + } + + if ( ! in_array( $product->get_type(), array( 'variable', 'variable-subscription' ), true ) ) { + // This is counterintuitive, but if a non-variable product is passed, + // we shouldn't forbid the express checkout button here. + return true; + } + + $combine_variations = $product->get_meta( 'allow_combination', true ); + + if ( 'yes' === $combine_variations ) { + /* If Combine variations is set to true, there are no rules on the variations. + But if min == max on the variable product with combined variations, qty is not set automatically. + => Can't show express checkout. */ + $product_min_qty = absint( $product->get_meta( 'minimum_allowed_quantity' ) ); + $product_max_qty = absint( $product->get_meta( 'maximum_allowed_quantity' ) ); + + if ( 1 < $product_min_qty && 0 < $product_max_qty && $product_min_qty === $product_max_qty ) { + + return false; + } + } else { + /* If Combine variations is set to false, there might be min/max rules on the variations. + => Check if any variation has them enabled. */ + $variations = $product->get_children(); + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + if ( 'yes' === $variation->get_meta( 'min_max_rules', true ) ) { + return false; + } + } + } + + return true; + } + + /** + * Check if global rules apply to a product. + * + * That is, if there are any global rules set, and the product is not excluded from them. + * + * Exclusion from group of rules applied through categories is done in can_skip_product_rules_validation(), + * as get_group_of_quantity_for_product returns 0 if the rule is set, but the product is excluded. + * + * @since 4.3.2 + * @version 4.3.2 + * + * @param WC_Product $product Product object. + * + * @return bool + */ + private function global_rules_apply_to_product( $product ) { + + if ( ! $product ) { + return false; + } + + // If no global rules are set, they don't apply to the product. + $min_quantity = $this->minimum_order_quantity; + $max_quantity = $this->maximum_order_quantity; + $min_value = $this->minimum_order_value; + $max_value = $this->maximum_order_value; + + if ( 1 >= $min_quantity && 0 === $max_quantity && 0 === $min_value && 0 === $max_value ) { + return false; + } + + // If product is excluded from order min/max quantity/value and category rules, they don't apply. + $mmq_exclude = $product->get_meta( 'minmax_cart_exclude' ); + + // If there are any global qty/value rules set and this product is excluded from them, they don't apply. + if ( 'yes' === $mmq_exclude && ( 1 < $min_quantity || 0 < $max_quantity || 0 < $min_value || 0 < $max_value ) ) { + return false; + } + + // Otherwise, rules apply. + return true; + } + + /** + * Check if the express checkout button(s) can be displayed on single product page. + * + * The only thing we can do from PHP is to hide the smart buttons if the min/max quantity or value is set. + * The end customer can increase the quantity and click the express checkout button and only at that point + * we can check if the quantity/value is within the allowed range. + * Since we can't validate this in PHP, we hide the express checkout button. + * + * @since 4.3.2 + * @version 4.3.2 + * + * @param WC_Product $product Product object. + * + * @return bool + */ + public function can_display_express_checkout( $product ) { + + /* If global MMQ rules apply to the product, + we can't verify the quantity/value in PHP, so we can't display the express checkout button. */ + if ( $this->global_rules_apply_to_product( $product ) ) { + return false; + } + + /* If the product has rules that need to be validated, + (and we can't verify the quantity in PHP), we can't display the express checkout button. */ + if ( ! $this->can_skip_product_rules_validation( $product ) ) { + return false; + } + + /* If the product is a variable product and there are variations with min/max rules, + we can't verify the quantity in PHP, so we can't display the express checkout button. + + We can test for variable products after the test for simple products, because all the cases + that are handled incorrectly by product_always_conforms_to_rules() allow this check to run: + - if there are no rules on variations, product_always_conforms_to_rules is almost correct for variable products, too. + - if there are no rules on variable product, but there are rules on variation + -> product_always_conforms_to_rules will return true, but this will be corrected to false here. + - if min == max on the variable product, but there are rules on the variation + -> product_always_conforms_to_rules will return true, but this will be corrected to false here. + - if min == max on the variable product and combine variations is true, qty is not forced, + -> product_always_conforms_to_rules will return true, but this will be corrected to false here. */ + if ( ! $this->variable_product_supports_express_checkout( $product ) ) { + return false; + } + + // Otherwise, we allow the express checkout button to be displayed. + return true; + } } add_action( 'plugins_loaded', array( 'WC_Min_Max_Quantities', 'get_instance' ) );