diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index 77af416eac3..b067a2c18a6 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -489,26 +489,15 @@ class WC_Admin_Setup_Wizard { countries->get_european_union_countries(), true ) ) { - $tracking_opt_out = false; - } - - // Respect the DNT header. - if ( ! empty( $_SERVER['HTTP_DNT'] ) ) { // WPCS: input var ok. - $tracking_opt_out = false; - } ?>

- /> - + +

' . esc_html__( 'Read more about what we collect.', 'woocommerce' ) . ''; ?>

@@ -537,7 +526,7 @@ class WC_Admin_Setup_Wizard { $currency_code = sanitize_text_field( $_POST['currency_code'] ); $product_type = sanitize_text_field( $_POST['product_type'] ); $sell_in_person = isset( $_POST['sell_in_person'] ) && ( 'yes' === sanitize_text_field( $_POST['sell_in_person'] ) ); - $tracking = isset( $_POST['wc_tracker_optin'] ) && ( 'yes' === sanitize_text_field( $_POST['wc_tracker_optin'] ) ); + $tracking = isset( $_POST['wc_tracker_checkbox'] ) && ( 'yes' === sanitize_text_field( $_POST['wc_tracker_checkbox'] ) ); // @codingStandardsIgnoreEnd update_option( 'woocommerce_store_address', $address ); update_option( 'woocommerce_store_address_2', $address_2 ); diff --git a/includes/class-wc-post-types.php b/includes/class-wc-post-types.php index 2849a253d93..09d810292bd 100644 --- a/includes/class-wc-post-types.php +++ b/includes/class-wc-post-types.php @@ -26,6 +26,7 @@ class WC_Post_Types { add_filter( 'rest_api_allowed_post_types', array( __CLASS__, 'rest_api_allowed_post_types' ) ); add_action( 'woocommerce_after_register_post_type', array( __CLASS__, 'maybe_flush_rewrite_rules' ) ); add_action( 'woocommerce_flush_rewrite_rules', array( __CLASS__, 'flush_rewrite_rules' ) ); + add_filter( 'gutenberg_can_edit_post_type', array( __CLASS__, 'gutenberg_can_edit_post_type' ), 10, 2 ); } /** @@ -567,6 +568,17 @@ class WC_Post_Types { flush_rewrite_rules(); } + /** + * Disable Gutenberg for products. + * + * @param bool $can_edit Whether the post type can be edited or not. + * @param string $post_type The post type being checked. + * @return bool + */ + public static function gutenberg_can_edit_post_type( $can_edit, $post_type ) { + return 'product' === $post_type ? false : $can_edit; + } + /** * Add Product Support to Jetpack Omnisearch. */ diff --git a/includes/cli/class-wc-cli-rest-command.php b/includes/cli/class-wc-cli-rest-command.php index be2a050835d..2b0a218739e 100644 --- a/includes/cli/class-wc-cli-rest-command.php +++ b/includes/cli/class-wc-cli-rest-command.php @@ -206,6 +206,10 @@ class WC_CLI_REST_Command { $method = 'GET'; } + if ( ! isset( $assoc_args['per_page'] ) || empty( $assoc_args['per_page'] ) ) { + $assoc_args['per_page'] = '100'; + } + list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $items = array_column( $body, 'id' ); diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php index 3df9a58a0c2..588d15eea10 100644 --- a/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php @@ -35,6 +35,14 @@ class WC_Gateway_Paypal_Request { */ protected $notify_url; + /** + * Endpoint for requests to PayPal. + * + * @var string + */ + protected $endpoint; + + /** * Constructor. * @@ -53,15 +61,16 @@ class WC_Gateway_Paypal_Request { * @return string */ public function get_request_url( $order, $sandbox = false ) { + if ( $sandbox ) { + $this->endpoint = 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&'; + } else { + $this->endpoint = 'https://www.paypal.com/cgi-bin/webscr?'; + } $paypal_args = http_build_query( $this->get_paypal_args( $order ), '', '&' ); WC_Gateway_Paypal::log( 'PayPal Request Args for order ' . $order->get_order_number() . ': ' . wc_print_r( $paypal_args, true ) ); - if ( $sandbox ) { - return 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' . $paypal_args; - } else { - return 'https://www.paypal.com/cgi-bin/webscr?' . $paypal_args; - } + return $this->endpoint . $paypal_args; } /** @@ -72,12 +81,90 @@ class WC_Gateway_Paypal_Request { * @return string */ protected function limit_length( $string, $limit = 127 ) { - if ( strlen( $string ) > $limit ) { - $string = substr( $string, 0, $limit - 3 ) . '...'; + // As the output is to be used in http_build_query which applies URL encoding, the string needs to be + // cut as if it was URL-encoded, but returned non-encoded (it will be encoded by http_build_query later). + $url_encoded_str = rawurlencode( $string ); + + if ( strlen( $url_encoded_str ) > $limit ) { + $string = rawurldecode( substr( $url_encoded_str, 0, $limit - 3 ) . '...' ); } return $string; } + /** + * Get transaction args for paypal request, except for line item args. + * + * @param WC_Order $order Order object. + * @return array + */ + protected function get_transaction_args( $order ) { + return array_merge( + array( + 'cmd' => '_cart', + 'business' => $this->gateway->get_option( 'email' ), + 'no_note' => 1, + 'currency_code' => get_woocommerce_currency(), + 'charset' => 'utf-8', + 'rm' => is_ssl() ? 2 : 1, + 'upload' => 1, + 'return' => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ), + 'cancel_return' => esc_url_raw( $order->get_cancel_order_url_raw() ), + 'page_style' => $this->gateway->get_option( 'page_style' ), + 'image_url' => esc_url_raw( $this->gateway->get_option( 'image_url' ) ), + 'paymentaction' => $this->gateway->get_option( 'paymentaction' ), + 'bn' => 'WooThemes_Cart', + 'invoice' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ), + 'custom' => wp_json_encode( + array( + 'order_id' => $order->get_id(), + 'order_key' => $order->get_order_key(), + ) + ), + 'notify_url' => $this->limit_length( $this->notify_url, 255 ), + 'first_name' => $this->limit_length( $order->get_billing_first_name(), 32 ), + 'last_name' => $this->limit_length( $order->get_billing_last_name(), 64 ), + 'address1' => $this->limit_length( $order->get_billing_address_1(), 100 ), + 'address2' => $this->limit_length( $order->get_billing_address_2(), 100 ), + 'city' => $this->limit_length( $order->get_billing_city(), 40 ), + 'state' => $this->get_paypal_state( $order->get_billing_country(), $order->get_billing_state() ), + 'zip' => $this->limit_length( wc_format_postcode( $order->get_billing_postcode(), $order->get_billing_country() ), 32 ), + 'country' => $this->limit_length( $order->get_billing_country(), 2 ), + 'email' => $this->limit_length( $order->get_billing_email() ), + ), + $this->get_phone_number_args( $order ), + $this->get_shipping_args( $order ) + ); + } + + /** + * If the default request with line items is too long, generate a new one with only one line item. + * + * If URL is longer than 2,083 chars, ignore line items and send cart to Paypal as a single item. + * One item's name can only be 127 characters long, so the URL should not be longer than limit. + * URL character limit via: + * https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer. + * + * @param WC_Order $order Order to be sent to Paypal. + * @param array $paypal_args Arguments sent to Paypal in the request. + * @return array + */ + protected function fix_request_length( $order, $paypal_args ) { + $max_paypal_length = 2083; + $query_candidate = http_build_query( $paypal_args, '', '&' ); + + if ( strlen( $this->endpoint . $query_candidate ) <= $max_paypal_length ) { + return $paypal_args; + } + + return apply_filters( + 'woocommerce_paypal_args', array_merge( + $this->get_transaction_args( $order ), + $this->get_line_item_args( $order, true ) + ), $order + ); + + } + /** * Get PayPal Args for passing to PP. * @@ -87,45 +174,14 @@ class WC_Gateway_Paypal_Request { protected function get_paypal_args( $order ) { WC_Gateway_Paypal::log( 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url ); - return apply_filters( + $paypal_args = apply_filters( 'woocommerce_paypal_args', array_merge( - array( - 'cmd' => '_cart', - 'business' => $this->gateway->get_option( 'email' ), - 'no_note' => 1, - 'currency_code' => get_woocommerce_currency(), - 'charset' => 'utf-8', - 'rm' => is_ssl() ? 2 : 1, - 'upload' => 1, - 'return' => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ), - 'cancel_return' => esc_url_raw( $order->get_cancel_order_url_raw() ), - 'page_style' => $this->gateway->get_option( 'page_style' ), - 'image_url' => esc_url_raw( $this->gateway->get_option( 'image_url' ) ), - 'paymentaction' => $this->gateway->get_option( 'paymentaction' ), - 'bn' => 'WooThemes_Cart', - 'invoice' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ), - 'custom' => wp_json_encode( - array( - 'order_id' => $order->get_id(), - 'order_key' => $order->get_order_key(), - ) - ), - 'notify_url' => $this->limit_length( $this->notify_url, 255 ), - 'first_name' => $this->limit_length( $order->get_billing_first_name(), 32 ), - 'last_name' => $this->limit_length( $order->get_billing_last_name(), 64 ), - 'address1' => $this->limit_length( $order->get_billing_address_1(), 100 ), - 'address2' => $this->limit_length( $order->get_billing_address_2(), 100 ), - 'city' => $this->limit_length( $order->get_billing_city(), 40 ), - 'state' => $this->get_paypal_state( $order->get_billing_country(), $order->get_billing_state() ), - 'zip' => $this->limit_length( wc_format_postcode( $order->get_billing_postcode(), $order->get_billing_country() ), 32 ), - 'country' => $this->limit_length( $order->get_billing_country(), 2 ), - 'email' => $this->limit_length( $order->get_billing_email() ), - ), - $this->get_phone_number_args( $order ), - $this->get_shipping_args( $order ), + $this->get_transaction_args( $order ), $this->get_line_item_args( $order ) ), $order ); + + return $this->fix_request_length( $order, $paypal_args ); } /** @@ -181,60 +237,82 @@ class WC_Gateway_Paypal_Request { } /** - * Get line item args for paypal request. + * Get shipping cost line item args for paypal request. + * + * @param WC_Order $order Order object. + * @param bool $force_one_line_item Whether one line item was forced by validation or URL length. + * @return array + */ + protected function get_shipping_cost_line_item( $order, $force_one_line_item ) { + $line_item_args = array(); + $shipping_total = $order->get_shipping_total(); + if ( $force_one_line_item ) { + $shipping_total += $order->get_shipping_tax(); + } + + // Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max). + // We also check that shipping is not the **only** cost as PayPal won't allow payment + // if the items have no cost. + if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) { + $line_item_args['shipping_1'] = $this->number_format( $shipping_total, $order ); + } elseif ( $order->get_shipping_total() > 0 ) { + /* translators: %s: Order shipping method */ + $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $shipping_total, $order ) ); + } + + return $line_item_args; + } + + /** + * Get line item args for paypal request as a single line item. * * @param WC_Order $order Order object. * @return array */ - protected function get_line_item_args( $order ) { + protected function get_line_item_args_single_item( $order ) { + $this->delete_line_items(); - /** - * Try passing a line item per product if supported. - */ - if ( ( ! wc_tax_enabled() || ! wc_prices_include_tax() ) && $this->prepare_line_items( $order ) ) { + $all_items_name = $this->get_order_item_names( $order ); + $this->add_line_item( $all_items_name ? $all_items_name : __( 'Order', 'woocommerce' ), 1, $this->number_format( $order->get_total() - $this->round( $order->get_shipping_total() + $order->get_shipping_tax(), $order ), $order ), $order->get_order_number() ); + $line_item_args = $this->get_shipping_cost_line_item( $order, true ); - $line_item_args = array(); + return array_merge( $line_item_args, $this->get_line_items() ); + } + + /** + * Get line item args for paypal request. + * + * @param WC_Order $order Order object. + * @param bool $force_one_line_item Create only one item for this order. + * @return array + */ + protected function get_line_item_args( $order, $force_one_line_item = false ) { + if ( wc_tax_enabled() && wc_prices_include_tax() || ! $this->line_items_valid( $order ) ) { + $force_one_line_item = true; + } + + $line_item_args = array(); + if ( $force_one_line_item ) { + /** + * Send order as a single item. + * + * For shipping, we longer use shipping_1 because paypal ignores it if *any* shipping rules are within paypal, and paypal ignores anything over 5 digits (999.99 is the max). + */ + $line_item_args = $this->get_line_item_args_single_item( $order ); + } else { + /** + * Passing a line item per product if supported. + */ + $this->prepare_line_items( $order ); $line_item_args['tax_cart'] = $this->number_format( $order->get_total_tax(), $order ); if ( $order->get_total_discount() > 0 ) { $line_item_args['discount_amount_cart'] = $this->number_format( $this->round( $order->get_total_discount(), $order ), $order ); } - // Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max). - // We also check that shipping is not the **only** cost as PayPal won't allow payment - // if the items have no cost. - if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) { - $line_item_args['shipping_1'] = $this->number_format( $order->get_shipping_total(), $order ); - } elseif ( $order->get_shipping_total() > 0 ) { - /* translators: %s: Order shipping method */ - $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $order->get_shipping_total(), $order ) ); - } - + $line_item_args = array_merge( $line_item_args, $this->get_shipping_cost_line_item( $order, false ) ); $line_item_args = array_merge( $line_item_args, $this->get_line_items() ); - } else { - /** - * Send order as a single item. - * - * For shipping, we longer use shipping_1 because paypal ignores it if *any* shipping rules are within paypal, and paypal ignores anything over 5 digits (999.99 is the max). - */ - $this->delete_line_items(); - - $line_item_args = array(); - $all_items_name = $this->get_order_item_names( $order ); - $this->add_line_item( $all_items_name ? $all_items_name : __( 'Order', 'woocommerce' ), 1, $this->number_format( $order->get_total() - $this->round( $order->get_shipping_total() + $order->get_shipping_tax(), $order ), $order ), $order->get_order_number() ); - - // Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max). - // We also check that shipping is not the **only** cost as PayPal won't allow payment - // if the items have no cost. - if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) { - $line_item_args['shipping_1'] = $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ); - } elseif ( $order->get_shipping_total() > 0 ) { - /* translators: %s: Order shipping method */ - $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) ); - } - - $line_item_args = array_merge( $line_item_args, $this->get_line_items() ); } return $line_item_args; @@ -316,40 +394,55 @@ class WC_Gateway_Paypal_Request { } /** - * Get line items to send to paypal. + * Check if the order has valid line items to use for PayPal request. * - * @param WC_Order $order Order object. + * The line items are invalid in case of mismatch in totals or if any amount < 0. + * + * @param WC_Order $order Order to be examined. * @return bool */ - protected function prepare_line_items( $order ) { - $this->delete_line_items(); - $calculated_total = 0; + protected function line_items_valid( $order ) { + $negative_item_amount = false; + $calculated_total = 0; // Products. foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { if ( 'fee' === $item['type'] ) { $item_line_total = $this->number_format( $item['line_total'], $order ); - $line_item = $this->add_line_item( $item->get_name(), 1, $item_line_total ); $calculated_total += $item_line_total; + } else { + $item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order ); + $calculated_total += $item_line_total * $item->get_quantity(); + } + + if ( $item_line_total < 0 ) { + $negative_item_amount = true; + } + } + $mismatched_totals = $this->number_format( $calculated_total + $order->get_total_tax() + $this->round( $order->get_shipping_total(), $order ) - $this->round( $order->get_total_discount(), $order ), $order ) !== $this->number_format( $order->get_total(), $order ); + return ! $negative_item_amount && ! $mismatched_totals; + } + + /** + * Get line items to send to paypal. + * + * @param WC_Order $order Order object. + */ + protected function prepare_line_items( $order ) { + $this->delete_line_items(); + + // Products. + foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { + if ( 'fee' === $item['type'] ) { + $item_line_total = $this->number_format( $item['line_total'], $order ); + $this->add_line_item( $item->get_name(), 1, $item_line_total ); } else { $product = $item->get_product(); $sku = $product ? $product->get_sku() : ''; $item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order ); - $line_item = $this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku ); - $calculated_total += $item_line_total * $item->get_quantity(); - } - - if ( ! $line_item ) { - return false; + $this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku ); } } - - // Check for mismatched totals. - if ( $this->number_format( $calculated_total + $order->get_total_tax() + $this->round( $order->get_shipping_total(), $order ) - $this->round( $order->get_total_discount(), $order ), $order ) !== $this->number_format( $order->get_total(), $order ) ) { - return false; - } - - return true; } /** @@ -359,15 +452,10 @@ class WC_Gateway_Paypal_Request { * @param int $quantity Item quantity. * @param float $amount Amount. * @param string $item_number Item number. - * @return bool successfully added or not */ protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) { $index = ( count( $this->line_items ) / 4 ) + 1; - if ( $amount < 0 || $index > 9 ) { - return false; - } - $item = apply_filters( 'woocommerce_paypal_line_item', array( 'item_name' => html_entity_decode( wc_trim_string( $item_name ? $item_name : __( 'Item', 'woocommerce' ), 127 ), ENT_NOQUOTES, 'UTF-8' ), @@ -381,8 +469,6 @@ class WC_Gateway_Paypal_Request { $this->line_items[ 'quantity_' . $index ] = $item['quantity']; $this->line_items[ 'amount_' . $index ] = $item['amount']; $this->line_items[ 'item_number_' . $index ] = $this->limit_length( $item['item_number'], 127 ); - - return true; } /** diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php index c917a8aeec3..05f00fb47c6 100644 --- a/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php @@ -65,6 +65,7 @@ abstract class WC_Gateway_Paypal_Response { protected function payment_complete( $order, $txn_id = '', $note = '' ) { $order->add_order_note( $note ); $order->payment_complete( $txn_id ); + WC()->cart->empty_cart(); } /** diff --git a/includes/import/class-wc-product-csv-importer.php b/includes/import/class-wc-product-csv-importer.php index efcad3f9811..8e3c646cdd8 100644 --- a/includes/import/class-wc-product-csv-importer.php +++ b/includes/import/class-wc-product-csv-importer.php @@ -491,7 +491,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { */ public function parse_backorders_field( $value ) { if ( empty( $value ) ) { - return ''; + return 'no'; } $value = $this->parse_bool_field( $value ); @@ -502,7 +502,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { return $value ? 'yes' : 'no'; } - return ''; + return 'no'; } /** diff --git a/includes/wc-product-functions.php b/includes/wc-product-functions.php index eee69f05bff..23a8c1a7327 100644 --- a/includes/wc-product-functions.php +++ b/includes/wc-product-functions.php @@ -999,7 +999,7 @@ function wc_get_price_excluding_tax( $product, $args = array() ) { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); - $return_price = WC_Tax::round( $line_price - wc_round_tax_total( array_sum( $remove_taxes ) ) ); + $return_price = $line_price - array_sum( $remove_taxes ); } else { $return_price = $line_price; } diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php index 617499ab525..d68b1e65466 100644 --- a/includes/wc-template-functions.php +++ b/includes/wc-template-functions.php @@ -893,6 +893,10 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); + if ( isset( $args['attributes']['aria-label'] ) ) { + $args['attributes']['aria-label'] = strip_tags( $args['attributes']['aria-label'] ); + } + wc_get_template( 'loop/add-to-cart.php', $args ); } } diff --git a/templates/global/form-login.php b/templates/global/form-login.php index 2f509421eff..f335396844f 100644 --- a/templates/global/form-login.php +++ b/templates/global/form-login.php @@ -33,11 +33,11 @@ if ( is_user_logged_in() ) {

- +

- +

diff --git a/templates/myaccount/form-edit-account.php b/templates/myaccount/form-edit-account.php index 13647ecc143..9c243e5a439 100644 --- a/templates/myaccount/form-edit-account.php +++ b/templates/myaccount/form-edit-account.php @@ -25,11 +25,11 @@ do_action( 'woocommerce_before_edit_account_form' ); ?>

- +

- +

@@ -41,7 +41,7 @@ do_action( 'woocommerce_before_edit_account_form' ); ?>

- +

@@ -49,15 +49,15 @@ do_action( 'woocommerce_before_edit_account_form' ); ?>

- +

- +

- +

diff --git a/templates/myaccount/form-login.php b/templates/myaccount/form-login.php index b2821aa1ca7..259b3cb28a5 100644 --- a/templates/myaccount/form-login.php +++ b/templates/myaccount/form-login.php @@ -41,11 +41,11 @@ if ( ! defined( 'ABSPATH' ) ) {

- +

- +

@@ -81,21 +81,21 @@ if ( ! defined( 'ABSPATH' ) ) {

- +

- +

- +

diff --git a/templates/myaccount/form-lost-password.php b/templates/myaccount/form-lost-password.php index 4e0ac11d55c..d953490e161 100644 --- a/templates/myaccount/form-lost-password.php +++ b/templates/myaccount/form-lost-password.php @@ -25,7 +25,7 @@ wc_print_notices(); ?>

- +

diff --git a/templates/myaccount/form-reset-password.php b/templates/myaccount/form-reset-password.php index f8bc4bbf22b..5804e108692 100644 --- a/templates/myaccount/form-reset-password.php +++ b/templates/myaccount/form-reset-password.php @@ -25,11 +25,11 @@ wc_print_notices(); ?>

- +

- +

diff --git a/templates/single-product/review-meta.php b/templates/single-product/review-meta.php index f8f44b8a2b0..8e626fb78df 100644 --- a/templates/single-product/review-meta.php +++ b/templates/single-product/review-meta.php @@ -36,8 +36,9 @@ if ( '0' === $comment->comment_approved ) { ?> if ( 'yes' === get_option( 'woocommerce_review_rating_verification_label' ) && $verified ) { echo '(' . esc_attr__( 'verified owner', 'woocommerce' ) . ') '; } + ?> - +

get_id() ); + $post_object = get_post( $upsell->get_id() ); setup_postdata( $GLOBALS['post'] =& $post_object ); diff --git a/tests/unit-tests/gateways/paypal/request.php b/tests/unit-tests/gateways/paypal/request.php new file mode 100644 index 00000000000..3c98f1a8f60 --- /dev/null +++ b/tests/unit-tests/gateways/paypal/request.php @@ -0,0 +1,373 @@ +products. + * + * @param int $product_count Number of products to create. + */ + protected function create_products( $product_count = 30 ) { + $this->products = array(); + for ( $i = 0; $i < $product_count; $i++ ) { + $product = WC_Helper_Product::create_simple_product(); + $product->set_name( 'Dummy Product ' . $i ); + $this->products[] = $product; + + } + + // To test limit length properly, we need a product with a name that is shorter than 127 chars, + // but longer than 127 chars when URL-encoded. + if ( array_key_exists( 0, $this->products ) ) { + $this->products[0]->set_name( 'Dummy Product 😎😎😎😎😎😎😎😎😎😎😎' ); + } + + } + + /** + * Add products from $this->products to $order as items, clearing existing order items. + * + * @param WC_Order $order Order to which the products should be added. + * @param array $prices Array of prices to use for created products. Leave empty for default prices. + */ + protected function add_products_to_order( $order, $prices = array() ) { + // Remove previous items. + foreach ( $order->get_items() as $item ) { + $order->remove_item( $item->get_id() ); + } + + // Add new products. + $prod_count = 0; + foreach ( $this->products as $product ) { + $item = new WC_Order_Item_Product(); + $item->set_props( array( + 'product' => $product, + 'quantity' => 3, + 'subtotal' => $prices ? $prices[ $prod_count ] : wc_get_price_excluding_tax( $product, array( 'qty' => 3 ) ), + 'total' => $prices ? $prices[ $prod_count ] : wc_get_price_excluding_tax( $product, array( 'qty' => 3 ) ), + ) ); + + $item->save(); + $order->add_item( $item ); + + $prod_count++; + } + + } + + /** + * Initialize the Paypal gateway and Request objects. + */ + public function setUp() { + parent::setUp(); + + $bootstrap = WC_Unit_Tests_Bootstrap::instance(); + include_once $bootstrap->plugin_dir . '/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php'; + + $this->paypal_gateway = new WC_Gateway_Paypal(); + $this->paypal_request = new WC_Gateway_Paypal_Request( $this->paypal_gateway ); + } + + + /** + * Create Paypal request URL for $product_count number of products. + * + * @param int $product_count Number of products to include in the order. + * @param bool $testmode Whether to test using sandbox or not. + * @param array $product_prices Array of prices to use for created products. Leave empty for default prices. + * @param bool $calc_order_totals Whether the WC_Order::calculate_totals() should be triggered when creating order. + * @return string + * @throws WC_Data_Exception + */ + protected function get_request_url( $product_count, $testmode, $product_prices = array(), $calc_order_totals = true ) { + // Create products. + $this->create_products( $product_count ); + + $this->order = WC_Helper_Order::create_order( $this->user ); + $this->add_products_to_order( $this->order, $product_prices ); + + // Set payment method to Paypal. + $payment_gateways = WC()->payment_gateways->payment_gateways(); + $this->order->set_payment_method( $payment_gateways['paypal'] ); + + // Add tax. + if ( wc_tax_enabled() ) { + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '11.0000', + 'tax_rate_name' => 'TAX', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + $tax_item = new WC_Order_Item_Tax(); + $tax_item->set_rate( 100 ); + $tax_item->set_tax_total( 100 ); + $tax_item->set_shipping_tax_total( 100 ); + $this->order->add_item( $tax_item ); + $this->order->save(); + } + + $this->order->calculate_shipping(); + if ( $calc_order_totals ) { + $this->order->calculate_totals(); + } + + return $this->paypal_request->get_request_url( $this->order, $testmode ); + } + + /** + * Clean up order, tax, deletes all products in order, too. + */ + protected function clean_up() { + global $wpdb; + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates" ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations" ); + + WC_Helper_Order::delete_order( $this->order->get_id() ); + + } + + /** + * Check if the shipping tax is included in the total according to $shipping_tax_included. + * + * @param array $query_array Request URL parsed into associative array. + * @param bool $shipping_tax_included Whether the shipping tax should be included or not. + */ + protected function check_shipping_tax( $query_array, $shipping_tax_included ) { + $shipping_total = $this->order->get_shipping_total(); + if ( $shipping_tax_included ) { + $shipping_total += $this->order->get_shipping_tax(); + } + $epsilon = 0.01; + $this->assertTrue( abs( $shipping_total - floatval( $query_array['shipping_1'] ) ) < $epsilon, + 'Shipping tax mismatch: shipping total=' . $shipping_total . ' vs request shipping=' . $query_array['shipping_1'] ); + + } + + /** + * Test common order asserts. + * + * @param string $request_url Paypal request URL. + * @param bool $testmode Whether Paypal sandbox is used or not. + */ + protected function check_order_common_props( $request_url, $testmode ) { + if ( $testmode ) { + $this->assertEquals( 'https://www.sandbox.paypal.com', substr( $request_url, 0, 30 ) ); + } else { + $this->assertEquals( 'https://www.paypal.com', substr( $request_url, 0, 22 ) ); + } + $this->assertLessThanOrEqual( 2083, strlen( $request_url ) ); + } + + /** + * Test large order with 30 items, URL length > 2083 characters. + * + * @param bool $shipping_tax_included Whether the shipping tax should be included or not. + * @param bool $testmode Whether to use Paypal sandbox. + * @throws WC_Data_Exception + */ + protected function check_large_order( $shipping_tax_included, $testmode ) { + $request_url = $this->get_request_url( 30, $testmode ); + $this->check_order_common_props( $request_url, $testmode ); + + // Check fields limited to 127 characters for length. + $fields_limited_to_127_chars = array( + 'invoice', + 'item_name_1', + 'item_number_1', + ); + + $query_string = wp_parse_url( $request_url, PHP_URL_QUERY ) + ? wp_parse_url( $request_url, PHP_URL_QUERY ) + : ''; + $query_array = array(); + parse_str( $query_string, $query_array ); + foreach ( $fields_limited_to_127_chars as $field_name ) { + $this->assertLessThanOrEqual( 127, strlen( $query_array[ $field_name ] ) ); + } + + // Check that there is actually only one item for order with URL length > limit. + $this->assertFalse( array_key_exists( 'item_name_2', $query_array ) ); + + // Check that non-line item parameters are included. + $this->assertTrue( array_key_exists( 'cmd', $query_array ) ); + $this->assertEquals( '_cart', $query_array['cmd'] ); + + $this->check_shipping_tax( $query_array, $shipping_tax_included ); + + // Remove order and created products. + $this->clean_up(); + + } + + + /** + * Return true if value is < 0, false otherwise. + * + * @param int|float $value Tested value. + * @return bool + */ + protected function is_negative( $value ) { + return $value < 0; + } + + /** + * Test small order with fewer items, URL length should be < 2083 characters. + * + * @param int $product_count Number of products to include in the order. + * @param bool $shipping_tax_included Whether the shipping tax should be included or not. + * @param bool $testmode Whether to use Paypal sandbox. + * @param array $product_prices Array of prices to use for created products. Leave empty for default prices. + * @param bool $calc_order_totals Whether the WC_Order::calculate_totals() should be triggered when creating order. + * @throws WC_Data_Exception + */ + protected function check_small_order( $product_count, $shipping_tax_included, $testmode, $product_prices = array(), $calc_order_totals = true ) { + $request_url = $this->get_request_url( $product_count, $testmode, $product_prices, $calc_order_totals ); + $this->check_order_common_props( $request_url, $testmode ); + + $query_string = wp_parse_url( $request_url, PHP_URL_QUERY ) + ? wp_parse_url( $request_url, PHP_URL_QUERY ) + : ''; + $query_array = array(); + parse_str( $query_string, $query_array ); + + // Check that there are $product_count line items in the request URL. + // However, if shipping tax is included, there is only one item. + if ( $shipping_tax_included || array_filter( $product_prices, array( $this, 'is_negative' ) ) || ! $calc_order_totals ) { + $product_count = 1; + } + + for ( $i = 1; $i <= $product_count; $i++ ) { + $this->assertTrue( array_key_exists( 'item_name_' . $i, $query_array ), 'Item name ' . $i . ' does not exist' ); + $this->assertTrue( array_key_exists( 'quantity_' . $i, $query_array ) ); + $this->assertTrue( array_key_exists( 'amount_' . $i, $query_array ) ); + $this->assertTrue( array_key_exists( 'item_number_' . $i, $query_array ) ); + + // Check that non-line item parameters are included. + $this->assertTrue( array_key_exists( 'cmd', $query_array ) ); + $this->assertEquals( '_cart', $query_array['cmd'] ); + + $this->assertLessThanOrEqual( 127, strlen( $query_array[ 'item_name_' . $i ] ) ); + $this->assertLessThanOrEqual( 127, strlen( $query_array[ 'item_number_' . $i ] ) ); + + } + + $this->check_shipping_tax( $query_array, $shipping_tax_included ); + + // Remove order and created products. + $this->clean_up(); + } + + /** + * Test order with one product having negative amount. + * Amount < 0 forces tax inclusion and single line item, since WC_Gateway_Paypal_Request::prepare_line_items() + * will return false. + * + * @param bool $testmode Whether PayPal request should point to sandbox or live production. + * @throws WC_Data_Exception + */ + protected function check_negative_amount( $testmode ) { + $shipping_tax_included = true; + $product_prices = array( 6, 6, 6, 6, -3 ); + $this->check_small_order( count( $product_prices ), $shipping_tax_included, $testmode, $product_prices ); + } + + /** + * Test order with totals mismatched. + * This forces tax inclusion and single line item, since WC_Gateway_Paypal_Request::prepare_line_items() + * will return false. + * + * @param bool $testmode Whether PayPal request should point to sandbox or live production. + * @throws WC_Data_Exception + */ + protected function check_totals_mismatch( $testmode ) { + // totals mismatch forces tax inclusion and single line item. + $shipping_tax_included = true; + $this->check_small_order( 5, $shipping_tax_included, $testmode, array(), false ); + } + + /** + * Test for request_url() method. + * + * @throws WC_Data_Exception + */ + public function test_request_url() { + // User set up. + $this->user = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + wp_set_current_user( $this->user ); + + // wc_tax_enabled(), wc_prices_include_tax() and WC_Gateway_Paypal_Request::prepare_line_items() determine if + // shipping tax should be included, these are the correct options. + // Note that prepare_line_items() can return false in 2 cases, tested separately below: + // - order totals mismatch and + // - item amount < 0. + $correct_options = array( + // woocommerce_calc_taxes, woocommerce_prices_include_tax, $shipping_tax_included values. + array( 'no', 'no', false ), + array( 'yes', 'no', false ), + // array( 'no', 'yes', false ), // this is not a valid option due to definition of wc_prices_include_tax(). + array( 'yes', 'yes', true ), + ); + + // One test without sandbox. + $testmode = false; + update_option( 'woocommerce_calc_taxes', 'no' ); + update_option( 'woocommerce_prices_include_tax', 'no' ); + $shipping_tax_included = false; + $this->check_small_order( 5, $shipping_tax_included, $testmode ); + + // Other tests with sandbox active. + $testmode = true; + foreach ( $correct_options as $values ) { + update_option( 'woocommerce_calc_taxes', $values[0] ); + update_option( 'woocommerce_prices_include_tax', $values[1] ); + $shipping_tax_included = $values[2]; + + // Test order with < 9 items (URL shorter than limit). + $this->check_small_order( 5, $shipping_tax_included, $testmode ); + + // Test order with >9 items with URL shorter than limit. + $this->check_small_order( 11, $shipping_tax_included, $testmode ); + + // Test order with URL longer than limit. + // Many items in order -> forced to use one line item -> shipping tax included. + $this->check_large_order( true, $testmode ); + + // Test amount < 0. + $this->check_negative_amount( $testmode ); + + // Check order totals mismatch. + $this->check_totals_mismatch( $testmode ); + + } + + } + +} + diff --git a/tests/unit-tests/importer/product.php b/tests/unit-tests/importer/product.php index 28f4fe9d5ac..01633f00408 100644 --- a/tests/unit-tests/importer/product.php +++ b/tests/unit-tests/importer/product.php @@ -306,7 +306,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { 'tax_class' => 'standard', 'stock_status' => 'instock', 'stock_quantity' => '', - 'backorders' => '', + 'backorders' => 'no', 'sold_individually' => '', 'weight' => '', 'length' => '', @@ -358,7 +358,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { 'tax_class' => 'standard', 'stock_status' => 'instock', 'stock_quantity' => '', - 'backorders' => '', + 'backorders' => 'no', 'sold_individually' => '', 'weight' => '', 'length' => '', @@ -394,7 +394,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { 'tax_class' => '', 'stock_status' => 'outofstock', 'stock_quantity' => '', - 'backorders' => '', + 'backorders' => 'no', 'sold_individually' => '', 'weight' => '', 'length' => '', @@ -446,7 +446,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { 'tax_class' => 'standard', 'stock_status' => 'instock', 'stock_quantity' => 6, - 'backorders' => '', + 'backorders' => 'no', 'sold_individually' => '', 'weight' => 1.0, 'length' => 2.0, @@ -536,7 +536,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { 'tax_class' => '', 'stock_status' => 'instock', 'stock_quantity' => '', - 'backorders' => '', + 'backorders' => 'no', 'sold_individually' => '', 'weight' => '', 'length' => '',