PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.7.5
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.7.5
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / includes / classes / apis / trait-api-permission.php
commercebird / includes / classes / apis Last commit date
class-api-for-cmbird.php 7 months ago class-api-for-exact-webhooks.php 5 months ago class-api-for-product-webhook.php 5 months ago class-api-for-shipping-status.php 9 months ago class-api-for-woo-order.php 4 months ago class-api-for-zoho-inventory.php 9 months ago class-commercebird-list-items-api-controller.php 10 months ago class-commercebird-media-api-controller.php 10 months ago class-commercebird-metadata-controller.php 5 months ago index.php 1 year ago trait-api-permission.php 7 months ago
trait-api-permission.php
317 lines
1 <?php
2
3 namespace CommerceBird\API;
4
5 use Exception;
6 // use CommerceBird\Admin\Actions\Ajax\ZohoInventoryAjax;
7 use CommerceBird\Admin\Actions\Ajax\SettingsAjax;
8 use WP_Error;
9 use WP_REST_Request;
10 use WP_REST_Response;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 trait Api {
17
18 private static string $namespace = 'v2';
19
20 private string $empty_response = 'No data found from webhook. check on ' . __METHOD__ . ' of ' . __CLASS__ . ' on line ' . __LINE__;
21
22 public static function endpoint(): string {
23 return get_rest_url() . self::$namespace . '/' . self::$endpoint;
24 }
25
26 public function permission_check() {
27 // Get all headers.
28 $headers = getallheaders();
29 // Check if the Authorization header is present.
30 if ( isset( $headers['Authorization'] ) ) {
31 $authorization = $headers['Authorization'];
32 if ( ! password_verify( 'commercebird-zi-webhook-token', $authorization ) ) {
33 return new WP_Error(
34 'rest_forbidden',
35 'You are not allowed to access this endpoint',
36 array(
37 'status' => 400,
38 'header' => $authorization,
39 )
40 );
41 }
42 }
43 $subscription = SettingsAjax::instance()->get_subscription_data();
44 if ( isset( $subscription['plan'] ) ) {
45 $subscription_plan = $subscription['plan'];
46 if ( stripos( $subscription_plan, 'Premium' ) === false ) {
47 return new WP_Error(
48 'rest_forbidden',
49 $subscription_plan,
50 array(
51 'status' => 403,
52 'data' => strpos( 'Premium', $subscription_plan ),
53 )
54 );
55 }
56 return true;
57 }
58 }
59 public function handle( WP_REST_Request $request ) {
60 // Rate limiting check with queuing support.
61 $rate_limit_response = $this->check_rate_limit_with_queue();
62 if ( is_wp_error( $rate_limit_response ) ) {
63 $response = new WP_REST_Response();
64 $response->set_data( $rate_limit_response->get_error_message() );
65 $response->set_status( 429 ); // Too Many Requests.
66 return rest_ensure_response( $response );
67 }
68
69 $response = new WP_REST_Response();
70 $response->set_data( $this->empty_response );
71 $response->set_status( 404 );
72 $data = $request->get_json_params();
73 if ( empty( $data ) ) {
74 return rest_ensure_response( $response );
75 }
76 if ( array_key_exists( 'JSONString', $data ) ) {
77 $data = str_replace( '\\', '', $data['JSONString'] );
78 }
79
80 // Create a unique key based on the payload.
81 $payload_hash = wp_hash( wp_json_encode( $data ) );
82 $lock_key = 'cmbird_processing_payload_' . $payload_hash;
83
84 // Check and set lock using update_option.
85 $lock_acquired = add_option( $lock_key, time() );
86 if ( ! $lock_acquired ) {
87 $response->set_data( 'Duplicate payload ignored. Already being processed.' );
88 $response->set_status( 200 );
89 return rest_ensure_response( $response );
90 }
91
92 // Process the payload.
93 if ( ! empty( $data ) ) {
94 try {
95 $response = $this->process( $data );
96 } catch ( Exception $exception ) {
97 $response->set_data( $exception->getMessage() );
98 $response->set_status( 500 );
99 }
100 }
101
102 // Clean up the lock after processing.
103 delete_option( $lock_key );
104 return rest_ensure_response( $response );
105 }
106
107 /**
108 * Rate limiting with queue support
109 * Queues requests when limits are reached instead of rejecting immediately
110 */
111 private function check_rate_limit_with_queue() {
112 $current_time = time();
113 $window_size = 60; // 1 minute window.
114 $max_requests_per_ip = 20; // Reduced per IP limit.
115 $max_requests_global = 60; // Reduced global limit.
116 $max_queue_size = 50; // Maximum queued requests.
117
118 // Get client IP.
119 $client_ip = $this->get_client_ip();
120 $ip_key = 'cmbird_rate_limit_ip_' . md5( $client_ip );
121 $global_key = 'cmbird_rate_limit_global';
122 $queue_key = 'cmbird_request_queue';
123
124 // Check current queue size.
125 $current_queue = get_option( $queue_key, array() );
126 $current_queue = is_array( $current_queue ) ? $current_queue : array();
127
128 // Clean expired queue items.
129 $current_queue = array_filter(
130 $current_queue,
131 function ( $item ) use ( $current_time ) {
132 return ( $current_time - $item['timestamp'] ) < 300; // 5 minute queue expiry.
133 }
134 );
135
136 // Check IP-based rate limit.
137 $ip_requests = get_transient( $ip_key );
138 if ( false === $ip_requests ) {
139 $ip_requests = array();
140 }
141
142 // Clean old requests (outside window).
143 $ip_requests = array_filter(
144 $ip_requests,
145 function ( $timestamp ) use ( $current_time, $window_size ) {
146 return ( $current_time - $timestamp ) < $window_size;
147 }
148 );
149
150 // Check global rate limit.
151 $global_requests = get_transient( $global_key );
152 if ( false === $global_requests ) {
153 $global_requests = array();
154 }
155
156 // Clean old requests (outside window).
157 $global_requests = array_filter(
158 $global_requests,
159 function ( $timestamp ) use ( $current_time, $window_size ) {
160 return ( $current_time - $timestamp ) < $window_size;
161 }
162 );
163
164 // Check if limits are exceeded.
165 $ip_limit_exceeded = count( $ip_requests ) >= $max_requests_per_ip;
166 $global_limit_exceeded = count( $global_requests ) >= $max_requests_global;
167
168 if ( $ip_limit_exceeded || $global_limit_exceeded ) {
169 // Try to queue the request if queue isn't full.
170 if ( count( $current_queue ) < $max_queue_size ) {
171 global $wp;
172 $queue_item = array(
173 'ip' => $client_ip,
174 'timestamp' => $current_time,
175 'request' => $wp->query_vars,
176 );
177 $current_queue[] = $queue_item;
178 update_option( $queue_key, $current_queue );
179
180 // Schedule processing of queue if not already scheduled.
181 if ( ! wp_next_scheduled( 'cmbird_process_webhook_queue' ) ) {
182 wp_schedule_single_event( $current_time + 30, 'cmbird_process_webhook_queue' );
183 }
184
185 return new WP_Error(
186 'request_queued',
187 'Request rate limit reached. Your request has been queued and will be processed shortly.',
188 array( 'status' => 202 ) // Accepted.
189 );
190 } else {
191 return new WP_Error(
192 'queue_full',
193 'Rate limit exceeded and queue is full. Please try again later.',
194 array( 'status' => 503 ) // Service Unavailable.
195 );
196 }
197 }
198
199 // Add current request to counters.
200 $ip_requests[] = $current_time;
201 $global_requests[] = $current_time;
202
203 // Store updated counters with expiration.
204 set_transient( $ip_key, $ip_requests, $window_size );
205 set_transient( $global_key, $global_requests, $window_size );
206
207 return true;
208 }
209
210 /**
211 * Rate limiting implementation
212 * Limits requests per IP and globally
213 */
214 private function check_rate_limit() {
215 $current_time = time();
216 $window_size = 60; // 1 minute window.
217 $max_requests_per_ip = 30; // Per IP limit.
218 $max_requests_global = 100; // Global limit.
219
220 // Get client IP.
221 $client_ip = $this->get_client_ip();
222 $ip_key = 'cmbird_rate_limit_ip_' . md5( $client_ip );
223 $global_key = 'cmbird_rate_limit_global';
224
225 // Check IP-based rate limit.
226 $ip_requests = get_transient( $ip_key );
227 if ( false === $ip_requests ) {
228 $ip_requests = array();
229 }
230
231 // Clean old requests (outside window).
232 $ip_requests = array_filter(
233 $ip_requests,
234 function ( $timestamp ) use ( $current_time, $window_size ) {
235 return ( $current_time - $timestamp ) < $window_size;
236 }
237 );
238
239 // Check if IP limit exceeded.
240 if ( count( $ip_requests ) >= $max_requests_per_ip ) {
241 return new WP_Error(
242 'rate_limit_exceeded',
243 'Rate limit exceeded for this IP. Maximum ' . $max_requests_per_ip . ' requests per minute allowed.',
244 array( 'status' => 429 )
245 );
246 }
247
248 // Check global rate limit.
249 $global_requests = get_transient( $global_key );
250 if ( false === $global_requests ) {
251 $global_requests = array();
252 }
253
254 // Clean old requests (outside window).
255 $global_requests = array_filter(
256 $global_requests,
257 function ( $timestamp ) use ( $current_time, $window_size ) {
258 return ( $current_time - $timestamp ) < $window_size;
259 }
260 );
261
262 // Check if global limit exceeded.
263 if ( count( $global_requests ) >= $max_requests_global ) {
264 return new WP_Error(
265 'global_rate_limit_exceeded',
266 'Global rate limit exceeded. Maximum ' . $max_requests_global . ' requests per minute allowed.',
267 array( 'status' => 429 )
268 );
269 }
270
271 // Add current request to counters.
272 $ip_requests[] = $current_time;
273 $global_requests[] = $current_time;
274
275 // Store updated counters with expiration.
276 set_transient( $ip_key, $ip_requests, $window_size );
277 set_transient( $global_key, $global_requests, $window_size );
278
279 return true;
280 }
281
282 /**
283 * Get client IP address safely
284 */
285 private function get_client_ip() {
286 // Check for various headers that might contain the real IP.
287 $ip_headers = array(
288 'HTTP_X_FORWARDED_FOR',
289 'HTTP_X_REAL_IP',
290 'HTTP_CLIENT_IP',
291 'HTTP_X_FORWARDED',
292 'HTTP_FORWARDED_FOR',
293 'HTTP_FORWARDED',
294 'REMOTE_ADDR',
295 );
296
297 foreach ( $ip_headers as $header ) {
298 if ( ! empty( $_SERVER[ $header ] ) ) {
299 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP address, validated below with filter_var.
300 $ip = $_SERVER[ $header ];
301 // Handle comma-separated IPs (X-Forwarded-For).
302 if ( strpos( $ip, ',' ) !== false ) {
303 $ip = trim( explode( ',', $ip )[0] );
304 }
305 // Validate IP.
306 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
307 return $ip;
308 }
309 }
310 }
311
312 // Fallback to REMOTE_ADDR.
313 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP address returned as-is.
314 return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
315 }
316 }
317