get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'SELECT * FROM %i WHERE connection_id = %d LIMIT 1', Database::api_keys_table(), $connection_id ) ); if ( ! $row ) { return null; } return self::normalize_row( $row ); } /** * Put a stored row into the shape callers expect: `api_key` in plaintext. * * A row whose ciphertext will not open (almost always a rotated AUTH_KEY) * is reported as no row at all. Handing back an unusable credential would * make the plugin send a broken key on every request; reporting "not * connected" instead routes the store into a state the UI shows and the * merchant can fix by reconnecting. * * A legacy plaintext row is re-encrypted in place on first read. There is * no separate migration script because rows are otherwise only written at * connect time, so an existing install would keep its plaintext key * indefinitely. * * @param object $row * @return object|null */ private static function normalize_row( $row ) { if ( Secret_Cipher::is_encrypted( $row->api_key ) ) { $plaintext = Secret_Cipher::decrypt( $row->api_key ); if ( null === $plaintext ) { return null; } $row->api_key = $plaintext; return $row; } if ( Secret_Cipher::is_available() ) { $encrypted = Secret_Cipher::encrypt( $row->api_key ); if ( null !== $encrypted ) { global $wpdb; $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching Database::api_keys_table(), [ 'api_key' => $encrypted ], [ 'id' => $row->id ], [ '%s' ], [ '%s' ] ); } } return $row; } /** * Get API key value for the current site connection. * * @return string|null The raw API key string or null. */ public static function get_current_key() { $connection = Connection_Repository::get_current(); if ( ! $connection ) { return null; } $row = self::get_by_connection( $connection->id ); return $row ? $row->api_key : null; } /** * Get the full API key row for the current site. * * @return object|null */ public static function get_current() { $connection = Connection_Repository::get_current(); if ( ! $connection ) { return null; } return self::get_by_connection( $connection->id ); } /** * Store an API key. * * @param array $data { * @type string $id Mintall key ID. * @type int $connection_id * @type string $platform_account_id * @type string $user_id * @type string $user_email * @type string $api_key Raw key. * } * @return bool */ public static function create( $data ) { global $wpdb; // Encrypt at rest when this site has key material outside the database. // When it does not, store the key exactly as before — never fail a // connect over encryption being unavailable. $stored_key = Secret_Cipher::encrypt( $data['api_key'] ); if ( null === $stored_key ) { $stored_key = $data['api_key']; } return (bool) $wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching Database::api_keys_table(), [ 'id' => $data['id'], 'connection_id' => $data['connection_id'], 'platform_account_id' => $data['platform_account_id'], 'user_id' => $data['user_id'], 'user_email' => $data['user_email'], 'api_key' => $stored_key, ], [ '%s', '%d', '%s', '%s', '%s', '%s' ] ); } /** * Delete API key by its ID. * * @param string $id */ public static function delete( $id ) { global $wpdb; $wpdb->delete( Database::api_keys_table(), [ 'id' => $id ], [ '%s' ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } }