PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | src/myyoast-client/infrastructure/token/user-token-storage.php +126 -42 27.7 → trunk View file →
@@ -6,8 +6,9 @@
6 6 use Exception;
7 7 use Yoast\WP\SEO\Helpers\User_Helper;
8 8 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Token_Storage_Exception;
9 9 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\User_Token_Storage_Interface;
10 +use Yoast\WP\SEO\MyYoast_Client\Domain\Resource_Indicator;
10 11 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
11 12 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption;
12 13 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption_Exception;
13 14 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config;
@@ -17,9 +18,16 @@
17 18
18 19 /**
19 20 * Stores and retrieves encrypted user-level tokens in wp_usermeta.
20 21 *
21 - * Used for authorization code flow tokens (user-specific).
22 + * Used for authorization code flow tokens (user-specific). Tokens are
23 + * bucketed per RFC 8707 resource indicator so a user can hold one token per
24 + * resource server. Each (issuer, user, resource bucket) maps to a separate
25 + * usermeta row.
26 + *
27 + * Key layout:
28 + * - Default bucket: _wpseo_myyoast_user_tokens_{issuer_key}
29 + * - Resource bucket: _wpseo_myyoast_user_tokens_{issuer_key}_{sha1_prefix}
22 30 */
23 31 class User_Token_Storage implements User_Token_Storage_Interface, LoggerAwareInterface {
24 32 use LoggerAwareTrait;
25 33
@@ -61,19 +69,10 @@
61 69 $this->logger = new NullLogger();
62 70 }
63 71
64 72 /**
65 - * Returns the issuer-scoped user meta key.
73 + * Stores a token set for a user (encrypted). The resource bucket is derived from the token's own resource indicator.
66 74 *
67 - * @return string The meta key.
68 - */
69 - private function get_meta_key(): string {
70 - return self::META_KEY_PREFIX . $this->issuer_config->get_issuer_key();
71 - }
72 -
73 - /**
74 - * Stores a token set for a user (encrypted).
75 - *
76 75 * @param int $user_id The user ID.
77 76 * @param Token_Set $token_set The token set to store.
78 77 *
79 78 * @return void
@@ -88,26 +87,115 @@
88 87 throw new Token_Storage_Exception( 'Failed to JSON-encode token set for storage.' );
89 88 }
90 89
91 90 $encrypted = $this->encryption->encrypt( $json, self::ENCRYPTION_CONTEXT );
92 - }
93 - catch ( Encryption_Exception $e ) {
91 + } catch ( Encryption_Exception $e ) {
94 92 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
95 93 throw new Token_Storage_Exception( 'Failed to encrypt token set for storage: ' . $e->getMessage(), 0, $e );
96 94 }
97 95
98 - $this->user_helper->update_meta( $user_id, $this->get_meta_key(), $encrypted );
96 + $this->user_helper->update_meta( $user_id, $this->get_meta_key( $token_set->get_resource_indicator() ), $encrypted );
99 97 }
100 98
101 99 /**
102 - * Retrieves the stored token set for a user.
100 + * Retrieves the stored token set for a user and resource bucket.
103 101 *
102 + * @param int $user_id The user ID.
103 + * @param Resource_Indicator $resource_indicator The resource indicator (use Resource_Indicator::default() for the default bucket).
104 + *
105 + * @return Token_Set|null The token set, or null if not stored or decryption fails.
106 + */
107 + public function get( int $user_id, Resource_Indicator $resource_indicator ): ?Token_Set {
108 + return $this->decrypt_and_decode( $user_id, $this->user_helper->get_meta( $user_id, $this->get_meta_key( $resource_indicator ), true ) );
109 + }
110 +
111 + /**
112 + * Deletes the stored token set for a user and resource bucket.
113 + *
114 + * @param int $user_id The user ID.
115 + * @param Resource_Indicator $resource_indicator The resource indicator (use Resource_Indicator::default() for the default bucket).
116 + *
117 + * @return void
118 + */
119 + public function delete( int $user_id, Resource_Indicator $resource_indicator ): void {
120 + $this->user_helper->delete_meta( $user_id, $this->get_meta_key( $resource_indicator ) );
121 + }
122 +
123 + /**
124 + * Returns every stored token set across resource buckets for a user.
125 + *
104 126 * @param int $user_id The user ID.
105 127 *
106 - * @return Token_Set|null The token set, or null if not stored or decryption fails.
128 + * @return Token_Set[] The stored token sets.
107 129 */
108 - public function get( int $user_id ): ?Token_Set {
109 - $stored = $this->user_helper->get_meta( $user_id, $this->get_meta_key(), true );
130 + public function get_all( int $user_id ): array {
131 + $tokens = [];
132 + $all_meta = $this->user_helper->get_meta( $user_id );
133 + if ( ! \is_array( $all_meta ) ) {
134 + return $tokens;
135 + }
136 + $prefix = $this->get_meta_key_prefix_for_current_issuer();
137 + foreach ( $all_meta as $key => $values ) {
138 + if ( \strpos( (string) $key, $prefix ) !== 0 ) {
139 + continue;
140 + }
141 + $stored = \is_array( $values ) ? ( $values[0] ?? '' ) : $values;
142 + $token = $this->decrypt_and_decode( $user_id, $stored );
143 + if ( $token !== null ) {
144 + $tokens[] = $token;
145 + }
146 + }
147 +
148 + return $tokens;
149 + }
150 +
151 + /**
152 + * Deletes every stored user token set across all users and resource buckets for the current issuer.
153 + *
154 + * @return void
155 + */
156 + public function delete_all(): void {
157 + $this->bulk_delete_by_prefix( $this->get_meta_key_prefix_for_current_issuer() );
158 + }
159 +
160 + /**
161 + * Deletes every stored user token set across all users, issuers, and resource buckets.
162 + *
163 + * @return void
164 + */
165 + public function delete_all_issuers(): void {
166 + $this->bulk_delete_by_prefix( self::META_KEY_PREFIX );
167 + }
168 +
169 + /**
170 + * Deletes every usermeta row whose meta_key starts with the given prefix.
171 + *
172 + * @param string $prefix The meta_key prefix.
173 + *
174 + * @return void
175 + */
176 + private function bulk_delete_by_prefix( string $prefix ): void {
177 + global $wpdb;
178 +
179 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk cleanup.
180 + $wpdb->query(
181 + $wpdb->prepare(
182 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bulk cleanup.
183 + "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
184 + $wpdb->esc_like( $prefix ) . '%',
185 + ),
186 + );
187 + }
188 +
189 + /**
190 + * Decrypts and decodes a stored meta value into a Token_Set.
191 + *
192 + * @param int $user_id The user ID (for logging context).
193 + * @param string|false|null $stored The stored value.
194 + *
195 + * @return Token_Set|null The token set, or null on absence/failure.
196 + */
197 + private function decrypt_and_decode( int $user_id, $stored ): ?Token_Set {
110 198 if ( ! \is_string( $stored ) || $stored === '' ) {
111 199 return null;
112 200 }
113 201
@@ -119,10 +207,9 @@
119 207 return null;
120 208 }
121 209
122 210 return Token_Set::from_array( $data );
123 - }
124 - catch ( Exception $e ) {
211 + } catch ( Exception $e ) {
125 212 $this->logger->error(
126 213 'Failed to decrypt stored user token for user {user_id}: {error}',
127 214 [
128 215 'user_id' => $user_id,
@@ -128,41 +215,38 @@
128 215 'user_id' => $user_id,
129 216 'error' => $e->getMessage(),
130 217 ],
131 218 );
219 +
132 220 return null;
133 221 }
134 222 }
135 223
136 224 /**
137 - * Deletes the stored token set for a user.
225 + * Returns the meta key prefix for the current issuer.
138 226 *
139 - * @param int $user_id The user ID.
140 - *
141 - * @return void
227 + * @return string The meta key prefix.
142 228 */
143 - public function delete( int $user_id ): void {
144 - $this->user_helper->delete_meta( $user_id, $this->get_meta_key() );
229 + private function get_meta_key_prefix_for_current_issuer(): string {
230 + return self::META_KEY_PREFIX . $this->issuer_config->get_issuer_key();
145 231 }
146 232
147 233 /**
148 - * Deletes all stored user token sets across all issuers.
149 - * This is used for cleanup on uninstall, as we cannot know which users had tokens stored.
150 - * Uses a LIKE match on the meta key prefix to ensure tokens from all issuers are removed.
234 + * Returns the meta key for a resource bucket.
151 235 *
152 - * @return void
236 + * The default bucket has no suffix and shares its key with pre-RFC-8707
237 + * installs. Explicit resource indicators get a sha1-hash suffix joined
238 + * by an underscore.
239 + *
240 + * @param Resource_Indicator $resource_indicator The resource indicator.
241 + *
242 + * @return string The meta key.
153 243 */
154 - public function delete_all(): void {
155 - global $wpdb;
244 + private function get_meta_key( Resource_Indicator $resource_indicator ): string {
245 + $key = $this->get_meta_key_prefix_for_current_issuer();
246 + if ( $resource_indicator->is_default() ) {
247 + return $key;
248 + }
156 249
157 - if ( isset( $wpdb ) ) {
158 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk cleanup on uninstall.
159 - $wpdb->query(
160 - $wpdb->prepare(
161 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bulk cleanup on uninstall.
162 - "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
163 - $wpdb->esc_like( self::META_KEY_PREFIX ) . '%',
164 - ),
165 - );
166 - }
250 + return $key . '_' . \substr( \sha1( $resource_indicator->value() ), 0, 12 );
167 251 }
168 252 }