init_hooks(); } /** * Get the single instance. * * @since 1.0.0 * @access public * * @return AB_Testing */ public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Register AJAX action hooks. * * @since 1.0.0 * @access private * * @return void */ private function init_hooks() { add_action( 'wp_ajax_acumenpopup_get_ab_tests', array( $this, 'get_tests' ) ); add_action( 'wp_ajax_acumenpopup_create_ab_test', array( $this, 'create_test' ) ); add_action( 'wp_ajax_acumenpopup_delete_ab_test', array( $this, 'delete_test' ) ); add_action( 'wp_ajax_acumenpopup_toggle_ab_test', array( $this, 'toggle_test' ) ); } /** * Verify nonce and capability for every request. * * @since 1.0.0 * @access private * * @return void Sends JSON error and exits on failure. */ private function verify_request() { check_ajax_referer( 'acumenpopup-nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 ); } } /** * Load all A/B tests from the database. * * @since 1.0.0 * @access private * * @return array */ private function load_tests() { $tests = get_option( $this->option_key, array() ); return is_array( $tests ) ? $tests : array(); } /** * AJAX: Return all A/B tests. * * @since 1.0.0 * @access public * * @return void */ public function get_tests() { $this->verify_request(); wp_send_json_success( array_values( $this->load_tests() ) ); } /** * AJAX: Create a new A/B test. * * @since 1.0.0 * @access public * * @return void */ public function create_test() { $this->verify_request(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $raw = isset( $_POST['test'] ) ? wp_unslash( $_POST['test'] ) : '{}'; $input = json_decode( $raw, true ); if ( ! is_array( $input ) ) { wp_send_json_error( array( 'message' => __( 'Invalid data.', 'acumen-popup' ) ) ); } $name = sanitize_text_field( $input['name'] ?? '' ); if ( empty( $name ) ) { wp_send_json_error( array( 'message' => __( 'Test name is required.', 'acumen-popup' ) ) ); } $variants = array_filter( array_map( 'absint', (array) ( $input['variants'] ?? array() ) ) ); if ( count( $variants ) < 2 ) { wp_send_json_error( array( 'message' => __( 'At least 2 variants with campaigns are required.', 'acumen-popup' ) ) ); } $winner_mode = in_array( $input['winner_mode'] ?? '', array( 'manual', 'automatic' ), true ) ? $input['winner_mode'] : 'manual'; $winner_param = in_array( $input['winner_param'] ?? '', $this->allowed_params, true ) ? $input['winner_param'] : 'total_conversions'; $duration = max( 1, min( 365, absint( $input['duration'] ?? 7 ) ) ); $test = array( 'id' => wp_generate_uuid4(), 'name' => $name, 'enabled' => ! empty( $input['enabled'] ), 'variants' => array_values( $variants ), 'winner_mode' => $winner_mode, 'winner_param'=> $winner_param, 'duration' => $duration, 'status' => ! empty( $input['enabled'] ) ? 'active' : 'inactive', 'created' => time(), ); $tests = $this->load_tests(); $tests[ $test['id'] ] = $test; update_option( $this->option_key, $tests ); wp_send_json_success( $test ); } /** * AJAX: Delete an A/B test by ID. * * @since 1.0.0 * @access public * * @return void */ public function delete_test() { $this->verify_request(); $id = sanitize_text_field( wp_unslash( $_POST['id'] ?? '' ) ); $tests = $this->load_tests(); if ( ! isset( $tests[ $id ] ) ) { wp_send_json_error( array( 'message' => __( 'Test not found.', 'acumen-popup' ) ) ); } unset( $tests[ $id ] ); update_option( $this->option_key, $tests ); wp_send_json_success(); } /** * AJAX: Toggle the enabled state of an A/B test. * * @since 1.0.0 * @access public * * @return void */ public function toggle_test() { $this->verify_request(); $id = sanitize_text_field( wp_unslash( $_POST['id'] ?? '' ) ); $enabled = ! empty( $_POST['enabled'] ) && 'true' === $_POST['enabled']; $tests = $this->load_tests(); if ( ! isset( $tests[ $id ] ) ) { wp_send_json_error( array( 'message' => __( 'Test not found.', 'acumen-popup' ) ) ); } $tests[ $id ]['enabled'] = $enabled; $tests[ $id ]['status'] = $enabled ? 'active' : 'inactive'; update_option( $this->option_key, $tests ); wp_send_json_success( $tests[ $id ] ); } /** * 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' ); } }