From b02bb725780df5b358f0951b34a5a1a16a3177c4 Mon Sep 17 00:00:00 2001 From: Sorin Marta Date: Thu, 16 Jul 2026 07:37:11 +0300 Subject: [PATCH 1/4] Fix 3204 --- includes/regions-api.php | 72 +++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/includes/regions-api.php b/includes/regions-api.php index 7b70be631..36cccdeb3 100644 --- a/includes/regions-api.php +++ b/includes/regions-api.php @@ -47,15 +47,20 @@ public function find_by_parent_name($parent_name, $parent_type, $type) { return $this->db->get_col( $this->db->prepare( $sql, $parent_name ) ); } - public function save($region) { + public function save( $region ) { + $region = $this->filter_region_columns( stripslashes_deep( $region ) ); + if ( ! isset( $region['ad_id'] ) || empty( $region['ad_id'] ) ) { return false; } - $region = stripslashes_deep( $region ); + $region['ad_id'] = absint( $region['ad_id'] ); + $region_id = absint( awpcp_array_data( 'id', 0, $region ) ); + + unset( $region['id'] ); - if ( intval( awpcp_array_data( 'id', null, $region ) ) > 0 ) { - $result = $this->db->update( AWPCP_TABLE_AD_REGIONS, $region, array( 'id' => $region['id'] ) ); + if ( $region_id > 0 ) { + $result = $this->db->update( AWPCP_TABLE_AD_REGIONS, $region, array( 'id' => $region_id ) ); } else { $result = $this->db->insert( AWPCP_TABLE_AD_REGIONS, $region ); } @@ -83,15 +88,64 @@ public function update_ad_regions( $ad, $regions, $max_regions = 1 ) { $this->delete_by_ad_id( $ad->ID ); $count = 0; - foreach ($regions as $region) { - if ( empty( implode( $region ) ) ) { + foreach ( $regions as $region ) { + $data = $this->prepare_submitted_region( $region ); + + if ( empty( implode( $data ) ) ) { continue; } - if ($count < $max_regions) { - $data = array_map( 'trim', $region ); - $this->save( array_merge( array( 'ad_id' => $ad->ID ), $data ) ); + + if ( $count < $max_regions ) { + $this->save( array_merge( $data, array( 'ad_id' => $ad->ID ) ) ); } + ++$count; } } + + /** + * Keep only columns defined by the regions table. + * + * @since x.x + * + * @param array $region Region data. + * @return array Filtered region data. + */ + private function filter_region_columns( $region ) { + if ( ! is_array( $region ) ) { + return array(); + } + + $allowed_columns = array( + 'id', + 'ad_id', + 'country', + 'county', + 'state', + 'city', + 'region_id', + ); + + return array_intersect_key( $region, array_flip( $allowed_columns ) ); + } + + /** + * Prepare user-editable region fields for storage. + * + * @since x.x + * + * @param array $region Submitted region data. + * @return array Filtered and normalized region data. + */ + private function prepare_submitted_region( $region ) { + if ( ! is_array( $region ) ) { + return array(); + } + + $allowed_fields = array_flip( array( 'country', 'county', 'state', 'city' ) ); + $region = array_intersect_key( $region, $allowed_fields ); + $region = array_filter( $region, 'is_scalar' ); + + return array_map( 'trim', $region ); + } } From d91257f16456500b884d77f48e73c6dfcc9ff659 Mon Sep 17 00:00:00 2001 From: Sorin Marta Date: Thu, 16 Jul 2026 13:07:14 +0300 Subject: [PATCH 2/4] Improve review comments --- includes/regions-api.php | 96 +++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/includes/regions-api.php b/includes/regions-api.php index 36cccdeb3..e926de9ef 100644 --- a/includes/regions-api.php +++ b/includes/regions-api.php @@ -50,12 +50,13 @@ public function find_by_parent_name($parent_name, $parent_type, $type) { public function save( $region ) { $region = $this->filter_region_columns( stripslashes_deep( $region ) ); - if ( ! isset( $region['ad_id'] ) || empty( $region['ad_id'] ) ) { + $region['ad_id'] = absint( isset( $region['ad_id'] ) ? $region['ad_id'] : 0 ); + + if ( empty( $region['ad_id'] ) ) { return false; } - $region['ad_id'] = absint( $region['ad_id'] ); - $region_id = absint( awpcp_array_data( 'id', 0, $region ) ); + $region_id = intval( awpcp_array_data( 'id', 0, $region ) ); unset( $region['id'] ); @@ -68,6 +69,47 @@ public function save( $region ) { return $result !== false; } + /** + * Allowlist region array keys to valid DB columns only. + * + * @since x.x + * + * @param mixed $region Region data. + * + * @return array + */ + private function filter_region_columns( $region ) { + if ( ! is_array( $region ) ) { + return array(); + } + + $allowed = array( 'id', 'ad_id', 'country', 'county', 'state', 'city', 'region_id' ); + + return array_intersect_key( $region, array_flip( $allowed ) ); + } + + /** + * Sanitise a user-submitted region to editable fields only. + * + * @since x.x + * + * @param mixed $region Region data. + * + * @return array + */ + private function prepare_submitted_region( $region ) { + if ( ! is_array( $region ) ) { + return array(); + } + + $allowed = array( 'country', 'county', 'state', 'city', 'region_id' ); + $region = array_intersect_key( $region, array_flip( $allowed ) ); + + $region = array_filter( $region, 'is_scalar' ); + + return array_map( 'trim', $region ); + } + public function delete_by_ad_id($ad_id) { $result = $this->db->query( $this->db->prepare( "DELETE FROM " . AWPCP_TABLE_AD_REGIONS . " WHERE ad_id = %s", $ad_id ) ); return $result !== false; @@ -84,10 +126,10 @@ public function find_by_ad_id($ad_id) { } public function update_ad_regions( $ad, $regions, $max_regions = 1 ) { - // remove existing regions before adding the new ones $this->delete_by_ad_id( $ad->ID ); $count = 0; + foreach ( $regions as $region ) { $data = $this->prepare_submitted_region( $region ); @@ -102,50 +144,4 @@ public function update_ad_regions( $ad, $regions, $max_regions = 1 ) { ++$count; } } - - /** - * Keep only columns defined by the regions table. - * - * @since x.x - * - * @param array $region Region data. - * @return array Filtered region data. - */ - private function filter_region_columns( $region ) { - if ( ! is_array( $region ) ) { - return array(); - } - - $allowed_columns = array( - 'id', - 'ad_id', - 'country', - 'county', - 'state', - 'city', - 'region_id', - ); - - return array_intersect_key( $region, array_flip( $allowed_columns ) ); - } - - /** - * Prepare user-editable region fields for storage. - * - * @since x.x - * - * @param array $region Submitted region data. - * @return array Filtered and normalized region data. - */ - private function prepare_submitted_region( $region ) { - if ( ! is_array( $region ) ) { - return array(); - } - - $allowed_fields = array_flip( array( 'country', 'county', 'state', 'city' ) ); - $region = array_intersect_key( $region, $allowed_fields ); - $region = array_filter( $region, 'is_scalar' ); - - return array_map( 'trim', $region ); - } } From 3a88456cd07edf65109f1c8ec89fa73bdb40883f Mon Sep 17 00:00:00 2001 From: Sorin Marta Date: Mon, 20 Jul 2026 20:41:02 +0300 Subject: [PATCH 3/4] Stub maybe_unserialize in payment unit tests. Payment transaction constructor calls WordPress maybe_unserialize; Brain Monkey suite needs that stub or create_transaction fatals. --- tests/suite/Payments/Gateways/test-PayPalStandard.php | 1 + tests/suite/Payments/test-payment-transaction.php | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/suite/Payments/Gateways/test-PayPalStandard.php b/tests/suite/Payments/Gateways/test-PayPalStandard.php index eb6bdcfe6..39fd50ec0 100644 --- a/tests/suite/Payments/Gateways/test-PayPalStandard.php +++ b/tests/suite/Payments/Gateways/test-PayPalStandard.php @@ -117,6 +117,7 @@ public function test_verified_payment_with_mismatched_transaction_id_is_invalid( */ private function create_transaction() { Functions\when( 'awpcp_current_user_is_admin' )->justReturn( false ); + Functions\when( 'maybe_unserialize' )->returnArg(); $transaction = new AWPCP_Payment_Transaction( array( 'id' => 'transaction-id' ) ); $transaction->add_item( 1, 'Listing', 'Listing fee', AWPCP_Payment_Transaction::PAYMENT_TYPE_MONEY, 9.99 ); diff --git a/tests/suite/Payments/test-payment-transaction.php b/tests/suite/Payments/test-payment-transaction.php index 44b81d9f4..6c236ada8 100644 --- a/tests/suite/Payments/test-payment-transaction.php +++ b/tests/suite/Payments/test-payment-transaction.php @@ -46,6 +46,7 @@ public function test_pending_payment_can_complete_transaction() { */ private function create_transaction() { Functions\when( 'awpcp_current_user_is_admin' )->justReturn( false ); + Functions\when( 'maybe_unserialize' )->returnArg(); return new AWPCP_Payment_Transaction( array( 'id' => 'transaction-id' ) ); } From f99c17eda08e811c55b7429a6aac0c3b02ff0021 Mon Sep 17 00:00:00 2001 From: Sorin Marta Date: Mon, 20 Jul 2026 20:42:51 +0300 Subject: [PATCH 4/4] Stub payment helpers in transaction status tests. set_status walks the full verification chain, so tests need get_awpcp_option, awpcp_payments_api, and a money item before asserting payment-status gates. --- tests/suite/Payments/test-payment-transaction.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/suite/Payments/test-payment-transaction.php b/tests/suite/Payments/test-payment-transaction.php index 6c236ada8..fb0457b17 100644 --- a/tests/suite/Payments/test-payment-transaction.php +++ b/tests/suite/Payments/test-payment-transaction.php @@ -45,9 +45,19 @@ public function test_pending_payment_can_complete_transaction() { * @return AWPCP_Payment_Transaction */ private function create_transaction() { + $payments = Mockery::mock( 'AWPCP_PaymentsAPI' ); + $payments->shouldReceive( 'get_transaction_payment_method' ) + ->andReturn( (object) array( 'slug' => 'paypal-standard' ) ); + Functions\when( 'awpcp_current_user_is_admin' )->justReturn( false ); + Functions\when( 'awpcp_user_is_admin' )->justReturn( false ); Functions\when( 'maybe_unserialize' )->returnArg(); + Functions\when( 'get_awpcp_option' )->justReturn( false ); + Functions\when( 'awpcp_payments_api' )->justReturn( $payments ); + + $transaction = new AWPCP_Payment_Transaction( array( 'id' => 'transaction-id' ) ); + $transaction->add_item( 1, 'Listing', 'Listing fee', AWPCP_Payment_Transaction::PAYMENT_TYPE_MONEY, 9.99 ); - return new AWPCP_Payment_Transaction( array( 'id' => 'transaction-id' ) ); + return $transaction; } }