'', 'service_id' => '', 'search_input_id' => '', 'search_button_id' => '', 'enable_on_front' => 1, 'testing_mode' => 1, 'search_mode' => '', 'search_page_id' => 0, 'rewrite_search_forms' => 1, 'redirect_s' => 1, 'traffic_percent' => '', ); } /** * Load and normalise plugin options. * * Existing 1.1.0 installs that already filled a search input ID keep overlay * mode so their search popup does not change on upgrade. * * Testing Mode is on for new installs and off when an existing options row * has no testing_mode key, so live sites do not hide the widget on upgrade. * * @return array */ public static function get_plugin_options() { $stored = get_option( self::OPTION_NAME, false ); if ( false === $stored || ! is_array( $stored ) ) { $options = self::default_options(); } else { $options = wp_parse_args( $stored, self::default_options() ); if ( ! array_key_exists( 'testing_mode', $stored ) ) { $options['testing_mode'] = 0; } } $allowed_modes = array( 'none', 'inline', 'overlay' ); if ( ! in_array( $options['search_mode'], $allowed_modes, true ) ) { $options['search_mode'] = ! empty( $options['search_input_id'] ) ? 'overlay' : 'none'; } $options['search_page_id'] = absint( $options['search_page_id'] ); $options['rewrite_search_forms'] = ! empty( $options['rewrite_search_forms'] ) ? 1 : 0; $options['redirect_s'] = ! empty( $options['redirect_s'] ) ? 1 : 0; $options['enable_on_front'] = ! empty( $options['enable_on_front'] ) ? 1 : 0; $options['testing_mode'] = ! empty( $options['testing_mode'] ) ? 1 : 0; $options['traffic_percent'] = self::normalise_traffic_percent( $options['traffic_percent'] ?? '' ); return $options; } /** * Permalink of the configured Site Search page, or empty string. * * @param array|null $options Optional options array. * @return string */ public static function get_search_page_url( $options = null ) { if ( null === $options ) { $options = self::get_plugin_options(); } $page_id = absint( $options['search_page_id'] ); if ( $page_id < 1 ) { return ''; } $permalink = get_permalink( $page_id ); if ( ! $permalink || 'publish' !== get_post_status( $page_id ) ) { return ''; } return $permalink; } /** * Coerce traffic_percent to '' or an integer 0–100. * * Empty means all visitors (the script attribute is omitted). * * @param mixed $value Raw option value. * @return string|int */ public static function normalise_traffic_percent( $value ) { if ( $value === '' || $value === null ) { return ''; } if ( ! is_numeric( $value ) ) { return ''; } return max( 0, min( 100, (int) $value ) ); } /** * Value for data-traffic-percent, or null when the attribute should be omitted. * * @param array $options Plugin options. * @return string|null */ public static function traffic_percent_attribute( $options ) { if ( ! empty( $options['testing_mode'] ) ) { return null; } $percent = self::normalise_traffic_percent( $options['traffic_percent'] ?? '' ); if ( '' === $percent || 100 === $percent ) { return null; } return (string) $percent; } /** * Constructor */ private function __construct() { $this->load_dependencies(); $this->init_hooks(); } /** * Load required dependencies */ private function load_dependencies() { if ( is_admin() ) { require_once AIRGENTIC_PLUGIN_DIR . 'includes/admin-settings.php'; } } /** * Initialize WordPress hooks */ private function init_hooks() { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_script' ) ); add_filter( 'script_loader_tag', array( $this, 'add_data_attributes_to_script' ), 10, 3 ); add_shortcode( 'airgentic_service_hub', array( $this, 'service_hub_shortcode' ) ); add_shortcode( 'airgentic_search', array( $this, 'search_shortcode' ) ); add_filter( 'get_search_form', array( $this, 'rewrite_search_form' ), 20 ); add_filter( 'render_block', array( $this, 'rewrite_search_block' ), 10, 2 ); add_action( 'template_redirect', array( $this, 'redirect_wordpress_search' ) ); } /** * Get plugin options * * @return array */ private function get_options() { if ( empty( $this->options ) ) { $this->options = self::get_plugin_options(); } return $this->options; } /** * Whether Account ID, Service ID, and Enable on Front-end are set. * * @param array $options Plugin options. * @return bool */ private function is_front_end_enabled( $options ) { return ! empty( $options['enable_on_front'] ) && ! empty( $options['account_id'] ) && ! empty( $options['service_id'] ); } /** * Whether this request should load Airgentic on the front-end. * * Testing Mode serves Airgentic only to logged-in WordPress users, at 100% * (no data-traffic-percent). Anonymous visitors get no script, form rewrite, * or ?s= redirect. * * @param array $options Plugin options. * @return bool */ private function should_serve_front_end( $options ) { if ( ! $this->is_front_end_enabled( $options ) ) { return false; } if ( ! empty( $options['testing_mode'] ) && ! is_user_logged_in() ) { return false; } return true; } /** * Whether native WordPress search forms should be rewritten for inline search. * * @param array $options Plugin options. * @return bool */ private function should_rewrite_search_forms( $options ) { return $this->should_serve_front_end( $options ) && 'inline' === $options['search_mode'] && ! empty( $options['rewrite_search_forms'] ) && '' !== self::get_search_page_url( $options ); } /** * Enqueue the Airgentic script */ public function enqueue_script() { $options = $this->get_options(); if ( ! $this->should_serve_front_end( $options ) ) { return; } wp_enqueue_script( self::SCRIPT_HANDLE, 'https://chat.airgentic.com/_js/airgentic-1.4.js', array(), AIRGENTIC_VERSION, array( 'in_footer' => false ) ); if ( $this->should_rewrite_search_forms( $options ) ) { wp_add_inline_script( self::SCRIPT_HANDLE, $this->theme_search_form_rewrite_script( self::get_search_page_url( $options ) ), 'before' ); } } /** * Browser-side rewrite for search forms PHP never sees. * * The get_search_form and core/search filters only reach forms WordPress * renders. Many themes echo their own header search form (name="s", posting * to the homepage), which then relies on the ?s= redirect. A redirect is a * fresh page load, so the Airgentic script cannot tell the visitor's own * submit from a shared link and will not send a question to the agent. This * script turns those forms into the same shape as a rewritten Search block: * action = search page, name="query", autocomplete marker. Forms rendered * later are caught at submit time. The admin bar search is left alone. * * @param string $url Search page URL. * @return string JavaScript. */ private function theme_search_form_rewrite_script( $url ) { $encoded_url = wp_json_encode( $url ); return <<get_options(); $data_attrs = 'data-account-id="' . esc_attr( $options['account_id'] ) . '" ' . 'data-service-id="' . esc_attr( $options['service_id'] ) . '"'; $traffic_percent = self::traffic_percent_attribute( $options ); if ( null !== $traffic_percent ) { $data_attrs .= ' data-traffic-percent="' . esc_attr( $traffic_percent ) . '"'; } $mode = $options['search_mode']; if ( 'overlay' === $mode ) { if ( ! empty( $options['search_input_id'] ) ) { $data_attrs .= ' data-search-input-id="' . esc_attr( $options['search_input_id'] ) . '"'; } if ( ! empty( $options['search_button_id'] ) ) { $data_attrs .= ' data-search-button-id="' . esc_attr( $options['search_button_id'] ) . '"'; } } elseif ( 'inline' === $mode ) { if ( ! empty( $options['rewrite_search_forms'] ) ) { $data_attrs .= ' data-search-autocomplete="true"'; } if ( ! empty( $options['search_input_id'] ) ) { $data_attrs .= ' data-autocomplete-input-id="' . esc_attr( $options['search_input_id'] ) . '"'; } if ( ! empty( $options['search_button_id'] ) ) { $data_attrs .= ' data-autocomplete-button-id="' . esc_attr( $options['search_button_id'] ) . '"'; } } $tag = str_replace( 'id="airgentic-widget-js"', 'id="airgentic-script"', $tag ); $tag = str_replace( "id='airgentic-widget-js'", "id='airgentic-script'", $tag ); $tag = str_replace( '