diff --git a/composer.json b/composer.json index dc6f763..468de55 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,18 @@ "require-dev": { "automattic/vipwpcs": "2.3.3", "dealerdirect/phpcodesniffer-composer-installer": "v0.7.2", - "friendsofphp/php-cs-fixer": "v3.14.4", + "friendsofphp/php-cs-fixer": "v3.17.0", "phpcompatibility/phpcompatibility-wp": "2.1.4", - "sirbrillig/phpcs-variable-analysis": "v2.11.10", - "squizlabs/php_codesniffer": "3.7.1", - "woocommerce/woocommerce-sniffs": "^0.1.3", + "sirbrillig/phpcs-variable-analysis": "v2.11.16", + "squizlabs/php_codesniffer": "3.7.2", + "woocommerce/woocommerce-sniffs": "0.1.3", "wp-coding-standards/wpcs": "2.3.0", - "wp-phpunit/wp-phpunit": "6.1.1", + "wp-phpunit/wp-phpunit": "6.2.0", "yoast/phpunit-polyfills": "^1.0.5" }, "require": { - "php": ">=7.2.0", - "ext-json": "*", - "firebase/php-jwt": "v6.4.0", + "php": ">=7.2", + "firebase/php-jwt": "v6.5.0", "ramsey/uuid": "^4.2.3", "salesforce/handlebars-php": "^3.0.1", "vlucas/phpdotenv": "^v5.5.0" @@ -36,9 +35,6 @@ "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true - }, - "platform": { - "php": "7.4" } }, "scripts": { diff --git a/includes/API.php b/includes/API.php index 25f0c26..4d9a46c 100644 --- a/includes/API.php +++ b/includes/API.php @@ -52,11 +52,10 @@ public function __construct() { /* * These filters allow changes to the WC REST API response * Note: I needed to init WC API patches earlier than rest_dispatch_request for validation patch + * Note: The rest_request_before_callbacks filter needs any early priority because we may use in the loaded classes */ -// add_filter( 'rest_pre_dispatch', array( $this, 'rest_pre_dispatch' ), 10, 3 ); - add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 ); + add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 5, 3 ); add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request' ), 10, 4 ); - add_filter( 'rest_endpoints', array( $this, 'rest_endpoints' ), 99, 1 ); $this->prevent_messages(); } @@ -78,6 +77,7 @@ private function prevent_messages() { */ public function rest_allowed_cors_headers( array $allow_headers ): array { $allow_headers[] = 'X-WCPOS'; + $allow_headers[] = 'X-HTTP-Method-Override'; return $allow_headers; } @@ -180,14 +180,9 @@ public function rest_pre_dispatch( $result, $server, $request ) { /** * Filters the response before executing any REST API callbacks. * - * Allows plugins to perform additional validation after a - * request is initialized and matched to a registered route, - * but before it is executed. + * NOTE: route matching and authentication have run at this point. * - * Note that this filter will not be called for requests that - * fail to authenticate or match to a registered route. - * - * @since 4.7.0 + * We use this hook to determine the controller class and load our duck punches. * * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. * Usually a WP_REST_Response or WP_Error. @@ -262,50 +257,4 @@ public function rest_dispatch_request( $dispatch_result, $request, $route, $hand return $dispatch_result; } - - /** - * Filters the array of available REST API endpoints. - * - * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped - * to an array of callbacks for the endpoint. These take the format - * `'/path/regex' => array( $callback, $bitmask )` or - * `'/path/regex' => array( array( $callback, $bitmask ). - * - * @return array - */ - public function rest_endpoints( array $endpoints ): array { - // This is a hack to allow order creation without an email address - // @TODO - there must be a better way to this? - // @NOTE - WooCommercePOS\API\Orders is loaded after validation checks, so can't put it there - if ( isset( $endpoints['/wc/v3/orders'] ) ) { - $endpoints['/wc/v3/orders'][1]['args']['billing']['properties']['email']['format'] = ''; - // add ordering by status, customer_id, payment_method and total to orders endpoint - $endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'status'; - $endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'customer_id'; - $endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'payment_method'; - $endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'total'; - } - if ( isset( $endpoints['/wc/v3/orders/(?P[\d]+)'] ) ) { - $endpoints['/wc/v3/orders/(?P[\d]+)'][1]['args']['billing']['properties']['email']['format'] = ''; - } - - - // add ordering by meta_value to customers endpoint - if ( isset( $endpoints['/wc/v3/customers'] ) ) { - // allow ordering by first_name, last_name, email, role, username - $endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'first_name'; - $endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'last_name'; - $endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'email'; - $endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'role'; - $endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'username'; - } - - // add ordering by stock_quantity to products endpoint - if ( isset( $endpoints['/wc/v3/products'] ) ) { - // allow ordering by meta_value - $endpoints['/wc/v3/products'][0]['args']['orderby']['enum'][] = 'stock_quantity'; - } - - return $endpoints; - } } diff --git a/includes/API/Customers.php b/includes/API/Customers.php index 34071c1..c2a1d3b 100644 --- a/includes/API/Customers.php +++ b/includes/API/Customers.php @@ -23,11 +23,68 @@ class Customers { public function __construct( WP_REST_Request $request ) { $this->request = $request; + add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 ); add_filter( 'woocommerce_rest_customer_query', array( $this, 'customer_query' ), 10, 2 ); add_filter( 'woocommerce_rest_prepare_customer', array( $this, 'customer_response' ), 10, 3 ); add_filter( 'users_where', array( $this, 'users_where' ), 10, 2 ); } + /** + * Filters the response before executing any REST API callbacks. + * + * We can use this filter to bypass data validation checks + * + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param WP_REST_Request $request Request used to generate the response. + */ + public function rest_request_before_callbacks( $response, $handler, $request ) { + if ( is_wp_error( $response ) ) { + // Check if the error code 'rest_invalid_param' exists + if ( $response->get_error_message( 'rest_invalid_param' ) ) { + // Get the error data for 'rest_invalid_param' + $error_data = $response->get_error_data( 'rest_invalid_param' ); + + // Check if the invalid parameter was 'orderby' + if ( array_key_exists( 'orderby', $error_data['params'] ) ) { + // Get the 'orderby' details + $orderby_details = $error_data['details']['orderby']; + + // Get the 'orderby' request + $orderby_request = $request->get_param( 'orderby' ); + + // Extended 'orderby' values + $orderby_extended = array( + 'first_name', + 'last_name', + 'email', + 'role', + 'username', + ); + + // Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values + if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) { + unset( $error_data['params']['orderby'], $error_data['details']['orderby'] ); + } + } + + // Check if $error_data['params'] is empty + if ( empty( $error_data['params'] ) ) { + return null; + } else { + // Remove old error data and add new error data + $error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.'; + + $response->remove( 'rest_invalid_param' ); + $response->add( 'rest_invalid_param', $error_message, $error_data ); + } + } + } + + return $response; + } + /** * Filter customer data returned from the REST API. diff --git a/includes/API/Orders.php b/includes/API/Orders.php index 7c84159..55231ca 100644 --- a/includes/API/Orders.php +++ b/includes/API/Orders.php @@ -11,6 +11,9 @@ use WCPOS\WooCommercePOS\Logger; use WP_REST_Request; use WP_REST_Response; +use function in_array; +use function is_array; +use function is_callable; use const WCPOS\WooCommercePOS\PLUGIN_NAME; use WC_Order_Query; use WP_Query; @@ -33,6 +36,8 @@ public function __construct( WP_REST_Request $request ) { $this->incoming_shop_order(); } + add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 ); + add_filter( 'woocommerce_rest_shop_order_object_query', array( $this, 'order_query' ), 10, 2 ); add_filter('woocommerce_rest_pre_insert_shop_order_object', array( $this, 'pre_insert_shop_order_object', @@ -50,6 +55,88 @@ public function __construct( WP_REST_Request $request ) { add_filter( 'option_woocommerce_tax_based_on', array( $this, 'tax_based_on' ), 10, 2 ); } + /** + * Filters the response before executing any REST API callbacks. + * + * We can use this filter to bypass data validation checks + * + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param WP_REST_Request $request Request used to generate the response. + */ + public function rest_request_before_callbacks( $response, $handler, $request ) { + if ( is_wp_error( $response ) ) { + // Check if the error code 'rest_invalid_param' exists + if ( $response->get_error_message( 'rest_invalid_param' ) ) { + // Get the error data for 'rest_invalid_param' + $error_data = $response->get_error_data( 'rest_invalid_param' ); + + // Check if the invalid parameter was 'line_items' + if ( array_key_exists( 'line_items', $error_data['params'] ) ) { + // Get the 'line_items' details + $line_items_details = $error_data['details']['line_items']; + + // Check if 'line_items[X][quantity]' has 'rest_invalid_type' + // Use a regular expression to match 'line_items[X][quantity]', where X is a number + if ( $line_items_details['code'] === 'rest_invalid_type' && + preg_match( '/^line_items\[\d+\]\[quantity\]$/', $line_items_details['data']['param'] ) ) { + if ( woocommerce_pos_get_settings( 'general', 'decimal_qty' ) ) { + unset( $error_data['params']['line_items'], $error_data['details']['line_items'] ); + } + } + } + + // Check if the invalid parameter was 'billing' + if ( array_key_exists( 'billing', $error_data['params'] ) ) { + // Get the 'billing' details + $billing_details = $error_data['details']['billing']; + + // Check if 'billing' has 'rest_invalid_email' + if ( $billing_details['code'] === 'rest_invalid_email' ) { + unset( $error_data['params']['billing'], $error_data['details']['billing'] ); + } + } + + // Check if the invalid parameter was 'orderby' + if ( array_key_exists( 'orderby', $error_data['params'] ) ) { + // Get the 'orderby' details + $orderby_details = $error_data['details']['orderby']; + + // Get the 'orderby' request + $orderby_request = $request->get_param( 'orderby' ); + + // Extended 'orderby' values + $orderby_extended = array( + 'status', + 'customer_id', + 'payment_method', + 'total', + ); + + // Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values + if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) { + unset( $error_data['params']['orderby'], $error_data['details']['orderby'] ); + } + } + + // Check if $error_data['params'] is empty + if ( empty( $error_data['params'] ) ) { + return null; + } else { + // Remove old error data and add new error data + $error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.'; + + $response->remove( 'rest_invalid_param' ); + $response->add( 'rest_invalid_param', $error_message, $error_data ); + } + } + } + + return $response; + } + + public function incoming_shop_order(): void { $raw_data = $this->request->get_json_params(); @@ -97,6 +184,19 @@ public function test_email() { return true; } + /** + * Filter the query arguments for a request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + * + * @return array $args Key value array of query var to query value. + */ + public function order_query( $args, WP_REST_Request $request ) { + + return $args; + } + /** * @param $order * @param $request @@ -191,18 +291,18 @@ public function rest_set_order_item( $item, $posted ): void { $variation = wc_get_product( (int) $posted['variation_id'] ); $valid_keys = array(); - if ( \is_callable( array( $variation, 'get_variation_attributes' ) ) ) { + if ( is_callable( array( $variation, 'get_variation_attributes' ) ) ) { foreach ( $variation->get_variation_attributes() as $attribute_name => $attribute ) { $valid_keys[] = str_replace( 'attribute_', '', $attribute_name ); } - if ( isset( $posted['meta_data'] ) && \is_array( $posted['meta_data'] ) ) { + if ( isset( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { foreach ( $posted['meta_data'] as $meta ) { // fix initial item creation if ( isset( $meta['attr_id'] ) ) { if ( 0 == $meta['attr_id'] ) { // not a taxonomy - if ( \in_array( strtolower( $meta['display_key'] ), $valid_keys, true ) ) { + if ( in_array( strtolower( $meta['display_key'] ), $valid_keys, true ) ) { $item->add_meta_data( strtolower( $meta['display_key'] ), $meta['display_value'], true ); } } else { @@ -219,7 +319,7 @@ public function rest_set_order_item( $item, $posted ): void { } } // fix subsequent overwrites - if ( wc_attribute_taxonomy_id_by_name( $meta['key'] ) || \in_array( $meta['key'], $valid_keys, true ) ) { + if ( wc_attribute_taxonomy_id_by_name( $meta['key'] ) || in_array( $meta['key'], $valid_keys, true ) ) { $item->add_meta_data( $meta['key'], $meta['value'], true ); } } @@ -295,28 +395,28 @@ public function orderby_additions( array $clauses, WP_Query $wp_query ): array { // add option to order by status if ( 'status' === $wp_query->query_vars['orderby'] ) { $clauses['join'] .= " LEFT JOIN {$wpdb->prefix}posts AS order_posts ON {$wpdb->prefix}posts.ID = order_posts.ID "; - $clauses['orderby'] = " order_posts.post_status " . $order; + $clauses['orderby'] = ' order_posts.post_status ' . $order; } // add option to order by customer_id if ( 'customer_id' === $wp_query->query_vars['orderby'] ) { $clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS customer_meta ON {$wpdb->prefix}posts.ID = customer_meta.post_id "; $clauses['where'] .= " AND customer_meta.meta_key = '_customer_user' "; - $clauses['orderby'] = " customer_meta.meta_value " . $order; + $clauses['orderby'] = ' customer_meta.meta_value ' . $order; } // add option to order by payment_method if ( 'payment_method' === $wp_query->query_vars['orderby'] ) { $clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS payment_method_meta ON {$wpdb->prefix}posts.ID = payment_method_meta.post_id "; $clauses['where'] .= " AND payment_method_meta.meta_key = '_payment_method' "; - $clauses['orderby'] = " payment_method_meta.meta_value " . $order; + $clauses['orderby'] = ' payment_method_meta.meta_value ' . $order; } // add option to order by total if ( 'total' === $wp_query->query_vars['orderby'] ) { $clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS total_meta ON {$wpdb->prefix}posts.ID = total_meta.post_id "; $clauses['where'] .= " AND total_meta.meta_key = '_order_total' "; - $clauses['orderby'] = " total_meta.meta_value+0 " . $order; + $clauses['orderby'] = ' total_meta.meta_value+0 ' . $order; } } diff --git a/includes/API/Products.php b/includes/API/Products.php index b0d2975..419c5ea 100644 --- a/includes/API/Products.php +++ b/includes/API/Products.php @@ -9,6 +9,7 @@ use WP_REST_Request; use WP_REST_Response; use WC_Product_Query; +use function is_array; class Products { private $request; @@ -21,6 +22,7 @@ class Products { public function __construct( WP_REST_Request $request ) { $this->request = $request; + add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 ); add_filter( 'woocommerce_rest_prepare_product_object', array( $this, 'product_response' ), 10, 3 ); add_filter( 'woocommerce_rest_product_object_query', array( $this, 'product_query' ), 10, 2 ); add_filter( 'posts_search', array( $this, 'posts_search' ), 10, 2 ); @@ -42,6 +44,69 @@ public function add_barcode_to_product_schema( $schema ): array { return $schema; } + /** + * Filters the response before executing any REST API callbacks. + * + * We can use this filter to bypass data validation checks + * + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param WP_REST_Request $request Request used to generate the response. + */ + public function rest_request_before_callbacks( $response, $handler, $request ) { + if ( is_wp_error( $response ) ) { + // Check if the error code 'rest_invalid_param' exists + if ( $response->get_error_message( 'rest_invalid_param' ) ) { + // Get the error data for 'rest_invalid_param' + $error_data = $response->get_error_data( 'rest_invalid_param' ); + + // Check if the invalid parameter was 'line_items' + if ( array_key_exists( 'stock_quantity', $error_data['params'] ) ) { + // Get the 'line_items' details + $line_items_details = $error_data['details']['stock_quantity']; + + // + if ( $line_items_details['code'] === 'rest_invalid_type' && woocommerce_pos_get_settings( 'general', 'decimal_qty' ) ) { + unset( $error_data['params']['stock_quantity'], $error_data['details']['stock_quantity'] ); + } + } + + // Check if the invalid parameter was 'orderby' + if ( array_key_exists( 'orderby', $error_data['params'] ) ) { + // Get the 'orderby' details + $orderby_details = $error_data['details']['orderby']; + + // Get the 'orderby' request + $orderby_request = $request->get_param( 'orderby' ); + + // Extended 'orderby' values + $orderby_extended = array( + 'stock_quantity', + ); + + // Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values + if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) { + unset( $error_data['params']['orderby'], $error_data['details']['orderby'] ); + } + } + + // Check if $error_data['params'] is empty + if ( empty( $error_data['params'] ) ) { + return null; + } else { + // Remove old error data and add new error data + $error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.'; + + $response->remove( 'rest_invalid_param' ); + $response->add( 'rest_invalid_param', $error_message, $error_data ); + } + } + } + + return $response; + } + /** * Fires after a single object is created or updated via the REST API. * @@ -193,10 +258,10 @@ public function orderby_stock_quantity( array $clauses, WP_Query $wp_query ): ar // Order the query results: records with _stock meta_value first, then NULL, then items without _stock meta_key $order = isset( $wp_query->query_vars['order'] ) ? $wp_query->query_vars['order'] : 'DESC'; - $clauses['orderby'] = "CASE"; - $clauses['orderby'] .= " WHEN stock_meta.meta_value IS NOT NULL THEN 1"; - $clauses['orderby'] .= " WHEN stock_meta.meta_value IS NULL THEN 2"; - $clauses['orderby'] .= " ELSE 3"; + $clauses['orderby'] = 'CASE'; + $clauses['orderby'] .= ' WHEN stock_meta.meta_value IS NOT NULL THEN 1'; + $clauses['orderby'] .= ' WHEN stock_meta.meta_value IS NULL THEN 2'; + $clauses['orderby'] .= ' ELSE 3'; $clauses['orderby'] .= " END {$order}, COALESCE(stock_meta.meta_value+0, 0) {$order}"; } @@ -288,7 +353,7 @@ private function get_thumbnail( int $id ): string { $image = wp_get_attachment_image_src( $thumb_id, 'shop_thumbnail' ); } - if ( \is_array( $image ) ) { + if ( is_array( $image ) ) { return $image[0]; } diff --git a/includes/Init.php b/includes/Init.php index b231648..aa7f6cf 100644 --- a/includes/Init.php +++ b/includes/Init.php @@ -138,7 +138,7 @@ public function rest_pre_serve_request( bool $served, WP_HTTP_Response $result, */ public function send_headers(): void { // some server convert HEAD to GET method, so use this query param instead - if ( isset( $_GET['wcpos_http_method'] ) && 'head' == $_GET['wcpos_http_method'] ) { + if ( isset( $_GET['_method'] ) && 'head' == strtolower( $_GET['_method'] ) ) { header( 'Access-Control-Allow-Origin: *' ); header( 'Access-Control-Expose-Headers: Link' ); } diff --git a/readme.txt b/readme.txt index 0a9b5f5..e846a8b 100644 --- a/readme.txt +++ b/readme.txt @@ -63,9 +63,11 @@ There is more information on our website at [https://wcpos.com](https://wcpos.co == Changelog == -= 1.1.1 - 2023/05/xx = += 1.2.0 - 2023/06/xx = * Improvement: remove private meta data from Order Preview modal * Bug Fix: 'Cannot use object of type Closure as array' in the API.php file +* Bug Fix: Creating orders with decimal quantity +* Bug Fix: Update product with decimal quantity = 1.1.0 - 2023/05/19 = * Fix: disable Lite Speed Cache for POS page