RateLimitStore
\Benecaster\Support\RateLimitStore
The shared fixed-window counter behind every rate limit in the plugin — today the REST limiter and the [benecaster_follower_signup] abuse throttle.
If you need a limit, call hit(). Do not hand-roll another get_transient / set_transient pair. That is what this class exists to prevent. The other limiter, Benecaster\REST\RateLimiter, only works for REST routes because it hooks rest_pre_dispatch and matches on route patterns — the follower signup form is a plain front-end POST and never passes through it. A third consumer is already anticipated. Three bespoke transient guards, each with its own off-by-one in the window reset, is the outcome this class was written to avoid.
Storage is transients, so an object cache (Redis, Memcached) is used automatically where one is present and the database is the fallback.
It is a fixed window, not a sliding one. Up to 2 × limit requests can land across a window boundary. That is the standard trade-off and is fine for abuse control. It is not fine if you need a hard guarantee, and this class is the wrong tool for that.
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
hit( string $key, int $limit, int $window ): bool |
Public | — | Records one request against a key and returns true when it is over quota and should be refused. The attempt is counted even when it is refused — otherwise a caller who keeps hammering gets a fresh allowance the moment the window rolls, which is the behaviour the limit exists to stop. A $limit or $window of 0 or less disables the limit entirely and returns false; on a public form that is not 'unlimited', it is no limit at all. |
seconds_until_reset( string $key, int $window ): int |
Public | — | Seconds until the window holding this key rolls over, or 0 if none does. For a Retry-After header, or for telling an admin when a block lifts. Never surface it to an anonymous submitter on a per-address limit — see hash(). |
hash( string $identifier ): string |
Public | — | Salted SHA-256 of an identifier, truncated to 32 hex characters. Everything personal MUST go through this before it becomes a key. Transient names land in wp_options on a site with no object cache, so a raw email address or IP in a key is that data sitting in the database in clear text, in a place nobody thinks to look when handling an erasure request. Uses the same benecaster_ip_hash_salt as the download and feed access logs. Truncation is deliberate too: long enough that collisions are not a practical concern, short enough to stay clear of the 172-character transient-name ceiling once a prefix and scope are added. |
client_ip(): string |
Public | — | The requesting client's IP as a plain string, filtered through benecaster_rate_limit_client_ip. REMOTE_ADDR only — X-Forwarded-For is not trusted and must not be trusted by default. Any client can send that header with any value, so honouring it turns every per-IP limit in the plugin into no limit at all, silently: the limiter still appears to work and every request just looks like a new visitor. |
Hooks Fired
Notes
Why client_ip() has a filter when other gates deliberately do not.
The safe default is wrong for a large minority of sites. Behind Cloudflare or any reverse proxy
REMOTE_ADDR is the proxy's address, so every visitor shares one bucket and a per-IP limit
becomes a site-wide one. A site in that position must opt in explicitly, and is responsible for
reading a header its own proxy sets and overwrites.
Do not read this as precedent for putting a knob in front of any other gate. It widens who counts as "one visitor"; it cannot lift a limit. The unlicensed follower cap shipped with a filter in front of it and had to have it removed, because an exposed knob turns a guard into configuration.
One override affects every per-IP limit in the plugin, not just the form you were thinking about when you added it.