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/token-storage.php +119 -16 27.7 → trunk View file →
@@ -5,8 +5,9 @@
5 5
6 6 use Exception;
7 7 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Token_Storage_Exception;
8 8 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Token_Storage_Interface;
9 +use Yoast\WP\SEO\MyYoast_Client\Domain\Resource_Indicator;
9 10 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
10 11 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption;
11 12 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption_Exception;
12 13 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config;
@@ -14,13 +15,18 @@
14 15 use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait;
15 16 use YoastSEO_Vendor\Psr\Log\NullLogger;
16 17
17 18 /**
18 - * Stores and retrieves encrypted site-level tokens as a WordPress option.
19 + * Stores and retrieves encrypted site-level tokens as WordPress options.
19 20 *
20 - * Used for client_credentials tokens (site-level, no user context).
21 - * The option key is scoped by issuer so that switching issuers
22 - * isolates all stored data.
21 + * Used for client_credentials tokens (site-level, no user context). Tokens
22 + * are bucketed per RFC 8707 resource indicator so a site can hold one token
23 + * per resource server. Each (issuer, resource bucket) pair maps to a
24 + * separate option row.
25 + *
26 + * Key layout:
27 + * - Default bucket: wpseo_myyoast_site_tokens_{issuer_key}
28 + * - Resource bucket: wpseo_myyoast_site_tokens_{issuer_key}_{sha1_prefix}
23 29 */
24 30 class Token_Storage implements Token_Storage_Interface, LoggerAwareInterface {
25 31 use LoggerAwareTrait;
26 32
@@ -53,9 +59,9 @@
53 59 $this->logger = new NullLogger();
54 60 }
55 61
56 62 /**
57 - * Stores a token set (encrypted).
63 + * Stores a token set (encrypted). The resource bucket is derived from the token's own resource indicator.
58 64 *
59 65 * @param Token_Set $token_set The token set to store.
60 66 *
61 67 * @return void
@@ -76,18 +82,104 @@
76 82 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
77 83 throw new Token_Storage_Exception( 'Failed to encrypt token set for storage: ' . $e->getMessage(), 0, $e );
78 84 }
79 85
80 - \update_option( $this->get_option_key(), $encrypted, false );
86 + \update_option( $this->get_option_key( $token_set->get_resource_indicator() ), $encrypted, false );
81 87 }
82 88
83 89 /**
84 - * Retrieves the stored token set.
90 + * Retrieves the stored token set for a resource bucket.
85 91 *
92 + * @param Resource_Indicator $resource_indicator The resource indicator (use Resource_Indicator::default() for the default bucket).
93 + *
86 94 * @return Token_Set|null The token set, or null if not stored or decryption fails.
87 95 */
88 - public function get(): ?Token_Set {
89 - $stored = \get_option( $this->get_option_key(), '' );
96 + public function get( Resource_Indicator $resource_indicator ): ?Token_Set {
97 + return $this->decrypt_and_decode( \get_option( $this->get_option_key( $resource_indicator ), '' ) );
98 + }
99 +
100 + /**
101 + * Deletes the stored token set for a resource bucket.
102 + *
103 + * @param Resource_Indicator $resource_indicator The resource indicator (use Resource_Indicator::default() for the default bucket).
104 + *
105 + * @return void
106 + */
107 + public function delete( Resource_Indicator $resource_indicator ): void {
108 + \delete_option( $this->get_option_key( $resource_indicator ) );
109 + }
110 +
111 + /**
112 + * Returns every stored token set across resource buckets.
113 + *
114 + * @return Token_Set[] The stored token sets.
115 + */
116 + public function get_all(): array {
117 + global $wpdb;
118 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Accuracy over performance.
119 + $rows = $wpdb->get_results(
120 + $wpdb->prepare(
121 + "SELECT option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
122 + $wpdb->esc_like( $this->get_option_key_prefix_for_current_issuer() ) . '%',
123 + ),
124 + \ARRAY_A,
125 + );
126 + $tokens = [];
127 + foreach ( ( \is_array( $rows ) ? $rows : [] ) as $row ) {
128 + $token = $this->decrypt_and_decode( ( $row['option_value'] ?? '' ) );
129 + if ( $token !== null ) {
130 + $tokens[] = $token;
131 + }
132 + }
133 +
134 + return $tokens;
135 + }
136 +
137 + /**
138 + * Deletes every stored token set across resource buckets for the current issuer.
139 + *
140 + * @return void
141 + */
142 + public function delete_all(): void {
143 + $this->bulk_delete_by_prefix( $this->get_option_key_prefix_for_current_issuer() );
144 + }
145 +
146 + /**
147 + * Deletes every stored token set across all issuers and resource buckets.
148 + *
149 + * @return void
150 + */
151 + public function delete_all_issuers(): void {
152 + $this->bulk_delete_by_prefix( self::OPTION_KEY_PREFIX );
153 + }
154 +
155 + /**
156 + * Deletes every option whose name starts with the given prefix.
157 + *
158 + * @param string $prefix The option-name prefix.
159 + *
160 + * @return void
161 + */
162 + private function bulk_delete_by_prefix( string $prefix ): void {
163 + global $wpdb;
164 +
165 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk cleanup.
166 + $wpdb->query(
167 + $wpdb->prepare(
168 + "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
169 + $wpdb->esc_like( $prefix ) . '%',
170 + ),
171 + );
172 + }
173 +
174 + /**
175 + * Decrypts and decodes a stored option value into a Token_Set.
176 + *
177 + * @param string|false|null $stored The stored value.
178 + *
179 + * @return Token_Set|null The token set, or null on absence/failure.
180 + */
181 + private function decrypt_and_decode( $stored ): ?Token_Set {
90 182 if ( ! \is_string( $stored ) || $stored === '' ) {
91 183 return null;
92 184 }
93 185
@@ -107,21 +199,32 @@
107 199 }
108 200 }
109 201
110 202 /**
111 - * Deletes the stored token set.
203 + * Returns the option key prefix for the current issuer.
112 204 *
113 - * @return void
205 + * @return string The option key prefix.
114 206 */
115 - public function delete(): void {
116 - \delete_option( $this->get_option_key() );
207 + private function get_option_key_prefix_for_current_issuer(): string {
208 + return self::OPTION_KEY_PREFIX . $this->issuer_config->get_issuer_key();
117 209 }
118 210
119 211 /**
120 - * Returns the issuer-scoped option key.
212 + * Returns the option key for a resource bucket.
121 213 *
214 + * The default bucket has no suffix and shares its key with pre-RFC-8707
215 + * installs. Explicit resource indicators get a sha1-hash suffix joined
216 + * by an underscore.
217 + *
218 + * @param Resource_Indicator $resource_indicator The resource indicator.
219 + *
122 220 * @return string The option key.
123 221 */
124 - private function get_option_key(): string {
125 - return self::OPTION_KEY_PREFIX . $this->issuer_config->get_issuer_key();
222 + private function get_option_key( Resource_Indicator $resource_indicator ): string {
223 + $key = $this->get_option_key_prefix_for_current_issuer();
224 + if ( $resource_indicator->is_default() ) {
225 + return $key;
226 + }
227 +
228 + return $key . '_' . \substr( \sha1( $resource_indicator->value() ), 0, 12 );
126 229 }
127 230 }