'GET', 'timeout' => 15, 'headers' => [], ]; $args = array_merge( $defaults, $args ); $args['headers'] = array_merge( [ 'Authorization' => 'Bearer ' . $settings['api_key'], 'Accept' => 'application/json', 'User-Agent' => 'AgentsiaWP/' . AGENTSIA_AGENTS_VERSION . ' (' . home_url() . ')', ], (array) $args['headers'] ); $url = self::build_url( $path ); $res = wp_remote_request( $url, $args ); if ( is_wp_error( $res ) ) { return $res; } $code = wp_remote_retrieve_response_code( $res ); $body = wp_remote_retrieve_body( $res ); $data = json_decode( $body, true ); if ( $code >= 400 ) { if ( is_array( $data ) && isset( $data['error'] ) ) { $message = (string) $data['error']; } else { /* translators: %d: HTTP status code */ $message = sprintf( __( 'Erreur HTTP %d', 'agentsia-agents' ), $code ); } return new \WP_Error( 'agentsia_api_error', $message, [ 'status' => $code ] ); } return is_array( $data ) ? $data : []; } public static function verify_key(): array { $result = self::request( '/api/public/wp/me' ); if ( is_wp_error( $result ) ) { return [ 'ok' => false, 'message' => $result->get_error_message(), ]; } return [ 'ok' => true, 'company' => $result['company'] ?? null, 'scopes' => $result['scopes'] ?? [], ]; } public static function list_chatbots( bool $fresh = false ): array { return self::cached_list( 'chatbots', '/api/public/wp/chatbots', 'chatbots', $fresh ); } public static function list_booking_calendars( bool $fresh = false ): array { return self::cached_list( 'booking_calendars', '/api/public/wp/booking-calendars', 'calendars', $fresh, [ self::class, 'remember_booking_paths' ] ); } /** * Chemin public du calendrier `$slug` de l'espace de la clé (`/book//`), ou null quand l'API * n'en donne pas : app d'avant les adresses par espace, calendrier absent de la liste, chemin illisible. * * Appelé à l'affichage des pages publiques : la table slug → chemin est gardée un jour (un chemin ne change * qu'avec le slug de l'espace, et l'app redirige les anciens). Un slug inconnu ne relance l'API qu'une fois * par CACHE_TTL, avec un délai court, pour qu'une API lente ou injoignable ne bloque pas chaque page vue. */ public static function booking_calendar_path( string $slug ): ?string { $entry = get_transient( self::CACHE_TRANSIENT_PREFIX . 'booking_paths' ); $paths = is_array( $entry ) && isset( $entry['paths'] ) && is_array( $entry['paths'] ) ? $entry['paths'] : []; if ( isset( $paths[ $slug ] ) ) { return $paths[ $slug ]; } $checked_at = is_array( $entry ) ? (int) ( $entry['checked_at'] ?? 0 ) : 0; if ( time() - $checked_at < self::CACHE_TTL ) { return null; } $result = self::request( '/api/public/wp/booking-calendars', [ 'timeout' => self::RENDER_TIMEOUT ] ); if ( is_wp_error( $result ) ) { // Échec retenu CACHE_TTL ; les chemins déjà connus restent servis. self::store_booking_paths( $paths ); return null; } $calendars = isset( $result['calendars'] ) && is_array( $result['calendars'] ) ? $result['calendars'] : []; set_transient( self::CACHE_TRANSIENT_PREFIX . 'booking_calendars', $calendars, self::CACHE_TTL ); return self::remember_booking_paths( $calendars )[ $slug ] ?? null; } /** * Table slug → chemin tirée d'une liste fraîche de calendriers. Clé = slug passé par sanitize_title, comme * l'attribut du shortcode et le réglage ; à slugs confondus, le slug exact l'emporte. */ public static function remember_booking_paths( array $calendars ): array { $paths = []; foreach ( $calendars as $cal ) { if ( ! is_array( $cal ) || ! isset( $cal['slug'] ) || ! is_string( $cal['slug'] ) ) { continue; } $key = sanitize_title( $cal['slug'] ); $path = self::sanitize_booking_path( $cal['path'] ?? null ); if ( $key === '' || $path === null ) { continue; } if ( ! isset( $paths[ $key ] ) || $key === $cal['slug'] ) { $paths[ $key ] = $path; } } self::store_booking_paths( $paths ); return $paths; } /** * Chemin reçu de l'API, gardé seulement sous la forme `/book/<1 ou 2 segments [a-z0-9_-]>` : `/book//`, * ou `/book/` pour un calendrier sans espace. Majuscules admises (anciens slugs saisis à la main), jamais * `.`, `..`, `%` ni autre préfixe, puisqu'il est collé derrière l'adresse de l'app. */ private static function sanitize_booking_path( $path ): ?string { if ( ! is_string( $path ) || ! preg_match( '#^/book(?:/[a-z0-9_-]{1,120}){1,2}$#iD', $path ) ) { return null; } return $path; } private static function store_booking_paths( array $paths ): void { set_transient( self::CACHE_TRANSIENT_PREFIX . 'booking_paths', [ 'paths' => $paths, 'checked_at' => time(), ], self::BOOKING_PATHS_TTL ); } private static function cached_list( string $cache_key, string $path, string $response_key, bool $fresh, ?callable $on_fetch = null ): array { $transient_key = self::CACHE_TRANSIENT_PREFIX . $cache_key; if ( ! $fresh ) { $cached = get_transient( $transient_key ); if ( is_array( $cached ) ) { return $cached; } } $result = self::request( $path ); if ( is_wp_error( $result ) ) { return []; } $list = isset( $result[ $response_key ] ) && is_array( $result[ $response_key ] ) ? $result[ $response_key ] : []; set_transient( $transient_key, $list, self::CACHE_TTL ); if ( $on_fetch !== null ) { $on_fetch( $list ); } return $list; } public static function clear_cache(): void { delete_transient( self::CACHE_TRANSIENT_PREFIX . 'chatbots' ); delete_transient( self::CACHE_TRANSIENT_PREFIX . 'booking_calendars' ); delete_transient( self::CACHE_TRANSIENT_PREFIX . 'booking_paths' ); } }