PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.3.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.3.0
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / modules / checkout / classes / class-cartflows-checkout-ajax.php

class-cartflows-checkout-ajax.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.3.0, at modules/checkout/classes/class-cartflows-checkout-ajax.php

649 lines 18.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Checkout Ajax.
4 *
5 * @package CartFlows
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12 /**
13 * Global Checkout
14 *
15 * @since 1.0.0
16 */
17 class Cartflows_Checkout_Ajax {
18
19
20 /**
21 * Member Variable
22 *
23 * @var object instance
24 */
25 private static $instance;
26
27 /**
28 * Initiator
29 */
30 public static function get_instance() {
31 if ( ! isset( self::$instance ) ) {
32 self::$instance = new self();
33 }
34 return self::$instance;
35 }
36
37 /**
38 * Constructor
39 */
40 public function __construct() {
41
42 /* Ajax Endpoint */
43 add_filter( 'woocommerce_ajax_get_endpoint', array( $this, 'get_ajax_endpoint' ), 10, 2 );
44
45 add_action( 'wp_ajax_wcf_woo_apply_coupon', array( $this, 'apply_coupon' ) );
46 add_action( 'wp_ajax_nopriv_wcf_woo_apply_coupon', array( $this, 'apply_coupon' ) );
47
48 add_action( 'wp_ajax_wcf_woo_remove_coupon', array( $this, 'remove_coupon' ) );
49 add_action( 'wp_ajax_nopriv_wcf_woo_remove_coupon', array( $this, 'remove_coupon' ) );
50
51 add_action( 'wp_ajax_wcf_woo_remove_cart_product', array( $this, 'wcf_woo_remove_cart_product' ) );
52 add_action( 'wp_ajax_nopriv_wcf_woo_remove_cart_product', array( $this, 'wcf_woo_remove_cart_product' ) );
53
54 add_action( 'wp_ajax_nopriv_wcf_check_email_exists', array( $this, 'check_email_exists' ) );
55 add_action( 'wp_ajax_nopriv_wcf_woocommerce_login', array( $this, 'woocommerce_user_login' ) );
56
57 add_action( 'wp_ajax_wcf_upload_checkout_file', array( $this, 'upload_checkout_file' ) );
58 add_action( 'wp_ajax_nopriv_wcf_upload_checkout_file', array( $this, 'upload_checkout_file' ) );
59 }
60
61 /**
62 * Get ajax end points.
63 *
64 * @param string $endpoint_url end point URL.
65 * @param string $request end point request.
66 * @return string
67 */
68 public function get_ajax_endpoint( $endpoint_url, $request ) {
69 global $post;
70
71 if ( ! empty( $post ) && ! empty( $_SERVER['REQUEST_URI'] ) ) {
72
73 if ( _is_wcf_checkout_type() ) {
74
75 $query_args = array();
76 $url = $endpoint_url;
77
78 if ( mb_strpos( $endpoint_url, 'checkout', 0, 'utf-8' ) === false ) {
79
80 if ( '' === $request ) {
81 $query_args = array(
82 'wc-ajax' => '%%endpoint%%',
83 );
84 } else {
85 $query_args = array(
86 'wc-ajax' => $request,
87 );
88 }
89
90 $uri = explode( '?', esc_url_raw( $_SERVER['REQUEST_URI'] ), 2 );
91 $url = esc_url( $uri[0] );
92 }
93
94 $query_args['wcf_checkout_id'] = $post->ID;
95
96 $endpoint_url = add_query_arg( $query_args, $url );
97 }
98 }
99
100 return $endpoint_url;
101 }
102
103 /**
104 * Apply coupon on submit of custom coupon form.
105 */
106 public function apply_coupon() {
107 $response = '';
108
109 if ( ! check_ajax_referer( 'wcf-apply-coupon', 'security', false ) ) {
110 $response_data = array(
111 'status' => false,
112 'error' => __( 'Nonce validation failed', 'cartflows' ),
113 );
114 wp_send_json_error( $response_data );
115 }
116
117 // Update the billing email before adding a coupon required for coupon conditions.
118 $this->update_billing_email();
119
120 ob_start();
121
122 if ( ! empty( $_POST['coupon_code'] ) ) {
123 $result = WC()->cart->add_discount( sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) ) );
124 } else {
125 wc_add_notice( WC_Coupon::get_generic_coupon_error( WC_Coupon::E_WC_COUPON_PLEASE_ENTER ), 'error' );
126 }
127
128 $response = array(
129 'status' => $result,
130 'msg' => wc_print_notices( true ),
131 );
132
133 ob_clean(); // Clearing the uncessary echo HTML.
134 wp_send_json( $response );
135
136 die();
137 }
138
139 /**
140 * Remove coupon.
141 */
142 public function remove_coupon() {
143 check_ajax_referer( 'wcf-remove-coupon', 'security' );
144 $coupon = isset( $_POST['coupon_code'] ) ? sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) ) : false;
145
146 if ( empty( $coupon ) ) {
147 echo "<div class='woocommerce-error'>" . esc_html__( 'Sorry there was a problem removing this coupon.', 'cartflows' ) . '</div>';
148 } else {
149 WC()->cart->remove_coupon( $coupon );
150 echo "<div class='woocommerce-error'>" . esc_html__( 'Coupon has been removed.', 'cartflows' ) . '</div>';
151 }
152 wc_print_notices();
153 wp_die();
154 }
155
156 /**
157 * Remove cart item.
158 */
159 public function wcf_woo_remove_cart_product() {
160 check_ajax_referer( 'wcf-remove-cart-product', 'security' );
161 $product_key = isset( $_POST['p_key'] ) ? sanitize_text_field( wp_unslash( $_POST['p_key'] ) ) : false;
162 $product_id = isset( $_POST['p_id'] ) ? sanitize_text_field( wp_unslash( $_POST['p_id'] ) ) : '';
163 $product_title = get_the_title( $product_id );
164
165 $needs_shipping = false;
166 $is_order_bump = false;
167 $order_bump_id = '';
168
169 // Check if the product is an order bump before removing it.
170 if ( ! empty( $product_key ) ) {
171 $cart_item = WC()->cart->get_cart_item( $product_key );
172 if ( isset( $cart_item['cartflows_bump'] ) && $cart_item['cartflows_bump'] ) {
173 $is_order_bump = true;
174 $order_bump_id = isset( $cart_item['ob_id'] ) ? $cart_item['ob_id'] : '';
175 }
176
177 WC()->cart->remove_cart_item( $product_key );
178 $msg = "<div class='woocommerce-message'>" . $product_title . __( ' has been removed.', 'cartflows' ) . '</div>';
179 } else {
180 $msg = "<div class='woocommerce-message'>" . __( 'Sorry there was a problem removing ', 'cartflows' ) . $product_title;
181 }
182
183 foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) {
184 if ( $values['data']->needs_shipping() ) {
185 $needs_shipping = true;
186 break;
187 }
188 }
189
190 $response = array(
191 'need_shipping' => $needs_shipping,
192 'msg' => $msg,
193 'is_order_bump' => $is_order_bump,
194 'order_bump_id' => $order_bump_id,
195 );
196
197 echo wp_json_encode( $response );
198 wp_die();
199 }
200
201
202 /**
203 * Check email exist.
204 */
205 public function check_email_exists() {
206
207 check_ajax_referer( 'check-email-exist', 'security' );
208
209 $email_address = isset( $_POST['email_address'] ) ? sanitize_email( wp_unslash( $_POST['email_address'] ) ) : false;
210
211 $is_login_allowed = 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' );
212
213 // Security: Only check email existence when the checkout login UX needs it.
214 // Otherwise the boolean would leak registered emails for no UX benefit.
215 $is_exist = $is_login_allowed && email_exists( $email_address );
216
217 $response = array(
218 'success' => boolval( $is_exist ),
219 'is_login_allowed' => $is_login_allowed,
220 'msg' => $is_exist ? __( 'Email Exist.', 'cartflows' ) : __( 'Email not exist', 'cartflows' ),
221 );
222
223 wp_send_json_success( $response );
224 }
225
226 /**
227 * Update billing email address before applying the coupon. This is used for coupon conditions.
228 *
229 * @return void
230 * @since 2.0.12
231 */
232 public function update_billing_email() {
233
234 if ( ! wcf()->is_woo_active ) {
235 return;
236 }
237
238 if ( ! class_exists( 'Automattic\WooCommerce\Utilities\ArrayUtil' ) ) {
239 return;
240 }
241
242 // Sanitize the billing email.
243 $billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_email( wp_unslash( $_POST['billing_email'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
244
245 $billing_email = \Automattic\WooCommerce\Utilities\ArrayUtil::get_value_or_default(
246 array(
247 'billing_email' => $billing_email,
248 ),
249 'billing_email'
250 );
251
252 if ( is_string( $billing_email ) && is_email( $billing_email ) ) {
253 wc()->customer->set_billing_email( $billing_email );
254 }
255 }
256
257 /**
258 * Check email exist.
259 */
260 public function woocommerce_user_login() {
261
262 check_ajax_referer( 'woocommerce-login', 'security' );
263
264 $response = array(
265 'success' => false,
266 );
267
268 // wp_signon() accepts username or email via user_login; sanitize_email() would drop usernames.
269 $user_login = isset( $_POST['email'] ) ? sanitize_text_field( wp_unslash( $_POST['email'] ) ) : '';
270 $password = isset( $_POST['password'] ) ? wp_unslash( $_POST['password'] ) : ''; // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
271
272 $creds = array(
273 'user_login' => $user_login,
274 'user_password' => $password,
275 'remember' => false,
276 );
277
278 $user = wp_signon( $creds, false );
279
280 if ( ! is_wp_error( $user ) ) {
281 $response = array(
282 'success' => true,
283 );
284 } else {
285 // Mirror WC's process_login() so security plugins see the failure.
286 do_action( 'woocommerce_login_failed' );
287 // Generic error to prevent user enumeration.
288 $response['error'] = __( 'Invalid username or password.', 'cartflows' );
289 }
290
291 wp_send_json_success( $response );
292 }
293
294 /**
295 * Handle checkout file upload via AJAX.
296 *
297 * @since 2.2.2
298 * @return void
299 */
300 public function upload_checkout_file() {
301
302 if ( ! check_ajax_referer( 'wcf-file-upload', 'security', false ) ) {
303 wp_send_json_error(
304 array( 'error' => __( 'Nonce validation failed.', 'cartflows' ) )
305 );
306 }
307
308 // Security: this endpoint is nopriv, so bind every upload to a real checkout step's file field to prevent arbitrary writes.
309 $checkout_id = empty( $_POST['checkout_id'] ) ? 0 : absint( $_POST['checkout_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
310 $field_key = empty( $_POST['field_key'] ) ? '' : sanitize_text_field( wp_unslash( $_POST['field_key'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
311
312 if ( ! $this->is_valid_file_upload_field( $checkout_id, $field_key ) ) {
313 wp_send_json_error(
314 array( 'error' => __( 'This upload is not associated with a valid checkout file field.', 'cartflows' ) )
315 );
316 }
317
318 if ( empty( $_FILES['wcf_checkout_file']['tmp_name'] ) ) {
319 wp_send_json_error(
320 array( 'error' => __( 'No file uploaded.', 'cartflows' ) )
321 );
322 }
323
324 if ( ! function_exists( 'wp_handle_upload' ) ) {
325 require_once ABSPATH . 'wp-admin/includes/file.php';
326 }
327
328 $file = $_FILES['wcf_checkout_file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
329 $file['name'] = sanitize_file_name( $file['name'] );
330 $file['ext'] = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
331
332 $master_allowed = Cartflows_Helper::get_allowed_file_extensions();
333
334 $restrictions = $this->get_field_restrictions();
335
336 $allowed_extensions = empty( $restrictions['extensions'] )
337 ? $master_allowed
338 : array_values( array_intersect( $master_allowed, $restrictions['extensions'] ) );
339
340 if ( ! in_array( $file['ext'], $allowed_extensions, true ) ) {
341 wp_send_json_error( array( 'error' => __( 'File type is not allowed.', 'cartflows' ) ) );
342 }
343
344 if ( (int) $file['size'] > $restrictions['max_size'] ) {
345 wp_send_json_error( array( 'error' => __( 'File size exceeds the allowed limit.', 'cartflows' ) ) );
346 }
347
348 $check = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], wp_get_mime_types() );
349
350 if ( empty( $check['ext'] ) || $check['ext'] !== $file['ext'] ) {
351 wp_send_json_error( array( 'error' => __( 'Invalid or corrupted file.', 'cartflows' ) ) );
352 }
353
354 $result = $this->move_uploaded_file( $file );
355
356 wp_send_json_success(
357 array(
358 'success' => true,
359 'url' => esc_url_raw( $result['url'] ),
360 'filename' => sanitize_file_name( basename( $result['file'] ) ),
361 )
362 );
363 }
364
365 /**
366 * Resolve a checkout field from step meta, using the same accessors and gates as the renderer.
367 *
368 * @since 3.2.1
369 *
370 * @param int $checkout_id Checkout step post ID.
371 * @param string $field_key Billing/shipping field key.
372 * @return array|null The field definition when it is rendered on a checkout step, else null.
373 */
374 private function get_upload_field( $checkout_id, $field_key ) {
375
376 if ( empty( $checkout_id ) || empty( $field_key ) ) {
377 return null;
378 }
379
380 // Must be a checkout step specifically (post type is shared across all step types).
381 if ( CARTFLOWS_STEP_POST_TYPE !== get_post_type( $checkout_id ) || 'checkout' !== get_post_meta( $checkout_id, 'wcf-step-type', true ) ) {
382 return null;
383 }
384
385 // Custom checkout fields must be enabled, mirroring the render path gate.
386 if ( ! _is_wcf_meta_custom_checkout( $checkout_id ) ) {
387 return null;
388 }
389
390 $field_type = ( 0 === strpos( $field_key, 'shipping_' ) ) ? 'shipping' : 'billing';
391 $saved_fields = wcf()->options->get_checkout_meta_value( $checkout_id, 'wcf_field_order_' . $field_type );
392
393 if ( ! is_array( $saved_fields ) || empty( $saved_fields[ $field_key ] ) || ! is_array( $saved_fields[ $field_key ] ) ) {
394 return null;
395 }
396
397 return $saved_fields[ $field_key ];
398 }
399
400 /**
401 * Confirm an upload targets a rendered checkout file field.
402 *
403 * @since 3.2.1
404 *
405 * @param int $checkout_id Checkout step post ID.
406 * @param string $field_key Billing/shipping field key.
407 * @return bool True when the field is an enabled file-upload field on a checkout step.
408 */
409 private function is_valid_file_upload_field( $checkout_id, $field_key ) {
410
411 $field = $this->get_upload_field( $checkout_id, $field_key );
412
413 if ( null === $field || empty( $field['enabled'] ) ) {
414 return false;
415 }
416
417 return isset( $field['type'] ) && 'file' === $field['type'];
418 }
419
420 /**
421 * Retrieve file size and type restrictions from field settings.
422 *
423 * @since 2.2.2
424 *
425 * @return array{max_size:int, extensions:string[]} Field-level upload restrictions.
426 */
427 private function get_field_restrictions() {
428
429 $max_size = 5 * 1024 * 1024; // 5MB default
430 $extensions = array();
431
432 $field_key = empty( $_POST['field_key'] ) ? '' : sanitize_text_field( wp_unslash( $_POST['field_key'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
433 $checkout_id = empty( $_POST['checkout_id'] ) ? 0 : absint( $_POST['checkout_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
434
435 $field = $this->get_upload_field( $checkout_id, $field_key );
436
437 if ( null === $field ) {
438 return compact( 'max_size', 'extensions' );
439 }
440
441 $custom_attributes = empty( $field['custom_attributes'] ) || ! is_array( $field['custom_attributes'] )
442 ? array()
443 : $field['custom_attributes'];
444
445 if ( empty( $custom_attributes ) ) {
446 return compact( 'max_size', 'extensions' );
447 }
448
449 if ( ! empty( $custom_attributes['file_size'] ) && is_scalar( $custom_attributes['file_size'] ) ) {
450 $max_size = max( 1, absint( $custom_attributes['file_size'] ) ) * 1024 * 1024;
451 }
452
453 if ( ! empty( $custom_attributes['accepted_file_types'] ) ) {
454 $extensions = $this->normalize_extension_list( $custom_attributes['accepted_file_types'] );
455 }
456
457 return compact( 'max_size', 'extensions' );
458 }
459
460 /**
461 * Normalize accepted file types into a lowercase extension list.
462 *
463 * @since 2.2.2
464 *
465 * @param string $raw Raw accepted file types value.
466 * @return array Normalized extensions.
467 */
468 private function normalize_extension_list( $raw ) {
469
470 if ( ! is_string( $raw ) || '' === trim( $raw ) ) {
471 return array();
472 }
473
474 $extensions = array_map( 'strtolower', array_map( 'trim', explode( ',', $raw ) ) );
475
476 return array_values( array_filter( $extensions ) );
477 }
478
479 /**
480 * Absolute path and URL of the protected checkout upload directory.
481 *
482 * @since 3.2.1
483 *
484 * @return array Path and URL checkout uploads are stored under.
485 */
486 private function get_checkout_upload_dir() {
487
488 $upload_dir = wp_upload_dir();
489
490 return array(
491 'path' => trailingslashit( $upload_dir['basedir'] ) . 'cartflows-checkout',
492 'url' => trailingslashit( $upload_dir['baseurl'] ) . 'cartflows-checkout',
493 );
494 }
495
496 /**
497 * Point wp_handle_upload() at the protected checkout upload directory.
498 *
499 * @since 3.2.1
500 *
501 * @param array $dirs Upload directory data.
502 * @return array
503 */
504 public function set_checkout_upload_dir( $dirs ) {
505
506 $checkout_dir = $this->get_checkout_upload_dir();
507
508 $dirs['path'] = $checkout_dir['path'];
509 $dirs['url'] = $checkout_dir['url'];
510 $dirs['subdir'] = '';
511
512 return $dirs;
513 }
514
515 /**
516 * Create the checkout upload directory with a deny-all .htaccess and an index.php.
517 *
518 * Security: this endpoint is nopriv and the files are shopper-supplied, so the directory
519 * that holds them must never execute or list what it contains.
520 *
521 * @since 3.2.1
522 *
523 * @return void
524 */
525 private function protect_checkout_upload_dir() {
526
527 $checkout_dir = $this->get_checkout_upload_dir();
528
529 if ( ! wp_mkdir_p( $checkout_dir['path'] ) ) {
530 return;
531 }
532
533 if ( ! function_exists( 'WP_Filesystem' ) ) {
534 require_once ABSPATH . 'wp-admin/includes/file.php';
535 }
536
537 if ( ! WP_Filesystem() ) {
538 return;
539 }
540
541 global $wp_filesystem;
542
543 $htaccess = trailingslashit( $checkout_dir['path'] ) . '.htaccess';
544
545 if ( ! $wp_filesystem->exists( $htaccess ) ) {
546 $rules = "# Generated by CartFlows - checkout uploads must never be executed or served as code.\n";
547 $rules .= "<IfModule mod_php.c>\n\tphp_flag engine off\n</IfModule>\n";
548 $rules .= "<IfModule mod_php7.c>\n\tphp_flag engine off\n</IfModule>\n";
549 $rules .= "<IfModule mod_php8.c>\n\tphp_flag engine off\n</IfModule>\n";
550 $rules .= "<FilesMatch \"\\.(?i:php|phar|phtml|php[0-9]|phps|pl|py|cgi|shtml)$\">\n";
551 $rules .= "\t<IfModule mod_authz_core.c>\n\t\tRequire all denied\n\t</IfModule>\n";
552 $rules .= "\t<IfModule !mod_authz_core.c>\n\t\tDeny from all\n\t</IfModule>\n";
553 $rules .= "</FilesMatch>\n";
554
555 $wp_filesystem->put_contents( $htaccess, $rules, FS_CHMOD_FILE );
556 }
557
558 $index = trailingslashit( $checkout_dir['path'] ) . 'index.php';
559
560 if ( ! $wp_filesystem->exists( $index ) ) {
561 $wp_filesystem->put_contents( $index, "<?php\n// Silence is golden.\n", FS_CHMOD_FILE );
562 }
563 }
564
565 /**
566 * Re-encode an uploaded image so an appended payload cannot survive inside it.
567 *
568 * Security: a valid GIF or JPEG with PHP appended passes wp_check_filetype_and_ext();
569 * re-encoding keeps only the image data. An image that cannot be re-encoded is deleted.
570 *
571 * @since 3.2.1
572 *
573 * @param string $path Absolute path of the uploaded file.
574 * @return bool True when the stored file is safe to keep.
575 */
576 private function reencode_uploaded_image( $path ) {
577
578 $filetype = wp_check_filetype( $path );
579
580 if ( empty( $filetype['type'] ) || 0 !== strpos( $filetype['type'], 'image/' ) ) {
581 return true;
582 }
583
584 $editor = wp_get_image_editor( $path );
585
586 if ( is_wp_error( $editor ) ) {
587 wp_delete_file( $path );
588 return false;
589 }
590
591 $saved = $editor->save( $path );
592
593 if ( is_wp_error( $saved ) ) {
594 wp_delete_file( $path );
595 return false;
596 }
597
598 return true;
599 }
600
601
602 /**
603 * Move the uploaded file into the WordPress uploads directory.
604 *
605 * @since 2.2.2
606 *
607 * @param array $file Normalized file data.
608 * @return array Upload result.
609 */
610 private function move_uploaded_file( array $file ) {
611
612 $this->protect_checkout_upload_dir();
613
614 $checkout_dir = $this->get_checkout_upload_dir();
615 $file['name'] = wp_unique_filename( $checkout_dir['path'], 'wcf_' . wp_generate_uuid4() . '.' . $file['ext'] );
616
617 add_filter( 'upload_dir', array( $this, 'set_checkout_upload_dir' ) );
618
619 $result = wp_handle_upload(
620 $file,
621 array(
622 'test_form' => false,
623 'mimes' => wp_get_mime_types(),
624 )
625 );
626
627 remove_filter( 'upload_dir', array( $this, 'set_checkout_upload_dir' ) );
628
629 if ( isset( $result['error'] ) ) {
630 wp_send_json_error(
631 array( 'error' => esc_html( $result['error'] ) )
632 );
633 }
634
635 if ( ! $this->reencode_uploaded_image( $result['file'] ) ) {
636 wp_send_json_error(
637 array( 'error' => __( 'This image could not be processed. Please upload a different file.', 'cartflows' ) )
638 );
639 }
640
641 return $result;
642 }
643 }
644
645 /**
646 * Kicking this off by calling 'get_instance()' method
647 */
648 Cartflows_Checkout_Ajax::get_instance();
649