PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
← All changes | includes/Classes/Handler.php +72 -5 2.4.0 → 2.4.1 View file →
@@ -370,14 +370,18 @@
370 370 if ( empty( $row ) ) {
371 371 return false;
372 372 }
373 373
374 - // IPN confirmed — return verified DB data.
374 + // IPN confirmed — return verified DB data. The payer's e-mail and the PayPal transaction
375 + // id only for an unguessable order id: before 2.4.0 ids were `paypal_` . uniqid(), which is
376 + // time-based and can be enumerated, so anyone could read them off the return page.
375 377 if ( $row->status === 'Completed' ) {
378 + $unguessable = self::paypal_order_id_is_unguessable( $item_number );
379 +
376 380 return [
377 381 'amount' => floatval( $row->amount ),
378 - 'email' => sanitize_email( $row->email ),
379 - 'transaction_id' => sanitize_text_field( $row->transaction_id ),
382 + 'email' => $unguessable ? sanitize_email( $row->email ) : '',
383 + 'transaction_id' => $unguessable ? sanitize_text_field( $row->transaction_id ) : '',
380 384 'currency' => sanitize_text_field( $row->currency ),
381 385 'method' => 'paypal',
382 386 ];
383 387 }
@@ -386,8 +390,34 @@
386 390 return [ 'pending' => true, 'order_id' => $item_number ];
387 391 }
388 392
389 393 /**
394 + * Whether a PayPal order id came from new_order_id() (24 random characters), not the
395 + * enumerable `paypal_` . uniqid() of releases before 2.4.0.
396 + *
397 + * @since 2.4.1
398 + *
399 + * @param mixed $order_id The order id.
400 + * @return bool
401 + */
402 + public static function paypal_order_id_is_unguessable( $order_id ) {
403 + return is_string( $order_id ) && 1 === preg_match( '/^paypal_[0-9a-z]{24}$/', $order_id );
404 + }
405 +
406 + /**
407 + * Whether an order id of any gateway came from new_order_id() (`{gateway}_` + 24 random
408 + * characters), not the enumerable `{gateway}_` . uniqid() of releases before 2.4.0.
409 + *
410 + * @since 2.4.1
411 + *
412 + * @param mixed $order_id The order id.
413 + * @return bool
414 + */
415 + public static function order_id_is_unguessable( $order_id ) {
416 + return is_string( $order_id ) && 1 === preg_match( '/^[a-z0-9-]+_[0-9a-z]{24}$/', $order_id );
417 + }
418 +
419 + /**
390 420 * AJAX: check whether a PayPal order has been confirmed by IPN.
391 421 *
392 422 * Called by the frontend polling loop after the user lands on the return URL
393 423 * before the IPN fires. Returns {status:'pending'} or {status:'completed'}.
@@ -472,8 +502,35 @@
472 502 exit;
473 503 }
474 504
475 505 /**
506 + * Whether a notification's PayPal environment matches the order's.
507 + *
508 + * Orders since 2.4.1 record `paypal_mode` (`live` | `sandbox`) when created, and must match
509 + * exactly. An older order has no record, so a live notification is accepted (a real payment
510 + * must never be stranded) and a sandbox one only while the site's PayPal setting is not live.
511 + *
512 + * @since 2.4.1
513 + *
514 + * @param mixed $form_info The order's `form_fields_info`, unserialized.
515 + * @param string $ipn_mode `live` or `sandbox` (from the notification's `test_ipn`).
516 + * @return bool
517 + */
518 + public static function paypal_ipn_mode_matches( $form_info, $ipn_mode ) {
519 + $order_mode = is_array( $form_info ) && isset( $form_info['paypal_mode'] ) ? $form_info['paypal_mode'] : '';
520 +
521 + if ( 'live' === $order_mode || 'sandbox' === $order_mode ) {
522 + return $order_mode === $ipn_mode;
523 + }
524 +
525 + if ( 'live' === $ipn_mode ) {
526 + return true;
527 + }
528 +
529 + return 'yes' !== DB::get_settings( 'better_payment_settings_payment_paypal_live_mode' );
530 + }
531 +
532 + /**
476 533 * Apply a PayPal IPN to the order it names.
477 534 *
478 535 * Call ONLY with a notification handle_paypal_ipn() posted back and PayPal answered VERIFIED —
479 536 * nothing here authenticates the data. Changes nothing unless the notification is a
@@ -516,8 +573,16 @@
516 573 if ( $expected_email && $receiver && $expected_email !== $receiver ) {
517 574 return 'refused';
518 575 }
519 576
577 + // A sandbox notification is VERIFIED by PayPal's sandbox, which pays in play money to an
578 + // account anyone can create under any address. It must never confirm a live order.
579 + $ipn_mode = ! empty( $data['test_ipn'] ) ? 'sandbox' : 'live';
580 +
581 + if ( ! self::paypal_ipn_mode_matches( $form_info, $ipn_mode ) ) {
582 + return 'refused';
583 + }
584 +
520 585 // Validate the paid amount is not less than the expected order amount.
521 586 $paid_amount = ! empty( $data['mc_gross'] ) ? floatval( $data['mc_gross'] ) : 0;
522 587 if ( $paid_amount < floatval( $results->amount ) ) {
523 588 return 'refused';
@@ -1161,9 +1226,9 @@
1161 1226 "Cache-Control: no-cache",
1162 1227 );
1163 1228
1164 1229 $response = wp_safe_remote_get(
1165 - 'https://api.paystack.co/transaction/verify/'. $reference,
1230 + 'https://api.paystack.co/transaction/verify/' . rawurlencode( $reference ),
1166 1231 array(
1167 1232 'headers' => $header_info,
1168 1233 'timeout' => 70,
1169 1234 )
@@ -1829,9 +1894,11 @@
1829 1894 return [
1830 1895 'amount' => floatval( $result->amount ),
1831 1896 'currency' => sanitize_text_field( $result->currency ),
1832 1897 'currency_symbol' => $helper_obj->get_currency_symbol( $result->currency ),
1833 - 'transaction_id' => !empty( $result->transaction_id ) ? sanitize_text_field( $result->transaction_id ) : 'N/A',
1898 + // The order id is request data, and ids before 2.4.0 were `{gateway}_` . uniqid() —
1899 + // enumerable — so the gateway's transaction id is shown only for an unguessable id.
1900 + 'transaction_id' => ! empty( $result->transaction_id ) && self::order_id_is_unguessable( $order_id ) ? sanitize_text_field( $result->transaction_id ) : 'N/A',
1834 1901 'method' => sanitize_text_field( $result->source ),
1835 1902 ];
1836 1903 }
1837 1904