PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-tokens.php

class-tokens.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-connection/src/class-tokens.php

722 lines 22.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The Jetpack Connection Tokens class file.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8 namespace Automattic\Jetpack\Connection;
9
10 use Automattic\Jetpack\Constants;
11 use Automattic\Jetpack\Roles;
12 use DateInterval;
13 use DateTime;
14 use Exception;
15 use Jetpack_Options;
16 use WP_Error;
17
18 /**
19 * The Jetpack Connection Tokens class that manages tokens.
20 */
21 class Tokens {
22
23 const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
24
25 /**
26 * Datetime format.
27 */
28 const DATE_FORMAT_ATOM = 'Y-m-d\TH:i:sP';
29
30 /**
31 * Deletes all connection tokens and transients from the local Jetpack site.
32 */
33 public function delete_all() {
34 Jetpack_Options::delete_option(
35 array(
36 'blog_token',
37 'user_token',
38 'user_tokens',
39 )
40 );
41
42 $this->remove_lock();
43
44 /**
45 * Fires after all connection tokens have been deleted from the local site.
46 *
47 * `Jetpack_Options::delete_option()` fires no action of its own, so this is the only
48 * signal that the tokens backing the connection are gone. Anything holding derived
49 * state — a memoized connection status, a cached credential — must recompute from here.
50 *
51 * @since 9.1.1
52 */
53 do_action( 'jetpack_connection_tokens_deleted' );
54 }
55
56 /**
57 * Perform the API request to validate the blog and user tokens.
58 *
59 * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
60 *
61 * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
62 */
63 public function validate( $user_id = null ) {
64 $blog_id = Jetpack_Options::get_option( 'id' );
65 if ( ! $blog_id ) {
66 return new WP_Error( 'site_not_registered', 'Site not registered.' );
67 }
68 $url = sprintf(
69 '%s/%s/v%s/%s',
70 Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
71 'wpcom',
72 '2',
73 'sites/' . $blog_id . '/jetpack-token-health'
74 );
75
76 $user_token = $this->get_access_token( $user_id ? $user_id : get_current_user_id() );
77 $blog_token = $this->get_access_token();
78
79 // Cannot validate non-existent tokens.
80 if ( false === $user_token || false === $blog_token ) {
81 return false;
82 }
83
84 $method = 'POST';
85 $body = array(
86 'user_token' => $this->get_signed_token( $user_token ),
87 'blog_token' => $this->get_signed_token( $blog_token ),
88 );
89 $response = Client::_wp_remote_request( $url, compact( 'body', 'method' ) );
90
91 if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
92 return false;
93 }
94
95 $body = json_decode( wp_remote_retrieve_body( $response ), true );
96
97 return $body ? $body : false;
98 }
99
100 /**
101 * Perform the API request to validate only the blog.
102 *
103 * @since 9.8.0 Returns a WP_Error, not false, when the request fails.
104 *
105 * @return bool|WP_Error Boolean with the test result. WP_Error if test cannot be performed.
106 */
107 public function validate_blog_token() {
108 $blog_id = Jetpack_Options::get_option( 'id' );
109 if ( ! $blog_id ) {
110 return new WP_Error( 'site_not_registered', 'Site not registered.' );
111 }
112
113 // A missing blog token is broken, not unverifiable: the signed request would fail before it is sent.
114 if ( ! $this->get_access_token() ) {
115 return false;
116 }
117
118 $url = sprintf(
119 '%s/%s/v%s/%s',
120 Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
121 'wpcom',
122 '2',
123 'sites/' . $blog_id . '/jetpack-token-health/blog'
124 );
125
126 $method = 'GET';
127 $response = Client::remote_request( compact( 'url', 'method' ) );
128
129 if ( is_wp_error( $response ) ) {
130 return $response;
131 }
132
133 if ( ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
134 return new WP_Error( 'blog_token_check_failed', 'The blog token health check could not be performed.' );
135 }
136
137 $body = json_decode( wp_remote_retrieve_body( $response ), true );
138
139 return is_array( $body ) && isset( $body['is_healthy'] ) && true === $body['is_healthy'];
140 }
141
142 /**
143 * Obtains the auth token.
144 *
145 * @param array $data The request data.
146 * @param string $token_api_url The URL of the Jetpack "token" API.
147 * @return object|WP_Error Returns the auth token on success.
148 * Returns a WP_Error on failure.
149 */
150 public function get( $data, $token_api_url ) {
151 $roles = new Roles();
152 $role = $roles->translate_current_user_to_role();
153
154 if ( ! $role ) {
155 return new WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack-connection' ) );
156 }
157
158 $client_secret = $this->get_access_token();
159 if ( ! $client_secret ) {
160 return new WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack-connection' ) );
161 }
162
163 /**
164 * Filter the URL of the first time the user gets redirected back to your site for connection
165 * data processing.
166 *
167 * @since 1.7.0
168 * @since-jetpack 8.0.0
169 *
170 * @param string $redirect_url Defaults to the site admin URL.
171 */
172 $processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
173
174 $redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
175
176 /**
177 * Filter the URL to redirect the user back to when the authentication process
178 * is complete.
179 *
180 * @since 1.7.0
181 * @since-jetpack 8.0.0
182 *
183 * @param string $redirect_url Defaults to the site URL.
184 */
185 $redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
186
187 $redirect_uri = ( 'calypso' === $data['auth_type'] )
188 ? $data['redirect_uri']
189 : add_query_arg(
190 array(
191 'handler' => 'jetpack-connection-webhooks',
192 'action' => 'authorize',
193 '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
194 'redirect' => $redirect ? rawurlencode( $redirect ) : false,
195 ),
196 esc_url( $processing_url )
197 );
198
199 /**
200 * Filters the token request data.
201 *
202 * @since 1.7.0
203 * @since-jetpack 8.0.0
204 *
205 * @param array $request_data request data.
206 */
207 $body = apply_filters(
208 'jetpack_token_request_body',
209 array(
210 'client_id' => Jetpack_Options::get_option( 'id' ),
211 'client_secret' => $client_secret->secret,
212 'grant_type' => 'authorization_code',
213 'code' => $data['code'],
214 'redirect_uri' => $redirect_uri,
215 )
216 );
217
218 $args = array(
219 'method' => 'POST',
220 'body' => $body,
221 'headers' => array(
222 'Accept' => 'application/json',
223 ),
224 );
225 add_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
226 $response = Client::_wp_remote_request( $token_api_url, $args );
227 remove_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
228
229 if ( is_wp_error( $response ) ) {
230 return new WP_Error( 'token_http_request_failed', $response->get_error_message() );
231 }
232
233 $code = wp_remote_retrieve_response_code( $response );
234 $entity = wp_remote_retrieve_body( $response );
235
236 if ( $entity ) {
237 $json = json_decode( $entity );
238 } else {
239 $json = false;
240 }
241
242 if ( 200 !== $code || ! empty( $json->error ) ) {
243 if ( empty( $json->error ) ) {
244 return new WP_Error( 'unknown', '', $code );
245 }
246
247 /* translators: Error description string. */
248 $error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->error_description ) : '';
249
250 return new WP_Error( (string) $json->error, $error_description, $code );
251 }
252
253 if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
254 return new WP_Error( 'access_token', '', $code );
255 }
256
257 if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
258 return new WP_Error( 'token_type', '', $code );
259 }
260
261 if ( empty( $json->scope ) ) {
262 return new WP_Error( 'scope', 'No Scope', $code );
263 }
264
265 // TODO: get rid of the error silencer.
266 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
267 @list( $role, $hmac ) = explode( ':', $json->scope );
268 if ( empty( $role ) || empty( $hmac ) ) {
269 return new WP_Error( 'scope', 'Malformed Scope', $code );
270 }
271
272 if ( $this->sign_role( $role ) !== $json->scope ) {
273 return new WP_Error( 'scope', 'Invalid Scope', $code );
274 }
275
276 $cap = $roles->translate_role_to_cap( $role );
277 if ( ! $cap ) {
278 return new WP_Error( 'scope', 'No Cap', $code );
279 }
280
281 if ( ! current_user_can( $cap ) ) {
282 return new WP_Error( 'scope', 'current_user_cannot', $code );
283 }
284
285 return (string) $json->access_token;
286 }
287
288 /**
289 * Enters a user token into the user_tokens option
290 *
291 * @param int $user_id The user id.
292 * @param string $token The user token.
293 * @param bool $is_master_user Whether the user is the master user.
294 * @return bool
295 */
296 public function update_user_token( $user_id, $token, $is_master_user ) {
297 // Not designed for concurrent updates.
298 $user_tokens = $this->get_user_tokens();
299 if ( ! is_array( $user_tokens ) ) {
300 $user_tokens = array();
301 }
302 $user_tokens[ $user_id ] = $token;
303 if ( $is_master_user ) {
304 $master_user = $user_id;
305 $options = compact( 'user_tokens', 'master_user' );
306 } else {
307 $options = compact( 'user_tokens' );
308 }
309 $updated = Jetpack_Options::update_options( $options );
310
311 /**
312 * Fires when the user token gets replaced.
313 *
314 * @since 1.29.0
315 * @since 9.8.1 Fired from Tokens::update_user_token() so every write path (authorize, provisioning, CLI) clears stale connection errors, not just the REST endpoint.
316 *
317 * @param int $user_id User ID.
318 * @param string $token New user token.
319 */
320 do_action( 'jetpack_updated_user_token', $user_id, $token );
321
322 return $updated;
323 }
324
325 /**
326 * Sign a user role with the master access token.
327 * If not specified, will default to the current user.
328 *
329 * @access public
330 *
331 * @param string $role User role.
332 * @param int $user_id ID of the user.
333 * @return string Signed user role.
334 */
335 public function sign_role( $role, $user_id = null ) {
336 if ( empty( $user_id ) ) {
337 $user_id = (int) get_current_user_id();
338 }
339
340 if ( ! $user_id ) {
341 return false;
342 }
343
344 $token = $this->get_access_token();
345 if ( ! $token || is_wp_error( $token ) ) {
346 return false;
347 }
348
349 return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
350 }
351
352 /**
353 * Increases the request timeout value to 30 seconds.
354 *
355 * @return int Returns 30.
356 */
357 public function return_30() {
358 return 30;
359 }
360
361 /**
362 * Gets the requested token.
363 *
364 * Tokens are one of two types:
365 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
366 * though some sites can have multiple "Special" Blog Tokens (see below). These tokens
367 * are not associated with a user account. They represent the site's connection with
368 * the Jetpack servers.
369 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
370 *
371 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
372 * token, and $private is a secret that should never be displayed anywhere or sent
373 * over the network; it's used only for signing things.
374 *
375 * Blog Tokens can be "Normal" or "Special".
376 * * Normal: The result of a normal connection flow. They look like
377 * "{$random_string_1}.{$random_string_2}"
378 * That is, $token_key and $private are both random strings.
379 * Sites only have one Normal Blog Token. Normal Tokens are found in either
380 * Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
381 * constant (rare).
382 * * Special: A connection token for sites that have gone through an alternative
383 * connection flow. They look like:
384 * ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
385 * That is, $private is a random string and $token_key has a special structure with
386 * lots of semicolons.
387 * Most sites have zero Special Blog Tokens. Special tokens are only found in the
388 * JETPACK_BLOG_TOKEN constant.
389 *
390 * In particular, note that Normal Blog Tokens never start with ";" and that
391 * Special Blog Tokens always do.
392 *
393 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
394 * order:
395 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
396 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
397 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
398 *
399 * @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token.
400 * @param string|false $token_key If provided, check that the token matches the provided input.
401 * @param bool|true $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found.
402 *
403 * @return object|false|WP_Error
404 */
405 public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
406 if ( $this->is_locked() ) {
407 $this->delete_all();
408 return false;
409 }
410
411 $possible_special_tokens = array();
412 $possible_normal_tokens = array();
413 $user_tokens = $this->get_user_tokens();
414
415 if ( $user_id ) {
416 $resolved_user_id = true === $user_id ? (int) Jetpack_Options::get_option( 'master_user' ) : (int) $user_id;
417
418 if ( ! $user_tokens ) {
419 return $suppress_errors ? false : new WP_Error( 'no_user_tokens', __( 'No user tokens found', 'jetpack-connection' ), array( 'user_id' => $resolved_user_id ) );
420 }
421 if ( true === $user_id ) { // connection owner.
422 if ( ! $resolved_user_id ) {
423 return $suppress_errors ? false : new WP_Error( 'empty_master_user_option', __( 'No primary user defined', 'jetpack-connection' ) );
424 }
425 $user_id = $resolved_user_id;
426 }
427 if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
428 // translators: %s is the user ID.
429 return $suppress_errors ? false : new WP_Error( 'no_token_for_user', sprintf( __( 'No token for user %d', 'jetpack-connection' ), $user_id ), array( 'user_id' => (int) $user_id ) );
430 }
431 $user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
432 if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
433 // translators: %s is the user ID.
434 return $suppress_errors ? false : new WP_Error( 'token_malformed', sprintf( __( 'Token for user %d is malformed', 'jetpack-connection' ), $user_id ), array( 'user_id' => (int) $user_id ) );
435 }
436 if ( $user_token_chunks[2] !== (string) $user_id ) {
437 // translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token.
438 return $suppress_errors ? false : new WP_Error( 'user_id_mismatch', sprintf( __( 'Requesting user_id %1$d does not match token user_id %2$d', 'jetpack-connection' ), $user_id, $user_token_chunks[2] ), array( 'user_id' => (int) $user_id ) );
439 }
440 $possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
441 } else {
442 $stored_blog_token = Jetpack_Options::get_option( 'blog_token' );
443 if ( $stored_blog_token ) {
444 $possible_normal_tokens[] = $stored_blog_token;
445 }
446
447 $defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
448
449 if ( $defined_tokens_string ) {
450 $defined_tokens = explode( ',', $defined_tokens_string );
451 foreach ( $defined_tokens as $defined_token ) {
452 if ( ';' === $defined_token[0] ) {
453 $possible_special_tokens[] = $defined_token;
454 } else {
455 $possible_normal_tokens[] = $defined_token;
456 }
457 }
458 }
459 }
460
461 if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
462 $possible_tokens = $possible_normal_tokens;
463 } else {
464 $possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
465 }
466
467 if ( ! $possible_tokens ) {
468 // If no user tokens were found, it would have failed earlier, so this is about blog token.
469 return $suppress_errors ? false : new WP_Error( 'no_possible_tokens', __( 'No blog token found', 'jetpack-connection' ) );
470 }
471
472 $valid_token = false;
473
474 if ( false === $token_key ) {
475 // Use first token.
476 $valid_token = $possible_tokens[0];
477 } elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
478 // Use first normal token.
479 $valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
480 } else {
481 // Use the token matching $token_key or false if none.
482 // Ensure we check the full key.
483 $token_check = rtrim( $token_key, '.' ) . '.';
484
485 foreach ( $possible_tokens as $possible_token ) {
486 if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
487 $valid_token = $possible_token;
488 break;
489 }
490 }
491 }
492
493 if ( ! $valid_token ) {
494 if ( $user_id ) {
495 // translators: %d is the user ID.
496 return $suppress_errors ? false : new WP_Error( 'no_valid_user_token', sprintf( __( 'Invalid token for user %d', 'jetpack-connection' ), $user_id ), array( 'user_id' => (int) $user_id ) );
497 } else {
498 return $suppress_errors ? false : new WP_Error( 'no_valid_blog_token', __( 'Invalid blog token', 'jetpack-connection' ) );
499 }
500 }
501
502 return (object) array(
503 'secret' => $valid_token,
504 'external_user_id' => (int) $user_id,
505 );
506 }
507
508 /**
509 * Updates the blog token to a new value.
510 *
511 * @access public
512 *
513 * @param string $token the new blog token value.
514 * @return Boolean Whether updating the blog token was successful.
515 */
516 public function update_blog_token( $token ) {
517 return Jetpack_Options::update_option( 'blog_token', $token );
518 }
519
520 /**
521 * Unlinks the current user from the linked WordPress.com user.
522 *
523 * @access public
524 * @static
525 *
526 * @todo Refactor to properly load the XMLRPC client independently.
527 *
528 * @param int $user_id The user identifier.
529 *
530 * @return bool Whether the disconnection of the user was successful.
531 */
532 public function disconnect_user( $user_id ) {
533 $tokens = $this->get_user_tokens();
534 if ( ! $tokens ) {
535 return false;
536 }
537
538 if ( ! isset( $tokens[ $user_id ] ) ) {
539 return false;
540 }
541
542 unset( $tokens[ $user_id ] );
543
544 $this->update_user_tokens( $tokens );
545
546 return true;
547 }
548
549 /**
550 * Returns an array of user_id's that have user tokens for communicating with wpcom.
551 * Able to select by specific capability.
552 *
553 * @deprecated 1.30.0
554 * @see Manager::get_connected_users
555 *
556 * @param string $capability The capability of the user.
557 * @param int|null $limit How many connected users to get before returning.
558 * @return array Array of WP_User objects if found.
559 */
560 public function get_connected_users( $capability = 'any', $limit = null ) {
561 _deprecated_function( __METHOD__, '1.30.0' );
562 return ( new Manager( 'jetpack' ) )->get_connected_users( $capability, $limit );
563 }
564
565 /**
566 * Fetches a signed token.
567 *
568 * @param object $token the token.
569 * @return WP_Error|string a signed token
570 */
571 public function get_signed_token( $token ) {
572 if ( ! isset( $token->secret ) || empty( $token->secret ) ) {
573 return new WP_Error( 'invalid_token' );
574 }
575
576 list( $token_key, $token_secret ) = explode( '.', $token->secret );
577
578 $token_key = sprintf(
579 '%s:%d:%d',
580 $token_key,
581 Constants::get_constant( 'JETPACK__API_VERSION' ),
582 $token->external_user_id
583 );
584
585 $timestamp = time();
586
587 if ( function_exists( 'wp_generate_password' ) ) {
588 $nonce = wp_generate_password( 10, false );
589 } else {
590 $nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
591 }
592
593 $normalized_request_string = implode(
594 "\n",
595 array(
596 $token_key,
597 $timestamp,
598 $nonce,
599 )
600 ) . "\n";
601
602 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
603 $signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
604
605 $auth = array(
606 'token' => $token_key,
607 'timestamp' => $timestamp,
608 'nonce' => $nonce,
609 'signature' => $signature,
610 );
611
612 $header_pieces = array();
613 foreach ( $auth as $key => $value ) {
614 $header_pieces[] = sprintf( '%s="%s"', $key, $value );
615 }
616
617 return implode( ' ', $header_pieces );
618 }
619
620 /**
621 * Gets the list of user tokens
622 *
623 * @since 1.30.0
624 *
625 * @return bool|array An array of user tokens where keys are user IDs and values are the tokens. False if no user token is found.
626 */
627 public function get_user_tokens() {
628 return Jetpack_Options::get_option( 'user_tokens' );
629 }
630
631 /**
632 * Updates the option that stores the user tokens
633 *
634 * @since 1.30.0
635 *
636 * @param array $tokens An array of user tokens where keys are user IDs and values are the tokens.
637 * @return bool Was the option successfully updated?
638 *
639 * @todo add validate the input.
640 */
641 public function update_user_tokens( $tokens ) {
642 return Jetpack_Options::update_option( 'user_tokens', $tokens );
643 }
644
645 /**
646 * Lock the tokens to the current site URL.
647 *
648 * @param int $timespan How long the tokens should be locked, in seconds.
649 *
650 * @return bool
651 */
652 public function set_lock( $timespan = HOUR_IN_SECONDS ) {
653 try {
654 $expires = ( new DateTime() )->add( DateInterval::createFromDateString( (int) $timespan . ' seconds' ) );
655 } catch ( Exception $e ) {
656 return false;
657 }
658
659 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
660 return Jetpack_Options::update_option( 'token_lock', $expires->format( static::DATE_FORMAT_ATOM ) . '|||' . base64_encode( Urls::site_url() ) );
661 }
662
663 /**
664 * Remove the site lock from tokens.
665 *
666 * @return bool
667 */
668 public function remove_lock() {
669 Jetpack_Options::delete_option( 'token_lock' );
670
671 return true;
672 }
673
674 /**
675 * Check if the domain is locked, remove the lock if needed.
676 * Possible scenarios:
677 * - lock expired, site URL matches the lock URL: remove the lock, return false.
678 * - lock not expired, site URL matches the lock URL: return false.
679 * - site URL does not match the lock URL (expiration date is ignored): return true, do not remove the lock.
680 *
681 * @return bool
682 */
683 public function is_locked() {
684 $the_lock = Jetpack_Options::get_option( 'token_lock' );
685 if ( ! $the_lock ) {
686 // Not locked.
687 return false;
688 }
689
690 $the_lock = explode( '|||', $the_lock, 2 );
691 if ( count( $the_lock ) !== 2 ) {
692 // Something's wrong with the lock.
693 $this->remove_lock();
694 return false;
695 }
696
697 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
698 $locked_site_url = base64_decode( $the_lock[1] );
699 $expires = $the_lock[0];
700
701 $expiration_date = DateTime::createFromFormat( static::DATE_FORMAT_ATOM, $expires );
702 if ( false === $expiration_date || ! $locked_site_url ) {
703 // Something's wrong with the lock.
704 $this->remove_lock();
705 return false;
706 }
707
708 if ( Urls::site_url() === $locked_site_url ) {
709 if ( new DateTime() > $expiration_date ) {
710 // Site lock expired.
711 // Site URL matches, removing the lock.
712 $this->remove_lock();
713 }
714
715 return false;
716 }
717
718 // Site URL doesn't match.
719 return true;
720 }
721 }
722