init_hooks(); } /** * Get the single instance of the Handler class. * * @since 1.0.0 * @access public * * @return Handler The single instance. */ public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Initialize WordPress hooks. * * @since 1.0.0 * @access private * * @return void */ private function init_hooks() { add_action( 'wp_ajax_acumenpopup_form_submit', array( $this, 'handle_form_submission' ) ); add_action( 'wp_ajax_nopriv_acumenpopup_form_submit', array( $this, 'handle_form_submission' ) ); } /** * Handle form submission via AJAX. * * Validates the request, retrieves form configuration, * and sends the email. * * @since 1.0.0 * @access public * * @return void Sends JSON response and exits. */ public function handle_form_submission() { // Verify nonce. $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'acumenpopup_form_submit' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid security token.', 'acumen-popup' ) ), 403 ); } // Collect and sanitize posted data. $data = isset( $_POST['data'] ) ? $this->sanitize_form_data( wp_unslash( $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $form_id = isset( $_POST['formId'] ) ? sanitize_text_field( wp_unslash( $_POST['formId'] ) ) : ''; $popup_id = isset( $_POST['popupId'] ) ? sanitize_text_field( wp_unslash( $_POST['popupId'] ) ) : ''; $form_title = isset( $data['nx_form_title']['value'] ) ? sanitize_text_field( $data['nx_form_title']['value'] ) : esc_html__( 'Form Submission', 'acumen-popup' ); // Validate required fields. if ( empty( $popup_id ) || empty( $form_id ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Configuration error: Missing Popup ID or Form ID.', 'acumen-popup' ) ), 400 ); } // Get form configuration from the block. $config = $this->get_form_config( $popup_id, $form_id ); if ( ! $config ) { wp_send_json_error( array( 'message' => esc_html__( 'Configuration error: Form not found.', 'acumen-popup' ) ), 404 ); } // Extract email configuration (server-side only). $to = ! empty( $config['emailTo'] ) ? sanitize_email( $config['emailTo'] ) : get_option( 'admin_email' ); $subject = ! empty( $config['emailSubject'] ) ? sanitize_text_field( $config['emailSubject'] ) : esc_html__( 'New Form Submission', 'acumen-popup' ); $cc = ! empty( $config['emailCC'] ) ? sanitize_text_field( $config['emailCC'] ) : ''; $bcc = ! empty( $config['emailBCC'] ) ? sanitize_text_field( $config['emailBCC'] ) : ''; // Build email body. $body = $this->build_email_body( $form_title, $form_id, $popup_id, $data ); // Build email headers. $headers = $this->build_email_headers( $cc, $bcc ); // Send email. $sent = wp_mail( $to, $subject, $body, $headers ); if ( $sent ) { // Track conversion for analytics $args = array( 'post_type' => 'acumen_popup_cmpgn', 'meta_key' => 'acumenpopup_popup_id', // phpcs:ignore 'meta_value' => $popup_id, // phpcs:ignore 'fields' => 'ids', 'numberposts' => 1, 'post_status' => 'any', ); $posts = get_posts( $args ); $post_id = ! empty( $posts ) ? $posts[0] : ( is_numeric( $popup_id ) ? absint( $popup_id ) : 0 ); if ( $post_id ) { $current_conversions = (int) get_post_meta( $post_id, 'acumenpopup_conversions', true ); update_post_meta( $post_id, 'acumenpopup_conversions', $current_conversions + 1 ); } wp_send_json_success( array( 'message' => esc_html__( 'Form submitted successfully.', 'acumen-popup' ) ) ); } else { wp_send_json_error( array( 'message' => esc_html__( 'Failed to send email.', 'acumen-popup' ) ), 500 ); } } /** * Sanitize form data array. * * @since 1.0.0 * @access private * * @param array $data Raw form data. * * @return array Sanitized form data. */ private function sanitize_form_data( $data ) { if ( ! is_array( $data ) ) { return array(); } $sanitized = array(); foreach ( $data as $key => $item ) { $sanitized_key = sanitize_key( $key ); if ( is_array( $item ) ) { $sanitized[ $sanitized_key ] = array( 'label' => isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : '', 'value' => isset( $item['value'] ) ? wp_kses_post( $item['value'] ) : '', ); } else { $sanitized[ $sanitized_key ] = sanitize_text_field( $item ); } } return $sanitized; } /** * Build the email body from form data. * * @since 1.0.0 * @access private * * @param string $form_title The form title. * @param string $form_id The form ID. * @param string $popup_id The popup ID. * @param array $data The sanitized form data. * * @return string The formatted email body. */ private function build_email_body( $form_title, $form_id, $popup_id, $data ) { $lines = array( sprintf( 'Form: %s', esc_html( $form_title ) ), sprintf( 'Form ID: %s', esc_html( $form_id ) ), sprintf( 'Popup ID: %s', esc_html( $popup_id ) ), '', ); // Internal config keys to skip. $skip_keys = array( 'nx_form_title', 'nx_email_to', 'nx_email_subject', 'nx_email_cc', 'nx_email_bcc', ); foreach ( $data as $key => $item ) { if ( in_array( $key, $skip_keys, true ) ) { continue; } if ( is_array( $item ) ) { $label = isset( $item['label'] ) ? $item['label'] : $key; $value = isset( $item['value'] ) ? $item['value'] : ''; } else { $label = $key; $value = $item; } $lines[] = sprintf( '%s: %s', esc_html( $label ), esc_html( $value ) ); } return implode( "\n", $lines ); } /** * Build email headers with CC and BCC. * * @since 1.0.0 * @access private * * @param string $cc Carbon copy email address. * @param string $bcc Blind carbon copy email address. * * @return array Array of email headers. */ private function build_email_headers( $cc, $bcc ) { $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); if ( ! empty( $cc ) ) { $headers[] = 'Cc: ' . sanitize_email( $cc ); } if ( ! empty( $bcc ) ) { $headers[] = 'Bcc: ' . sanitize_email( $bcc ); } return $headers; } /** * Retrieve form configuration from the popup block. * * @since 1.0.0 * @access private * * @param string $popup_id_str The unique popup ID string. * @param string $form_id The unique form ID. * * @return array|false Form configuration array or false if not found. */ private function get_form_config( $popup_id_str, $form_id ) { // First try: Find the Campaign Post by meta. $posts = get_posts( array( 'post_type' => 'acumen_popup_cmpgn', 'meta_key' => 'acumenpopup_popup_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => sanitize_text_field( $popup_id_str ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'posts_per_page' => 1, 'fields' => 'ids', ) ); // Second try: If no posts found by meta, search all campaigns by block content. if ( empty( $posts ) ) { $all_campaigns = get_posts( array( 'post_type' => 'acumen_popup_cmpgn', 'post_status' => 'any', 'posts_per_page' => -1, ) ); foreach ( $all_campaigns as $campaign ) { $blocks = parse_blocks( $campaign->post_content ); $popup_block = $this->find_popup_block_by_id( $blocks, $popup_id_str ); if ( $popup_block ) { // Found matching popup, now search for form. $form_block = $this->find_form_block_recursive( $blocks, $form_id ); if ( $form_block && isset( $form_block['attrs'] ) ) { return $form_block['attrs']; } } } return false; } $post_id = $posts[0]; $post = get_post( $post_id ); if ( ! $post ) { return false; } $blocks = parse_blocks( $post->post_content ); $form_block = $this->find_form_block_recursive( $blocks, $form_id ); if ( $form_block && isset( $form_block['attrs'] ) ) { return $form_block['attrs']; } return false; } /** * Find popup block by acumenpopupId attribute. * * @since 1.0.0 * @access private * * @param array $blocks Array of parsed blocks. * @param string $popup_id The popup ID to find. * * @return array|null The popup block or null if not found. */ private function find_popup_block_by_id( $blocks, $popup_id ) { foreach ( $blocks as $block ) { if ( isset( $block['blockName'] ) && 'acumenpopup/popup-builder' === $block['blockName'] && isset( $block['attrs']['acumenpopupId'] ) && $block['attrs']['acumenpopupId'] === $popup_id ) { return $block; } // Search inner blocks. if ( ! empty( $block['innerBlocks'] ) ) { $found = $this->find_popup_block_by_id( $block['innerBlocks'], $popup_id ); if ( $found ) { return $found; } } } return null; } /** * Recursively search for a form block by ID. * * @since 1.0.0 * @access private * * @param array $blocks Array of parsed blocks. * @param string $form_id The form ID to find. * * @return array|null The form block or null if not found. */ private function find_form_block_recursive( $blocks, $form_id ) { foreach ( $blocks as $block ) { // Check if this is the form block we're looking for. if ( isset( $block['blockName'] ) && 'acumenpopup/form' === $block['blockName'] && isset( $block['attrs']['formId'] ) && $block['attrs']['formId'] === $form_id ) { return $block; } // Search inner blocks. if ( ! empty( $block['innerBlocks'] ) ) { $found = $this->find_form_block_recursive( $block['innerBlocks'], $form_id ); if ( $found ) { return $found; } } } return null; } /** * Prevent cloning of the instance. * * @since 1.0.0 * @access public * * @return void */ public function __clone() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cloning is forbidden.', 'acumen-popup' ), '1.0.0' ); } /** * Prevent unserializing of the instance. * * @since 1.0.0 * @access public * * @return void */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Unserializing is forbidden.', 'acumen-popup' ), '1.0.0' ); } }