PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.5.2
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.5.2
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 9 months ago LogWriter.php 10 months ago OptionStatus.php 10 months ago Singleton.php 1 year ago index.php 1 year ago
AjaxRequest.php
206 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 // Array to store registered AJAX requests.
14 private array $request = array();
15 // Array to store registered AJAX response.
16 private array $response = array( 'message' => 'Saved' );
17 // Array to store registered AJAX posted data.
18 private array $data = array();
19 // Array to store registered AJAX errors.
20 private array $errors = array();
21
22 private function load_actions() {
23 foreach ( self::ACTIONS as $action => $handler ) {
24 add_action(
25 $this->action( $action ),
26 array( $this, $handler ),
27 );
28 }
29 }
30
31 /**
32 * Serve data to AJAX request.
33 */
34 private function serve(): void {
35 if ( count( $this->errors ) > 0 ) {
36 wp_send_json_error( $this->errors );
37 }
38
39 wp_send_json_success( $this->response );
40 }
41
42 /**
43 * Verify AJAX request with enhanced security.
44 *
45 * @param array $keys The keys to verify (optional).
46 */
47 private function verify( array $keys = array() ): void {
48 // Early exit: these Ajax handlers should never be called by logged-out users.
49 // WordPress wp_ajax_ actions are only for logged-in users, so this shouldn't happen.
50 if ( ! is_user_logged_in() ) {
51 // If we reach here, something is wrong with the action registration.
52 // Just exit silently to prevent frontend errors.
53 wp_die( '', '', array( 'response' => 200 ) );
54 }
55
56 // Check if the request is coming from the admin area (for logged-in users).
57 $referer = wp_get_referer();
58 if ( ! $referer || strpos( $referer, admin_url() ) !== 0 ) {
59 $this->errors = array( 'message' => 'Invalid request origin' );
60 $this->serve();
61 return;
62 }
63
64 // Enhanced CSRF protection with proper nonce verification.
65 $security_token = $_REQUEST['security_token'] ?? $_GET['security_token'] ?? '';
66 $nonce_action = Template::NAME;
67
68 if ( ! wp_verify_nonce( $security_token, $nonce_action ) ) {
69 $this->errors = array( 'message' => 'Security check failed' );
70 $this->serve();
71 return;
72 }
73
74 // Rate limiting check.
75 if ( ! $this->check_rate_limit() ) {
76 $this->errors = array( 'message' => 'Too many requests' );
77 $this->serve();
78 return;
79 }
80
81 // Capability check - ensure user has proper permissions (same as menu page).
82 if ( ! current_user_can( 'manage_woocommerce' ) ) {
83 $this->errors = array( 'message' => 'Insufficient permissions' );
84 $this->serve();
85 return;
86 }
87
88 // Initialize response and errors.
89 $this->response = array( 'success' => true );
90 $this->errors = array();
91 $this->request = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST ) );
92
93 // Attempt to retrieve JSON if POST is empty.
94 if ( empty( $_POST ) ) {
95 $contents = trim( file_get_contents( 'php://input' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended.
96
97 if ( $this->is_json( $contents ) ) {
98 $json_data = json_decode( $contents, true );
99
100 if ( ! empty( $json_data ) ) {
101 // Deep sanitization of JSON data.
102 $this->data = empty( $keys ) ? $this->sanitize_deep( $json_data ) : $this->extract_data( $this->sanitize_deep( $json_data ), $keys );
103 }
104 }
105 } else {
106 // Sanitize POST data.
107 $this->data = empty( $keys ) ? $this->sanitize_deep( $_POST ) : $this->extract_data( $this->sanitize_deep( $_POST ), $keys );
108 }
109
110 // Log security events for audit.
111 $this->log_security_event();
112 }
113
114 /**
115 * Utility to check if a string is JSON.
116 */
117 private function is_json( string $string ): bool {
118 json_decode( $string );
119 return json_last_error() === JSON_ERROR_NONE;
120 }
121
122 /**
123 * Extracts data from an array using the given keys.
124 *
125 * @param array $sanitized The array from which to extract data.
126 * @param array $keys The keys to use for extraction.
127 *
128 * @return array The extracted data.
129 */
130 public function extract_data( array $sanitized, array $keys ): array {
131 return array_intersect_key( $sanitized, array_flip( $keys ) );
132 }
133
134 /**
135 * Register AJAX actions.
136 */
137 private function action( $action ): string {
138 return sprintf( 'wp_ajax_%s-%s', Template::NAME, $action );
139 }
140
141 /**
142 * Check rate limiting to prevent abuse.
143 *
144 * @return bool
145 */
146 private function check_rate_limit(): bool {
147 $user_id = get_current_user_id();
148 $transient_key = 'ajax_rate_limit_' . $user_id . '_' . $_SERVER['REMOTE_ADDR'];
149 $requests = get_transient( $transient_key );
150
151 if ( false === $requests ) {
152 set_transient( $transient_key, 1, MINUTE_IN_SECONDS );
153 return true;
154 }
155
156 if ( $requests >= 30 ) { // 30 requests per minute (more reasonable for frontend).
157 return false;
158 }
159
160 set_transient( $transient_key, $requests + 1, MINUTE_IN_SECONDS );
161 return true;
162 }
163
164 /**
165 * Deep sanitization of data arrays.
166 *
167 * @param mixed $data Data to sanitize.
168 * @return mixed Sanitized data.
169 */
170 private function sanitize_deep( $data ) {
171 if ( is_array( $data ) ) {
172 return array_map( array( $this, 'sanitize_deep' ), $data );
173 }
174
175 if ( is_string( $data ) ) {
176 return sanitize_text_field( $data );
177 }
178
179 return $data;
180 }
181
182 /**
183 * Log security events for audit trail.
184 */
185 private function log_security_event(): void {
186 $log_data = array(
187 'timestamp' => current_time( 'mysql' ),
188 'user_id' => get_current_user_id(),
189 'ip_address' => $_SERVER['REMOTE_ADDR'],
190 'action' => $_REQUEST['action'] ?? 'unknown',
191 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
192 );
193
194 // Store in transient for security monitoring.
195 $security_log = get_transient( 'commercebird_security_log' ) ?: array();
196 $security_log[] = $log_data;
197
198 // Keep only last 100 entries.
199 if ( count( $security_log ) > 100 ) {
200 $security_log = array_slice( $security_log, -100 );
201 }
202
203 set_transient( 'commercebird_security_log', $security_log, DAY_IN_SECONDS );
204 }
205 }
206