PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.6.0
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.6.0
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 / admin / includes / Traits / AjaxRequest.php
commercebird / admin / includes / Traits Last commit date
AjaxRequest.php 7 months ago LogWriter.php 10 months ago OptionStatus.php 10 months ago Singleton.php 1 year ago index.php 1 year ago
AjaxRequest.php
296 lines
1 <?php
2
3 namespace CommerceBird\Admin\Traits;
4
5 use CommerceBird\Admin\Template;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11 trait AjaxRequest {
12
13 /**
14 * Array to store registered AJAX requests.
15 *
16 * @var array
17 */
18 private array $request = array();
19
20 /**
21 * Array to store registered AJAX response.
22 *
23 * @var array
24 */
25 private array $response = array( 'message' => 'Saved' );
26
27 /**
28 * Array to store registered AJAX posted data.
29 *
30 * @var array
31 */
32 private array $data = array();
33
34 /**
35 * Array to store registered AJAX errors.
36 *
37 * @var array
38 */
39 private array $errors = array();
40
41 /**
42 * Load all registered AJAX actions
43 *
44 * Loops through the self::ACTIONS array and registers each action
45 * with WordPress using the add_action hook.
46 */
47 private function load_actions() {
48 foreach ( self::ACTIONS as $action => $handler ) {
49 add_action(
50 $this->action( $action ),
51 array( $this, $handler ),
52 );
53 }
54 }
55
56 /**
57 * Serve data to AJAX request.
58 */
59 private function serve(): void {
60 if ( count( $this->errors ) > 0 ) {
61 wp_send_json_error( $this->errors );
62 }
63
64 wp_send_json_success( $this->response );
65 }
66
67 /**
68 * Verify AJAX request with enhanced security.
69 *
70 * @param array $keys The keys to verify (optional).
71 */
72 private function verify( array $keys = array() ): void {
73 // Early exit: these Ajax handlers should never be called by logged-out users.
74 // WordPress wp_ajax_ actions are only for logged-in users, so this shouldn't happen.
75 if ( ! is_user_logged_in() ) {
76 // If we reach here, something is wrong with the action registration.
77 // Just exit silently to prevent frontend errors.
78 wp_die( '', '', array( 'response' => 200 ) );
79 }
80
81 // Check if the request is coming from the admin area (for logged-in users).
82 $referer = wp_get_referer();
83 if ( ! $referer || strpos( $referer, admin_url() ) !== 0 ) {
84 $this->errors = array( 'message' => 'Invalid request origin' );
85 $this->serve();
86 return;
87 }
88
89 // Enhanced CSRF protection with proper nonce verification.
90 $security_token = $_REQUEST['security_token'] ?? $_GET['security_token'] ?? '';
91 $nonce_action = Template::NAME;
92
93 if ( ! wp_verify_nonce( $security_token, $nonce_action ) ) {
94 $this->errors = array( 'message' => 'Security check failed' );
95 $this->serve();
96 return;
97 }
98
99 // Rate limiting check - only for actions that make Zoho API calls.
100 if ( $this->requires_zoho_api_call() && ! $this->check_zoho_rate_limit() ) {
101 $this->errors = array( 'message' => 'Zoho API rate limit exceeded. Please wait and try again.' );
102 $this->serve();
103 return;
104 }
105
106 // Capability check - ensure user has proper permissions (same as menu page).
107 if ( ! current_user_can( 'manage_woocommerce' ) ) {
108 $this->errors = array( 'message' => 'Insufficient permissions' );
109 $this->serve();
110 return;
111 }
112
113 // Initialize response and errors.
114 $this->response = array( 'success' => true );
115 $this->errors = array();
116 $this->request = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST ) );
117
118 // Attempt to retrieve JSON if POST is empty.
119 if ( empty( $_POST ) ) {
120 $contents = trim( file_get_contents( 'php://input' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended.
121
122 if ( $this->is_json( $contents ) ) {
123 $json_data = json_decode( $contents, true );
124
125 if ( ! empty( $json_data ) ) {
126 // Deep sanitization of JSON data.
127 $this->data = empty( $keys ) ? $this->sanitize_deep( $json_data ) : $this->extract_data( $this->sanitize_deep( $json_data ), $keys );
128 }
129 }
130 } else {
131 // Sanitize POST data.
132 $this->data = empty( $keys ) ? $this->sanitize_deep( $_POST ) : $this->extract_data( $this->sanitize_deep( $_POST ), $keys );
133 }
134
135 // Log security events for audit.
136 $this->log_security_event();
137 }
138
139 /**
140 * Utility to check if a string is JSON.
141 */
142 private function is_json( string $string ): bool {
143 json_decode( $string );
144 return json_last_error() === JSON_ERROR_NONE;
145 }
146
147 /**
148 * Extracts data from an array using the given keys.
149 *
150 * @param array $sanitized The array from which to extract data.
151 * @param array $keys The keys to use for extraction.
152 *
153 * @return array The extracted data.
154 */
155 public function extract_data( array $sanitized, array $keys ): array {
156 return array_intersect_key( $sanitized, array_flip( $keys ) );
157 }
158
159 /**
160 * Register AJAX actions.
161 *
162 * @param string $action The action to register.
163 * @return string The action name.
164 */
165 private function action( $action ): string {
166 return sprintf( 'wp_ajax_%s-%s', Template::NAME, $action );
167 }
168
169 /**
170 * Check if the current action requires Zoho API calls.
171 * Override this method in classes that need specific rate limiting rules.
172 *
173 * @return bool
174 */
175 protected function requires_zoho_api_call(): bool {
176 // Get the current action being processed.
177 $current_action = $_REQUEST['action'] ?? '';
178
179 // Actions that are internal AJAX calls (no Zoho API interaction).
180 $internal_ajax_actions = array(
181 'get_zoho_categories', // Fetches cached categories data.
182 'get_zoho_taxes', // Fetches cached tax data.
183 'get_zoho_locations', // Fetches cached location data.
184 'get_zoho_prices', // Fetches cached price data.
185 'get_wc_taxes', // WordPress internal data.
186 'save_zoho_', // All save operations are internal settings.
187 'get_zoho_connect', // Connection settings retrieval.
188 'reset_zoho_', // Reset operations are internal.
189 'is_connected', // Connection check uses cached data.
190 );
191
192 // Check if current action is internal (should NOT be rate limited).
193 foreach ( $internal_ajax_actions as $internal_action ) {
194 if ( strpos( $current_action, $internal_action ) !== false ) {
195 return false;
196 }
197 }
198
199 // Actions that make actual Zoho API calls (external requests - SHOULD be rate limited).
200 $zoho_api_actions = array(
201 'zoho_ajax_call_item', // Product sync to Zoho.
202 'zoho_ajax_call_variable_item_from_zoho', // Fetch variable products from Zoho.
203 'zoho_ajax_call_item_from_zoho', // Fetch simple products from Zoho.
204 'zoho_ajax_call_composite_item_from_zoho', // Fetch composite products from Zoho.
205 'zoho_ajax_call_composite_item', // Sync composite products to Zoho.
206 'zoho_ajax_call_parent_categories', // Sync parent categories (makes Zoho API calls).
207 'zoho_ajax_call_subcategories', // Sync subcategories (makes Zoho API calls).
208 'zoho_ajax_call_subcategories_start', // Start async subcategory sync.
209 'zoho_ajax_call_subcategories_batch', // Process subcategory sync batch.
210 'zoho_ajax_call_subcategories_status', // Get subcategory sync status.
211 'zoho_ajax_call_remove_duplicates', // Remove duplicates (makes Zoho API calls).
212 'import_zoho_contacts', // Contact sync from Zoho.
213 'convert_guest', // Convert guest customers (Zoho API calls).
214 );
215
216 // Check if current action makes Zoho API calls.
217 foreach ( $zoho_api_actions as $api_action ) {
218 if ( strpos( $current_action, $api_action ) !== false ) {
219 return true;
220 }
221 }
222
223 // Default to no rate limiting for unknown actions.
224 return false;
225 }
226
227 /**
228 * Check Zoho API rate limit (based on Zoho's own rate limiting).
229 *
230 * @return bool
231 */
232 private function check_zoho_rate_limit(): bool {
233 // Check if Zoho has indicated rate limit exceeded.
234 $zoho_rate_limit_exceeded = get_option( 'cmbird_zoho_rate_limit_exceeded', false );
235
236 if ( $zoho_rate_limit_exceeded ) {
237 // Check if enough time has passed since the rate limit was hit.
238 $rate_limit_time = get_option( 'cmbird_zoho_rate_limit_time', 0 );
239 $current_time = time();
240
241 // Zoho typically resets limits every minute, so wait 65 seconds to be safe.
242 if ( ( $current_time - $rate_limit_time ) < 65 ) {
243 return false;
244 } else {
245 // Enough time has passed, reset the rate limit flag.
246 update_option( 'cmbird_zoho_rate_limit_exceeded', false );
247 delete_option( 'cmbird_zoho_rate_limit_time' );
248 }
249 }
250
251 return true;
252 }
253
254 /**
255 * Deep sanitization of data arrays.
256 *
257 * @param mixed $data Data to sanitize.
258 * @return mixed Sanitized data.
259 */
260 private function sanitize_deep( $data ) {
261 if ( is_array( $data ) ) {
262 return array_map( array( $this, 'sanitize_deep' ), $data );
263 }
264
265 if ( is_string( $data ) ) {
266 return sanitize_text_field( $data );
267 }
268
269 return $data;
270 }
271
272 /**
273 * Log security events for audit trail.
274 */
275 private function log_security_event(): void {
276 $log_data = array(
277 'timestamp' => current_time( 'mysql' ),
278 'user_id' => get_current_user_id(),
279 'ip_address' => $_SERVER['REMOTE_ADDR'],
280 'action' => $_REQUEST['action'] ?? 'unknown',
281 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
282 );
283
284 // Store in transient for security monitoring.
285 $security_log = get_transient( 'commercebird_security_log' ) ?: array();
286 $security_log[] = $log_data;
287
288 // Keep only last 100 entries.
289 if ( count( $security_log ) > 100 ) {
290 $security_log = array_slice( $security_log, -100 );
291 }
292
293 set_transient( 'commercebird_security_log', $security_log, DAY_IN_SECONDS );
294 }
295 }
296