Handle complex form fields when submitting checkout form

Our current way of handling the checkout form via ajax does not match the WC behavior which submits them in urlencoded request instead of JSON. When it is submitted as JSON object PHP does not parse it for $_POST etc., and we do not get its handling of arrays, breaking some plugin.
Now submitting the form as an urlencoded string inside JSON and parsing via `parse_str` which seems to handle it the same as $_POST.
The parsing is handled in `RequestData` to avoid duplicating it in multiple places and to keep our weird sanitization here. Not sure if it's a good idea to sanitize so early, but for now keeping it like this to avoid major refactoring or introducing new vulnerabilities.
This commit is contained in:
Alex P 2023-07-13 14:43:14 +03:00
parent 81f6340897
commit 639e8409c8
No known key found for this signature in database
GPG key ID: 54487A734A204D71
3 changed files with 12 additions and 5 deletions

View file

@ -50,8 +50,6 @@ class CheckoutActionHandler {
const formSelector = this.config.context === 'checkout' ? 'form.checkout' : 'form#order_review'; const formSelector = this.config.context === 'checkout' ? 'form.checkout' : 'form#order_review';
const formData = new FormData(document.querySelector(formSelector)); const formData = new FormData(document.querySelector(formSelector));
// will not handle fields with multiple values (checkboxes, <select multiple>), but we do not care about this here
const formJsonObj = Object.fromEntries(formData.entries());
const createaccount = jQuery('#createaccount').is(":checked") ? true : false; const createaccount = jQuery('#createaccount').is(":checked") ? true : false;
@ -72,7 +70,8 @@ class CheckoutActionHandler {
order_id:this.config.order_id, order_id:this.config.order_id,
payment_method: paymentMethod, payment_method: paymentMethod,
funding_source: fundingSource, funding_source: fundingSource,
form: formJsonObj, // send as urlencoded string to handle complex fields via PHP functions the same as normal form submit
form_encoded: new URLSearchParams(formData).toString(),
createaccount: createaccount createaccount: createaccount
}) })
}).then(function (res) { }).then(function (res) {

View file

@ -6,7 +6,6 @@ export default class FormValidator {
async validate(form) { async validate(form) {
const formData = new FormData(form); const formData = new FormData(form);
const formJsonObj = Object.fromEntries(formData.entries());
const res = await fetch(this.url, { const res = await fetch(this.url, {
method: 'POST', method: 'POST',
@ -16,7 +15,7 @@ export default class FormValidator {
credentials: 'same-origin', credentials: 'same-origin',
body: JSON.stringify({ body: JSON.stringify({
nonce: this.nonce, nonce: this.nonce,
form: formJsonObj, form_encoded: new URLSearchParams(formData).toString(),
}), }),
}); });

View file

@ -53,6 +53,11 @@ class RequestData {
} }
$this->dequeue_nonce_fix(); $this->dequeue_nonce_fix();
if ( isset( $json['form_encoded'] ) ) {
$json['form'] = array();
parse_str( $json['form_encoded'], $json['form'] );
}
$sanitized = $this->sanitize( $json ); $sanitized = $this->sanitize( $json );
return $sanitized; return $sanitized;
} }
@ -80,6 +85,10 @@ class RequestData {
private function sanitize( array $assoc_array ): array { private function sanitize( array $assoc_array ): array {
$data = array(); $data = array();
foreach ( (array) $assoc_array as $raw_key => $raw_value ) { foreach ( (array) $assoc_array as $raw_key => $raw_value ) {
if ( $raw_key === 'form_encoded' ) {
$data[ $raw_key ] = $raw_value;
continue;
}
if ( ! is_array( $raw_value ) ) { if ( ! is_array( $raw_value ) ) {
// Not sure if it is a good idea to sanitize everything at this level, // Not sure if it is a good idea to sanitize everything at this level,
// but should be fine for now since we do not send any HTML or multi-line texts via ajax. // but should be fine for now since we do not send any HTML or multi-line texts via ajax.