(string) $pairing_code, 'site_url' => home_url( '/' ), 'wp_version' => get_bloginfo( 'version' ), 'plugin_version' => AEO_CONNECT_VERSION, ); $response = self::request( 'POST', $url, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 !== $code ) { $message = ( is_array( $data ) && ! empty( $data['error'] ) && is_string( $data['error'] ) ) ? sanitize_text_field( $data['error'] ) : sprintf( /* translators: %d: HTTP status code. */ __( 'The pairing request was rejected (HTTP %d).', 'aeo-tool-connect' ), $code ); return new WP_Error( 'aeo_connect_pair_failed', $message ); } if ( ! is_array( $data ) || empty( $data['site_key'] ) || ! is_string( $data['site_key'] ) || empty( $data['domain'] ) || ! is_string( $data['domain'] ) ) { return new WP_Error( 'aeo_connect_pair_invalid', __( 'Unexpected response from everlange.com during pairing.', 'aeo-tool-connect' ) ); } self::store_site_key( $data['site_key'] ); update_option( 'aeo_connect_domain', sanitize_text_field( $data['domain'] ), false ); return sanitize_text_field( $data['domain'] ); } /** * Pull the optimization bundle for the paired domain. * * GET {API_BASE}/api/v1/connect?action=pull&domain={domain} * Header: X-Site-Key. * * @return array|WP_Error Decoded bundle on success. WP_Error with code * 'aeo_connect_revoked' when the SaaS returns 403. */ public static function pull() { $site_key = self::get_site_key(); $domain = (string) get_option( 'aeo_connect_domain', '' ); if ( '' === $site_key || '' === $domain ) { return new WP_Error( 'aeo_connect_not_paired', __( 'This site is not paired with everlange.com yet.', 'aeo-tool-connect' ) ); } $url = AEO_CONNECT_API_BASE . '/api/v1/connect?action=pull&domain=' . rawurlencode( $domain ); $response = self::request( 'GET', $url, array( 'headers' => array( 'X-Site-Key' => $site_key ), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); if ( 403 === $code ) { return new WP_Error( 'aeo_connect_revoked', __( 'The site key has been revoked from the everlange.com dashboard.', 'aeo-tool-connect' ) ); } if ( 200 !== $code ) { return new WP_Error( 'aeo_connect_pull_failed', sprintf( /* translators: %d: HTTP status code. */ __( 'Could not fetch optimizations from everlange.com (HTTP %d).', 'aeo-tool-connect' ), $code ) ); } $bundle = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $bundle ) || ! isset( $bundle['version'] ) ) { return new WP_Error( 'aeo_connect_pull_invalid', __( 'everlange.com returned an invalid optimization bundle.', 'aeo-tool-connect' ) ); } return $bundle; } /** * Send the post-apply telemetry ping. * * POST {API_BASE}/api/v1/connect?action=ping * Header: X-Site-Key. * * @param int $applied_version The bundle version currently applied. * @param string[] $modules_active Active module slugs (e.g. jsonld, robots, llms). * @return true|WP_Error */ public static function ping( $applied_version, $modules_active ) { $site_key = self::get_site_key(); if ( '' === $site_key ) { return new WP_Error( 'aeo_connect_not_paired', __( 'This site is not paired with everlange.com yet.', 'aeo-tool-connect' ) ); } $body = array( 'applied_version' => (int) $applied_version, 'modules_active' => array_values( array_map( 'sanitize_key', (array) $modules_active ) ), 'wp_version' => get_bloginfo( 'version' ), 'plugin_version' => AEO_CONNECT_VERSION, ); $response = self::request( 'POST', AEO_CONNECT_API_BASE . '/api/v1/connect?action=ping', array( 'headers' => array( 'Content-Type' => 'application/json', 'X-Site-Key' => $site_key, ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { return new WP_Error( 'aeo_connect_ping_failed', sprintf( /* translators: %d: HTTP status code. */ __( 'Telemetry ping to everlange.com failed (HTTP %d).', 'aeo-tool-connect' ), $code ) ); } return true; } /** * Perform an HTTP request with one retry on 5xx or network/timeout error. * * @param string $method 'GET' or 'POST'. * @param string $url Request URL. * @param array $args wp_remote_* args (headers, body...). * @return array|WP_Error wp_remote_* response array or WP_Error. */ private static function request( $method, $url, $args = array() ) { $defaults = array( 'timeout' => self::TIMEOUT, 'redirection' => 2, 'user-agent' => 'AEO-Tool-Connect/' . AEO_CONNECT_VERSION . '; ' . home_url( '/' ), ); $args = array_merge( $defaults, $args ); $response = null; $attempt = 0; do { $attempt++; if ( 'POST' === $method ) { $response = wp_remote_post( $url, $args ); } else { $response = wp_remote_get( $url, $args ); } $retry = false; if ( is_wp_error( $response ) ) { // Network error or timeout: retry once. $retry = true; } else { $status = (int) wp_remote_retrieve_response_code( $response ); if ( $status >= 500 ) { $retry = true; } } } while ( $retry && $attempt < 2 ); return $response; } /** * Store the site key, lightly obfuscated. Never logged. * * @param string $site_key Plain site key (sk_live_...). * @return void */ public static function store_site_key( $site_key ) { $obfuscated = base64_encode( self::xor_bytes( (string) $site_key, self::obfuscation_key() ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- light at-rest obfuscation, not encryption. update_option( 'aeo_connect_site_key', 'k1:' . $obfuscated, false ); } /** * Retrieve the plain site key, or '' when not paired. * * @return string */ public static function get_site_key() { $stored = get_option( 'aeo_connect_site_key', '' ); if ( ! is_string( $stored ) || '' === $stored ) { return ''; } if ( 0 !== strpos( $stored, 'k1:' ) ) { return ''; } $decoded = base64_decode( substr( $stored, 3 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- reverse of light at-rest obfuscation. if ( false === $decoded ) { return ''; } return self::xor_bytes( $decoded, self::obfuscation_key() ); } /** * Delete the stored site key. * * @return void */ public static function delete_site_key() { delete_option( 'aeo_connect_site_key' ); } /** * Derive a per-install obfuscation key from wp_salt(). * * @return string 32 raw bytes. */ private static function obfuscation_key() { return hash( 'sha256', wp_salt( 'auth' ) . '|aeo-tool-connect', true ); } /** * XOR a string with a repeating key. * * @param string $value Input bytes. * @param string $key Key bytes. * @return string */ private static function xor_bytes( $value, $key ) { $out = ''; $key_len = strlen( $key ); $len = strlen( $value ); if ( 0 === $key_len ) { return $value; } for ( $i = 0; $i < $len; $i++ ) { $out .= $value[ $i ] ^ $key[ $i % $key_len ]; } return $out; } }