Merge pull request #30960 from woocommerce/fix/27103-rate-limiter
Store WC_Rate_Limit Entries in a Custom Table
This commit is contained in:
commit
fa0464930c
|
@ -165,6 +165,10 @@ class WC_Install {
|
|||
'wc_update_560_create_refund_returns_page',
|
||||
'wc_update_560_db_version',
|
||||
),
|
||||
'6.0.0' => array(
|
||||
'wc_update_600_migrate_rate_limit_options',
|
||||
'wc_update_600_db_version',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -524,6 +528,7 @@ class WC_Install {
|
|||
wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' );
|
||||
wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
|
||||
wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' );
|
||||
wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' );
|
||||
|
||||
$ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+';
|
||||
|
||||
|
@ -545,6 +550,7 @@ class WC_Install {
|
|||
wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' );
|
||||
wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' );
|
||||
wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' );
|
||||
wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_rate_limits' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1039,6 +1045,13 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
|
|||
`timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (`order_id`, `product_id`)
|
||||
) $collate;
|
||||
CREATE TABLE {$wpdb->prefix}woocommerce_rate_limits (
|
||||
rate_limit_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
rate_limit_key varchar(200) NOT NULL,
|
||||
rate_limit_expiry BIGINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (rate_limit_id),
|
||||
UNIQUE KEY rate_limit_key (rate_limit_key($max_index_length))
|
||||
) $collate;
|
||||
";
|
||||
|
||||
|
@ -1074,6 +1087,7 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
|
|||
"{$wpdb->prefix}woocommerce_tax_rate_locations",
|
||||
"{$wpdb->prefix}woocommerce_tax_rates",
|
||||
"{$wpdb->prefix}wc_reserved_stock",
|
||||
"{$wpdb->prefix}woocommerce_rate_limits",
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,13 +32,57 @@ defined( 'ABSPATH' ) || exit;
|
|||
class WC_Rate_Limiter {
|
||||
|
||||
/**
|
||||
* Constructs Option name from action identifier.
|
||||
* Cache group.
|
||||
*/
|
||||
const CACHE_GROUP = 'wc_rate_limit';
|
||||
|
||||
/**
|
||||
* Hook in methods.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'woocommerce_cleanup_rate_limits', array( __CLASS__, 'cleanup' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs key name from action identifier.
|
||||
* Left in for backwards compatibility.
|
||||
*
|
||||
* @param string $action_id Identifier of the action.
|
||||
* @return string
|
||||
*/
|
||||
public static function storage_id( $action_id ) {
|
||||
return 'woocommerce_rate_limit_' . $action_id;
|
||||
return $action_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cache prefix.
|
||||
*
|
||||
* @param string $action_id Identifier of the action.
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_cache_key( $action_id ) {
|
||||
return WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $action_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached rate limit.
|
||||
*
|
||||
* @param string $action_id Identifier of the action.
|
||||
* @return bool|int
|
||||
*/
|
||||
protected static function get_cached( $action_id ) {
|
||||
return wp_cache_get( self::get_cache_key( $action_id ), self::CACHE_GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a rate limit.
|
||||
*
|
||||
* @param string $action_id Identifier of the action.
|
||||
* @param int $expiry Timestamp when the limit expires.
|
||||
* @return bool
|
||||
*/
|
||||
protected static function set_cache( $action_id, $expiry ) {
|
||||
return wp_cache_set( self::get_cache_key( $action_id ), $expiry, self::CACHE_GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,10 +92,27 @@ class WC_Rate_Limiter {
|
|||
* @return bool
|
||||
*/
|
||||
public static function retried_too_soon( $action_id ) {
|
||||
$next_try_allowed_at = get_option( self::storage_id( $action_id ) );
|
||||
global $wpdb;
|
||||
|
||||
$next_try_allowed_at = self::get_cached( $action_id );
|
||||
|
||||
if ( false === $next_try_allowed_at ) {
|
||||
$next_try_allowed_at = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT rate_limit_expiry
|
||||
FROM {$wpdb->prefix}woocommerce_rate_limits
|
||||
WHERE rate_limit_key = %s
|
||||
",
|
||||
$action_id
|
||||
)
|
||||
);
|
||||
|
||||
self::set_cache( $action_id, $next_try_allowed_at );
|
||||
}
|
||||
|
||||
// No record of action running, so action is allowed to run.
|
||||
if ( false === $next_try_allowed_at ) {
|
||||
if ( null === $next_try_allowed_at ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -72,8 +133,41 @@ class WC_Rate_Limiter {
|
|||
* @return bool True if the option setting was successful, false otherwise.
|
||||
*/
|
||||
public static function set_rate_limit( $action_id, $delay ) {
|
||||
$option_name = self::storage_id( $action_id );
|
||||
global $wpdb;
|
||||
|
||||
$next_try_allowed_at = time() + $delay;
|
||||
return update_option( $option_name, $next_try_allowed_at );
|
||||
|
||||
$result = $wpdb->replace(
|
||||
$wpdb->prefix . 'woocommerce_rate_limits',
|
||||
array(
|
||||
'rate_limit_key' => $action_id,
|
||||
'rate_limit_expiry' => $next_try_allowed_at,
|
||||
),
|
||||
array( '%s', '%d' )
|
||||
);
|
||||
|
||||
self::set_cache( $action_id, $next_try_allowed_at );
|
||||
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired rate limits from the database and clear caches.
|
||||
*/
|
||||
public static function cleanup() {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->prefix}woocommerce_rate_limits WHERE rate_limit_expiry < %d",
|
||||
time()
|
||||
)
|
||||
);
|
||||
|
||||
if ( class_exists( 'WC_Cache_Helper' ) ) {
|
||||
WC_Cache_Helper::invalidate_cache_group( self::CACHE_GROUP );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WC_Rate_Limiter::init();
|
||||
|
|
|
@ -2297,3 +2297,41 @@ function wc_update_560_create_refund_returns_page() {
|
|||
function wc_update_560_db_version() {
|
||||
WC_Install::update_db_version( '5.6.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate rate limit options to the new table.
|
||||
*
|
||||
* See @link https://github.com/woocommerce/woocommerce/issues/27103.
|
||||
*/
|
||||
function wc_update_600_migrate_rate_limit_options() {
|
||||
global $wpdb;
|
||||
|
||||
$rate_limits = $wpdb->get_results(
|
||||
"
|
||||
SELECT option_name, option_value
|
||||
FROM $wpdb->options
|
||||
WHERE option_name LIKE 'woocommerce_rate_limit_add_payment_method_%'
|
||||
",
|
||||
ARRAY_A
|
||||
);
|
||||
$prefix_length = strlen( 'woocommerce_rate_limit_' );
|
||||
|
||||
foreach ( $rate_limits as $rate_limit ) {
|
||||
$new_delay = (int) $rate_limit['option_value'] - time();
|
||||
|
||||
// Migrate the limit if it hasn't expired yet.
|
||||
if ( 0 < $new_delay ) {
|
||||
$action_id = substr( $rate_limit['option_name'], $prefix_length );
|
||||
WC_Rate_Limiter::set_rate_limit( $action_id, $new_delay );
|
||||
}
|
||||
|
||||
delete_option( $rate_limit['option_name'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DB version to 6.0.0.
|
||||
*/
|
||||
function wc_update_600_db_version() {
|
||||
WC_Install::update_db_version( '6.0.0' );
|
||||
}
|
||||
|
|
|
@ -37,16 +37,69 @@ class WC_Tests_Rate_Limiter extends WC_Unit_Test_Case {
|
|||
$rate_limit_id_1 = $action_identifier . $user_1_id;
|
||||
$rate_limit_id_2 = $action_identifier . $user_2_id;
|
||||
|
||||
WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, 1 );
|
||||
WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, 0 );
|
||||
|
||||
$this->assertEquals( true, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon allowed action to run too soon before the delay.' );
|
||||
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user before the delay.' );
|
||||
|
||||
// As retired_too_soon bails if current time <= limit, the actual time needs to be at least 1 second after the limit.
|
||||
sleep( 2 );
|
||||
sleep( 1 );
|
||||
|
||||
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon did not allow action to run after the designated delay.' );
|
||||
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user after the designated delay.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test setting the limit and running rate limited actions when missing cache.
|
||||
*/
|
||||
public function test_rate_limit_limits_without_cache() {
|
||||
$action_identifier = 'action_1';
|
||||
$user_1_id = 10;
|
||||
$user_2_id = 15;
|
||||
|
||||
$rate_limit_id_1 = $action_identifier . $user_1_id;
|
||||
$rate_limit_id_2 = $action_identifier . $user_2_id;
|
||||
|
||||
WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, 0 );
|
||||
// Clear cached value for user 1.
|
||||
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_1 ), WC_Rate_Limiter::CACHE_GROUP );
|
||||
|
||||
$this->assertEquals( true, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon allowed action to run too soon before the delay.' );
|
||||
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user before the delay.' );
|
||||
|
||||
// Clear cached values for both users.
|
||||
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_1 ), WC_Rate_Limiter::CACHE_GROUP );
|
||||
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_2 ), WC_Rate_Limiter::CACHE_GROUP );
|
||||
|
||||
// As retired_too_soon bails if current time <= limit, the actual time needs to be at least 1 second after the limit.
|
||||
sleep( 1 );
|
||||
|
||||
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon did not allow action to run after the designated delay.' );
|
||||
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user after the designated delay.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that rate limit option migration only processes unexpired limits.
|
||||
*/
|
||||
public function test_rate_limit_option_migration() {
|
||||
global $wpdb;
|
||||
|
||||
// Add some options to be migrated.
|
||||
add_option( 'woocommerce_rate_limit_add_payment_method_123', time() + 1000 );
|
||||
add_option( 'woocommerce_rate_limit_add_payment_method_234', time() - 1 );
|
||||
|
||||
// Run the migration function.
|
||||
include_once WC_Unit_Tests_Bootstrap::instance()->plugin_dir . '/includes/wc-update-functions.php';
|
||||
wc_update_600_migrate_rate_limit_options();
|
||||
|
||||
// Ensure that only the _123 limit was migrated.
|
||||
$migrated = $wpdb->get_col( "SELECT rate_limit_key FROM {$wpdb->prefix}woocommerce_rate_limits" );
|
||||
|
||||
$this->assertCount( 1, $migrated );
|
||||
$this->assertEquals( 'add_payment_method_123', $migrated[0] );
|
||||
|
||||
// Verify that all rate limit options were deleted.
|
||||
$this->assertFalse( get_option( 'woocommerce_rate_limit_add_payment_method_123' ) );
|
||||
$this->assertFalse( get_option( 'woocommerce_rate_limit_add_payment_method_234' ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' );
|
|||
wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' );
|
||||
wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
|
||||
wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' );
|
||||
wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' );
|
||||
|
||||
/*
|
||||
* Only remove ALL product and page data if WC_REMOVE_ALL_DATA constant is set to true in user's
|
||||
|
|
Loading…
Reference in New Issue