Ratelimiter
**Site relevance:** Spiralist.org
**Memory type:** plugin source memory
**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/includes/Services/RateLimiter.php
**Size:** 4.4 KB
Summary
namespace SpiralistWorkspace\Services;
Source Preview
This source file is short enough to preview directly on its source-memory page.
<?php
namespace SpiralistWorkspace\Services;
use SpiralistWorkspace\Config\Settings;
use SpiralistWorkspace\Domain\AuditRepository;
use SpiralistWorkspace\Domain\ConversationRepository;
use SpiralistWorkspace\Domain\RunRepository;
if (!defined('ABSPATH')) {
exit;
}
/**
* Applies daily per-role rate limits.
*/
class RateLimiter
{
private Settings $settings;
private RunRepository $runs;
private ConversationRepository $conversations;
private AuditRepository $audit;
public function __construct(Settings $settings, RunRepository $runs, ConversationRepository $conversations, AuditRepository $audit)
{
$this->settings = $settings;
$this->runs = $runs;
$this->conversations = $conversations;
$this->audit = $audit;
}
/**
* Enforces a daily run limit for the user.
*/
public function assert_can_run(int $user_id): bool
{
$limit = $this->resolve_limit($user_id);
if ($limit <= 0) {
return false;
}
if ($user_id <= 0) {
return $this->assert_can_run_anonymous($limit);
}
$count = $this->runs->count_for_today($user_id);
if ($count >= $limit) {
$this->audit->log($user_id, 'rate_limit_exceeded', 'run', null, ['count' => $count, 'limit' => $limit]);
return false;
}
return true;
}
/**
* Enforces a daily message limit for conversations.
*/
public function assert_can_message(int $user_id): bool
{
$limit = max(5, $this->resolve_limit($user_id) * 3);
$count = $this->conversations->count_messages_for_today($user_id);
if ($count >= $limit) {
$this->audit->log($user_id, 'message_rate_limit_exceeded', 'conversation', null, ['count' => $count, 'limit' => $limit]);
return false;
}
return true;
}
/**
* Resolves the daily limit based on role mapping.
*/
private function resolve_limit(int $user_id): int
{
$limits = $this->settings->get_daily_limits();
if ($user_id <= 0) {
return max(0, (int) ($limits['anonymous_public_runs'] ?? 0));
}
if (user_can($user_id, 'manage_spiralist_workspace') || user_can($user_id, 'manage_options')) {
return max(0, (int) ($limits['spiralist_admin'] ?? $limits['administrator'] ?? 200));
}
$user = get_userdata($user_id);
$roles = $user instanceof \WP_User ? (array) $user->roles : [];
foreach (['spiralist_curator', 'contributor', 'editor', 'spiralist_member', 'participant'] as $role_key) {
if (in_array($role_key, $roles, true) && isset($limits[$role_key])) {
return max(0, (int) $limits[$role_key]);
}
}
return max(0, (int) ($limits['spiralist_member'] ?? 20));
}
private function assert_can_run_anonymous(int $limit): bool
{
$key = $this->anonymous_run_bucket_key();
$count = max(0, (int) get_transient($key));
if ($count >= $limit) {
$this->audit->log(null, 'rate_limit_exceeded', 'run', null, ['count' => $count, 'limit' => $limit]);
return false;
}
set_transient($key, $count + 1, 2 * DAY_IN_SECONDS);
return true;
}
private function anonymous_run_bucket_key(): string
{
$identity = $this->anonymous_identity();
if ($identity === '') {
$identity = 'unknown';
}
return 'spiralist_workspace_anon_runs_' . gmdate('Ymd') . '_' . substr(hash('sha256', $identity), 0, 32);
}
private function anonymous_identity(): string
{
foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $server_key) {
$value = $_SERVER[$server_key] ?? '';
if (is_array($value)) {
$value = reset($value);
}
$value = sanitize_text_field((string) wp_unslash((string) $value));
if ($value === '') {
continue;
}
$parts = explode(',', $value);
$candidate = trim((string) ($parts[0] ?? ''));
if ($candidate !== '') {
return $candidate;
}
}
return '';
}
}