PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.4.1
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.4.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 / 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
371 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 $data['plan'] = array_column( isset( $data['line_items'] ) ? $data['line_items'] : array(), 'name' );
135 set_transient( 'subscription_details', $data, WEEK_IN_SECONDS );
136 } else {
137 $data = array(
138 'status' => 'error',
139 'message' => $data['message'],
140 );
141 }
142 return $data;
143 }
144
145
146 /**
147 * Resets the settings details.
148 *
149 * @return void
150 */
151 public function settings_reset(): void {
152 $this->verify();
153 delete_option( self::OPTIONS['settings']['token'] );
154 delete_option( self::OPTIONS['settings']['id'] );
155 delete_option( self::OPTIONS['settings']['email'] );
156 $this->response = array( 'message' => 'Reset successfully!' );
157 $this->serve();
158 }
159
160 /**
161 * Retrieves the settings details.
162 *
163 * @return void
164 */
165 public function settings_get(): void {
166 $this->verify();
167 $token = get_option( self::OPTIONS['settings']['token'], '' );
168 $id = get_option( self::OPTIONS['settings']['id'], '' );
169 $email = get_option( self::OPTIONS['settings']['email'], '' );
170 $this->response['token'] = $token;
171 $this->response['id'] = $id;
172 $this->response['email'] = $email;
173 $this->serve();
174 }
175
176 /**
177 * Sets the settings for the class with enhanced validation.
178 *
179 * @return void
180 */
181 public function settings_set(): void {
182 $this->verify( self::FORMS['settings'] );
183
184 try {
185 if ( $this->data ) {
186 // Enhanced input validation
187 $id = $this->validate_subscription_id( $this->data['id'] ?? '' );
188 $email = $this->validate_email( $this->data['email'] ?? '' );
189 $token = $this->validate_token( $this->data['token'] ?? '' );
190
191 if ( ! $id || ! $email || ! $token ) {
192 $this->errors = array( 'message' => 'Invalid input data provided' );
193 $this->serve();
194 return;
195 }
196
197 // Update options with validated data
198 update_option( self::OPTIONS['settings']['id'], $id );
199 update_option( self::OPTIONS['settings']['email'], $email );
200 update_option( self::OPTIONS['settings']['token'], $token );
201
202 // Clear cached subscription data when settings change
203 delete_transient( 'subscription_details' );
204
205 $this->response = array( 'message' => 'Settings saved successfully' );
206 } else {
207 $this->errors = array( 'message' => 'No data provided' );
208 }
209 } catch (Exception $exception) {
210 $this->errors = array( 'message' => 'Failed to save settings' );
211 }
212 $this->serve();
213 }
214
215 /**
216 * Validate subscription ID format.
217 *
218 * @param string $id Subscription ID to validate.
219 * @return string|false Validated ID or false if invalid.
220 */
221 private function validate_subscription_id( $id ) {
222 $id = sanitize_text_field( $id );
223 return is_numeric( $id ) && $id > 0 ? $id : false;
224 }
225
226 /**
227 * Validate email format and sanitize.
228 *
229 * @param string $email Email to validate.
230 * @return string|false Validated email or false if invalid.
231 */
232 private function validate_email( $email ) {
233 $email = sanitize_email( $email );
234 return is_email( $email ) ? $email : false;
235 }
236
237 /**
238 * Validate token format.
239 *
240 * @param string $token Token to validate.
241 * @return string|false Validated token or false if invalid.
242 */
243 private function validate_token( $token ) {
244 $token = sanitize_text_field( $token );
245 // Basic token validation - adjust pattern as needed
246 return preg_match( '/^[a-zA-Z0-9_-]+$/', $token ) ? $token : false;
247 }
248
249 /**
250 * Sanitize subscription data before sending to frontend.
251 *
252 * @param array $data Raw subscription data.
253 * @return array Sanitized data.
254 */
255 /**
256 * Sanitize subscription data before sending to frontend.
257 *
258 * @param array $data Raw subscription data.
259 * @return array Sanitized data.
260 */
261 private function sanitize_subscription_data( $data ) {
262 if ( ! is_array( $data ) ) {
263 return array();
264 }
265
266 // Remove or hash sensitive billing information
267 if ( isset( $data['billing'] ) ) {
268 $safe_billing = array();
269 // Include safe billing fields that frontend expects
270 $safe_billing_fields = array( 'first_name', 'company', 'country', 'email' );
271 foreach ( $safe_billing_fields as $billing_field ) {
272 if ( isset( $data['billing'][ $billing_field ] ) ) {
273 if ( $billing_field === 'email' ) {
274 $safe_billing[ $billing_field ] = sanitize_email( $data['billing'][ $billing_field ] );
275 } else {
276 $safe_billing[ $billing_field ] = sanitize_text_field( $data['billing'][ $billing_field ] );
277 }
278 }
279 }
280 $data['billing'] = $safe_billing;
281 }
282
283 // Define all safe fields that the frontend expects
284 $safe_fields = array(
285 'status',
286 'plan',
287 'currency',
288 'total',
289 'needs_payment',
290 'next_payment_date_gmt',
291 'payment_url',
292 'variation_id',
293 );
294 $sanitized = array();
295
296 foreach ( $safe_fields as $field ) {
297 if ( isset( $data[ $field ] ) ) {
298 if ( is_array( $data[ $field ] ) ) {
299 $sanitized[ $field ] = array_map( 'sanitize_text_field', $data[ $field ] );
300 } else {
301 if ( $field === 'needs_payment' ) {
302 $sanitized[ $field ] = (bool) $data[ $field ];
303 } else {
304 $sanitized[ $field ] = sanitize_text_field( $data[ $field ] );
305 }
306 }
307 }
308 }
309
310 // Handle fee_lines array specially since it contains objects
311 if ( isset( $data['fee_lines'] ) && is_array( $data['fee_lines'] ) ) {
312 $sanitized['fee_lines'] = array();
313 foreach ( $data['fee_lines'] as $fee_line ) {
314 if ( is_array( $fee_line ) ) {
315 $sanitized_fee = array();
316 $fee_fields = array( 'id', 'name', 'tax_class', 'tax_status' );
317 foreach ( $fee_fields as $fee_field ) {
318 if ( isset( $fee_line[ $fee_field ] ) ) {
319 if ( $fee_field === 'id' ) {
320 $sanitized_fee[ $fee_field ] = (int) $fee_line[ $fee_field ];
321 } else {
322 $sanitized_fee[ $fee_field ] = sanitize_text_field( $fee_line[ $fee_field ] );
323 }
324 }
325 }
326 $sanitized['fee_lines'][] = $sanitized_fee;
327 }
328 }
329 }
330
331 // Include billing data
332 if ( isset( $data['billing'] ) ) {
333 $sanitized['billing'] = $data['billing'];
334 }
335
336 return $sanitized;
337 }
338
339 /**
340 * Get empty subscription structure that matches frontend expectations.
341 *
342 * @return array Empty subscription structure.
343 */
344 private function get_empty_subscription_structure(): array {
345 return array(
346 'status' => '',
347 'currency' => '',
348 'total' => '',
349 'fee_lines' => array(),
350 'payment_url' => '',
351 'needs_payment' => false,
352 'next_payment_date_gmt' => '',
353 'variation_id' => array(),
354 'plan' => array(),
355 'billing' => array(
356 'first_name' => '',
357 'company' => '',
358 'country' => '',
359 'email' => '',
360 ),
361 );
362 }
363
364 /**
365 * Function to get the token from the settings.
366 */
367 public function get_token() {
368 return get_option( self::OPTIONS['settings']['token'], '' );
369 }
370 }
371