Empty when nothing is stored OR when it cannot be read. */ public function get( string $integration ): array { $stored = get_option( self::option( $integration ), '' ); if ( ! is_string( $stored ) || '' === $stored ) { return array(); } $json = Secrets::decrypt( $stored ); if ( null === $json ) { return array(); } $values = json_decode( $json, true ); return is_array( $values ) ? array_map( 'strval', $values ) : array(); } /** * Whether something is stored that can no longer be read. * * The distinction {@see get()} cannot express on its own: an empty array means both "never * connected" and "connected, but the salts moved". Those need opposite messages — one is an * invitation, the other is an apology — so the screen asks this to tell them apart. * * @param string $integration Connector id. */ public function needs_reconnect( string $integration ): bool { $stored = get_option( self::option( $integration ), '' ); if ( ! is_string( $stored ) || '' === $stored ) { return false; } return null === Secrets::decrypt( $stored ); } /** * Store a connector's values, replacing whatever was there. * * @param string $integration Connector id. * @param array $values Field key => value. * * @return bool False when this host cannot encrypt at all, which is the one case that must not * silently fall back to plaintext. */ public function put( string $integration, array $values ): bool { if ( ! Secrets::available() ) { return false; } $json = wp_json_encode( array_map( 'strval', $values ) ); if ( ! is_string( $json ) ) { return false; } $sealed = Secrets::encrypt( $json ); if ( '' === $sealed ) { return false; } // autoload 'no': these are read when an integration runs, not when a visitor loads a page. update_option( self::option( $integration ), $sealed, false ); return true; } /** * Merge values over what is stored. * * The connect form's real shape: somebody re-saving a connection to change the account name has * not retyped the API key, and the client sends a placeholder rather than the real one. Callers * drop the untouched keys and merge the rest, so a partial save cannot blank a secret. * * @param string $integration Connector id. * @param array $values Field key => value. */ public function merge( string $integration, array $values ): bool { return $this->put( $integration, array_merge( $this->get( $integration ), $values ) ); } /** * Forget one connector's credentials. * * @param string $integration Connector id. */ public function forget( string $integration ): void { delete_option( self::option( $integration ) ); } /** * Whether anything at all is stored, readable or not. * * @param string $integration Connector id. */ public function stored( string $integration ): bool { $value = get_option( self::option( $integration ), '' ); return is_string( $value ) && '' !== $value; } /** * @param string $integration Connector id. */ private static function option( string $integration ): string { return self::PREFIX . sanitize_key( $integration ); } }