Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.74% covered (danger)
40.74%
22 / 54
16.67% covered (danger)
16.67%
1 / 6
CRAP
n/a
0 / 0
wpcom_editor_welcome_guides_atomic_is_editor_page_load
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_editor_welcome_guides_atomic_get_cached
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_editor_welcome_guides_atomic_store
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_editor_welcome_guides_atomic_request
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
wpcom_editor_welcome_guides_atomic_get_dismissed
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
wpcom_editor_welcome_guides_atomic_record_dismissed
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
6.01
1<?php
2/**
3 * On Atomic sites, fetches the welcome guides the user has dismissed on WordPress.com, caches
4 * them in user meta, and reports new dismissals back.
5 *
6 * @package automattic/jetpack-mu-wpcom
7 */
8
9declare( strict_types = 1 );
10
11use Automattic\Jetpack\Connection\Client;
12
13/**
14 * User meta key for the cached guides: when they were last fetched and which ones are dismissed.
15 */
16const WPCOM_EDITOR_WELCOME_GUIDES_CACHE_META = 'wpcom_editor_welcome_guides';
17
18/**
19 * How long the copy is served before an editor screen asks WordPress.com again.
20 */
21const WPCOM_EDITOR_WELCOME_GUIDES_CACHE_TTL = 5 * MINUTE_IN_SECONDS;
22
23/**
24 * Whether the page being loaded is an editor screen.
25 * Only editor screens fetch from WordPress.com; every other page uses the cached guides.
26 *
27 * @return bool
28 */
29function wpcom_editor_welcome_guides_atomic_is_editor_page_load(): bool {
30    global $pagenow;
31
32    return in_array( $pagenow, array( 'post.php', 'post-new.php', 'site-editor.php', 'widgets.php', 'customize.php' ), true );
33}
34
35/**
36 * The cached guides for the user, or an empty cache when nothing is stored yet.
37 *
38 * @param int $user_id User ID.
39 * @return array{checked: int, dismissed: array}
40 */
41function wpcom_editor_welcome_guides_atomic_get_cached( int $user_id ): array {
42    $cached = get_user_meta( $user_id, WPCOM_EDITOR_WELCOME_GUIDES_CACHE_META, true );
43
44    return array(
45        'checked'   => (int) ( $cached['checked'] ?? 0 ),
46        'dismissed' => is_array( $cached['dismissed'] ?? null ) ? $cached['dismissed'] : array(),
47    );
48}
49
50/**
51 * Caches the dismissed guides for the user and records the time.
52 *
53 * @param int   $user_id   User ID.
54 * @param array $dismissed Map of preferences scope to dismissed guide keys.
55 * @return void
56 */
57function wpcom_editor_welcome_guides_atomic_store( int $user_id, array $dismissed ): void {
58    update_user_meta(
59        $user_id,
60        WPCOM_EDITOR_WELCOME_GUIDES_CACHE_META,
61        array(
62            'checked'   => time(),
63            'dismissed' => $dismissed,
64        )
65    );
66}
67
68/**
69 * Calls the `wpcom/v2/me/editor-welcome-guides` endpoint as the current user.
70 *
71 * @param string     $method HTTP method.
72 * @param array|null $body   JSON body for a POST.
73 * @return array|null The dismissed guides from the response, or null when the call failed.
74 */
75function wpcom_editor_welcome_guides_atomic_request( string $method, ?array $body = null ): ?array {
76    if ( ! class_exists( Client::class ) ) {
77        return null;
78    }
79
80    $response = Client::wpcom_json_api_request_as_user(
81        '/me/editor-welcome-guides',
82        '2',
83        array(
84            'method'  => $method,
85            'timeout' => 3,
86        ),
87        $body
88    );
89    if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
90        return null;
91    }
92
93    $data = json_decode( wp_remote_retrieve_body( $response ), true );
94
95    return is_array( $data ) && is_array( $data['dismissed'] ?? null ) ? $data['dismissed'] : null;
96}
97
98/**
99 * Supplies the guides the user has dismissed, from the cache, fetching from WordPress.com
100 * first when an editor screen loads for the current user and the cache is missing or stale.
101 * A failed fetch keeps the cache and still records the time, so nothing retries until the TTL passes.
102 *
103 * @param array $dismissed Value passed into the filter, not used: the cached guides replace it.
104 * @param int   $user_id   User ID.
105 * @return array Map of preferences scope to dismissed guide keys.
106 */
107function wpcom_editor_welcome_guides_atomic_get_dismissed( $dismissed, $user_id ): array {
108    $user_id = (int) $user_id;
109    $cached  = wpcom_editor_welcome_guides_atomic_get_cached( $user_id );
110    if (
111        time() - $cached['checked'] <= WPCOM_EDITOR_WELCOME_GUIDES_CACHE_TTL
112        || get_current_user_id() !== $user_id
113        || ! wpcom_editor_welcome_guides_atomic_is_editor_page_load()
114    ) {
115        return $cached['dismissed'];
116    }
117
118    $record = wpcom_editor_welcome_guides_atomic_request( 'GET' ) ?? $cached['dismissed'];
119    wpcom_editor_welcome_guides_atomic_store( $user_id, $record );
120
121    return $record;
122}
123add_filter( 'wpcom_editor_welcome_guides_dismissed', 'wpcom_editor_welcome_guides_atomic_get_dismissed', 10, 2 );
124
125/**
126 * Sends the guides the cache does not know yet to WordPress.com, and caches the full list it returns.
127 *
128 * @param int   $user_id   User ID.
129 * @param array $dismissed Map of preferences scope to guide keys just dismissed.
130 * @return void
131 */
132function wpcom_editor_welcome_guides_atomic_record_dismissed( $user_id, $dismissed ): void {
133    $user_id = (int) $user_id;
134    if ( get_current_user_id() !== $user_id ) {
135        return;
136    }
137
138    $known = wpcom_editor_welcome_guides_atomic_get_cached( $user_id )['dismissed'];
139    $new   = array();
140    foreach ( $dismissed as $scope => $guides ) {
141        $missing = array_values( array_diff( (array) $guides, (array) ( $known[ $scope ] ?? array() ) ) );
142        if ( ! empty( $missing ) ) {
143            $new[ $scope ] = $missing;
144        }
145    }
146    if ( empty( $new ) ) {
147        return;
148    }
149
150    $record = wpcom_editor_welcome_guides_atomic_request( 'POST', array( 'dismissed' => $new ) );
151    if ( null !== $record ) {
152        wpcom_editor_welcome_guides_atomic_store( $user_id, $record );
153    }
154}
155add_action( 'wpcom_editor_welcome_guides_dismiss', 'wpcom_editor_welcome_guides_atomic_record_dismissed', 10, 2 );