Skip to content

Commit 6b225b4

Browse files
committed
fix issues with decimal product quantities
1 parent 9620731 commit 6b225b4

File tree

7 files changed

+251
-82
lines changed

7 files changed

+251
-82
lines changed

composer.json

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,18 @@
1212
"require-dev": {
1313
"automattic/vipwpcs": "2.3.3",
1414
"dealerdirect/phpcodesniffer-composer-installer": "v0.7.2",
15-
"friendsofphp/php-cs-fixer": "v3.14.4",
15+
"friendsofphp/php-cs-fixer": "v3.17.0",
1616
"phpcompatibility/phpcompatibility-wp": "2.1.4",
17-
"sirbrillig/phpcs-variable-analysis": "v2.11.10",
18-
"squizlabs/php_codesniffer": "3.7.1",
19-
"woocommerce/woocommerce-sniffs": "^0.1.3",
17+
"sirbrillig/phpcs-variable-analysis": "v2.11.16",
18+
"squizlabs/php_codesniffer": "3.7.2",
19+
"woocommerce/woocommerce-sniffs": "0.1.3",
2020
"wp-coding-standards/wpcs": "2.3.0",
21-
"wp-phpunit/wp-phpunit": "6.1.1",
21+
"wp-phpunit/wp-phpunit": "6.2.0",
2222
"yoast/phpunit-polyfills": "^1.0.5"
2323
},
2424
"require": {
25-
"php": ">=7.2.0",
26-
"ext-json": "*",
27-
"firebase/php-jwt": "v6.4.0",
25+
"php": ">=7.2",
26+
"firebase/php-jwt": "v6.5.0",
2827
"ramsey/uuid": "^4.2.3",
2928
"salesforce/handlebars-php": "^3.0.1",
3029
"vlucas/phpdotenv": "^v5.5.0"
@@ -36,9 +35,6 @@
3635
"sort-packages": true,
3736
"allow-plugins": {
3837
"dealerdirect/phpcodesniffer-composer-installer": true
39-
},
40-
"platform": {
41-
"php": "7.4"
4238
}
4339
},
4440
"scripts": {

includes/API.php

Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,10 @@ public function __construct() {
5252
/*
5353
* These filters allow changes to the WC REST API response
5454
* Note: I needed to init WC API patches earlier than rest_dispatch_request for validation patch
55+
* Note: The rest_request_before_callbacks filter needs any early priority because we may use in the loaded classes
5556
*/
56-
// add_filter( 'rest_pre_dispatch', array( $this, 'rest_pre_dispatch' ), 10, 3 );
57-
add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 );
57+
add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 5, 3 );
5858
add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request' ), 10, 4 );
59-
add_filter( 'rest_endpoints', array( $this, 'rest_endpoints' ), 99, 1 );
6059

6160
$this->prevent_messages();
6261
}
@@ -78,6 +77,7 @@ private function prevent_messages() {
7877
*/
7978
public function rest_allowed_cors_headers( array $allow_headers ): array {
8079
$allow_headers[] = 'X-WCPOS';
80+
$allow_headers[] = 'X-HTTP-Method-Override';
8181

8282
return $allow_headers;
8383
}
@@ -180,14 +180,9 @@ public function rest_pre_dispatch( $result, $server, $request ) {
180180
/**
181181
* Filters the response before executing any REST API callbacks.
182182
*
183-
* Allows plugins to perform additional validation after a
184-
* request is initialized and matched to a registered route,
185-
* but before it is executed.
183+
* NOTE: route matching and authentication have run at this point.
186184
*
187-
* Note that this filter will not be called for requests that
188-
* fail to authenticate or match to a registered route.
189-
*
190-
* @since 4.7.0
185+
* We use this hook to determine the controller class and load our duck punches.
191186
*
192187
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
193188
* Usually a WP_REST_Response or WP_Error.
@@ -262,50 +257,4 @@ public function rest_dispatch_request( $dispatch_result, $request, $route, $hand
262257

263258
return $dispatch_result;
264259
}
265-
266-
/**
267-
* Filters the array of available REST API endpoints.
268-
*
269-
* @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
270-
* to an array of callbacks for the endpoint. These take the format
271-
* `'/path/regex' => array( $callback, $bitmask )` or
272-
* `'/path/regex' => array( array( $callback, $bitmask ).
273-
*
274-
* @return array
275-
*/
276-
public function rest_endpoints( array $endpoints ): array {
277-
// This is a hack to allow order creation without an email address
278-
// @TODO - there must be a better way to this?
279-
// @NOTE - WooCommercePOS\API\Orders is loaded after validation checks, so can't put it there
280-
if ( isset( $endpoints['/wc/v3/orders'] ) ) {
281-
$endpoints['/wc/v3/orders'][1]['args']['billing']['properties']['email']['format'] = '';
282-
// add ordering by status, customer_id, payment_method and total to orders endpoint
283-
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'status';
284-
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'customer_id';
285-
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'payment_method';
286-
$endpoints['/wc/v3/orders'][0]['args']['orderby']['enum'][] = 'total';
287-
}
288-
if ( isset( $endpoints['/wc/v3/orders/(?P<id>[\d]+)'] ) ) {
289-
$endpoints['/wc/v3/orders/(?P<id>[\d]+)'][1]['args']['billing']['properties']['email']['format'] = '';
290-
}
291-
292-
293-
// add ordering by meta_value to customers endpoint
294-
if ( isset( $endpoints['/wc/v3/customers'] ) ) {
295-
// allow ordering by first_name, last_name, email, role, username
296-
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'first_name';
297-
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'last_name';
298-
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'email';
299-
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'role';
300-
$endpoints['/wc/v3/customers'][0]['args']['orderby']['enum'][] = 'username';
301-
}
302-
303-
// add ordering by stock_quantity to products endpoint
304-
if ( isset( $endpoints['/wc/v3/products'] ) ) {
305-
// allow ordering by meta_value
306-
$endpoints['/wc/v3/products'][0]['args']['orderby']['enum'][] = 'stock_quantity';
307-
}
308-
309-
return $endpoints;
310-
}
311260
}

includes/API/Customers.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,68 @@ class Customers {
2323
public function __construct( WP_REST_Request $request ) {
2424
$this->request = $request;
2525

26+
add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 );
2627
add_filter( 'woocommerce_rest_customer_query', array( $this, 'customer_query' ), 10, 2 );
2728
add_filter( 'woocommerce_rest_prepare_customer', array( $this, 'customer_response' ), 10, 3 );
2829
add_filter( 'users_where', array( $this, 'users_where' ), 10, 2 );
2930
}
3031

32+
/**
33+
* Filters the response before executing any REST API callbacks.
34+
*
35+
* We can use this filter to bypass data validation checks
36+
*
37+
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
38+
* Usually a WP_REST_Response or WP_Error.
39+
* @param array $handler Route handler used for the request.
40+
* @param WP_REST_Request $request Request used to generate the response.
41+
*/
42+
public function rest_request_before_callbacks( $response, $handler, $request ) {
43+
if ( is_wp_error( $response ) ) {
44+
// Check if the error code 'rest_invalid_param' exists
45+
if ( $response->get_error_message( 'rest_invalid_param' ) ) {
46+
// Get the error data for 'rest_invalid_param'
47+
$error_data = $response->get_error_data( 'rest_invalid_param' );
48+
49+
// Check if the invalid parameter was 'orderby'
50+
if ( array_key_exists( 'orderby', $error_data['params'] ) ) {
51+
// Get the 'orderby' details
52+
$orderby_details = $error_data['details']['orderby'];
53+
54+
// Get the 'orderby' request
55+
$orderby_request = $request->get_param( 'orderby' );
56+
57+
// Extended 'orderby' values
58+
$orderby_extended = array(
59+
'first_name',
60+
'last_name',
61+
'email',
62+
'role',
63+
'username',
64+
);
65+
66+
// Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values
67+
if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) {
68+
unset( $error_data['params']['orderby'], $error_data['details']['orderby'] );
69+
}
70+
}
71+
72+
// Check if $error_data['params'] is empty
73+
if ( empty( $error_data['params'] ) ) {
74+
return null;
75+
} else {
76+
// Remove old error data and add new error data
77+
$error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.';
78+
79+
$response->remove( 'rest_invalid_param' );
80+
$response->add( 'rest_invalid_param', $error_message, $error_data );
81+
}
82+
}
83+
}
84+
85+
return $response;
86+
}
87+
3188

3289
/**
3390
* Filter customer data returned from the REST API.

includes/API/Orders.php

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
use WCPOS\WooCommercePOS\Logger;
1212
use WP_REST_Request;
1313
use WP_REST_Response;
14+
use function in_array;
15+
use function is_array;
16+
use function is_callable;
1417
use const WCPOS\WooCommercePOS\PLUGIN_NAME;
1518
use WC_Order_Query;
1619
use WP_Query;
@@ -33,6 +36,8 @@ public function __construct( WP_REST_Request $request ) {
3336
$this->incoming_shop_order();
3437
}
3538

39+
add_filter( 'rest_request_before_callbacks', array( $this, 'rest_request_before_callbacks' ), 10, 3 );
40+
add_filter( 'woocommerce_rest_shop_order_object_query', array( $this, 'order_query' ), 10, 2 );
3641
add_filter('woocommerce_rest_pre_insert_shop_order_object', array(
3742
$this,
3843
'pre_insert_shop_order_object',
@@ -50,6 +55,88 @@ public function __construct( WP_REST_Request $request ) {
5055
add_filter( 'option_woocommerce_tax_based_on', array( $this, 'tax_based_on' ), 10, 2 );
5156
}
5257

58+
/**
59+
* Filters the response before executing any REST API callbacks.
60+
*
61+
* We can use this filter to bypass data validation checks
62+
*
63+
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
64+
* Usually a WP_REST_Response or WP_Error.
65+
* @param array $handler Route handler used for the request.
66+
* @param WP_REST_Request $request Request used to generate the response.
67+
*/
68+
public function rest_request_before_callbacks( $response, $handler, $request ) {
69+
if ( is_wp_error( $response ) ) {
70+
// Check if the error code 'rest_invalid_param' exists
71+
if ( $response->get_error_message( 'rest_invalid_param' ) ) {
72+
// Get the error data for 'rest_invalid_param'
73+
$error_data = $response->get_error_data( 'rest_invalid_param' );
74+
75+
// Check if the invalid parameter was 'line_items'
76+
if ( array_key_exists( 'line_items', $error_data['params'] ) ) {
77+
// Get the 'line_items' details
78+
$line_items_details = $error_data['details']['line_items'];
79+
80+
// Check if 'line_items[X][quantity]' has 'rest_invalid_type'
81+
// Use a regular expression to match 'line_items[X][quantity]', where X is a number
82+
if ( $line_items_details['code'] === 'rest_invalid_type' &&
83+
preg_match( '/^line_items\[\d+\]\[quantity\]$/', $line_items_details['data']['param'] ) ) {
84+
if ( woocommerce_pos_get_settings( 'general', 'decimal_qty' ) ) {
85+
unset( $error_data['params']['line_items'], $error_data['details']['line_items'] );
86+
}
87+
}
88+
}
89+
90+
// Check if the invalid parameter was 'billing'
91+
if ( array_key_exists( 'billing', $error_data['params'] ) ) {
92+
// Get the 'billing' details
93+
$billing_details = $error_data['details']['billing'];
94+
95+
// Check if 'billing' has 'rest_invalid_email'
96+
if ( $billing_details['code'] === 'rest_invalid_email' ) {
97+
unset( $error_data['params']['billing'], $error_data['details']['billing'] );
98+
}
99+
}
100+
101+
// Check if the invalid parameter was 'orderby'
102+
if ( array_key_exists( 'orderby', $error_data['params'] ) ) {
103+
// Get the 'orderby' details
104+
$orderby_details = $error_data['details']['orderby'];
105+
106+
// Get the 'orderby' request
107+
$orderby_request = $request->get_param( 'orderby' );
108+
109+
// Extended 'orderby' values
110+
$orderby_extended = array(
111+
'status',
112+
'customer_id',
113+
'payment_method',
114+
'total',
115+
);
116+
117+
// Check if 'orderby' has 'rest_not_in_enum', but is in the extended 'orderby' values
118+
if ( $orderby_details['code'] === 'rest_not_in_enum' && in_array( $orderby_request, $orderby_extended, true ) ) {
119+
unset( $error_data['params']['orderby'], $error_data['details']['orderby'] );
120+
}
121+
}
122+
123+
// Check if $error_data['params'] is empty
124+
if ( empty( $error_data['params'] ) ) {
125+
return null;
126+
} else {
127+
// Remove old error data and add new error data
128+
$error_message = 'Invalid parameter(s): ' . implode( ', ', array_keys( $error_data['params'] ) ) . '.';
129+
130+
$response->remove( 'rest_invalid_param' );
131+
$response->add( 'rest_invalid_param', $error_message, $error_data );
132+
}
133+
}
134+
}
135+
136+
return $response;
137+
}
138+
139+
53140

54141
public function incoming_shop_order(): void {
55142
$raw_data = $this->request->get_json_params();
@@ -97,6 +184,19 @@ public function test_email() {
97184
return true;
98185
}
99186

187+
/**
188+
* Filter the query arguments for a request.
189+
*
190+
* @param array $args Key value array of query var to query value.
191+
* @param WP_REST_Request $request The request used.
192+
*
193+
* @return array $args Key value array of query var to query value.
194+
*/
195+
public function order_query( $args, WP_REST_Request $request ) {
196+
197+
return $args;
198+
}
199+
100200
/**
101201
* @param $order
102202
* @param $request
@@ -191,18 +291,18 @@ public function rest_set_order_item( $item, $posted ): void {
191291
$variation = wc_get_product( (int) $posted['variation_id'] );
192292
$valid_keys = array();
193293

194-
if ( \is_callable( array( $variation, 'get_variation_attributes' ) ) ) {
294+
if ( is_callable( array( $variation, 'get_variation_attributes' ) ) ) {
195295
foreach ( $variation->get_variation_attributes() as $attribute_name => $attribute ) {
196296
$valid_keys[] = str_replace( 'attribute_', '', $attribute_name );
197297
}
198298

199-
if ( isset( $posted['meta_data'] ) && \is_array( $posted['meta_data'] ) ) {
299+
if ( isset( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) {
200300
foreach ( $posted['meta_data'] as $meta ) {
201301
// fix initial item creation
202302
if ( isset( $meta['attr_id'] ) ) {
203303
if ( 0 == $meta['attr_id'] ) {
204304
// not a taxonomy
205-
if ( \in_array( strtolower( $meta['display_key'] ), $valid_keys, true ) ) {
305+
if ( in_array( strtolower( $meta['display_key'] ), $valid_keys, true ) ) {
206306
$item->add_meta_data( strtolower( $meta['display_key'] ), $meta['display_value'], true );
207307
}
208308
} else {
@@ -219,7 +319,7 @@ public function rest_set_order_item( $item, $posted ): void {
219319
}
220320
}
221321
// fix subsequent overwrites
222-
if ( wc_attribute_taxonomy_id_by_name( $meta['key'] ) || \in_array( $meta['key'], $valid_keys, true ) ) {
322+
if ( wc_attribute_taxonomy_id_by_name( $meta['key'] ) || in_array( $meta['key'], $valid_keys, true ) ) {
223323
$item->add_meta_data( $meta['key'], $meta['value'], true );
224324
}
225325
}
@@ -295,28 +395,28 @@ public function orderby_additions( array $clauses, WP_Query $wp_query ): array {
295395
// add option to order by status
296396
if ( 'status' === $wp_query->query_vars['orderby'] ) {
297397
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}posts AS order_posts ON {$wpdb->prefix}posts.ID = order_posts.ID ";
298-
$clauses['orderby'] = " order_posts.post_status " . $order;
398+
$clauses['orderby'] = ' order_posts.post_status ' . $order;
299399
}
300400

301401
// add option to order by customer_id
302402
if ( 'customer_id' === $wp_query->query_vars['orderby'] ) {
303403
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS customer_meta ON {$wpdb->prefix}posts.ID = customer_meta.post_id ";
304404
$clauses['where'] .= " AND customer_meta.meta_key = '_customer_user' ";
305-
$clauses['orderby'] = " customer_meta.meta_value " . $order;
405+
$clauses['orderby'] = ' customer_meta.meta_value ' . $order;
306406
}
307407

308408
// add option to order by payment_method
309409
if ( 'payment_method' === $wp_query->query_vars['orderby'] ) {
310410
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS payment_method_meta ON {$wpdb->prefix}posts.ID = payment_method_meta.post_id ";
311411
$clauses['where'] .= " AND payment_method_meta.meta_key = '_payment_method' ";
312-
$clauses['orderby'] = " payment_method_meta.meta_value " . $order;
412+
$clauses['orderby'] = ' payment_method_meta.meta_value ' . $order;
313413
}
314414

315415
// add option to order by total
316416
if ( 'total' === $wp_query->query_vars['orderby'] ) {
317417
$clauses['join'] .= " LEFT JOIN {$wpdb->prefix}postmeta AS total_meta ON {$wpdb->prefix}posts.ID = total_meta.post_id ";
318418
$clauses['where'] .= " AND total_meta.meta_key = '_order_total' ";
319-
$clauses['orderby'] = " total_meta.meta_value+0 " . $order;
419+
$clauses['orderby'] = ' total_meta.meta_value+0 ' . $order;
320420
}
321421
}
322422

0 commit comments

Comments
 (0)