-
Notifications
You must be signed in to change notification settings - Fork 4
/
class-ajax.php
135 lines (113 loc) · 3.59 KB
/
class-ajax.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
/**
* Code to run on WooCommerce AJAX checkout.
*
* When "Place Order" is clicked, this should record the user's IP address and check they have not placed too many
* orders recently.
*
* @author BrianHenryIE <BrianHenryIE@gmail.com>
* @link https://BrianHenryIE.com
* @since 1.0.0
* @package brianhenryie/bh-wc-checkout-rate-limiter
*/
namespace BrianHenryIE\Checkout_Rate_Limiter\WooCommerce;
use BrianHenryIE\Checkout_Rate_Limiter\RateLimit\Rate;
use BrianHenryIE\Checkout_Rate_Limiter\Settings_Interface;
use BrianHenryIE\Checkout_Rate_Limiter\WP_Rate_Limiter\WordPress_Rate_Limiter;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
/**
* Hooked on wc_ajax_checkout earlier than WooCommerce's own processing code.
*
* @see WordPress_RateLimiter
*
* Class Ajax
* @package brianhenryie/bh-wc-checkout-rate-limiter
*/
class Ajax {
use LoggerAwareTrait;
/**
* The plugin's settings.
*
* @var Settings_Interface
*/
protected Settings_Interface $settings;
/**
* Instantiate.
*
* @param Settings_Interface $settings The plugin settings.
* @param LoggerInterface $logger PSR logger.
*/
public function __construct( Settings_Interface $settings, LoggerInterface $logger ) {
$this->settings = $settings;
$this->setLogger( $logger );
}
/**
* On rate limit exceeded, return a 429 JSON error to the client.
* On success, function returns so the checkout can be processed as normal.
*
* No `Retry-After` header is added, since this is targeted at the WooCommerce AJAX checkout.
*
* @hooked wc_ajax_checkout
*/
public function rate_limit_checkout(): void {
if ( ! $this->settings->is_enabled() ) {
$this->logger->debug( 'Not enabled / no limits set' );
return;
}
$limits = $this->settings->get_checkout_rate_limits();
if ( empty( $limits ) ) {
$this->logger->debug( 'No limits set' );
return;
}
$ip_address = \WC_Geolocation::get_ip_address();
$block = false;
foreach ( $limits as $interval => $allowed_access_count ) {
$this->logger->debug( "Checking {$ip_address} rate limit {$allowed_access_count} per {$interval} seconds." );
$rate = Rate::custom( $allowed_access_count, $interval );
$rate_limiter = new WordPress_Rate_Limiter( $rate, 'checkout' );
try {
$status = $rate_limiter->limitSilently( $ip_address );
} catch ( \RuntimeException $e ) {
$this->logger->error(
'Rate Limiter encountered an error when storing the access count.',
array(
'exception' => $e,
)
);
// The behaviour here on an error is to NOT rate-limit.
continue;
}
/**
* TODO: Log the $_REQUEST data (but remove credit card details).
*
* @see WC_Checkout::get_posted_data()
*/
if ( $status->limitExceeded() ) {
$this->logger->notice(
"{$ip_address} blocked with {$status->getRemainingAttempts()} remaining attempts for rate limit {$allowed_access_count} per {$interval} seconds.",
array(
'interval' => $interval,
'allowed_access_count' => $allowed_access_count,
'status' => $status,
'ip_address' => $ip_address,
)
);
$block = true;
} else {
$this->logger->debug(
"{$ip_address} allowed with {$status->getRemainingAttempts()} remaining attempts for rate limit {$allowed_access_count} per {$interval} seconds.",
array(
'interval' => $interval,
'allowed_access_count' => $allowed_access_count,
'status' => $status,
)
);
}
}
if ( $block ) {
// No real point adding headers here.
wp_send_json_error( null, 429 );
}
}
}