0 && ($state['site_url'] ?? '') === untrailingslashit(get_option('home')) && ($state['api_url'] ?? '') === untrailingslashit($api) && ($state['environment'] ?? '') === $environment; } private static function guard(string $action): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('A site administrator must manage this connection.', 'chat4u')); } check_admin_referer($action); } private static function finish(string $message = ''): void { if ($message) { set_transient('chat4u_admin_notice_' . get_current_user_id(), $message, 120); } wp_safe_redirect(admin_url('options-general.php?page=chat4u')); exit; } public static function connect(): void { self::guard('chat4u_connect'); if (is_multisite() || apply_filters('chat4u_headless', false)) { self::finish(__('Multisite and headless installations are not supported yet.', 'chat4u')); } $api = defined('CHAT4U_API_URL') ? CHAT4U_API_URL : 'https://api.chat4u.ai'; $environment = defined('CHAT4U_ENVIRONMENT') ? CHAT4U_ENVIRONMENT : 'production'; $home = untrailingslashit(get_option('home')); if (wp_parse_url($api, PHP_URL_SCHEME) !== 'https') { self::finish(__('Chat4U requires a verified HTTPS service address.', 'chat4u')); } $state = Chat4U_Protocol::connection(); $pending = $state['pending'] ?? []; // Retry only the same identity and service. Never poll an obsolete // destination or reuse its proof after configuration/address changes. if (($state['site_url'] ?? '') !== $home || ($pending['api_url'] ?? '') !== untrailingslashit($api) || ($pending['environment'] ?? '') !== $environment || ($pending['expires'] ?? 0) < time()) { unset($state['pending']); } if (isset($state['pending']) && !empty($state['pending']['connect_url'])) { $checked = self::synchronize(); $state = Chat4U_Protocol::connection(); if (!is_wp_error($checked) && ($checked['state'] ?? '') === 'connected' && !isset($state['pending']) && !isset($checked['pairing_state'])) { self::finish(__('Your website is connected. Current status appears below.', 'chat4u')); } } $preserve = self::retains_connection($state); $previous = $state['relocation_previous'] ?? $state; $pending = isset($state['pending']) && $state['pending']['expires'] >= time() ? $state['pending'] : ['scope' => wp_generate_uuid4(), 'generation' => 0, 'secret' => bin2hex(random_bytes(32)), 'environment' => $environment, 'api_url' => untrailingslashit($api), 'expires' => time() + 900]; $pending['placement_grant'] = 1; $state['site_uuid'] = self::site_identity($state, $home); $state['site_url'] = $home; $state['pending'] = $pending; if (!$preserve) { $state['state'] = 'pending'; $state['widget_enabled'] = false; } if (isset($previous['site_url'], $previous['secret']) && $previous['site_url'] !== $home) { $state['relocation_previous'] = array_intersect_key($previous, array_flip(['scope', 'generation', 'secret', 'site_url'])); } update_option('chat4u_connection', $state, false); $payload = ['pairing_id' => $pending['scope'], 'site_uuid' => $state['site_uuid'], 'blog_id' => get_current_blog_id(), 'site_url' => $state['site_url'], 'rest_url' => rest_url(), 'secret' => $pending['secret'], 'environment' => $environment, 'protocol' => 1]; $body = wp_json_encode($payload + Chat4U_Protocol::relocation($previous, $payload)); $response = wp_safe_remote_post($api . '/api/v1/wordpress/pairings', ['timeout' => 30, 'redirection' => 0, 'headers' => ['Content-Type' => 'application/json'], 'body' => $body, 'limit_response_size' => 16384]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { self::finish(__('The connection could not start. Check that your host permits Chat4U HTTPS requests, then try again. Your site content is unchanged.', 'chat4u')); } $result = json_decode(wp_remote_retrieve_body($response), true); $destination = $result['connect_url'] ?? ''; $frontend = defined('CHAT4U_APP_URL') ? CHAT4U_APP_URL : 'https://app.chat4u.ai'; if (Chat4U_Protocol::origin($destination) !== Chat4U_Protocol::origin($frontend) || wp_parse_url($destination, PHP_URL_PATH) !== '/wordpress/connect') { self::finish(__('The connection destination could not be verified. Check your Chat4U service configuration.', 'chat4u')); } $state['pending']['connect_url'] = $destination; update_option('chat4u_connection', $state, false); // Destination is restricted to the configured service origin and path; // allow that one external host only for this validated handoff. $allow_frontend = static fn($hosts) => array_unique([...$hosts, wp_parse_url($frontend, PHP_URL_HOST)]); add_filter('allowed_redirect_hosts', $allow_frontend); try { wp_safe_redirect($destination, 302, 'Chat4U'); } finally { remove_filter('allowed_redirect_hosts', $allow_frontend); } exit; } public static function refresh(): void { self::guard('chat4u_refresh'); $result = self::synchronize(); self::finish(is_wp_error($result) ? $result->get_error_message() : __('Connection checked. Current status appears below.', 'chat4u')); } public static function synchronize() { $state = Chat4U_Protocol::connection(); if (!isset($state['pending'])) { if (($state['state'] ?? '') !== 'connected') { return ['state' => $state['state'] ?? 'disconnected']; } $result = Chat4U_Protocol::remote('installations/' . $state['scope'] . '/status', [], $state); if (is_wp_error($result)) { if ($result->get_error_code() === 'chat4u_connection_revoked') { self::address_changed(); return ['state' => 'needs_relink', 'updated' => true]; } return $result; } $state['connection'] = $result['connection']; update_option('chat4u_connection', $state, false); return ['state' => $result['state'], 'updated' => true, 'coverage' => $result['connection']['source']['coverage'] ?? null, 'can_use' => $result['connection']['source']['can_use'] ?? false]; } $pending = $state['pending']; $result = Chat4U_Protocol::remote('pairings/' . $pending['scope'] . '/status', [], $pending); if (is_wp_error($result)) { return $result; } if (($result['state'] ?? '') === 'connected') { $state = array_merge($state, $pending, ['scope' => $result['installation_id'], 'generation' => $result['generation'], 'site_uuid' => $result['site_uuid'] ?? $state['site_uuid'], 'state' => 'connected', 'connection' => $result['connection']]); unset($state['pending'], $state['connect_url'], $state['expires'], $state['relocation_previous']); delete_option('chat4u_pending_delivery'); update_option('chat4u_connection', $state, false); return ['state' => 'connected']; } if (in_array($result['state'] ?? '', ['expired', 'disconnected'], true)) { // A verified terminal pairing must not be reused by the owner's // explicit retry. Unverified responses above preserve the pending key. unset($state['pending']); if (self::retains_connection($state)) { update_option('chat4u_connection', $state, false); return ['state' => 'connected', 'updated' => true, 'pairing_state' => $result['state']]; } $state['state'] = 'needs_relink'; $state['widget_enabled'] = false; update_option('chat4u_connection', $state, false); return ['state' => 'needs_relink', 'updated' => true]; } return self::retains_connection($state) ? ['state' => 'connected', 'pairing_state' => $result['state'] ?? 'pending'] : ['state' => $result['state'] ?? 'pending']; } public static function save(): void { self::guard('chat4u_settings'); $state = Chat4U_Protocol::connection(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- guard() above verifies both manage_options and the chat4u_settings nonce before reading POST. if (!Chat4U_Placement::local_save(isset($_POST['widget_enabled']))) { self::finish(__('The display setting changed elsewhere. Review it and save again.', 'chat4u')); } self::finish(__('Widget setting saved. Check your published website to verify visibility.', 'chat4u')); } public static function address_changed(): void { $state = Chat4U_Protocol::connection(); if ($state) { unset($state['pending']); $state['state'] = 'needs_relink'; $state['widget_enabled'] = false; update_option('chat4u_connection', $state, false); } } public static function enqueue(): void { $state = Chat4U_Protocol::connection(); if (is_admin() || ($state['state'] ?? '') !== 'connected' || ($state['site_url'] ?? '') !== untrailingslashit(get_option('home'))) { return; } wp_enqueue_script('chat4u-native', plugins_url('assets/loader.js', CHAT4U_FILE), [], CHAT4U_VERSION, ['strategy' => 'defer', 'in_footer' => true]); } public static function script_attributes(string $tag, string $handle): string { if ($handle !== 'chat4u-native') { return $tag; } $state = Chat4U_Protocol::connection(); $attributes = ' data-chat4u-placement="' . esc_attr(rest_url('chat4u/v1/placement-state')) . '" data-chat4u-installation="' . esc_attr($state['scope']) . '" data-chat4u-generation="' . esc_attr((string) $state['generation']) . '" data-chat4u-api="' . esc_attr($state['api_url']) . '"'; return str_replace('