[self::class, 'sanitizeApiKey']]); } /** * Settings API sanitizer for the API key: encrypt before storage so it never * lands in wp_options as plaintext. The field is rendered empty on purpose * (we never echo the secret) — an empty submit means "unchanged", so keep the * stored ciphertext rather than wiping the key. */ public static function sanitizeApiKey($input): string { $input = sanitize_text_field((string) $input); if ($input === '') { return (string) get_option('SENTINELO_api_key', ''); } return Config::encodeApiKey($input); } public function enqueueAssets(string $hook): void { if ($hook !== 'settings_page_sentinelo') { return; } wp_enqueue_style( 'sentinelo-admin', SENTINELO_PLUGIN_URL . 'assets/css/admin.css', [], SENTINELO_VERSION ); wp_enqueue_script( 'sentinelo-admin', SENTINELO_PLUGIN_URL . 'assets/js/admin.js', ['jquery'], SENTINELO_VERSION, true ); wp_localize_script('sentinelo-admin', 'sentineloAdmin', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('SENTINELO_verify_nonce'), 'i18n' => [ 'checking' => __('Checking…', 'sentinelo'), 'error' => __('Error', 'sentinelo'), 'networkError' => __('Network error', 'sentinelo'), ], ]); } public function renderPage(): void { require SENTINELO_PLUGIN_DIR . 'templates/admin/settings.php'; } public function ajaxVerifyConnection(): void { check_ajax_referer('SENTINELO_verify_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Insufficient permissions.', 'sentinelo')]); } // The settings form no longer renders the stored key (encrypted at rest, // never echoed). On a re-verify of an already-connected site the field is // empty, so fall back to the stored key instead of rejecting — otherwise the // site can never re-run verify to (re)provision its webhook_secret. $postedKey = sanitize_text_field(wp_unslash($_POST['api_key'] ?? '')); $apiKey = $postedKey !== '' ? $postedKey : Config::getApiKey(); if (empty($apiKey)) { wp_send_json_error(['message' => __('API key required.', 'sentinelo')]); } if ($postedKey !== '') { Config::setApiKey($postedKey); } // L'adresse de la plateforme n'est plus postée : elle est dans le greffon. $saasUrl = SENTINELO_DEFAULT_SAAS_URL; $siteUuid = Config::get('site_uuid'); $verifyUrl = trailingslashit($saasUrl) . 'api/sites/verify'; Logger::info('settings', 'Vérification connexion vers : ' . $verifyUrl); $response = wp_remote_post( $verifyUrl, [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Sentinelo-API-Key' => $apiKey, ], 'body' => wp_json_encode([ 'site_url' => site_url(), 'wp_version' => get_bloginfo('version'), 'php_version' => phpversion(), ]), 'timeout' => 30, ] ); if (is_wp_error($response)) { Logger::error('settings', 'Erreur wp_remote_post : ' . $response->get_error_message()); wp_send_json_error(['message' => $response->get_error_message()]); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 200 && !empty($body['site_uuid'])) { Config::set('site_uuid', sanitize_text_field($body['site_uuid'])); if (!empty($body['api_key'])) { Config::setApiKey(sanitize_text_field($body['api_key'])); } // Store the webhook secret handed back by the backend — without it // WebhookSender short-circuits and outbound webhooks are never sent. if (!empty($body['webhook_secret'])) { Config::set('webhook_secret', sanitize_text_field($body['webhook_secret'])); } // UTC, like every other timestamp the agent persists: HealthEndpoint // derives uptime from it against a UTC clock. Config::set('connected_at', Timestamps::store(new \DateTimeImmutable('now', new \DateTimeZone('UTC')))); wp_send_json_success([ 'message' => __('Connection successful!', 'sentinelo'), 'site_uuid' => $body['site_uuid'], ]); } $errorMessage = $body['message'] ?? $body['error'] ?? __('Verification failed.', 'sentinelo'); wp_send_json_error([ 'message' => sprintf('[%d] %s', $code, $errorMessage), ]); } }