Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.00% covered (success)
96.00%
24 / 25
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Enablement_Setting
96.00% covered (success)
96.00%
24 / 25
66.67% covered (warning)
66.67%
2 / 3
8
0.00% covered (danger)
0.00%
0 / 1
 register
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 sanitize_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reset_onboarding_on_reactivation
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
1<?php
2/**
3 * Public entry point for hosts that expose the dashboard's opt-in setting.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics;
9
10/**
11 * Exposes the site's dashboard opt-in through core's `wp/v2/settings` route.
12 *
13 * This is the one thing the package has to serve while the dashboard is switched off, so hosts
14 * register it outside their enablement checks. Everything else lives behind
15 * {@see Dashboard_Support_Routes}, which only boots once the dashboard is already on.
16 *
17 * Reads and writes both address the stored opt-in, so this route reports the same value as any
18 * other reader of the option. An override such as our rollout sticker turns the dashboard on
19 * without touching the opt-in and deliberately does not surface here: whether the dashboard
20 * actually booted is `analytics.enabled` in script data.
21 *
22 * Writing cannot take effect in the request that writes it: Jetpack resolves the flag once, on
23 * `plugins_loaded`. Clients are expected to reload.
24 *
25 * @since 0.6.0
26 */
27class Enablement_Setting {
28
29    /**
30     * Site option holding the customer's own opt-in to the dashboard.
31     *
32     * Lives here rather than on {@see Analytics} so that registering the setting does not pull the
33     * whole dashboard class in behind it: this runs on REST requests of every kind, including ones
34     * with the dashboard switched off. Jetpack::is_premium_analytics_enabled() spells the name a
35     * third time, since it has to answer before this package is known to be loadable at all.
36     *
37     * @since 0.6.0
38     */
39    const ENABLED_OPTION = 'jetpack_premium_analytics_enabled';
40
41    /**
42     * Preferences scope the dashboard stores its per-user state under. Mirrors
43     * `DASHBOARD_PREFERENCES_SCOPE` in `packages/data/src/defaults/remembered-preset.ts`.
44     *
45     * @since 0.6.0
46     */
47    const PREFERENCES_SCOPE = 'jetpack-premium-analytics/dashboard';
48
49    /**
50     * Preferences key the dashboard sets once a reader has been through the onboarding. Mirrors
51     * `routes/dashboard/hooks/constants.ts`.
52     *
53     * @since 0.6.0
54     */
55    const ONBOARDING_KEY = 'onboardingCompletedAt';
56
57    /**
58     * Declare the setting so core's settings route exposes it.
59     *
60     * Call on `rest_api_init`: core builds the settings route from the registered settings at
61     * priority 99, so anything later would leave the option off the route's write schema. Both
62     * hosts may call this; a repeat call re-declares the same setting and is harmless.
63     *
64     * @since 0.6.0
65     *
66     * @return void
67     */
68    public static function register() {
69        register_setting(
70            'general',
71            self::ENABLED_OPTION,
72            array(
73                'type'              => 'boolean',
74                'default'           => false,
75                'show_in_rest'      => true,
76                'sanitize_callback' => array( __CLASS__, 'sanitize_enabled' ),
77                'description'       => __( 'Whether the Premium Analytics dashboard is enabled for this site.', 'jetpack-premium-analytics-pkg' ),
78            )
79        );
80        add_action( 'update_option_' . self::ENABLED_OPTION, array( __CLASS__, 'reset_onboarding_on_reactivation' ), 10, 2 );
81    }
82
83    /**
84     * Store the opt-in as 0 or 1.
85     *
86     * Without this, switching the dashboard off stores `''`: core's settings controller hands
87     * `update_option()` a bare `false`, which reaches the options table as an empty string. The
88     * REST schema does not accept that as a boolean, so the next read reports `null` rather than
89     * `false`, and a later `null` write answers 500 `rest_invalid_stored_value`.
90     *
91     * @since 0.6.0
92     *
93     * @param mixed $value Value being written.
94     * @return int
95     */
96    public static function sanitize_enabled( $value ) {
97        return (int) rest_sanitize_boolean( $value );
98    }
99
100    /**
101     * Forget that the reader switching the dashboard back on has seen the onboarding.
102     *
103     * Someone who switched off and came back gets the tour again; a first activation, a
104     * switch-off, and everyone else's preference are left alone. Riding on the option write means
105     * only writes made while the setting is registered count, which is every product path: the
106     * classic Stats banner and menu, and the dashboard's own switch-off, all go through REST.
107     *
108     * The preference lives in core's persisted preferences user meta, whose client-side layer
109     * keeps a localStorage copy and takes whichever is newer, so `_modified` moves with the change.
110     *
111     * @since 0.6.0
112     *
113     * @param mixed $old_value Previous option value.
114     * @param mixed $value     New option value.
115     * @return void
116     */
117    public static function reset_onboarding_on_reactivation( $old_value, $value ) {
118        if ( (bool) $old_value || ! (bool) $value ) {
119            return;
120        }
121
122        $user_id = get_current_user_id();
123        if ( ! $user_id ) {
124            return;
125        }
126
127        global $wpdb;
128        $meta_key    = $wpdb->get_blog_prefix() . 'persisted_preferences';
129        $preferences = get_user_meta( $user_id, $meta_key, true );
130
131        if ( ! is_array( $preferences ) || ! isset( $preferences[ self::PREFERENCES_SCOPE ][ self::ONBOARDING_KEY ] ) ) {
132            return;
133        }
134
135        unset( $preferences[ self::PREFERENCES_SCOPE ][ self::ONBOARDING_KEY ] );
136        $preferences['_modified'] = gmdate( 'Y-m-d\TH:i:s\Z' );
137
138        update_user_meta( $user_id, $meta_key, $preferences );
139    }
140}