Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
41.27% covered (danger)
41.27%
26 / 63
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Open_State_Store
41.27% covered (danger)
41.27%
26 / 63
50.00% covered (danger)
50.00%
3 / 6
67.86
0.00% covered (danger)
0.00%
0 / 1
 fetch
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 update
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 get_cached
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 normalize
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 cache
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 cache_key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Open_State_Store file.
4 *
5 * @package automattic/jetpack-agents-manager
6 */
7
8namespace Automattic\Jetpack\Agents_Manager;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Status\Host;
12
13/**
14 * Reads and writes the Agents Manager open state.
15 *
16 * The state is a global, per-user wpcom preference behind the
17 * `/agents-manager/state` endpoint. How the server reads it depends on the site:
18 *
19 * - wpcom Simple: the preference is local, so read `calypso_preferences` directly.
20 * - WoA / self-hosted: the preference is remote, so reads/writes go through this
21 *   store's local REST route, which calls wpcom over the Jetpack Connection and
22 *   caches the result in a per-user transient. Latency-sensitive readers (the
23 *   server-side pre-render) use that transient to skip the round-trip.
24 */
25class Open_State_Store {
26
27    /**
28     * Transient key prefix for the cached per-user open state.
29     *
30     * @var string
31     */
32    private const TRANSIENT_PREFIX = 'agents_manager_open_state_';
33
34    /**
35     * Default state values.
36     *
37     * @var array
38     */
39    public const DEFAULTS = array(
40        'agents_manager_open'              => false,
41        'agents_manager_docked'            => false,
42        'agents_manager_minimized'         => false,
43        'agents_manager_floating_position' => 'right',
44        'agents_manager_router_history'    => null,
45        'agents_manager_last_activity'     => null,
46    );
47
48    /**
49     * Fetch the open state from wpcom and refresh the cache.
50     *
51     * @return array|\WP_Error Normalized state, or WP_Error when the request fails.
52     */
53    public static function fetch() {
54        $body = Client::wpcom_json_api_request_as_user(
55            '/agents-manager/state',
56            '2',
57            array( 'method' => 'GET' )
58        );
59
60        if ( is_wp_error( $body ) ) {
61            return $body;
62        }
63
64        $response = json_decode( wp_remote_retrieve_body( $body ), true );
65        $state    = self::normalize( is_array( $response ) ? $response : array() );
66
67        self::cache( $state );
68
69        return $state;
70    }
71
72    /**
73     * Persist the open state to wpcom and refresh the cache.
74     *
75     * @param array $state Partial state to update (subset of DEFAULTS keys).
76     * @return array|\WP_Error Normalized state, or WP_Error when the request fails.
77     */
78    public static function update( array $state ) {
79        $body = Client::wpcom_json_api_request_as_user(
80            '/agents-manager/state',
81            '2',
82            array( 'method' => 'POST' ),
83            array( 'state' => $state )
84        );
85
86        if ( is_wp_error( $body ) ) {
87            return $body;
88        }
89
90        $response = json_decode( wp_remote_retrieve_body( $body ), true );
91
92        if ( ! is_array( $response ) ) {
93            return new \WP_Error(
94                'invalid_response',
95                'Invalid response from WPCOM endpoint',
96                array( 'status' => 500 )
97            );
98        }
99
100        $normalized = self::normalize( $response );
101
102        self::cache( $normalized );
103
104        return $normalized;
105    }
106
107    /**
108     * Read the current user's open state from the fastest local source.
109     *
110     * For latency-sensitive callers like the server-side pre-render: Simple sites
111     * read `calypso_preferences` directly, everywhere else uses the cached
112     * transient (see the class docblock). Returns null when nothing is known yet,
113     * so callers can skip pre-rendering until the frontend sets the real state.
114     *
115     * @return array|null `{ agents_manager_open, agents_manager_docked }` or null.
116     */
117    public static function get_cached() {
118        $user_id = get_current_user_id();
119        if ( ! $user_id ) {
120            return null;
121        }
122
123        // Simple sites have the preference locally, so read it directly (the
124        // transient is never primed there).
125        if ( ( new Host() )->is_wpcom_simple() && function_exists( '\get_user_attribute' ) ) {
126            $calypso_prefs = \get_user_attribute( $user_id, 'calypso_preferences' );
127            if ( ! is_array( $calypso_prefs ) ) {
128                return null;
129            }
130
131            return array(
132                'agents_manager_open'   => (bool) ( $calypso_prefs['agents_manager_open'] ?? false ),
133                'agents_manager_docked' => (bool) ( $calypso_prefs['agents_manager_docked'] ?? false ),
134            );
135        }
136
137        $cached = get_transient( self::cache_key( $user_id ) );
138
139        return is_array( $cached ) ? $cached : null;
140    }
141
142    /**
143     * Normalize a raw endpoint response into the full state shape.
144     *
145     * @param array $response Raw decoded response.
146     * @return array Normalized state with all DEFAULTS keys present.
147     */
148    private static function normalize( array $response ): array {
149        return array(
150            'agents_manager_open'              => (bool) ( $response['agents_manager_open'] ?? self::DEFAULTS['agents_manager_open'] ),
151            'agents_manager_docked'            => (bool) ( $response['agents_manager_docked'] ?? self::DEFAULTS['agents_manager_docked'] ),
152            'agents_manager_minimized'         => (bool) ( $response['agents_manager_minimized'] ?? self::DEFAULTS['agents_manager_minimized'] ),
153            'agents_manager_floating_position' => $response['agents_manager_floating_position'] ?? self::DEFAULTS['agents_manager_floating_position'],
154            'agents_manager_router_history'    => $response['agents_manager_router_history'] ?? self::DEFAULTS['agents_manager_router_history'],
155            'agents_manager_last_activity'     => $response['agents_manager_last_activity'] ?? self::DEFAULTS['agents_manager_last_activity'],
156        );
157    }
158
159    /**
160     * Cache the open/docked bits in a per-user transient.
161     *
162     * Only used on the remote (WoA / self-hosted) path — it's what get_cached()
163     * reads there. Simple sites read `calypso_preferences` directly and skip this.
164     *
165     * @param array $state Normalized state.
166     */
167    private static function cache( array $state ): void {
168        $user_id = get_current_user_id();
169        if ( ! $user_id ) {
170            return;
171        }
172
173        /**
174         * Filter how long the cached open state lives.
175         *
176         * It's refreshed on every read/write through this store, so the TTL mainly
177         * caps how long a value changed elsewhere (e.g. in Calypso) stays stale.
178         *
179         * @since 0.4.0
180         *
181         * @param int $ttl Cache lifetime in seconds.
182         */
183        $ttl = (int) apply_filters( 'agents_manager_open_state_cache_ttl', WEEK_IN_SECONDS );
184
185        set_transient(
186            self::cache_key( $user_id ),
187            array(
188                'agents_manager_open'   => (bool) ( $state['agents_manager_open'] ?? false ),
189                'agents_manager_docked' => (bool) ( $state['agents_manager_docked'] ?? false ),
190            ),
191            $ttl
192        );
193    }
194
195    /**
196     * Build the per-user transient key.
197     *
198     * @param int $user_id User ID.
199     * @return string
200     */
201    private static function cache_key( int $user_id ): string {
202        return self::TRANSIENT_PREFIX . $user_id;
203    }
204}