PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.1
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.1
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
notificationx / includes / MCP / RateLimiter.php

RateLimiter.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar 3.3.1, at includes/MCP/RateLimiter.php

128 lines 3.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Per-IP rate limiter for the NotificationX MCP endpoint.
4 *
5 * Only *failed* credential attempts count toward the limit — a request that
6 * presents no credential (the normal OAuth opening probe) is never penalised.
7 * A fixed window is used so a stranded client can simply wait it out rather
8 * than being locked out indefinitely by its own retries.
9 *
10 * @package NotificationX\MCP
11 */
12
13 namespace NotificationX\MCP;
14
15 use NotificationX\GetInstance;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * @method static RateLimiter get_instance( $args = null )
23 */
24 class RateLimiter {
25
26 use GetInstance;
27
28 const PREFIX = 'nx_mcp_rl_';
29
30 /**
31 * Max failed attempts allowed inside the window.
32 *
33 * @return int
34 */
35 protected function max_fails() {
36 $max = defined( 'NOTIFICATIONX_MCP_MAX_FAILS' ) ? (int) NOTIFICATIONX_MCP_MAX_FAILS : 10;
37 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Prefixed with nx_ per NotificationX convention.
38 return (int) apply_filters( 'nx_mcp_rate_limit_max', $max );
39 }
40
41 /**
42 * Lockout window length in seconds.
43 *
44 * @return int
45 */
46 protected function window() {
47 $window = defined( 'NOTIFICATIONX_MCP_LOCKOUT_SECONDS' ) ? (int) NOTIFICATIONX_MCP_LOCKOUT_SECONDS : 900;
48 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Prefixed with nx_ per NotificationX convention.
49 return (int) apply_filters( 'nx_mcp_rate_limit_window', $window );
50 }
51
52 /**
53 * Client IP. REMOTE_ADDR only by default — forwarded headers are spoofable
54 * and only trusted if a site opts in via the filter.
55 *
56 * @return string
57 */
58 protected function client_ip() {
59 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
60 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Prefixed with nx_ per NotificationX convention.
61 return (string) apply_filters( 'nx_mcp_client_ip', $ip );
62 }
63
64 /**
65 * Transient key for the current client.
66 *
67 * @return string
68 */
69 protected function key() {
70 return self::PREFIX . md5( $this->client_ip() );
71 }
72
73 /**
74 * Whether the client is currently locked out.
75 *
76 * @return bool
77 */
78 public function is_locked() {
79 $state = get_transient( $this->key() );
80 return is_array( $state ) && ! empty( $state['count'] ) && $state['count'] >= $this->max_fails();
81 }
82
83 /**
84 * Seconds until the current lockout ends (0 if not locked).
85 *
86 * @return int
87 */
88 public function retry_after() {
89 $state = get_transient( $this->key() );
90 if ( is_array( $state ) && ! empty( $state['until'] ) ) {
91 return max( 0, (int) $state['until'] - time() );
92 }
93 return 0;
94 }
95
96 /**
97 * Record a failed authentication attempt. Fixed window: the expiry is set
98 * once when the window opens and not extended by later failures.
99 *
100 * @return void
101 */
102 public function record_failure() {
103 $key = $this->key();
104 $window = $this->window();
105 $state = get_transient( $key );
106
107 if ( ! is_array( $state ) || empty( $state['until'] ) || $state['until'] <= time() ) {
108 $state = array(
109 'count' => 0,
110 'until' => time() + $window,
111 );
112 }
113
114 $state['count']++;
115 $ttl = max( 1, $state['until'] - time() );
116 set_transient( $key, $state, $ttl );
117 }
118
119 /**
120 * Clear the lockout state (called after a successful authentication).
121 *
122 * @return void
123 */
124 public function clear() {
125 delete_transient( $this->key() );
126 }
127 }
128