isset( $secret['access_token'] ) ? (string) $secret['access_token'] : '', 'refresh_token' => isset( $secret['refresh_token'] ) ? (string) $secret['refresh_token'] : '', ); } /** * Persist the scoped token pair. autoload OFF so it never rides along on * unrelated page loads, and is never exposed via alloptions. */ public static function set_tokens( $access_token, $refresh_token ) { // Delete + add to guarantee the autoload flag is 'no' (update_option // keeps the existing autoload value if the row already exists). delete_option( self::SECRET ); add_option( self::SECRET, array( 'access_token' => (string) $access_token, 'refresh_token' => (string) $refresh_token, ), '', 'no' ); } /** * Read the project's HMAC signing secret ('' when we hold none, which simply * means user ids are sent unsigned / display-only). */ public static function get_signing_secret() { $v = get_option( self::SIGNING_SECRET, '' ); return is_string( $v ) ? trim( $v ) : ''; } /** * Persist (or clear, with '') the signing secret. autoload OFF — it is only * needed when rendering for a logged-in user, and must never ride along in * alloptions. */ public static function set_signing_secret( $secret ) { $secret = is_string( $secret ) ? trim( $secret ) : ''; delete_option( self::SIGNING_SECRET ); if ( '' === $secret ) { return; } add_option( self::SIGNING_SECRET, $secret, '', 'no' ); } /** * Save everything returned by a successful token exchange/refresh. * * @param array $data Decoded response from wordpress-token. * @param string $site_url The paired site URL (home_url()). */ public static function save_pairing( array $data, $site_url ) { self::set_tokens( isset( $data['access_token'] ) ? $data['access_token'] : '', isset( $data['refresh_token'] ) ? $data['refresh_token'] : '' ); $expires_at = isset( $data['expires_at'] ) ? strtotime( (string) $data['expires_at'] ) : 0; $values = array( 'connection_status' => 'connected', 'paired_project_id' => isset( $data['project_id'] ) ? sanitize_text_field( $data['project_id'] ) : '', 'project_name' => isset( $data['project_name'] ) ? sanitize_text_field( $data['project_name'] ) : '', 'embed_key' => isset( $data['embed_key'] ) ? sanitize_text_field( $data['embed_key'] ) : '', 'paired_plan' => isset( $data['plan'] ) ? sanitize_text_field( $data['plan'] ) : '', 'token_expires_at' => $expires_at ? $expires_at : 0, 'paired_site_url' => esc_url_raw( $site_url ), ); // The signing secret for this project, if the backend issued one. This is // what lets the plugin sign logged-in user ids server-side, so a WordPress // site gets VERIFIED identity without its owner writing a line of code. // Absent on older backends — then ids are simply sent unsigned. if ( ! empty( $data['signing_secret'] ) ) { self::set_signing_secret( (string) $data['signing_secret'] ); } // Record the first pairing time once. if ( '' === (string) self::get( 'paired_at' ) ) { $values['paired_at'] = time(); } // Auto-fill the Widget Key with the embed key so the widget goes live // immediately after connecting — but never clobber a key the user set. $current_key = trim( (string) self::get( 'widget_key' ) ); if ( '' === $current_key && ! empty( $values['embed_key'] ) ) { $values['widget_key'] = $values['embed_key']; } self::update( $values ); } /** * Update only the rotated tokens + new expiry (used after a refresh). */ public static function update_tokens_from_refresh( array $data ) { self::set_tokens( isset( $data['access_token'] ) ? $data['access_token'] : '', isset( $data['refresh_token'] ) ? $data['refresh_token'] : '' ); $expires_at = isset( $data['expires_at'] ) ? strtotime( (string) $data['expires_at'] ) : 0; self::update( array( 'token_expires_at' => $expires_at ? $expires_at : 0 ) ); } /** * Clear all pairing state (disconnect). The Widget Key is intentionally * LEFT in place so the widget keeps rendering after disconnect. */ public static function clear_pairing() { $opts = get_option( self::OPTIONS, array() ); if ( is_array( $opts ) ) { foreach ( self::$pairing_keys as $k ) { unset( $opts[ $k ] ); } update_option( self::OPTIONS, $opts ); } delete_option( self::SECRET ); // The signing secret came from the pairing, so it goes with it. The // widget keeps working; logged-in ids simply revert to unsigned // (display-only) until the site is connected again. delete_option( self::SIGNING_SECRET ); } }