*/ public static function record( $env = null ) { $env = null === $env ? Env::current() : $env; $record = get_option( self::OPTION_PREFIX . $env, array() ); return is_array( $record ) ? $record : array(); } /** * Persist a record. * * @param array $record Record. * @param string|null $env Environment. * @return void */ private static function save( array $record, $env = null ) { $env = null === $env ? Env::current() : $env; update_option( self::OPTION_PREFIX . $env, $record, false ); } /** * Store a fresh connection after the OAuth exchange. * * @param string $token Connection token. * @param string|null $env Environment. * @return bool */ public static function store_token( $token, $env = null ) { $encrypted = self::encrypt( (string) $token ); if ( null === $encrypted ) { return false; } self::save( array( 'token' => $encrypted, 'status' => 'connected', 'connected_at' => time(), ), $env ); return true; } /** * Merge fields into the record. * * @param array $fields Fields. * @param string|null $env Environment. * @return void */ public static function merge( array $fields, $env = null ) { self::save( array_merge( self::record( $env ), $fields ), $env ); } /** * The decrypted token, or null. * * @param string|null $env Environment. * @return string|null */ public static function token( $env = null ) { $record = self::record( $env ); return empty( $record['token'] ) ? null : self::decrypt( (string) $record['token'] ); } /** * "connected", "reconnect" or "disconnected". * * @param string|null $env Environment. * @return string */ public static function status( $env = null ) { $record = self::record( $env ); if ( empty( $record['token'] ) ) { return 'disconnected'; } return 'reconnect' === ( $record['status'] ?? '' ) ? 'reconnect' : 'connected'; } /** * Public account id (md5 of the owner id) used in CDN paths — data, never a secret. * * @param string|null $env Environment. * @return string */ public static function account_id( $env = null ) { $record = self::record( $env ); $id = isset( $record['account'] ) ? (string) $record['account'] : ''; return preg_match( '/^[a-f0-9]{32}$/', $id ) ? $id : ''; } /** * Whether the storefront can render (an account id is known; a token in * "reconnect" still serves already-published experiences). * * @param string|null $env Environment. * @return bool */ public static function can_serve( $env = null ) { return '' !== self::account_id( $env ); } /** * Last health answer from Thridify. * * @param string|null $env Environment. * @return array */ public static function health( $env = null ) { $record = self::record( $env ); return isset( $record['health'] ) && is_array( $record['health'] ) ? $record['health'] : array(); } /** * Thridify rejected the token: keep the account id (published experiences * keep rendering) and ask the merchant to connect again. * * @param string|null $env Environment. * @return void */ public static function mark_reconnect( $env = null ) { if ( 'disconnected' !== self::status( $env ) ) { self::merge( array( 'status' => 'reconnect' ), $env ); } } /** * Forget the connection. * * @param string|null $env Environment. * @return void */ public static function forget( $env = null ) { $env = null === $env ? Env::current() : $env; delete_option( self::OPTION_PREFIX . $env ); } /** * Encryption key derived from this site's salts. * * @return string 32 raw bytes. */ private static function key() { return hash( 'sha256', wp_salt( 'auth' ) . '|thridify|' . wp_salt( 'secure_auth' ), true ); } /** * Encrypt with libsodium (bundled with PHP 7.2+ and polyfilled by WordPress). * * @param string $plaintext Token. * @return string|null */ private static function encrypt( $plaintext ) { if ( ! function_exists( 'sodium_crypto_secretbox' ) ) { return null; } try { $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); } catch ( \Exception $e ) { return null; } $cipher = sodium_crypto_secretbox( $plaintext, $nonce, self::key() ); return 'sodium:' . sodium_bin2hex( $nonce . $cipher ); } /** * Decrypt a stored token. * * @param string $payload Stored value. * @return string|null */ private static function decrypt( $payload ) { if ( 0 !== strpos( $payload, 'sodium:' ) || ! function_exists( 'sodium_crypto_secretbox_open' ) ) { return null; } try { $raw = sodium_hex2bin( substr( $payload, 7 ) ); } catch ( \Exception $e ) { return null; } if ( strlen( $raw ) <= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ) { return null; } $plain = sodium_crypto_secretbox_open( substr( $raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), substr( $raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), self::key() ); return false === $plain ? null : $plain; } }