PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / trunk
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster vtrunk
2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 1.8.1 All 42 releases
sellkit / includes / block-editor / blocks / optin / helper.php

helper.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster trunk, at includes/block-editor/blocks/optin/helper.php

716 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Sellkit\Blocks\Helpers\Optin;
3
4 defined( 'ABSPATH' ) || die();
5
6 use Sellkit\Funnel\Contacts\Base_Contacts;
7
8 /**
9 * Optin block helper class.
10 *
11 * @since 2.3.0
12 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
13 */
14 class Helper {
15 /**
16 * Current post id.
17 *
18 * @since 2.3.0
19 * @var integer|null
20 */
21 private $post_id = null;
22
23 /**
24 * Current block client id.
25 *
26 * @since 2.3.0
27 * @var string|null
28 */
29 private $client_id = null;
30
31 /**
32 * Current block attributes.
33 *
34 * @since 2.3.0
35 * @var array
36 */
37 public $attributes = [];
38
39 /**
40 * Holds all the responses.
41 *
42 * @access public
43 * @var array
44 */
45 public $response = [
46 'message' => [],
47 'errors' => [],
48 'admin_errors' => [],
49 ];
50
51 /**
52 * Holds all the messages.
53 *
54 * @access private
55 * @var array
56 */
57 private $messages = [];
58
59 /**
60 * Holds the reponse state.
61 *
62 * @access public
63 * @var bool
64 */
65 public $is_success = true;
66
67 /**
68 * Holds a record of the user-filled form.
69 *
70 * @access public
71 * @var array
72 */
73 public $form_data;
74
75 /**
76 * Holds the API key.
77 *
78 * @access private
79 * @var string
80 */
81 public $api_key;
82
83 /**
84 * Holds the API URL.
85 *
86 * @access private
87 * @var string
88 */
89 public $api_url;
90
91 /**
92 * Class constructor.
93 *
94 * @since 2.3.0
95 */
96 public function __construct() {
97 add_action( 'wp_ajax_submit_optin_block_form', [ $this, 'handle_frontend' ] );
98 add_action( 'wp_ajax_nopriv_submit_optin_block_form', [ $this, 'handle_frontend' ] );
99
100 add_action( 'admin_post_sellkit_download_file', [ $this, 'handle_file_download' ] );
101 add_action( 'admsellkitin_post_nopriv_sellkit_download_file', [ $this, 'handle_file_download' ] );
102 }
103
104 /**
105 * Handle form submission.
106 *
107 * @since 2.3.0
108 */
109 public function handle_frontend() {
110 if ( false === check_ajax_referer( 'sellkit_block', 'nonce', false ) ) {
111 wp_send_json_error( esc_html__( 'Error: Nonce mismatch or expired. Please reload the page and retry.', 'sellkit' ) );
112 }
113
114 $this->post_id = (int) filter_input( INPUT_POST, 'post_id' );
115 $this->client_id = filter_input( INPUT_POST, 'client_id' );
116 $funnel_page_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
117
118 $this->form_data = $_POST;
119
120 $funnel = \Sellkit_Funnel::get_instance( $funnel_page_id );
121
122 if ( ! empty( $funnel->current_step_data['type']['key'] ) && 'sales-page' === $funnel->current_step_data['type']['key'] ) {
123 $this
124 ->add_response( 'errors', esc_html__( 'Opt-in widget should not be used in the sales page step.', 'sellkit' ) )
125 ->set_success( false );
126
127 wp_send_json_error( $this->response );
128 }
129
130 $this->get_optin_block_data();
131
132 if ( empty( $this->form_data['fields'] ) ) {
133 $this
134 ->add_response( 'errors', esc_html__( 'There is no field available to submit the opt-in form.', 'sellkit' ) )
135 ->set_success( false );
136
137 wp_send_json_error( $this->response );
138 }
139
140 $this
141 ->validate_form()
142 ->set_custom_messages()
143 ->run_actions()
144 ->send_response();
145 }
146
147 /**
148 * Get messages.
149 *
150 * @since 2.3.0
151 * @access public
152 * @return array
153 */
154 public function get_messages() {
155 $this->set_custom_messages();
156
157 return $this->messages;
158 }
159
160 /**
161 * Validate the form based on form ID.
162 *
163 * @since 1.5.0
164 * @access public
165 */
166 public function set_custom_messages() {
167 $this->messages = [
168 'success' => esc_html__( 'The form was sent successfully!', 'sellkit' ),
169 'error' => esc_html__( 'Please check the errors.', 'sellkit' ),
170 'required' => esc_html__( 'Required', 'sellkit' ),
171 ];
172
173 if ( ! $this->attributes ) {
174 return $this;
175 }
176
177 $enable = isset( $this->attributes['customMessagesEnabled'] ) ? $this->attributes['customMessagesEnabled'] : '';
178
179 if ( ! $enable ) {
180 return $this;
181 }
182
183 $success = isset( $this->attributes['successCustomMessage'] ) ? $this->attributes['successCustomMessage'] : $this->messages['success'];
184 $errors = isset( $this->attributes['errorsCustomMessage'] ) ? $this->attributes['errorsCustomMessage'] : $this->messages['error'];
185 $required = isset( $this->attributes['requiredCustomMessage'] ) ? $this->attributes['requiredCustomMessage'] : $this->messages['required'];
186
187 $this->messages = [
188 'success' => $success,
189 'error' => $errors,
190 'required' => $required,
191 ];
192
193 return $this;
194 }
195
196 /**
197 * Get optin block data.
198 *
199 * @since 2.3.0
200 * @access private
201 * @return void
202 */
203 private function get_optin_block_data() {
204 if ( empty( $this->post_id ) || ! has_block( 'sellkit-blocks/optin', $this->post_id ) ) {
205 return;
206 }
207
208 $post_content = get_post_field( 'post_content', $this->post_id );
209 $blocks = parse_blocks( $post_content );
210
211 foreach ( $blocks as $block ) {
212 if ( 'sellkit-blocks/optin' === $block['blockName'] && $block['attrs']['clientId'] === $this->client_id ) {
213 $this->attributes = $block['attrs'];
214 return;
215 }
216 }
217 }
218
219 /**
220 * Validate the form based on form ID.
221 *
222 * @since 2.3.0
223 * @access public
224 */
225 public function validate_form() {
226 if ( ! empty( $this->attributes ) ) {
227 return $this;
228 }
229
230 $this
231 ->add_response( 'message', esc_html__( 'There\'s something wrong. The form is not valid.', 'sellkit' ) )
232 ->set_success( false )
233 ->send_response();
234 }
235
236 /**
237 * Run all the specified actions.
238 *
239 * @since 2.3.0
240 * @access public
241 */
242 public function run_actions() {
243 $selected_crm = isset( $this->attributes['selectedCRM'] ) ? $this->attributes['selectedCRM'] : [];
244
245 if ( ! empty( $selected_crm ) ) {
246 foreach ( $selected_crm as $crm ) {
247 if ( ! isset( $this->attributes[ $crm ] ) && 'webHook' !== $crm ) {
248 return $this->add_response( 'admin_errors', "{$crm}: " . esc_html__( 'Missing configuration.', 'sellkit' ) );
249 }
250
251 $crm_name = strtolower( $crm );
252 $class_path = "block-editor/blocks/optin/crm/{$crm_name}";
253
254 sellkit()->load_files( [
255 $class_path,
256 ] );
257
258 $class_name = str_replace( ' ', '_', ucwords( $crm ) );
259 $class_name = "Sellkit\Blocks\Optin\\{$class_name}";
260
261 if ( class_exists( $class_name ) ) {
262 $class_name::run( $this );
263 }
264 }
265 }
266
267 $this->handle_download_redirect();
268
269 return $this;
270 }
271
272 /**
273 * Handle download redirect.
274 *
275 * @since 2.3.0
276 */
277 private function handle_download_redirect() {
278 if ( ! $this->is_success ) {
279 return;
280 }
281
282 $download_source = ! empty( $this->attributes['afterSubmitDownload'] ) ? $this->attributes['afterSubmitDownload'] : '';
283
284 switch ( $download_source ) {
285 case 'file':
286 $this->download_file();
287 break;
288 case 'url':
289 $this->download_url();
290 break;
291 }
292
293 $this->redirect();
294 }
295
296 /**
297 * Handle file download.
298 *
299 * @since 2.3.0
300 * @access private
301 * @return void
302 */
303 private function download_file() {
304 $file = ! empty( $this->attributes['afterSubmitDownloadMedia'] ) ? $this->attributes['afterSubmitDownloadMedia'] : '';
305
306 if ( empty( $file['filename'] ) ) {
307 return;
308 }
309
310 if ( ! file_exists( $file['path'] ) ) {
311 $admin_error = esc_html__( 'Download error: The file doesn\'t exist anymore.', 'sellkit' );
312 return $this->add_response( 'admin_errors', $admin_error );
313 }
314
315 $args = [
316 'action' => 'sellkit_download_file',
317 'file' => base64_encode( $file['path'] ),
318 '_wpnonce' => wp_create_nonce(),
319 ];
320
321 $url = add_query_arg( $args, admin_url( 'admin-post.php' ) );
322 return $this->add_response( 'downloadURL', $url );
323 }
324
325 /**
326 * Handle file download.
327 *
328 * @since 2.3.0
329 * @access private
330 * @return void
331 */
332 private function download_url() {
333 $url = ! empty( $this->attributes['afterSubmitDownloadURL'] ) ? $this->attributes['afterSubmitDownloadURL'] : '';
334
335 if ( empty( $url ) ) {
336 return;
337 }
338
339 if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
340 $admin_error = esc_html__( 'Download error: Invalid file URL.', 'sellkit' );
341 return $this->add_response( 'admin_errors', $admin_error );
342 }
343
344 return $this->add_response( 'downloadURL', $url );
345 }
346
347 /**
348 * Handles decision node if there is no page on the decision in funnel.
349 *
350 * @param object $funnel_old_data The old funnel data.
351 * @since 2.3.0
352 * @return array|null
353 */
354 private function handle_decisiton( $funnel_old_data ) {
355 if ( ! empty( $funnel_old_data->next_step_data['page_id'] ) ) {
356 return;
357 }
358
359 if ( empty( $funnel_old_data->current_step_data['page_id'] ) ) {
360 return;
361 }
362
363 $current_step_id = $funnel_old_data->current_step_data['page_id'];
364
365 $funnel = new \Sellkit_Funnel( $current_step_id );
366 $decicion_data = $funnel->next_step_data;
367 $conditions = $decicion_data['data']['conditions'];
368 $funnel_data = get_post_meta( $funnel->funnel_id, 'nodes', true );
369 $next_no = $funnel_data[ $decicion_data['targets'][1]['nodeId'] ];
370 $next_yes = $funnel_data[ $decicion_data['targets'][0]['nodeId'] ];
371 $is_valid = sellkit_conditions_validation( $conditions );
372 $next_step = $next_no;
373
374 if ( $is_valid ) {
375 $next_step = $next_yes;
376 }
377
378 return $next_step;
379 }
380
381 /**
382 * Redirect to the next step.
383 *
384 * @since 2.3.0
385 * @access private
386 * @return void
387 */
388 private function redirect() {
389 $target = ! empty( $this->attributes['afterSubmitRedirect'] ) ? $this->attributes['afterSubmitRedirect'] : 'funnel';
390
391 // When redirect target is funnel next step.
392 if ( 'funnel' === $target ) {
393 $funnel = sellkit_funnel();
394
395 if ( empty( $funnel->next_step_data ) || empty( $funnel->next_step_data['page_id'] ) ) {
396 $next_step_data = $this->handle_decisiton( $funnel );
397
398 if ( empty( $next_step_data ) ) {
399 $this
400 ->set_success( false )
401 ->add_response( 'errors', esc_html__( 'Internal server error: failed to find next funnel step.', 'sellkit' ) );
402 return;
403 }
404
405 $funnel->next_step_data = $next_step_data;
406 }
407
408 $next_page_id = sellkit_funnel()->next_step_data['page_id'];
409 $current_url = filter_input( INPUT_POST, 'referrer', FILTER_SANITIZE_URL );
410 $current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
411
412 wp_parse_str( $current_query, $params );
413 unset( $params['sellkit_step'] );
414
415 // Nonce is already checked in AJAX handler class, so we ignore its phpcs warning.
416 $next_step_url = add_query_arg( $params, get_permalink( intval( $next_page_id ) ) ); //phpcs:ignore WordPress.Security.NonceVerification
417 return $this->add_response( 'redirectURL', $next_step_url );
418 }
419
420 $url = ! empty( $this->attributes['afterSubmitRedirectURL'] ) ? $this->attributes['afterSubmitRedirectURL'] : '';
421
422 // When redirect target is a custom URL.
423 if ( empty( $url ) ) {
424 return;
425 }
426
427 if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
428 $admin_error = esc_html__( 'Redirect error: Invalid URL.', 'sellkit' );
429 return $this->add_response( 'admin_errors', $admin_error );
430 }
431
432 return $this->add_response( 'redirectURL', $url );
433 }
434
435 /**
436 * Set form state to success/error.
437 *
438 * @param boolean $bool True or false.
439 *
440 * @since 1.5.0
441 * @access public
442 */
443 public function set_success( $bool ) {
444 $this->is_success = $bool;
445 return $this;
446 }
447
448 /**
449 * Add response to ajax response.
450 *
451 * @param string $type Response type.
452 * @param string $text Response text.
453 * @param string $text_key Response text key.
454 *
455 * @since 2.3.0
456 * @access public
457 */
458 public function add_response( $type, $text = '', $text_key = '' ) {
459 if ( ! empty( $text_key ) ) {
460 $this->response[ $type ][ $text_key ] = $text;
461 return $this;
462 }
463
464 $this->response[ $type ][] = $text;
465 return $this;
466 }
467
468 /**
469 * Send success/fail response.
470 *
471 * @since 2.3.0
472 * @access public
473 */
474 public function send_response() {
475 if ( ! current_user_can( 'administrator' ) ) {
476 unset( $this->response['admin_errors'] );
477 } else {
478 // Flatten admin_errors.
479 $this->response['admin_errors'] = array_values( $this->response['admin_errors'] );
480 }
481
482 if ( $this->is_success ) {
483 if ( empty( $this->response['message'] ) ) {
484 $this->add_response( 'message', $this->messages['success'] );
485 }
486
487 Base_Contacts::step_is_passed();
488
489 wp_send_json_success( $this->response );
490 }
491
492 if ( ! empty( $this->response['errors'] ) ) {
493 $this->add_response( 'message', $this->messages['error'] );
494 }
495
496 wp_send_json_error( $this->response );
497 }
498
499 /**
500 * Called by hook and handles file download.
501 *
502 * @since 2.3.0
503 * @access public
504 * @SuppressWarnings(PHPMD.NPathComplexity)
505 */
506 public function handle_file_download() {
507 $file = filter_input( INPUT_GET, 'file' );
508 $nonce = filter_input( INPUT_GET, '_wpnonce' );
509
510 // Validate nonce.
511 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce ) ) {
512 wp_die( '<script>window.close();</script>' );
513 }
514
515 $file = base64_decode( $file ); // phpcs:ignore
516 $upload_dir = wp_get_upload_dir();
517
518 // Make sure file exists.
519 if ( empty( $file ) || ! file_exists( $file ) ) {
520 wp_die( '<script>window.close();</script>' );
521 }
522
523 $file_name = pathinfo( $file, PATHINFO_BASENAME );
524 $file_info = wp_check_filetype_and_ext( $file, $file_name );
525
526 $real_file_path = realpath( $file );
527
528 // Make sure the file exists and is inside the allowed directory.
529 if (
530 false === $real_file_path ||
531 ! file_exists( $real_file_path ) ||
532 0 !== strpos( wp_normalize_path( $file ), wp_normalize_path( $upload_dir['basedir'] ) )
533 ) {
534 wp_die( '<script>window.close();</script>' );
535 }
536
537 // Validate file extension and MIME type.
538 if ( empty( $file_info['ext'] ) || empty( $file_info['type'] ) ) {
539 wp_die( '<script>window.close();</script>' );
540 }
541
542 // Validate file path.
543 if (
544 strpos( $file, wp_normalize_path( WP_CONTENT_DIR . '/uploads/' ) ) === false ||
545 strpos( $file, wp_normalize_path( WP_CONTENT_DIR . '/uploads/' ) ) !== 0
546 ) {
547 wp_die( '<script>window.close();</script>' );
548 }
549
550 // Restrict the download to WP upload directory.
551 if (
552 strpos( $file, $upload_dir['basedir'] ) === false ||
553 strpos( $file, $upload_dir['basedir'] ) !== 0
554 ) {
555 wp_die( '<script>window.close();</script>' );
556 }
557
558 $file_ext = pathinfo( $file, PATHINFO_EXTENSION );
559
560 // Strip hash.
561 $file_name = str_replace( $file_ext, '', $file_name );
562 $file_parts = explode( '__', $file_name );
563 $file_name = array_shift( $file_parts );
564 $file_name .= '.' . $file_ext;
565
566 header( 'Content-Description: File Transfer' );
567 header( 'Content-Type: application/octet-stream' );
568 header( 'Content-Disposition: attachment; filename="' . $file_name . '"' );
569 header( 'Expires: 0' );
570 header( 'Cache-Control: must-revalidate' );
571 header( 'Pragma: public' );
572 header( 'Content-Length: ' . filesize( $file ) );
573 // phpcs:ignore WordPress.WP.AlternativeFunctions
574 readfile( $file );
575 }
576
577 /**
578 * Get API params.
579 *
580 * @param string $crm The CRM name.
581 * @param string $title The CRM title.
582 * @param bool $has_url Whether the CRM has URL.
583 * @return void|object
584 */
585 public function check_api_params( $crm, $title, $has_url = false ) {
586 $option_name = "{$crm}_api_key";
587
588 $this->api_key = sellkit_get_option( $option_name, '' );
589
590 if ( $has_url ) {
591 $option_name = "{$crm}_api_url";
592 $this->api_url = sellkit_get_option( $option_name, '' );
593
594 if ( empty( $this->api_url ) ) {
595 return $this->add_response( 'admin_errors', "{$title}: " . esc_html__( 'Missing API URL.', 'sellkit' ) );
596 }
597 }
598
599 if ( empty( $this->api_key ) ) {
600 return $this->add_response( 'admin_errors', "{$title}: " . esc_html__( 'Missing API credentials.', 'sellkit' ) );
601 }
602 }
603
604 /**
605 * Creates and returns a text that notifies the user that a field should be made required.
606 *
607 * @param string $title Title of the CRM.
608 * @param string $field_label Label of the field.
609 * @return string
610 *
611 * @access protected
612 * @since 2.3.0
613 */
614 protected function get_make_require_notice( $title, $field_label ) {
615 return sprintf(
616 /* translators: 1: Action name 2: Field name */
617 esc_attr__( '%1$s: %2$s is required by api endpoint, but the corresponding field is not made required in your form.', 'sellkit' ),
618 $title,
619 $field_label
620 );
621 }
622
623 /**
624 * Creates and returns a text that notifies the user that a field should be made required.
625 *
626 * @param string $title Title of the CRM.
627 * @param string $field_label Label of the field.
628 * @return string
629 *
630 * @access protected
631 * @since 2.3.0
632 */
633 protected function make_missing_field( $title, $field_label ) {
634 return sprintf(
635 /* translators: 1: Action name 2: Field name */
636 esc_attr__( '%1$s: %2$s field is missing, please check the created form fields.', 'sellkit' ),
637 $title,
638 $field_label
639 );
640 }
641
642 /**
643 * Check required fields.
644 *
645 * @param array $fields The fields.
646 * @param array $requird_fields The required fields.
647 * @return mixed
648 */
649 public function check_required_fields( $fields, $requird_fields ) {
650 foreach ( $requird_fields as $field ) {
651 $value = isset( $fields[ $field ] ) ? $fields[ $field ] : null;
652 if ( ! is_null( $value ) ) {
653 continue;
654 }
655
656 return $this->add_response( 'admin_errors', $this->get_make_require_notice( 'Active Campaign', strtoupper( $field ) ) );
657 }
658 }
659
660 /**
661 * Get Client IP Address.
662 *
663 * @return string
664 *
665 * @since 1.5.0
666 * @access public
667 * @static
668 */
669 public static function get_client_ip() {
670 $ip_address = '';
671 $server_headers = [
672 'HTTP_CLIENT_IP',
673 'HTTP_X_FORWARDED_FOR',
674 'HTTP_X_FORWARDED',
675 'HTTP_FORWARDED_FOR',
676 'HTTP_FORWARDED',
677 'REMOTE_ADDR',
678 ];
679
680 // phpcs:disable WordPress.Security.ValidatedSanitizedInput
681 foreach ( $server_headers as $header ) {
682 if ( isset( $_SERVER[ $header ] ) ) {
683 $ip_address = $_SERVER[ $header ];
684 break;
685 }
686 }
687 // phpcs:enable
688
689 return $ip_address;
690 }
691
692 /**
693 * Get address field.
694 *
695 * @param mixed $field The field.
696 * @param string $index The index.
697 * @return mixed
698 */
699 public function get_address_field( $field, $index ) {
700 if ( ! is_array( $field ) ) {
701 return $field;
702 }
703
704 if ( ! isset( $field['address'] ) ) {
705 return $field;
706 }
707
708 if ( isset( $field[ $index ] ) ) {
709 return $field[ $index ];
710 }
711
712 return $field;
713 }
714 }
715
716