PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-tokens.php

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

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