Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
30 / 40
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Surface_Visibility
75.00% covered (warning)
75.00%
30 / 40
60.00% covered (warning)
60.00%
3 / 5
11.56
0.00% covered (danger)
0.00%
0 / 1
 is_visible
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 is_optin_available
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 inject_optin_availability
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 register_optin_route
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
1
 handle_optin
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * The discoverability cohort for the Jetpack SEO surface, and the opt-in
4 * that flips it: whether the admin menu registers on this site, and the
5 * REST route existing self-hosted installs use to switch over.
6 *
7 * @package automattic/jetpack-seo-package
8 */
9
10namespace Automattic\Jetpack\SEO;
11
12use Automattic\Jetpack\Modules;
13use Automattic\Jetpack\Status\Host;
14
15/**
16 * Decides whether the SEO surface is discoverable and handles the opt-in.
17 */
18class Surface_Visibility {
19
20    /**
21     * Whether the Jetpack SEO surface should be discoverable (admin menu registered).
22     *
23     * WordPress.com sites (Simple + Atomic) are always discoverable — how SEO presents
24     * there is a Dotcom decision, independent of the self-hosted rollout. On self-hosted
25     * sites the durable {@see Initializer::VISIBILITY_OPTION} cohort flag decides: fresh installs
26     * are seeded visible, existing installs stay hidden until they opt in. Defaults to
27     * hidden when the option is absent (e.g. before the plugin's seed has run), so an
28     * existing site is never surprised by the new surface before its cohort is recorded.
29     *
30     * @return bool
31     */
32    public static function is_visible() {
33        if ( class_exists( 'Automattic\\Jetpack\\Status\\Host' ) && ( new Host() )->is_wpcom_platform() ) {
34            return true;
35        }
36
37        return (bool) get_option( Initializer::VISIBILITY_OPTION, false );
38    }
39
40    /**
41     * Whether to offer an existing install the chance to opt into the new SEO experience.
42     *
43     * The single source of truth for the opt-in surfaces (legacy Traffic-page banner, My
44     * Jetpack card). True only when the SEO product is available and the surface isn't
45     * visible yet — and since {@see self::is_visible()} already returns true for
46     * WordPress.com and for self-hosted installs that have opted in, "not visible"
47     * cleanly means "a self-hosted install that hasn't opted in".
48     *
49     * @return bool
50     */
51    public static function is_optin_available() {
52        return Initializer::is_available() && ! self::is_visible();
53    }
54
55    /**
56     * Expose whether this install should be offered the SEO opt-in, onto
57     * `window.JetpackScriptData.seo.optin_available` for other admin surfaces (e.g. the
58     * legacy Traffic-page banner). Only hooked when the feature flag is on, so the field is
59     * simply absent otherwise.
60     *
61     * @param array $data Script data being injected onto the page.
62     * @return array
63     */
64    public static function inject_optin_availability( $data ) {
65        if ( ! is_array( $data ) ) {
66            $data = array();
67        }
68
69        $data[ Initializer::SCRIPT_DATA_KEY ]['optin_available'] = self::is_optin_available();
70        // Read by the legacy Traffic page to hide its SEO / Sitemaps sections once the
71        // site is on the new experience (fresh install / opted-in / WordPress.com), so the
72        // two surfaces never show at once. The legacy sections stay for self-hosted installs
73        // that haven't opted in.
74        $data[ Initializer::SCRIPT_DATA_KEY ]['surface_visible'] = self::is_visible();
75
76        return $data;
77    }
78
79    /**
80     * Register the REST route for checking availability and opting into the SEO dashboard.
81     *
82     * Lives on the `jetpack/v4` namespace and is registered ahead of the cohort gate, so a
83     * site whose SEO surface is still hidden can reach it from the legacy Traffic page or
84     * My Jetpack. See {@see self::handle_optin()}.
85     *
86     * @return void
87     */
88    public static function register_optin_route() {
89        register_rest_route(
90            'jetpack/v4',
91            '/seo/opt-in',
92            array(
93                array(
94                    'methods'             => \WP_REST_Server::READABLE,
95                    'callback'            => function () {
96                        return rest_ensure_response( array( 'optin_available' => self::is_optin_available() ) );
97                    },
98                    'permission_callback' => function () {
99                        return current_user_can( 'manage_options' );
100                    },
101                ),
102                array(
103                    'methods'             => \WP_REST_Server::CREATABLE,
104                    'callback'            => array( __CLASS__, 'handle_optin' ),
105                    'permission_callback' => function () {
106                        return current_user_can( 'manage_options' );
107                    },
108                ),
109            )
110        );
111    }
112
113    /**
114     * Opt an existing install into the new SEO experience: mark the surface visible and
115     * activate the `seo-tools` module, then hand back the dashboard URL to redirect to.
116     *
117     * Idempotent — re-opting-in is harmless. `Modules::activate()` is called with
118     * `$exit = false, $redirect = false`; the defaults would `exit()` and send a 302,
119     * which break a REST response.
120     *
121     * @return \WP_REST_Response
122     */
123    public static function handle_optin() {
124        update_option( Initializer::VISIBILITY_OPTION, true );
125
126        if ( class_exists( 'Automattic\\Jetpack\\Modules' ) ) {
127            ( new Modules() )->activate( 'seo-tools', false, false );
128        }
129
130        return rest_ensure_response(
131            array(
132                'success'  => true,
133                'redirect' => Admin_Page::get_url(),
134            )
135        );
136    }
137}