Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.10% covered (success)
93.10%
27 / 29
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Protected_Owner
93.10% covered (success)
93.10%
27 / 29
83.33% covered (warning)
83.33%
5 / 6
15.07
0.00% covered (danger)
0.00%
0 / 1
 get
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 set
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 repoint
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_locked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_locked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * The Jetpack Connection Protected Owner class file.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use 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 */
21class 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 the owner WordPress.com has confirmed for this site.
44     *
45     * @since 9.3.0
46     * @since 9.6.0 No longer records how the owner was confirmed.
47     *
48     * @param int $wpcom_user_id The owner's WordPress.com user ID, as confirmed by WordPress.com.
49     * @param int $local_user_id The owner's local WordPress user ID. Required here, though the
50     *                           anchor treats it as a re-pointable cache rather than the match
51     *                           key, so a caller that legitimately does not know it yet would
52     *                           need this relaxed.
53     * @return bool Whether the anchor is now stored as requested.
54     */
55    public static function set( $wpcom_user_id, $local_user_id ) {
56        $wpcom_user_id = absint( $wpcom_user_id );
57        $local_user_id = absint( $local_user_id );
58
59        // A zero ID would store an anchor `get()` rejects.
60        if ( ! $wpcom_user_id || ! $local_user_id ) {
61            return false;
62        }
63
64        $anchor = array(
65            'wpcom_user_id' => $wpcom_user_id,
66            'local_user_id' => $local_user_id,
67            'confirmed_at'  => gmdate( 'Y-m-d\TH:i:s\Z' ),
68        );
69
70        if ( Jetpack_Options::update_option( self::OPTION, $anchor ) ) {
71            return true;
72        }
73
74        // `update_option()` reports false for an unchanged value as well as for a failed write.
75        return Jetpack_Options::get_option( self::OPTION ) === $anchor;
76    }
77
78    /**
79     * Point the anchor's cached local user ID at a different local user.
80     *
81     * The match key is `wpcom_user_id`; `local_user_id` is a cache of where that identity lives on
82     * this site, and it legitimately moves when the owner reconnects under another local account.
83     * Deliberately narrow: `confirmed_at` records when the owner was originally confirmed, and
84     * re-pointing a cache is not a new confirmation.
85     *
86     * @since 9.5.0
87     *
88     * @param int $local_user_id The local user the anchored identity now holds.
89     * @return bool Whether the anchor now names that local user.
90     */
91    public static function repoint( $local_user_id ) {
92        $local_user_id = absint( $local_user_id );
93        $anchor        = self::get();
94
95        if ( ! $anchor || ! $local_user_id ) {
96            return false;
97        }
98
99        // Defaulted: `get()` only requires `wpcom_user_id`, so a partial anchor reaches here.
100        if ( (int) ( $anchor['local_user_id'] ?? 0 ) === $local_user_id ) {
101            return true;
102        }
103
104        $anchor['local_user_id'] = $local_user_id;
105
106        if ( Jetpack_Options::update_option( self::OPTION, $anchor ) ) {
107            return true;
108        }
109
110        return Jetpack_Options::get_option( self::OPTION ) === $anchor;
111    }
112
113    /**
114     * Drop the anchor, unlocking ownership.
115     *
116     * Leaves `master_user` alone: clearing the lock does not change who the owner is.
117     *
118     * @internal Recovery and support flows only. Consumers must not call this.
119     * @since 9.3.0
120     *
121     * @return bool Whether the anchor was deleted.
122     */
123    public static function clear() {
124        return Jetpack_Options::delete_option( self::OPTION );
125    }
126
127    /**
128     * Get the anchor, but only while it protects somebody.
129     *
130     * The anchor is dropped the moment WordPress.com stops confirming it, so holding one and
131     * being protected by it are the same thing. Kept as the name gates read by, which says what
132     * the call site means rather than what the storage happens to be.
133     *
134     * @since 9.3.0
135     *
136     * @return array|null The anchor, or null when there is none.
137     */
138    public static function get_locked() {
139        return self::get();
140    }
141
142    /**
143     * Whether an anchor is set and locked.
144     *
145     * Deliberately independent of whether the current owner matches it: a mismatch is when
146     * ownership most needs to stay locked.
147     *
148     * @since 9.3.0
149     *
150     * @return bool
151     */
152    public static function is_locked() {
153        return null !== self::get_locked();
154    }
155}