'defer', 'in_footer' => true, ) ); } /** * The script URL to print for a stored connection. * * Agentency hands the plugin the STABLE loader URL * (`https://{app}/js/chatbot-widget.js`) at connect time, and that URL is * what always serves the current widget build — so a widget release * reaches this site with no reconnect and no plugin update. Connections * made before 2026-09-17 stored a content-hashed, cache-keyed form instead * (`/js/chatbot-widget.<12-hex>.js?v=.`), which pins the * site to the build it connected with forever. Fold that back onto the * stable path here, at render time, so every existing install starts * auto-updating the moment this plugin version is active. Anything that * is not an official loader URL (a customer-configured CDN host) is * printed exactly as stored. * * @param string $stored_url The `widget_url` from the connection. * @return string */ public static function widget_src( $stored_url ) { if ( ! is_string( $stored_url ) || '' === $stored_url ) { return ''; } return (string) preg_replace( '#^(https?://[^/?\#]+)/js/chatbot-widget(?:\.[a-f0-9]{12})?\.js(?:[?\#].*)?$#i', '$1/js/chatbot-widget.js', $stored_url ); } /** * Attach the widget's configuration attributes to its script tag. * * @param string $tag The full script tag. * @param string $handle The script handle. * @return string */ public static function add_attributes( $tag, $handle ) { if ( self::HANDLE !== $handle ) { return $tag; } $attributes = ''; foreach ( self::attributes() as $name => $value ) { $attributes .= sprintf( ' %s="%s"', esc_attr( $name ), esc_attr( $value ) ); } return str_replace( ' src=', $attributes . ' src=', $tag ); } /** * The `data-*` configuration the widget runtime reads. * * Only NON-DEFAULT values are emitted, mirroring the dashboard's embed * snippets: an option left blank here keeps inheriting whatever the owner * configures in Agentency, so changing the launcher position does not * require touching WordPress. `data-language` is the one attribute that * is always stated (see below). * * @return array */ public static function attributes() { $connection = Agentency_Options::connection(); $attributes = array( 'data-widget-token' => $connection['widget_token'], 'data-api-base-url' => $connection['api_base_url'], 'referrerpolicy' => 'strict-origin-when-cross-origin', 'crossorigin' => 'anonymous', ); $position = Agentency_Options::setting( 'position' ); if ( '' !== $position ) { $attributes['data-position'] = $position; } $launcher_size = Agentency_Options::setting( 'launcher_size' ); if ( '' !== $launcher_size ) { $attributes['data-launcher-size'] = $launcher_size; } foreach ( array( 'offset_x' => 'data-offset-x', 'offset_y' => 'data-offset-y', ) as $key => $attribute ) { $offset = Agentency_Options::setting( $key ); if ( '' !== $offset && (int) $offset > 0 ) { $attributes[ $attribute ] = (string) min( 200, max( 0, (int) $offset ) ); } } if ( Agentency_Options::setting( 'hide_launcher' ) ) { $attributes['data-hide-launcher'] = 'true'; } if ( Agentency_Options::setting( 'page_context' ) ) { $attributes['data-page-context'] = 'auto'; } $auto_open = Agentency_Options::setting( 'auto_open_delay' ); if ( '' !== $auto_open && (int) $auto_open >= 1 ) { $attributes['data-auto-open'] = (string) min( 60, (int) $auto_open ); } // Always stated, `default` included, matching the dashboard's Web-tab // snippet: the widget reads `default` as "follow the agent" without a // warning, and an explicit value tells anyone reading the page source // that the language is a deliberate choice rather than an omission. // Re-sanitised on read: a future catalogue change (or a hand-edited // option row) must degrade to `default` rather than emit a code the // widget rejects. $attributes['data-language'] = Agentency_Options::sanitize_language( Agentency_Options::setting( 'language' ) ); if ( Agentency_Options::setting( 'identify_users' ) && is_user_logged_in() ) { $user = wp_get_current_user(); // Display data only — the API treats it as untrusted and never as // an authentication signal, so no signing is involved. $attributes['data-user-id'] = (string) $user->ID; $attributes['data-user-name'] = $user->display_name; } return $attributes; } }