Skip to content

Instantly share code, notes, and snippets.

@ideadude
Created May 22, 2023 18:35
Show Gist options
  • Save ideadude/8a43edf81ce9bdcd9ec52f235573e268 to your computer and use it in GitHub Desktop.
Save ideadude/8a43edf81ce9bdcd9ec52f235573e268 to your computer and use it in GitHub Desktop.
Generic IP limiter class for WordPress.
<?php
// Check if this class has already been loaded.
if ( class_exists( 'Rate_Limiter' ) ) {
return;
}
/**
* RateLimiter Class
* Limit the number of times a user can perform an action within a given period of time.
*/
class Rate_Limiter {
private $action;
private $ip;
private $limit;
private $period; // in seconds
/**
* Constructor.
*
* @param string $action The action to limit.
* @param int $limit The number of times the action can be performed.
* @param int $period The period of time in seconds.
* @param string $ip The IP address of the user.
*/
public function __construct( $action, $limit, $period, $ip = null ) {
$this->action = $action;
$this->limit = apply_filters( 'rate_limiter_limit', $limit, $action );
$this->period = apply_filters( 'rate_limiter_period', $period, $action );
if ( empty( $ip ) ) {
$this->ip = $this->get_ip();
} else {
$this->ip = $ip;
}
}
/**
* Get the transient key to use for this action and ip combo.
*/
private function get_transient_key() {
if ( empty( $this->ip ) || empty( $this->action ) ) {
return false;
}
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $this->ip );
return 'rate_limiter_' . $this->action . "_" . $ip;
}
/**
* Get the limit.
*/
public function get_limit() {
return $this->limit;
}
/**
* Checks whether the current visitor is rate limited.
* If the limit is -1, then we don't limit.
* Otherwise we check the activity count vs the limit.
*/
public function is_rate_limited() {
$is_limited = false;
$activity = $this->get_activity();
if ( false !== $activity && $this->limit > -1 && count( $activity ) >= $this->limit ) {
$is_limited = true;
}
/**
* Allow filtering for this limiter.
*
* @param bool $is_limited Whether the current visitor is a limited.
* @param array $activity The list of potential spam activity.
*/
return apply_filters( 'rate_limiter_is_limited', $is_limited, $activity );
}
/**
* Tracks the activity for this limiter.
*/
public function track_activity() {
// If we can't determine the IP, let's bail.
if ( empty( $this->ip ) ) {
return false;
}
$activity = $this->get_activity();
$now = current_time( 'timestamp', true ); // UTC
array_unshift( $activity, $now );
// If we have more than the limit, don't bother storing them.
if ( count( $activity ) > $this->limit ) {
rsort( $activity );
$activity = array_slice( $activity, 0, max( $this->limit, 0 ) );
}
// Save to transient.
set_transient( $this->get_transient_key(), $activity, (int) $this->period );
return true;
}
/**
* Gets the activity for this limiter.
*/
public function get_activity() {
// If we can't determine the IP, let's bail.
if ( empty( $this->ip ) ) {
return false;
}
// Get the activity.
$activity = get_transient( $this->get_transient_key() );
if ( empty( $activity ) || ! is_array( $activity ) ) {
$activity = [];
}
// Remove old items.
$new_activity = [];
$now = current_time( 'timestamp', true ); // UTC
foreach( $activity as $item ) {
// Determine whether this item is recent enough to include.
if ( $item > $now-( $this->period ) ) {
$new_activity[] = $item;
}
}
return $new_activity;
}
/**
* Clears the activity for this limiter.
*/
public function clear_activity() {
// If we can't determine the IP, let's bail.
if ( empty( $this->ip ) ) {
return false;
}
delete_transient( $this->get_transient_key() );
return true;
}
/**
* Determines the user's actual IP address
*
* $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user
* is making their request through a proxy, or when the web server is behind
* a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather
* than the user's actual address.
*
* Copied from pmpro_get_ip() in Paid Memberships Pro.
* Modified from WP_Community_Events::get_unsafe_client_ip() in core WP.
* Modified from https://stackoverflow.com/a/2031935/450127, MIT license.
* Modified from https://github.com/geertw/php-ip-anonymizer, MIT license.
*
* SECURITY WARNING: This function is _NOT_ intended to be used in
* circumstances where the authenticity of the IP address matters. This does
* _NOT_ guarantee that the returned address is valid or accurate, and it can
* be easily spoofed.
*
*
* @return string|false The ip address on success or false on failure.
*/
public function get_ip() {
$client_ip = false;
// In order of preference, with the best ones for this purpose first.
// Added some from JetPack's Jetpack_Protect_Module::get_headers()
$address_headers = array(
'GD_PHP_HANDLER',
'HTTP_AKAMAI_ORIGIN_HOP',
'HTTP_CF_CONNECTING_IP',
'HTTP_CLIENT_IP',
'HTTP_FASTLY_CLIENT_IP',
'HTTP_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_INCAP_CLIENT_IP',
'HTTP_TRUE_CLIENT_IP',
'HTTP_X_CLIENTIP',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_X_FORWARDED',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_IP_TRAIL',
'HTTP_X_REAL_IP',
'HTTP_X_VARNISH',
'REMOTE_ADDR',
);
foreach ( $address_headers as $header ) {
if ( array_key_exists( $header, $_SERVER ) ) {
/*
* HTTP_X_FORWARDED_FOR can contain a chain of comma-separated
* addresses. The first one is the original client. It can't be
* trusted for authenticity, but we don't need to for this purpose.
*/
$address_chain = explode( ',', sanitize_text_field( $_SERVER[ $header ] ) );
$client_ip = trim( $address_chain[0] );
break;
}
}
if ( ! $client_ip ) {
return false;
}
// Sanitize the IP
$client_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $client_ip );
return $client_ip;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment