PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.4.5
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.4.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 / admin / includes / Traits / AjaxRequest.php
commercebird / admin / includes / Traits Last commit date
AjaxRequest.php 11 months ago LogWriter.php 1 year ago OptionStatus.php 1 year 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