PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.5.1
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.5.1
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 9 months ago class-api-for-exact-webhooks.php 9 months ago class-api-for-product-webhook.php 9 months ago class-api-for-shipping-status.php 9 months ago class-api-for-woo-order.php 9 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 9 months ago index.php 1 year ago trait-api-permission.php 9 months ago
trait-api-permission.php
314 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 $queue_item = array(
172 'ip' => $client_ip,
173 'timestamp' => $current_time,
174 'data' => $_POST, // Store request data.
175 );
176 $current_queue[] = $queue_item;
177 update_option( $queue_key, $current_queue );
178
179 // Schedule processing of queue if not already scheduled.
180 if ( ! wp_next_scheduled( 'cmbird_process_webhook_queue' ) ) {
181 wp_schedule_single_event( $current_time + 30, 'cmbird_process_webhook_queue' );
182 }
183
184 return new WP_Error(
185 'request_queued',
186 'Request rate limit reached. Your request has been queued and will be processed shortly.',
187 array( 'status' => 202 ) // Accepted.
188 );
189 } else {
190 return new WP_Error(
191 'queue_full',
192 'Rate limit exceeded and queue is full. Please try again later.',
193 array( 'status' => 503 ) // Service Unavailable.
194 );
195 }
196 }
197
198 // Add current request to counters.
199 $ip_requests[] = $current_time;
200 $global_requests[] = $current_time;
201
202 // Store updated counters with expiration.
203 set_transient( $ip_key, $ip_requests, $window_size );
204 set_transient( $global_key, $global_requests, $window_size );
205
206 return true;
207 }
208
209 /**
210 * Rate limiting implementation
211 * Limits requests per IP and globally
212 */
213 private function check_rate_limit() {
214 $current_time = time();
215 $window_size = 60; // 1 minute window.
216 $max_requests_per_ip = 30; // Per IP limit.
217 $max_requests_global = 100; // Global limit.
218
219 // Get client IP.
220 $client_ip = $this->get_client_ip();
221 $ip_key = 'cmbird_rate_limit_ip_' . md5( $client_ip );
222 $global_key = 'cmbird_rate_limit_global';
223
224 // Check IP-based rate limit.
225 $ip_requests = get_transient( $ip_key );
226 if ( false === $ip_requests ) {
227 $ip_requests = array();
228 }
229
230 // Clean old requests (outside window).
231 $ip_requests = array_filter(
232 $ip_requests,
233 function ( $timestamp ) use ( $current_time, $window_size ) {
234 return ( $current_time - $timestamp ) < $window_size;
235 }
236 );
237
238 // Check if IP limit exceeded.
239 if ( count( $ip_requests ) >= $max_requests_per_ip ) {
240 return new WP_Error(
241 'rate_limit_exceeded',
242 'Rate limit exceeded for this IP. Maximum ' . $max_requests_per_ip . ' requests per minute allowed.',
243 array( 'status' => 429 )
244 );
245 }
246
247 // Check global rate limit.
248 $global_requests = get_transient( $global_key );
249 if ( false === $global_requests ) {
250 $global_requests = array();
251 }
252
253 // Clean old requests (outside window).
254 $global_requests = array_filter(
255 $global_requests,
256 function ( $timestamp ) use ( $current_time, $window_size ) {
257 return ( $current_time - $timestamp ) < $window_size;
258 }
259 );
260
261 // Check if global limit exceeded.
262 if ( count( $global_requests ) >= $max_requests_global ) {
263 return new WP_Error(
264 'global_rate_limit_exceeded',
265 'Global rate limit exceeded. Maximum ' . $max_requests_global . ' requests per minute allowed.',
266 array( 'status' => 429 )
267 );
268 }
269
270 // Add current request to counters.
271 $ip_requests[] = $current_time;
272 $global_requests[] = $current_time;
273
274 // Store updated counters with expiration.
275 set_transient( $ip_key, $ip_requests, $window_size );
276 set_transient( $global_key, $global_requests, $window_size );
277
278 return true;
279 }
280
281 /**
282 * Get client IP address safely
283 */
284 private function get_client_ip() {
285 // Check for various headers that might contain the real IP.
286 $ip_headers = array(
287 'HTTP_X_FORWARDED_FOR',
288 'HTTP_X_REAL_IP',
289 'HTTP_CLIENT_IP',
290 'HTTP_X_FORWARDED',
291 'HTTP_FORWARDED_FOR',
292 'HTTP_FORWARDED',
293 'REMOTE_ADDR',
294 );
295
296 foreach ( $ip_headers as $header ) {
297 if ( ! empty( $_SERVER[ $header ] ) ) {
298 $ip = $_SERVER[ $header ];
299 // Handle comma-separated IPs (X-Forwarded-For).
300 if ( strpos( $ip, ',' ) !== false ) {
301 $ip = trim( explode( ',', $ip )[0] );
302 }
303 // Validate IP.
304 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
305 return $ip;
306 }
307 }
308 }
309
310 // Fallback to REMOTE_ADDR.
311 return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
312 }
313 }
314