PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.7
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.7
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
wordpress-seo / src / myyoast-client / domain / token-set.php

token-set.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.7, at src/myyoast-client/domain/token-set.php

286 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\MyYoast_Client\Domain;
4
5 use InvalidArgumentException;
6 use SensitiveParameter;
7
8 /**
9 * Immutable value object representing a set of OAuth tokens.
10 *
11 * Can represent both user-level tokens (from authorization code flow, with
12 * refresh_token and id_token) and site-level tokens (from client_credentials,
13 * access_token only).
14 */
15 class Token_Set {
16
17 /**
18 * Seconds before actual expiry to consider the token expired.
19 * Accounts for request latency and minor clock differences.
20 */
21 private const EXPIRY_BUFFER_SECONDS = 60;
22
23 /**
24 * The access token (opaque string — do not parse).
25 *
26 * @var string
27 */
28 private $access_token;
29
30 /**
31 * The Unix timestamp at which the access token expires.
32 *
33 * @var int
34 */
35 private $expires_at;
36
37 /**
38 * The refresh token, if available (user-level tokens only).
39 *
40 * @var string|null
41 */
42 private $refresh_token;
43
44 /**
45 * The OIDC ID token, if available (user-level tokens only).
46 *
47 * @var string|null
48 */
49 private $id_token;
50
51 /**
52 * The granted scope string.
53 *
54 * @var string|null
55 */
56 private $scope;
57
58 /**
59 * The token type (an Auth_Token_Type constant).
60 *
61 * @var string
62 */
63 private $token_type;
64
65 /**
66 * The number of consecutive refresh errors.
67 *
68 * @var int
69 */
70 private $error_count;
71
72 /**
73 * Token_Set constructor.
74 *
75 * @param string $access_token The access token.
76 * @param int $expires_at Unix timestamp of token expiry.
77 * @param string $token_type An Auth_Token_Type constant.
78 * @param string|null $refresh_token The refresh token.
79 * @param string|null $id_token The OIDC ID token.
80 * @param string|null $scope The granted scope.
81 * @param int $error_count The number of consecutive refresh errors.
82 *
83 * @throws InvalidArgumentException If required fields are empty or invalid.
84 */
85 public function __construct(
86 // phpcs:ignore PHPCompatibility.Attributes.NewAttributes.PHPNativeAttributeFound -- No-op on PHP < 8.2; redacts parameter from stack traces on PHP 8.2+.
87 #[SensitiveParameter]
88 string $access_token,
89 int $expires_at,
90 string $token_type = Auth_Token_Type::DPOP,
91 // phpcs:ignore PHPCompatibility.Attributes.NewAttributes.PHPNativeAttributeFound -- No-op on PHP < 8.2; redacts parameter from stack traces on PHP 8.2+.
92 #[SensitiveParameter]
93 ?string $refresh_token = null,
94 ?string $id_token = null,
95 ?string $scope = null,
96 int $error_count = 0
97 ) {
98 if ( $access_token === '' ) {
99 throw new InvalidArgumentException( 'Token_Set requires a non-empty access_token.' );
100 }
101 if ( $expires_at <= 0 ) {
102 throw new InvalidArgumentException( 'Token_Set requires a positive expires_at timestamp.' );
103 }
104 if ( $token_type === '' ) {
105 throw new InvalidArgumentException( 'Token_Set requires a non-empty token_type.' );
106 }
107
108 $this->access_token = $access_token;
109 $this->expires_at = $expires_at;
110 $this->token_type = $token_type;
111 $this->refresh_token = $refresh_token;
112 $this->id_token = $id_token;
113 $this->scope = $scope;
114 $this->error_count = $error_count;
115 }
116
117 /**
118 * Returns the access token.
119 *
120 * @return string
121 */
122 public function get_access_token(): string {
123 return $this->access_token;
124 }
125
126 /**
127 * Returns the Unix timestamp at which the access token expires.
128 *
129 * @return int
130 */
131 public function get_expires_at(): int {
132 return $this->expires_at;
133 }
134
135 /**
136 * Returns the token type.
137 *
138 * @return string
139 */
140 public function get_token_type(): string {
141 return $this->token_type;
142 }
143
144 /**
145 * Returns the refresh token, or null if not available.
146 *
147 * @return string|null
148 */
149 public function get_refresh_token(): ?string {
150 return $this->refresh_token;
151 }
152
153 /**
154 * Returns the OIDC ID token, or null if not available.
155 *
156 * @return string|null
157 */
158 public function get_id_token(): ?string {
159 return $this->id_token;
160 }
161
162 /**
163 * Returns the granted scope string, or null if not available.
164 *
165 * @return string|null
166 */
167 public function get_scope(): ?string {
168 return $this->scope;
169 }
170
171 /**
172 * Checks if the token set has the required scope(s).
173 * Returns true if AT LEAST all required scopes are granted, false otherwise.
174 *
175 * @param string[] $required_scopes The required scopes as an array of strings.
176 *
177 * @return bool True if all required scopes are granted, false otherwise.
178 */
179 public function has_scopes( array $required_scopes ): bool {
180 if ( $this->scope === null ) {
181 return \count( $required_scopes ) === 0;
182 }
183 $granted_scopes = \explode( ' ', $this->scope );
184 return \count( \array_diff( $required_scopes, $granted_scopes ) ) === 0;
185 }
186
187 /**
188 * Returns the number of consecutive refresh errors.
189 *
190 * @return int
191 */
192 public function get_error_count(): int {
193 return $this->error_count;
194 }
195
196 /**
197 * Returns a new Token_Set with an incremented error count.
198 *
199 * @return self
200 */
201 public function with_incremented_error_count(): self {
202 return new self(
203 $this->access_token,
204 $this->expires_at,
205 $this->token_type,
206 $this->refresh_token,
207 $this->id_token,
208 $this->scope,
209 $this->error_count + 1,
210 );
211 }
212
213 /**
214 * Whether the access token has expired.
215 *
216 * Uses a 60-second buffer to allow for request latency.
217 *
218 * @return bool
219 */
220 public function is_expired(): bool {
221 return \time() >= ( $this->expires_at - self::EXPIRY_BUFFER_SECONDS );
222 }
223
224 /**
225 * Converts the token set to an associative array for storage.
226 *
227 * @return array<string, string|int|null> The token set as an array.
228 */
229 public function to_array(): array {
230 return [
231 'access_token' => $this->access_token,
232 'expires_at' => $this->expires_at,
233 'token_type' => $this->token_type,
234 'refresh_token' => $this->refresh_token,
235 'id_token' => $this->id_token,
236 'scope' => $this->scope,
237 'error_count' => $this->error_count,
238 ];
239 }
240
241 /**
242 * Creates a Token_Set from a stored array.
243 *
244 * @param array<string, string|int|null> $data The stored array data.
245 *
246 * @return self
247 */
248 public static function from_array( array $data ): self {
249 return new self(
250 (string) ( $data['access_token'] ?? '' ),
251 (int) ( $data['expires_at'] ?? 0 ),
252 ( $data['token_type'] ?? Auth_Token_Type::DPOP ),
253 ( $data['refresh_token'] ?? null ),
254 ( $data['id_token'] ?? null ),
255 ( $data['scope'] ?? null ),
256 (int) ( $data['error_count'] ?? 0 ),
257 );
258 }
259
260 /**
261 * Creates a Token_Set from a token endpoint response.
262 *
263 * @param array<string, string|int|null> $response The parsed JSON response from the token endpoint.
264 *
265 * @return self
266 *
267 * @throws InvalidArgumentException If the response is missing a valid access_token.
268 */
269 public static function from_response( array $response ): self {
270 if ( empty( $response['access_token'] ) || ! \is_string( $response['access_token'] ) ) {
271 throw new InvalidArgumentException( 'Token response is missing a valid access_token.' );
272 }
273
274 $expires_in = (int) ( $response['expires_in'] ?? 900 );
275
276 return new self(
277 $response['access_token'],
278 ( \time() + $expires_in ),
279 ( $response['token_type'] ?? Auth_Token_Type::DPOP ),
280 ( $response['refresh_token'] ?? null ),
281 ( $response['id_token'] ?? null ),
282 ( $response['scope'] ?? null ),
283 );
284 }
285 }
286