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