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 / Actions / Ajax / SettingsAjax.php
commercebird / admin / includes / Actions / Ajax Last commit date
AcfAjax.php 1 year ago ExactOnlineAjax.php 11 months ago SettingsAjax.php 11 months ago ZohoCRMAjax.php 11 months ago ZohoInventoryAjax.php 11 months ago index.php 1 year ago
SettingsAjax.php
372 lines
1 <?php
2 /**
3 * CommerceBird Settings Ajax Handler
4 *
5 * @package CommerceBird\Admin\Actions\Ajax
6 */
7 namespace CommerceBird\Admin\Actions\Ajax;
8
9 use CommerceBird\Admin\Connectors\CommerceBird;
10 use CommerceBird\Admin\Traits\AjaxRequest;
11 use CommerceBird\Admin\Traits\OptionStatus;
12 use CommerceBird\Admin\Traits\Singleton;
13 use CommerceBird\Admin\Traits\LogWriter;
14 use WpOrg\Requests\Exception;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19 final class SettingsAjax {
20
21
22 use Singleton;
23 use AjaxRequest;
24 use OptionStatus;
25 use LogWriter;
26
27 private const FORMS = array(
28 'settings' => array(
29 'id',
30 'email',
31 'token',
32 ),
33 );
34
35 private const ACTIONS = array(
36 'get_subscription' => 'subscription_get',
37 'get_settings' => 'settings_get',
38 'save_settings' => 'settings_set',
39 'reset_settings' => 'settings_reset',
40 );
41
42 // private const SOURCE = 'settings';
43 private const OPTIONS = array(
44 'settings' => array(
45 'token' => 'commercebird-exact-online-token',
46 'id' => 'commercebird-subscription-id',
47 'email' => 'commercebird-woo-webhook-status',
48 ),
49 );
50
51 /**
52 * SettingsAjax constructor.
53 */
54 public function __construct() {
55 $this->load_actions();
56 }
57
58 /**
59 * Retrieves the subscription details with enhanced security.
60 *
61 * @return void
62 */
63 public function subscription_get(): void {
64 $this->verify();
65
66 // Validate subscription ID and email ownership
67 $subscription_id = get_option( self::OPTIONS['settings']['id'], 0 );
68 $stored_email = get_option( self::OPTIONS['settings']['email'], '' );
69
70 if ( empty( $subscription_id ) || empty( $stored_email ) ) {
71 $this->errors = array( 'message' => 'Missing subscription ID or email' );
72 $this->serve();
73 return;
74 }
75
76 $subscription_data = $this->get_subscription_data();
77
78 // Server-side email validation - critical security check
79 if ( isset( $subscription_data['billing']['email'] ) ) {
80 if ( $subscription_data['billing']['email'] !== $stored_email ) {
81 $this->errors = array(
82 'message' => 'Email mismatch: Access denied',
83 'code' => 'email_mismatch',
84 );
85
86 // Log potential security breach attempt
87 error_log( sprintf(
88 'CommerceBird Security: Email mismatch attempt. User ID: %d, IP: %s, Stored: %s, Attempted: %s',
89 get_current_user_id(),
90 $_SERVER['REMOTE_ADDR'],
91 $stored_email,
92 $subscription_data['billing']['email']
93 ) );
94
95 $this->serve();
96 return;
97 }
98 } else {
99 // If no subscription data or no billing email, return empty structure
100 $this->response = $this->get_empty_subscription_structure();
101 $this->serve();
102 return;
103 }
104
105 // Remove sensitive data before sending to frontend
106 $safe_data = $this->sanitize_subscription_data( $subscription_data );
107 $this->response = $safe_data;
108 $this->serve();
109 }
110
111 /**
112 * @description Function to get subscription data from CommerceBird API.
113 */
114 public function get_subscription_data(): array {
115 $transient = get_transient( 'subscription_details' );
116 if ( ! empty( $transient ) ) {
117 return $transient;
118 }
119 $subscription_id = get_option( self::OPTIONS['settings']['id'], 0 );
120 ;
121 $data = ( new CommerceBird() )->get_subscription(
122 array(
123 'subscriptionId' => $subscription_id,
124 )
125 );
126 if ( is_wp_error( $data ) ) {
127 return array(
128 'status' => 'error',
129 'message' => $data->get_error_message(),
130 );
131 }
132 if ( ! empty( $data ) && array_key_exists( 'status', $data ) && 'error' !== $data['status'] ) {
133 $data['variation_id'] = array_column( isset( $data['line_items'] ) ? $data['line_items'] : array(), 'variation_id' );
134 $plan_names = array_column( isset( $data['line_items'] ) ? $data['line_items'] : array(), 'name' );
135 $data['plan'] = implode( ', ', $plan_names );
136 set_transient( 'subscription_details', $data, WEEK_IN_SECONDS );
137 } else {
138 $data = array(
139 'status' => 'error',
140 'message' => $data['message'],
141 );
142 }
143 return $data;
144 }
145
146
147 /**
148 * Resets the settings details.
149 *
150 * @return void
151 */
152 public function settings_reset(): void {
153 $this->verify();
154 delete_option( self::OPTIONS['settings']['token'] );
155 delete_option( self::OPTIONS['settings']['id'] );
156 delete_option( self::OPTIONS['settings']['email'] );
157 $this->response = array( 'message' => 'Reset successfully!' );
158 $this->serve();
159 }
160
161 /**
162 * Retrieves the settings details.
163 *
164 * @return void
165 */
166 public function settings_get(): void {
167 $this->verify();
168 $token = get_option( self::OPTIONS['settings']['token'], '' );
169 $id = get_option( self::OPTIONS['settings']['id'], '' );
170 $email = get_option( self::OPTIONS['settings']['email'], '' );
171 $this->response['token'] = $token;
172 $this->response['id'] = $id;
173 $this->response['email'] = $email;
174 $this->serve();
175 }
176
177 /**
178 * Sets the settings for the class with enhanced validation.
179 *
180 * @return void
181 */
182 public function settings_set(): void {
183 $this->verify( self::FORMS['settings'] );
184
185 try {
186 if ( $this->data ) {
187 // Enhanced input validation
188 $id = $this->validate_subscription_id( $this->data['id'] ?? '' );
189 $email = $this->validate_email( $this->data['email'] ?? '' );
190 $token = $this->validate_token( $this->data['token'] ?? '' );
191
192 if ( ! $id || ! $email || ! $token ) {
193 $this->errors = array( 'message' => 'Invalid input data provided' );
194 $this->serve();
195 return;
196 }
197
198 // Update options with validated data
199 update_option( self::OPTIONS['settings']['id'], $id );
200 update_option( self::OPTIONS['settings']['email'], $email );
201 update_option( self::OPTIONS['settings']['token'], $token );
202
203 // Clear cached subscription data when settings change
204 delete_transient( 'subscription_details' );
205
206 $this->response = array( 'message' => 'Settings saved successfully' );
207 } else {
208 $this->errors = array( 'message' => 'No data provided' );
209 }
210 } catch (Exception $exception) {
211 $this->errors = array( 'message' => 'Failed to save settings' );
212 }
213 $this->serve();
214 }
215
216 /**
217 * Validate subscription ID format.
218 *
219 * @param string $id Subscription ID to validate.
220 * @return string|false Validated ID or false if invalid.
221 */
222 private function validate_subscription_id( $id ) {
223 $id = sanitize_text_field( $id );
224 return is_numeric( $id ) && $id > 0 ? $id : false;
225 }
226
227 /**
228 * Validate email format and sanitize.
229 *
230 * @param string $email Email to validate.
231 * @return string|false Validated email or false if invalid.
232 */
233 private function validate_email( $email ) {
234 $email = sanitize_email( $email );
235 return is_email( $email ) ? $email : false;
236 }
237
238 /**
239 * Validate token format.
240 *
241 * @param string $token Token to validate.
242 * @return string|false Validated token or false if invalid.
243 */
244 private function validate_token( $token ) {
245 $token = sanitize_text_field( $token );
246 // Basic token validation - adjust pattern as needed
247 return preg_match( '/^[a-zA-Z0-9_-]+$/', $token ) ? $token : false;
248 }
249
250 /**
251 * Sanitize subscription data before sending to frontend.
252 *
253 * @param array $data Raw subscription data.
254 * @return array Sanitized data.
255 */
256 /**
257 * Sanitize subscription data before sending to frontend.
258 *
259 * @param array $data Raw subscription data.
260 * @return array Sanitized data.
261 */
262 private function sanitize_subscription_data( $data ) {
263 if ( ! is_array( $data ) ) {
264 return array();
265 }
266
267 // Remove or hash sensitive billing information
268 if ( isset( $data['billing'] ) ) {
269 $safe_billing = array();
270 // Include safe billing fields that frontend expects
271 $safe_billing_fields = array( 'first_name', 'company', 'country', 'email' );
272 foreach ( $safe_billing_fields as $billing_field ) {
273 if ( isset( $data['billing'][ $billing_field ] ) ) {
274 if ( $billing_field === 'email' ) {
275 $safe_billing[ $billing_field ] = sanitize_email( $data['billing'][ $billing_field ] );
276 } else {
277 $safe_billing[ $billing_field ] = sanitize_text_field( $data['billing'][ $billing_field ] );
278 }
279 }
280 }
281 $data['billing'] = $safe_billing;
282 }
283
284 // Define all safe fields that the frontend expects
285 $safe_fields = array(
286 'status',
287 'plan',
288 'currency',
289 'total',
290 'needs_payment',
291 'next_payment_date_gmt',
292 'payment_url',
293 'variation_id',
294 );
295 $sanitized = array();
296
297 foreach ( $safe_fields as $field ) {
298 if ( isset( $data[ $field ] ) ) {
299 if ( is_array( $data[ $field ] ) ) {
300 $sanitized[ $field ] = array_map( 'sanitize_text_field', $data[ $field ] );
301 } else {
302 if ( $field === 'needs_payment' ) {
303 $sanitized[ $field ] = (bool) $data[ $field ];
304 } else {
305 $sanitized[ $field ] = sanitize_text_field( $data[ $field ] );
306 }
307 }
308 }
309 }
310
311 // Handle fee_lines array specially since it contains objects
312 if ( isset( $data['fee_lines'] ) && is_array( $data['fee_lines'] ) ) {
313 $sanitized['fee_lines'] = array();
314 foreach ( $data['fee_lines'] as $fee_line ) {
315 if ( is_array( $fee_line ) ) {
316 $sanitized_fee = array();
317 $fee_fields = array( 'id', 'name', 'tax_class', 'tax_status' );
318 foreach ( $fee_fields as $fee_field ) {
319 if ( isset( $fee_line[ $fee_field ] ) ) {
320 if ( $fee_field === 'id' ) {
321 $sanitized_fee[ $fee_field ] = (int) $fee_line[ $fee_field ];
322 } else {
323 $sanitized_fee[ $fee_field ] = sanitize_text_field( $fee_line[ $fee_field ] );
324 }
325 }
326 }
327 $sanitized['fee_lines'][] = $sanitized_fee;
328 }
329 }
330 }
331
332 // Include billing data
333 if ( isset( $data['billing'] ) ) {
334 $sanitized['billing'] = $data['billing'];
335 }
336
337 return $sanitized;
338 }
339
340 /**
341 * Get empty subscription structure that matches frontend expectations.
342 *
343 * @return array Empty subscription structure.
344 */
345 private function get_empty_subscription_structure(): array {
346 return array(
347 'status' => '',
348 'currency' => '',
349 'total' => '',
350 'fee_lines' => array(),
351 'payment_url' => '',
352 'needs_payment' => false,
353 'next_payment_date_gmt' => '',
354 'variation_id' => array(),
355 'plan' => '',
356 'billing' => array(
357 'first_name' => '',
358 'company' => '',
359 'country' => '',
360 'email' => '',
361 ),
362 );
363 }
364
365 /**
366 * Function to get the token from the settings.
367 */
368 public function get_token() {
369 return get_option( self::OPTIONS['settings']['token'], '' );
370 }
371 }
372