ContentCreationLimiter
in package
Throttles content creation in the API layer, equally for the front end (`ApiBridge`) and the external `:Api:*:save` with a JWT.
IMPORTANT: the throttle lives here, not in the form. reCAPTCHA and front limits sit in
components, so a logged-in user with a token (any registered user can call
/api/system/auth/login) would bypass them by calling the API directly. The manager is the
only place every insert passes through.
The IP comes from IRequest (the request of the visitor or client, the same PHP process),
never from the payload. In CLI it is null and only the user is counted.
Fail-open like the whole RateLimit: a storage outage does not block creation.
Table of Contents
Properties
- $audience : FieldAudienceContext
- $httpRequest : IRequest
- $rateLimit : RateLimit
- $rateLimitConfig : RateLimitConfig
Methods
- __construct() : mixed
- isActivationMailAllowed() : bool
- Returns whether another activation or verification email may be sent to `$subject` (normalized email).
- isAllowed() : bool
- Returns whether the user `$userId` may create another record of kind `$scope`.
- isAnonymousInsertAllowed() : bool
- Returns whether an anonymous visitor may create another record of kind `$scope`.
- resolveRemoteIp() : string|null
- hit() : bool
- isExemptCaller() : bool
- Returns whether the throttle does not apply.
Properties
$audience read-only
private
FieldAudienceContext
$audience
$httpRequest read-only
private
IRequest
$httpRequest
$rateLimit read-only
private
RateLimit
$rateLimit
$rateLimitConfig read-only
private
RateLimitConfig
$rateLimitConfig
Methods
__construct()
public
__construct(RateLimit $rateLimit, RateLimitConfig $rateLimitConfig, IRequest $httpRequest, FieldAudienceContext $audience) : mixed
Parameters
- $rateLimit : RateLimit
- $rateLimitConfig : RateLimitConfig
- $httpRequest : IRequest
- $audience : FieldAudienceContext
isActivationMailAllowed()
Returns whether another activation or verification email may be sent to `$subject` (normalized email).
public
isActivationMailAllowed(string $scope, string $subject) : bool
Applies to anonymous callers too, since it protects the recipient, not the sender
identity. Two dimensions, as in sendActivationEmail*: per IP (one attacker spraying
many addresses) and per address (a botnet targeting one mailbox).
Parameters
- $scope : string
- $subject : string
Return values
boolisAllowed()
Returns whether the user `$userId` may create another record of kind `$scope`.
public
isAllowed(string $scope, int|null $userId) : bool
An anonymous user ($userId null) always passes: anonymous paths have their own
protection (reCAPTCHA, per-IP limits of individual managers) and this throttle does not
count them. The external API cannot create without a token (jwt.guardMode: enforce,
save actions are not #[PublicAction]).
Parameters
- $scope : string
-
content kind that separates quotas (
bazaar-response,discussion-message) - $userId : int|null
Return values
boolisAnonymousInsertAllowed()
Returns whether an anonymous visitor may create another record of kind `$scope`.
public
isAnonymousInsertAllowed(string $scope, string $subject) : bool
Complements isAllowed(), which does not count anonymous users, for anonymous
paths where reCAPTCHA is not enough (it can be disabled in settings or bypassed),
typically the contact form. Two dimensions: per IP (from the server, never from the
payload; only a hash is in the key) and per normalized $subject (form email, lowercase
and trimmed). An empty subject is not counted, because its key would be shared by the
whole site. Uses its own config keys ratelimit.anonymousInsert; the activation email
quota is not borrowed. Exemptions (privileged or unknown context) and fail-open are the
same as in the other methods.
Parameters
- $scope : string
- $subject : string
Return values
boolresolveRemoteIp()
public
resolveRemoteIp() : string|null
Return values
string|nullhit()
private
hit(string $key, int $limit, int $window) : bool
Parameters
- $key : string
- $limit : int
- $window : int
Return values
boolisExemptCaller()
Returns whether the throttle does not apply.
private
isExemptCaller() : bool
Decided by the context that only the guard fills (FieldAudienceContext), never by the request:
- a privileged caller (an admin holding a role with
privileged = 1): manual admin work (address import, support replies) must not hit a throttle meant for a script with a registered user's token; - an unknown context (
isResolved() === false): the guard was not evaluated at all, e.g. the emergency modeapiRbac.internalMode: off(admin without limits), cron, CLI. Same boundary as the field filter: unknown means no filtering.
In production both modes are enforce, so on the API path the context is always set and
every unprivileged caller is throttled.