PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.3
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-protected-owner.php

class-protected-owner.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.3, at jetpack_vendor/automattic/jetpack-connection/src/class-protected-owner.php

164 lines 5.0 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 Protected Owner class file.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8 namespace Automattic\Jetpack\Connection;
9
10 use Jetpack_Options;
11
12 /**
13 * The local anchor naming the connection's protected owner.
14 *
15 * WordPress.com is authoritative on who the owner is; this records who the site was told to
16 * expect, so ownership stops following whoever connected first. Identity is always matched on
17 * `wpcom_user_id` — `local_user_id` is a re-pointable cache, never the match key.
18 *
19 * @since 9.3.0
20 */
21 class Protected_Owner {
22
23 const OPTION = 'protected_owner';
24
25 /**
26 * Get the anchor.
27 *
28 * @since 9.3.0
29 *
30 * @return array|null The anchor, or null when none is usable.
31 */
32 public static function get() {
33 $anchor = Jetpack_Options::get_option( self::OPTION );
34
35 if ( ! is_array( $anchor ) || empty( $anchor['wpcom_user_id'] ) ) {
36 return null;
37 }
38
39 return $anchor;
40 }
41
42 /**
43 * Record a confirmed protected owner and lock the anchor.
44 *
45 * @since 9.3.0
46 *
47 * @param int $wpcom_user_id The owner's WordPress.com user ID, as confirmed by WordPress.com.
48 * @param int $local_user_id The owner's local WordPress user ID. Required here, though the
49 * anchor treats it as a re-pointable cache rather than the match
50 * key, so a caller that legitimately does not know it yet would
51 * need this relaxed.
52 * @param string $confirmed_by How the confirmation was obtained, e.g. `popup` or `recovery`.
53 * Required, and travels to WordPress.com with the anchor: it names
54 * a mechanism rather than a local user, and a default here would
55 * record provenance nobody established.
56 * @return bool Whether the anchor is now stored as requested.
57 */
58 public static function set( $wpcom_user_id, $local_user_id, $confirmed_by ) {
59 $confirmed_by = sanitize_key( $confirmed_by );
60 $wpcom_user_id = absint( $wpcom_user_id );
61 $local_user_id = absint( $local_user_id );
62
63 // A zero ID would store an anchor `get()` rejects, and a blank mechanism is
64 // indistinguishable from one never recorded. Neither is worth persisting.
65 if ( ! $confirmed_by || ! $wpcom_user_id || ! $local_user_id ) {
66 return false;
67 }
68
69 $anchor = array(
70 'wpcom_user_id' => $wpcom_user_id,
71 'local_user_id' => $local_user_id,
72 'locked' => true,
73 'confirmed_at' => gmdate( 'Y-m-d\TH:i:s\Z' ),
74 'confirmed_by' => $confirmed_by,
75 );
76
77 if ( Jetpack_Options::update_option( self::OPTION, $anchor ) ) {
78 return true;
79 }
80
81 // `update_option()` reports false for an unchanged value as well as for a failed write.
82 return Jetpack_Options::get_option( self::OPTION ) === $anchor;
83 }
84
85 /**
86 * Point the anchor's cached local user ID at a different local user.
87 *
88 * The match key is `wpcom_user_id`; `local_user_id` is a cache of where that identity lives on
89 * this site, and it legitimately moves when the owner reconnects under another local account.
90 * Deliberately narrow: `confirmed_at` and `confirmed_by` record how the owner was originally
91 * confirmed, and re-pointing a cache is not a new confirmation.
92 *
93 * @since 9.5.0
94 *
95 * @param int $local_user_id The local user the anchored identity now holds.
96 * @return bool Whether the anchor now names that local user.
97 */
98 public static function repoint( $local_user_id ) {
99 $local_user_id = absint( $local_user_id );
100 $anchor = self::get();
101
102 if ( ! $anchor || ! $local_user_id ) {
103 return false;
104 }
105
106 // Defaulted: `get()` only requires `wpcom_user_id`, so a partial anchor reaches here.
107 if ( (int) ( $anchor['local_user_id'] ?? 0 ) === $local_user_id ) {
108 return true;
109 }
110
111 $anchor['local_user_id'] = $local_user_id;
112
113 if ( Jetpack_Options::update_option( self::OPTION, $anchor ) ) {
114 return true;
115 }
116
117 return Jetpack_Options::get_option( self::OPTION ) === $anchor;
118 }
119
120 /**
121 * Drop the anchor, unlocking ownership.
122 *
123 * Leaves `master_user` alone: clearing the lock does not change who the owner is.
124 *
125 * @internal Recovery and support flows only. Consumers must not call this.
126 * @since 9.3.0
127 *
128 * @return bool Whether the anchor was deleted.
129 */
130 public static function clear() {
131 return Jetpack_Options::delete_option( self::OPTION );
132 }
133
134 /**
135 * Get the anchor, but only while it is locked.
136 *
137 * An unlocked anchor names an owner without preventing ownership moving, so it protects
138 * nobody and callers gating on protection must not see it.
139 *
140 * @since 9.3.0
141 *
142 * @return array|null The locked anchor, or null when there is none.
143 */
144 public static function get_locked() {
145 $anchor = self::get();
146
147 return ( $anchor && ! empty( $anchor['locked'] ) ) ? $anchor : null;
148 }
149
150 /**
151 * Whether an anchor is set and locked.
152 *
153 * Deliberately independent of whether the current owner matches it: a mismatch is when
154 * ownership most needs to stay locked.
155 *
156 * @since 9.3.0
157 *
158 * @return bool
159 */
160 public static function is_locked() {
161 return null !== self::get_locked();
162 }
163 }
164