Skip to content

Commit

Permalink
feat: rate limit adding new payment methods by user (#3679)
Browse files Browse the repository at this point in the history
* feat: rate limit checkout attempts

* feat: lock rate limiting behind env constant

* fix: bail early if 0 second delay to avoid non-expiring transient

* feat: rate limit adding new payment methods by user

* fix: return error if rate limited

* fix: verify nonce to allow get_current_user_id()

* chore: destructure all properties of window.newspack_my_account
  • Loading branch information
dkoo authored Feb 19, 2025
1 parent 32c10da commit 0fd5ea5
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Newspack;

use Newspack\Reader_Activation;
use Newspack\WooCommerce_Connection;
use WP_Error;

defined( 'ABSPATH' ) || exit;
Expand All @@ -28,6 +29,7 @@ class WooCommerce_My_Account {
* @codeCoverageIgnore
*/
public static function init() {
\add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] );
\add_filter( 'woocommerce_account_menu_items', [ __CLASS__, 'my_account_menu_items' ], 1000 );
\add_filter( 'woocommerce_default_address_fields', [ __CLASS__, 'required_address_fields' ] );
\add_filter( 'woocommerce_billing_fields', [ __CLASS__, 'required_address_fields' ] );
Expand Down Expand Up @@ -56,6 +58,36 @@ public static function init() {
}
}

/**
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
NEWSPACK_API_NAMESPACE,
'/check-rate',
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'api_check_rate_limit' ],
'permission_callback' => '__return_true',
]
);
}

/**
* REST API handler for rate limit check.
*/
public static function api_check_rate_limit() {
$is_rate_limited = WooCommerce_Connection::rate_limit_by_user( 'add_payment_method', __( 'Please wait a moment before trying to add a new payment method.', 'newspack-plugin' ), true );
$response = [ 'success' => false ];
if ( ! \is_wp_error( $is_rate_limited ) && ! $is_rate_limited ) {
$response['success'] = true;
}
if ( \is_wp_error( $is_rate_limited ) ) {
$response['error'] = $is_rate_limited->get_error_message();
}
return new \WP_REST_Response( $response );
}

/**
* Enqueue front-end scripts.
*/
Expand All @@ -72,9 +104,12 @@ public static function enqueue_scripts() {
'my-account',
'newspack_my_account',
[
'labels' => [
'labels' => [
'cancel_subscription_message' => __( 'Are you sure you want to cancel this subscription?', 'newspack-plugin' ),
],
'rest_url' => get_rest_url(),
'should_rate_limit' => WooCommerce_Connection::rate_limiting_enabled(),
'nonce' => wp_create_nonce( 'wp_rest' ),
]
);
\wp_enqueue_style(
Expand Down
52 changes: 51 additions & 1 deletion includes/reader-revenue/my-account/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ function domReady( callback ) {

domReady( function () {
const cancelButton = document.querySelector( '.subscription_details .button.cancel' );
const { labels, nonce, rest_url, should_rate_limit } = newspack_my_account || {};

if ( cancelButton ) {
const confirmCancel = event => {
const message =
newspack_my_account?.labels?.cancel_subscription_message ||
labels?.cancel_subscription_message ||
'Are you sure you want to cancel this subscription?';

// eslint-disable-next-line no-alert
Expand All @@ -43,4 +44,53 @@ domReady( function () {
};
cancelButton.addEventListener( 'click', confirmCancel );
}

const addPaymentForm = document.getElementById( 'add_payment_method' );
if ( addPaymentForm && Boolean( should_rate_limit ) ) {
const errorContainer = document.querySelector( '.woocommerce-notices-wrapper' );
const submitButton = addPaymentForm.querySelector( 'input[type="submit"], button[type="submit"]' );
const rateLimit = function( e ) {
if ( addPaymentForm.hasAttribute( 'data-check-rate-limit' ) ) {
errorContainer.textContent = '';
submitButton.setAttribute( 'disabled', '' );
e.preventDefault();
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
// Return if the request is completed.
if ( xhr.readyState !== 4 ) {
return;
}

// Call onSuccess with parsed JSON if the request is successful.
if ( xhr.status >= 200 && xhr.status < 300 ) {
submitButton.removeAttribute( 'disabled' );
const data = JSON.parse( xhr.responseText );
if ( data?.success ) {
addPaymentForm.removeAttribute( 'data-check-rate-limit' );
addPaymentForm.requestSubmit( submitButton );
addPaymentForm.setAttribute( 'data-check-rate-limit', '1' );
}
if ( data?.error ) {
const error = document.createElement( 'div' );
const errorUl = document.createElement( 'ul' );
const errorLi = document.createElement( 'li' );
errorUl.classList.add( 'woocommerce-error' );
errorLi.textContent = data.error;
error.appendChild( errorUl );
errorUl.appendChild( errorLi );
errorContainer.appendChild( error );
errorContainer.scrollIntoView( { behavior: 'smooth' } );
}
}
};

xhr.open( 'GET', rest_url + 'newspack/v1/check-rate' );
xhr.setRequestHeader( 'X-WP-Nonce', nonce );
xhr.send();
}
};
addPaymentForm.setAttribute( 'data-check-rate-limit', '1' );
addPaymentForm.addEventListener( 'submit' , rateLimit, true );
submitButton.addEventListener( 'click', rateLimit, true );
}
} );
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ private static function get_client_ip() {
return null;
}

/**
* Check if rate limiting by user should be enabled for checkout and payment methods.
*
* @return bool
*/
public static function rate_limiting_enabled() {
return defined( 'NEWSPACK_CHECKOUT_RATE_LIMIT' ) && is_int( NEWSPACK_CHECKOUT_RATE_LIMIT ) && 0 !== NEWSPACK_CHECKOUT_RATE_LIMIT && class_exists( 'WC_Rate_Limiter' );
}

/**
* Check the rate limit for the current user or IP.
* Currently locked behind a NEWSPACK_CHECKOUT_RATE_LIMIT environment constant, for controlled rollout.
Expand All @@ -189,7 +198,7 @@ private static function get_client_ip() {
*/
public static function rate_limit_by_user( $action_name, $error_message = '', $return_error = false ) {
$rate_limited = false;
if ( ! defined( 'NEWSPACK_CHECKOUT_RATE_LIMIT' ) || ! class_exists( 'WC_Rate_Limiter' ) ) {
if ( ! self::rate_limiting_enabled() ) {
return $rate_limited;
}
if ( ! $error_message ) {
Expand Down

0 comments on commit 0fd5ea5

Please sign in to comment.