Initial commit: WooCommerce Email Discount Manager plugin
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Rate limiting for the wc-edm REST API.
|
||||
*
|
||||
* Uses a transient counter per identity (user ID when authenticated,
|
||||
* client IP otherwise) inside a rolling window.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple transient-backed request throttle.
|
||||
*/
|
||||
class WC_EDM_Rate_Limiter {
|
||||
|
||||
/**
|
||||
* Transient key prefix.
|
||||
*/
|
||||
const PREFIX = 'wc_edm_rl_';
|
||||
|
||||
/**
|
||||
* Whether rate limiting is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
/**
|
||||
* Filter whether the REST API is rate limited.
|
||||
*
|
||||
* @param bool $enabled Default true.
|
||||
*/
|
||||
return (bool) apply_filters( 'wc_edm_rate_limit_enabled', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum requests allowed per window.
|
||||
*
|
||||
* @param string $method HTTP method of the current request.
|
||||
* @return int
|
||||
*/
|
||||
public static function max_requests( $method = 'GET' ) {
|
||||
$default = self::is_write_method( $method ) ? 20 : 60;
|
||||
|
||||
/**
|
||||
* Filter the request allowance per window.
|
||||
*
|
||||
* Writes get a tighter default than reads.
|
||||
*
|
||||
* @param int $default Allowed requests.
|
||||
* @param string $method HTTP method.
|
||||
*/
|
||||
return max( 1, (int) apply_filters( 'wc_edm_rate_limit_max_requests', $default, $method ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the rolling window in seconds.
|
||||
*
|
||||
* @param string $method HTTP method of the current request.
|
||||
* @return int
|
||||
*/
|
||||
public static function window( $method = 'GET' ) {
|
||||
/**
|
||||
* Filter the rate limit window in seconds.
|
||||
*
|
||||
* @param int $seconds Default 60.
|
||||
* @param string $method HTTP method.
|
||||
*/
|
||||
return max( 1, (int) apply_filters( 'wc_edm_rate_limit_window', MINUTE_IN_SECONDS, $method ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a method mutates data.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_write_method( $method ) {
|
||||
return in_array( strtoupper( (string) $method ), array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity used to bucket requests.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function identity() {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( $user_id ) {
|
||||
return 'u' . $user_id;
|
||||
}
|
||||
|
||||
$ip = WC_EDM_Audit_Log::get_client_ip();
|
||||
|
||||
return 'i' . md5( $ip ? $ip : 'unknown' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this rate limit hit should be written to the audit log.
|
||||
*
|
||||
* A blocked caller that keeps hammering would otherwise write one log row
|
||||
* per rejected request, turning the audit log into an amplifier. Only the
|
||||
* first hit per identity per window is recorded.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_log_limit_hit() {
|
||||
$key = self::PREFIX . 'logged_' . self::identity();
|
||||
|
||||
if ( get_transient( $key ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_transient( $key, 1, self::window() );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a request and report whether the caller is over the limit.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @return array{limited:bool,count:int,max:int,retry_after:int}
|
||||
*/
|
||||
public static function check( $method = 'GET' ) {
|
||||
$max = self::max_requests( $method );
|
||||
$window = self::window( $method );
|
||||
|
||||
if ( ! self::is_enabled() ) {
|
||||
return array(
|
||||
'limited' => false,
|
||||
'count' => 0,
|
||||
'max' => $max,
|
||||
'retry_after' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
// The key carries the window slice, so each window gets its own
|
||||
// counter and expires on its own. Re-arming a single transient's TTL
|
||||
// on every request would instead extend the block indefinitely.
|
||||
$now = time();
|
||||
$slice = (int) floor( $now / $window );
|
||||
$retry_after = ( ( $slice + 1 ) * $window ) - $now;
|
||||
$bucket = self::is_write_method( $method ) ? 'w' : 'r';
|
||||
$key = self::PREFIX . $bucket . '_' . self::identity() . '_' . $slice;
|
||||
|
||||
$count = (int) get_transient( $key ) + 1;
|
||||
|
||||
set_transient( $key, $count, $window );
|
||||
|
||||
return array(
|
||||
'limited' => $count > $max,
|
||||
'count' => $count,
|
||||
'max' => $max,
|
||||
'retry_after' => max( 1, $retry_after ),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user