'equashop', 'slot' => 'pro', 'locale' => get_user_locale(), * ) ); * * The socle talks to the EquaMarket content server over HTTPS only. It owns the * cache (WP transients on the host site), the timeouts, the degradation and a * same-origin authenticated proxy so the browser never talks cross-origin. It * requires NOTHING of EquaMarket on the host site. * * Host integration points (all filters, sensible defaults): * equasuite_cc_content_base (string) — server base URL (default production marketing.equadev.fr). * equasuite_cc_optin (bool, $product_id) — has the host authorised server calls? * equasuite_cc_capability (string) — capability gating the local /consume proxy. * equasuite_cc_fallback_text (string, $product_id) — the host's locally-translated * (gettext) showcase fallback. * * Collision-safe: single shared class, all state scoped by product_id, namespaced * handles/transients/routes (equasuite_cc*). @see equasuite-content-client.php (loader). * * @package EquaSuite\ContentClient */ defined( 'ABSPATH' ) || exit; class EquaSuite_Content_Client { /** REST namespace of the local (same-origin) proxy on the host site. */ const REST_NS = 'equasuite-cc/v1'; /** Cache durations (seconds) — the frozen EquaSuite content-cache policy. */ const BANNER_CONTENT_TTL = 28800; // 8h. const BANNER_EARLY_REFRESH = 14400; // 4h (TTL/2) — banner refetches from mid-life. const BANNER_EMPTY_TTL = 3600; // 1h. const BANNER_TIMEOUT = 2.5; // seconds, off the render path. const PAGE_CONTENT_TTL = 3600; // 1h. const PAGE_EMPTY_TTL = 3600; // 1h. const PAGE_TIMEOUT = 10; // seconds, on the render path (clock-covered). /** @var string Absolute dir of the booted copy. */ private static $dir = ''; /** @var string Public URL of the booted copy (for assets). */ private static $url = ''; /** @var string Booted copy version. */ private static $version = '0.0.0'; /** @var bool */ private static $booted = false; /** @var bool Localised config emitted once. */ private static $localized = false; /** * Boots the socle (called once, for the highest-versioned embedded copy). * * @param string $dir Absolute directory of the winning copy. * @param string $version Its version. * @return void */ public static function boot( $dir, $version ) { if ( self::$booted ) { return; } self::$booted = true; self::$dir = wp_normalize_path( $dir ); self::$version = (string) $version; self::$url = self::dir_to_url( self::$dir ); add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) ); } /* ====================================================================== */ /* Host configuration (filterable, self-contained defaults). */ /* ====================================================================== */ /** * Content server base URL (production default; filterable for testing). * * @return string */ public static function remote_base() { return untrailingslashit( (string) apply_filters( 'equasuite_cc_content_base', 'https://marketing.equadev.fr/wp-json/equamarket/v1' ) ); } /** * Has the host authorised contacting the content server for this product? * * @param string $product_id Host product id. * @return bool */ public static function is_opted_in( $product_id ) { return (bool) apply_filters( 'equasuite_cc_optin', false, $product_id ); } /** * Capability gating the local /consume proxy. * * @return string */ public static function capability() { return (string) apply_filters( 'equasuite_cc_capability', 'manage_options' ); } /** * The host's locally-translated dignified showcase fallback text. * * @param string $product_id Host product id. * @return string */ public static function fallback_text( $product_id ) { return (string) apply_filters( 'equasuite_cc_fallback_text', 'This EquaSuite presentation is currently unavailable. Please try again later.', $product_id ); } /* ====================================================================== */ /* Assets — enqueue triggerable by the host on ITS hooks. */ /* ====================================================================== */ /** * Registers + enqueues the socle CSS/JS and localises the shared config. * Idempotent; call from the host's own admin_enqueue_scripts for its screen. * * @return void */ public static function enqueue() { if ( ! self::$booted ) { return; } wp_enqueue_style( 'equasuite-cc-client', self::$url . '/assets/equasuite-content-client.css', array(), self::$version ); wp_enqueue_script( 'equasuite-cc-client', self::$url . '/assets/equasuite-content-client.js', array(), self::$version, true ); if ( ! self::$localized ) { self::$localized = true; wp_localize_script( 'equasuite-cc-client', 'EQUASUITE_CC', array( 'rest' => esc_url_raw( rest_url( self::REST_NS . '/consume' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), // Client abort budget = server remote-fetch timeout + headroom for // the local proxy round-trip, so a cold-cache fetch that succeeds // server-side is not aborted before its response reaches the browser. 'bannerMs' => (int) round( self::BANNER_TIMEOUT * 1000 ) + 2000, 'pageMs' => (int) ( self::PAGE_TIMEOUT * 1000 ) + 3000, ) ); } } /* ====================================================================== */ /* Public mount API. */ /* ====================================================================== */ /** * Banner container with an inline first paint from cache (no network). The * host echoes the returned string at its chosen insertion point. * * @param array $args product_id (required), slot (required), locale (optional). * @return string */ public static function render_banner( array $args ) { self::enqueue(); // defensive; best practice is to enqueue on the host hook. $product = self::norm_product( isset( $args['product_id'] ) ? $args['product_id'] : '' ); $slot = sanitize_key( isset( $args['slot'] ) ? $args['slot'] : '' ); $locale = self::norm_locale( isset( $args['locale'] ) ? $args['locale'] : get_user_locale() ); $optin = self::is_opted_in( $product ); // Only paint cached server content while opted in — if the host revoked the // opt-in, the banner shows nothing (consistent with the showcase, which // shows its local fallback). No stale server content survives an opt-out. $seed = $optin ? self::servable_from_cache( 'banner', $product, $slot, $locale ) : array( 'status' => 'empty' ); $inline = ( 'content' === $seed['status'] ) ? '
' . wp_kses_post( $seed['html'] ) . '
' : ''; return sprintf( '
%s
', '' === $inline ? '' : ' is-shown', esc_attr( $product ), esc_attr( $slot ), esc_attr( $locale ), $optin ? '1' : '0', $inline ); } /** * Showcase container. JS fetches on the render path (clock) then shows content * or the host's dignified fallback. `