[], 'errors' => [], 'admin_errors' => [], ]; /** * Holds all the messages. * * @access private * @var array */ private $messages = []; /** * Holds the reponse state. * * @access public * @var bool */ public $is_success = true; /** * Holds a record of the user-filled form. * * @access public * @var array */ public $form_data; /** * Holds the API key. * * @access private * @var string */ public $api_key; /** * Holds the API URL. * * @access private * @var string */ public $api_url; /** * Class constructor. * * @since 2.3.0 */ public function __construct() { add_action( 'wp_ajax_submit_optin_block_form', [ $this, 'handle_frontend' ] ); add_action( 'wp_ajax_nopriv_submit_optin_block_form', [ $this, 'handle_frontend' ] ); add_action( 'admin_post_sellkit_download_file', [ $this, 'handle_file_download' ] ); add_action( 'admsellkitin_post_nopriv_sellkit_download_file', [ $this, 'handle_file_download' ] ); } /** * Handle form submission. * * @since 2.3.0 */ public function handle_frontend() { if ( false === check_ajax_referer( 'sellkit_block', 'nonce', false ) ) { wp_send_json_error( esc_html__( 'Error: Nonce mismatch or expired. Please reload the page and retry.', 'sellkit' ) ); } $this->post_id = (int) filter_input( INPUT_POST, 'post_id' ); $this->client_id = filter_input( INPUT_POST, 'client_id' ); $funnel_page_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT ); $this->form_data = $_POST; $funnel = \Sellkit_Funnel::get_instance( $funnel_page_id ); if ( ! empty( $funnel->current_step_data['type']['key'] ) && 'sales-page' === $funnel->current_step_data['type']['key'] ) { $this ->add_response( 'errors', esc_html__( 'Opt-in widget should not be used in the sales page step.', 'sellkit' ) ) ->set_success( false ); wp_send_json_error( $this->response ); } $this->get_optin_block_data(); if ( empty( $this->form_data['fields'] ) ) { $this ->add_response( 'errors', esc_html__( 'There is no field available to submit the opt-in form.', 'sellkit' ) ) ->set_success( false ); wp_send_json_error( $this->response ); } $this ->validate_form() ->set_custom_messages() ->run_actions() ->send_response(); } /** * Get messages. * * @since 2.3.0 * @access public * @return array */ public function get_messages() { $this->set_custom_messages(); return $this->messages; } /** * Validate the form based on form ID. * * @since 1.5.0 * @access public */ public function set_custom_messages() { $this->messages = [ 'success' => esc_html__( 'The form was sent successfully!', 'sellkit' ), 'error' => esc_html__( 'Please check the errors.', 'sellkit' ), 'required' => esc_html__( 'Required', 'sellkit' ), ]; if ( ! $this->attributes ) { return $this; } $enable = isset( $this->attributes['customMessagesEnabled'] ) ? $this->attributes['customMessagesEnabled'] : ''; if ( ! $enable ) { return $this; } $success = isset( $this->attributes['successCustomMessage'] ) ? $this->attributes['successCustomMessage'] : $this->messages['success']; $errors = isset( $this->attributes['errorsCustomMessage'] ) ? $this->attributes['errorsCustomMessage'] : $this->messages['error']; $required = isset( $this->attributes['requiredCustomMessage'] ) ? $this->attributes['requiredCustomMessage'] : $this->messages['required']; $this->messages = [ 'success' => $success, 'error' => $errors, 'required' => $required, ]; return $this; } /** * Get optin block data. * * @since 2.3.0 * @access private * @return void */ private function get_optin_block_data() { if ( empty( $this->post_id ) || ! has_block( 'sellkit-blocks/optin', $this->post_id ) ) { return; } $post_content = get_post_field( 'post_content', $this->post_id ); $blocks = parse_blocks( $post_content ); foreach ( $blocks as $block ) { if ( 'sellkit-blocks/optin' === $block['blockName'] && $block['attrs']['clientId'] === $this->client_id ) { $this->attributes = $block['attrs']; return; } } } /** * Validate the form based on form ID. * * @since 2.3.0 * @access public */ public function validate_form() { if ( ! empty( $this->attributes ) ) { return $this; } $this ->add_response( 'message', esc_html__( 'There\'s something wrong. The form is not valid.', 'sellkit' ) ) ->set_success( false ) ->send_response(); } /** * Run all the specified actions. * * @since 2.3.0 * @access public */ public function run_actions() { $selected_crm = isset( $this->attributes['selectedCRM'] ) ? $this->attributes['selectedCRM'] : []; if ( ! empty( $selected_crm ) ) { foreach ( $selected_crm as $crm ) { if ( ! isset( $this->attributes[ $crm ] ) && 'webHook' !== $crm ) { return $this->add_response( 'admin_errors', "{$crm}: " . esc_html__( 'Missing configuration.', 'sellkit' ) ); } $crm_name = strtolower( $crm ); $class_path = "block-editor/blocks/optin/crm/{$crm_name}"; sellkit()->load_files( [ $class_path, ] ); $class_name = str_replace( ' ', '_', ucwords( $crm ) ); $class_name = "Sellkit\Blocks\Optin\\{$class_name}"; if ( class_exists( $class_name ) ) { $class_name::run( $this ); } } } $this->handle_download_redirect(); return $this; } /** * Handle download redirect. * * @since 2.3.0 */ private function handle_download_redirect() { if ( ! $this->is_success ) { return; } $download_source = ! empty( $this->attributes['afterSubmitDownload'] ) ? $this->attributes['afterSubmitDownload'] : ''; switch ( $download_source ) { case 'file': $this->download_file(); break; case 'url': $this->download_url(); break; } $this->redirect(); } /** * Handle file download. * * @since 2.3.0 * @access private * @return void */ private function download_file() { $file = ! empty( $this->attributes['afterSubmitDownloadMedia'] ) ? $this->attributes['afterSubmitDownloadMedia'] : ''; if ( empty( $file['filename'] ) ) { return; } if ( ! file_exists( $file['path'] ) ) { $admin_error = esc_html__( 'Download error: The file doesn\'t exist anymore.', 'sellkit' ); return $this->add_response( 'admin_errors', $admin_error ); } $args = [ 'action' => 'sellkit_download_file', 'file' => base64_encode( $file['path'] ), '_wpnonce' => wp_create_nonce(), ]; $url = add_query_arg( $args, admin_url( 'admin-post.php' ) ); return $this->add_response( 'downloadURL', $url ); } /** * Handle file download. * * @since 2.3.0 * @access private * @return void */ private function download_url() { $url = ! empty( $this->attributes['afterSubmitDownloadURL'] ) ? $this->attributes['afterSubmitDownloadURL'] : ''; if ( empty( $url ) ) { return; } if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { $admin_error = esc_html__( 'Download error: Invalid file URL.', 'sellkit' ); return $this->add_response( 'admin_errors', $admin_error ); } return $this->add_response( 'downloadURL', $url ); } /** * Handles decision node if there is no page on the decision in funnel. * * @param object $funnel_old_data The old funnel data. * @since 2.3.0 * @return array|null */ private function handle_decisiton( $funnel_old_data ) { if ( ! empty( $funnel_old_data->next_step_data['page_id'] ) ) { return; } if ( empty( $funnel_old_data->current_step_data['page_id'] ) ) { return; } $current_step_id = $funnel_old_data->current_step_data['page_id']; $funnel = new \Sellkit_Funnel( $current_step_id ); $decicion_data = $funnel->next_step_data; $conditions = $decicion_data['data']['conditions']; $funnel_data = get_post_meta( $funnel->funnel_id, 'nodes', true ); $next_no = $funnel_data[ $decicion_data['targets'][1]['nodeId'] ]; $next_yes = $funnel_data[ $decicion_data['targets'][0]['nodeId'] ]; $is_valid = sellkit_conditions_validation( $conditions ); $next_step = $next_no; if ( $is_valid ) { $next_step = $next_yes; } return $next_step; } /** * Redirect to the next step. * * @since 2.3.0 * @access private * @return void */ private function redirect() { $target = ! empty( $this->attributes['afterSubmitRedirect'] ) ? $this->attributes['afterSubmitRedirect'] : 'funnel'; // When redirect target is funnel next step. if ( 'funnel' === $target ) { $funnel = sellkit_funnel(); if ( empty( $funnel->next_step_data ) || empty( $funnel->next_step_data['page_id'] ) ) { $next_step_data = $this->handle_decisiton( $funnel ); if ( empty( $next_step_data ) ) { $this ->set_success( false ) ->add_response( 'errors', esc_html__( 'Internal server error: failed to find next funnel step.', 'sellkit' ) ); return; } $funnel->next_step_data = $next_step_data; } $next_page_id = sellkit_funnel()->next_step_data['page_id']; $current_url = filter_input( INPUT_POST, 'referrer', FILTER_SANITIZE_URL ); $current_query = wp_parse_url( $current_url, PHP_URL_QUERY ); wp_parse_str( $current_query, $params ); unset( $params['sellkit_step'] ); // Nonce is already checked in AJAX handler class, so we ignore its phpcs warning. $next_step_url = add_query_arg( $params, get_permalink( intval( $next_page_id ) ) ); //phpcs:ignore WordPress.Security.NonceVerification return $this->add_response( 'redirectURL', $next_step_url ); } $url = ! empty( $this->attributes['afterSubmitRedirectURL'] ) ? $this->attributes['afterSubmitRedirectURL'] : ''; // When redirect target is a custom URL. if ( empty( $url ) ) { return; } if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { $admin_error = esc_html__( 'Redirect error: Invalid URL.', 'sellkit' ); return $this->add_response( 'admin_errors', $admin_error ); } return $this->add_response( 'redirectURL', $url ); } /** * Set form state to success/error. * * @param boolean $bool True or false. * * @since 1.5.0 * @access public */ public function set_success( $bool ) { $this->is_success = $bool; return $this; } /** * Add response to ajax response. * * @param string $type Response type. * @param string $text Response text. * @param string $text_key Response text key. * * @since 2.3.0 * @access public */ public function add_response( $type, $text = '', $text_key = '' ) { if ( ! empty( $text_key ) ) { $this->response[ $type ][ $text_key ] = $text; return $this; } $this->response[ $type ][] = $text; return $this; } /** * Send success/fail response. * * @since 2.3.0 * @access public */ public function send_response() { if ( ! current_user_can( 'administrator' ) ) { unset( $this->response['admin_errors'] ); } else { // Flatten admin_errors. $this->response['admin_errors'] = array_values( $this->response['admin_errors'] ); } if ( $this->is_success ) { if ( empty( $this->response['message'] ) ) { $this->add_response( 'message', $this->messages['success'] ); } Base_Contacts::step_is_passed(); wp_send_json_success( $this->response ); } if ( ! empty( $this->response['errors'] ) ) { $this->add_response( 'message', $this->messages['error'] ); } wp_send_json_error( $this->response ); } /** * Called by hook and handles file download. * * @since 2.3.0 * @access public * @SuppressWarnings(PHPMD.NPathComplexity) */ public function handle_file_download() { $file = filter_input( INPUT_GET, 'file' ); $nonce = filter_input( INPUT_GET, '_wpnonce' ); // Validate nonce. if ( empty( $nonce ) || ! wp_verify_nonce( $nonce ) ) { wp_die( '' ); } $file = base64_decode( $file ); // phpcs:ignore $upload_dir = wp_get_upload_dir(); // Make sure file exists. if ( empty( $file ) || ! file_exists( $file ) ) { wp_die( '' ); } $file_name = pathinfo( $file, PATHINFO_BASENAME ); $file_info = wp_check_filetype_and_ext( $file, $file_name ); $real_file_path = realpath( $file ); // Make sure the file exists and is inside the allowed directory. if ( false === $real_file_path || ! file_exists( $real_file_path ) || 0 !== strpos( wp_normalize_path( $file ), wp_normalize_path( $upload_dir['basedir'] ) ) ) { wp_die( '' ); } // Validate file extension and MIME type. if ( empty( $file_info['ext'] ) || empty( $file_info['type'] ) ) { wp_die( '' ); } // Validate file path. if ( strpos( $file, wp_normalize_path( WP_CONTENT_DIR . '/uploads/' ) ) === false || strpos( $file, wp_normalize_path( WP_CONTENT_DIR . '/uploads/' ) ) !== 0 ) { wp_die( '' ); } // Restrict the download to WP upload directory. if ( strpos( $file, $upload_dir['basedir'] ) === false || strpos( $file, $upload_dir['basedir'] ) !== 0 ) { wp_die( '' ); } $file_ext = pathinfo( $file, PATHINFO_EXTENSION ); // Strip hash. $file_name = str_replace( $file_ext, '', $file_name ); $file_parts = explode( '__', $file_name ); $file_name = array_shift( $file_parts ); $file_name .= '.' . $file_ext; header( 'Content-Description: File Transfer' ); header( 'Content-Type: application/octet-stream' ); header( 'Content-Disposition: attachment; filename="' . $file_name . '"' ); header( 'Expires: 0' ); header( 'Cache-Control: must-revalidate' ); header( 'Pragma: public' ); header( 'Content-Length: ' . filesize( $file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions readfile( $file ); } /** * Get API params. * * @param string $crm The CRM name. * @param string $title The CRM title. * @param bool $has_url Whether the CRM has URL. * @return void|object */ public function check_api_params( $crm, $title, $has_url = false ) { $option_name = "{$crm}_api_key"; $this->api_key = sellkit_get_option( $option_name, '' ); if ( $has_url ) { $option_name = "{$crm}_api_url"; $this->api_url = sellkit_get_option( $option_name, '' ); if ( empty( $this->api_url ) ) { return $this->add_response( 'admin_errors', "{$title}: " . esc_html__( 'Missing API URL.', 'sellkit' ) ); } } if ( empty( $this->api_key ) ) { return $this->add_response( 'admin_errors', "{$title}: " . esc_html__( 'Missing API credentials.', 'sellkit' ) ); } } /** * Creates and returns a text that notifies the user that a field should be made required. * * @param string $title Title of the CRM. * @param string $field_label Label of the field. * @return string * * @access protected * @since 2.3.0 */ protected function get_make_require_notice( $title, $field_label ) { return sprintf( /* translators: 1: Action name 2: Field name */ esc_attr__( '%1$s: %2$s is required by api endpoint, but the corresponding field is not made required in your form.', 'sellkit' ), $title, $field_label ); } /** * Creates and returns a text that notifies the user that a field should be made required. * * @param string $title Title of the CRM. * @param string $field_label Label of the field. * @return string * * @access protected * @since 2.3.0 */ protected function make_missing_field( $title, $field_label ) { return sprintf( /* translators: 1: Action name 2: Field name */ esc_attr__( '%1$s: %2$s field is missing, please check the created form fields.', 'sellkit' ), $title, $field_label ); } /** * Check required fields. * * @param array $fields The fields. * @param array $requird_fields The required fields. * @return mixed */ public function check_required_fields( $fields, $requird_fields ) { foreach ( $requird_fields as $field ) { $value = isset( $fields[ $field ] ) ? $fields[ $field ] : null; if ( ! is_null( $value ) ) { continue; } return $this->add_response( 'admin_errors', $this->get_make_require_notice( 'Active Campaign', strtoupper( $field ) ) ); } } /** * Get Client IP Address. * * @return string * * @since 1.5.0 * @access public * @static */ public static function get_client_ip() { $ip_address = ''; $server_headers = [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ]; // phpcs:disable WordPress.Security.ValidatedSanitizedInput foreach ( $server_headers as $header ) { if ( isset( $_SERVER[ $header ] ) ) { $ip_address = $_SERVER[ $header ]; break; } } // phpcs:enable return $ip_address; } /** * Get address field. * * @param mixed $field The field. * @param string $index The index. * @return mixed */ public function get_address_field( $field, $index ) { if ( ! is_array( $field ) ) { return $field; } if ( ! isset( $field['address'] ) ) { return $field; } if ( isset( $field[ $index ] ) ) { return $field[ $index ]; } return $field; } }