Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.03% covered (danger)
31.03%
9 / 29
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Surface_Visibility
31.03% covered (danger)
31.03%
9 / 29
60.00% covered (warning)
60.00%
3 / 5
42.80
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
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 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 (the {@see Initializer::FEATURE_FILTER}
45     * flag is on) and the surface isn't visible yet — and since {@see self::is_visible()}
46     * already returns true for WordPress.com and for self-hosted installs that have opted in,
47     * "not visible" 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 (bool) apply_filters( Initializer::FEATURE_FILTER, false ) && ! 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 opt-in REST route that switches an existing self-hosted install over to
81     * the new SEO experience.
82     *
83     * Lives on the `jetpack/v4` namespace and is registered ahead of the cohort gate, so a
84     * site whose SEO surface is still hidden can reach it from the legacy Traffic page or
85     * My Jetpack. See {@see self::handle_optin()}.
86     *
87     * @return void
88     */
89    public static function register_optin_route() {
90        register_rest_route(
91            'jetpack/v4',
92            '/seo/opt-in',
93            array(
94                'methods'             => \WP_REST_Server::CREATABLE,
95                'callback'            => array( __CLASS__, 'handle_optin' ),
96                'permission_callback' => function () {
97                    return current_user_can( 'manage_options' );
98                },
99            )
100        );
101    }
102
103    /**
104     * Opt an existing install into the new SEO experience: mark the surface visible and
105     * activate the `seo-tools` module, then hand back the dashboard URL to redirect to.
106     *
107     * Idempotent — re-opting-in is harmless. `Modules::activate()` is called with
108     * `$exit = false, $redirect = false`; the defaults would `exit()` and send a 302,
109     * which break a REST response.
110     *
111     * @return \WP_REST_Response
112     */
113    public static function handle_optin() {
114        update_option( Initializer::VISIBILITY_OPTION, true );
115
116        if ( class_exists( 'Automattic\\Jetpack\\Modules' ) ) {
117            ( new Modules() )->activate( 'seo-tools', false, false );
118        }
119
120        return rest_ensure_response(
121            array(
122                'success'  => true,
123                'redirect' => admin_url( 'admin.php?page=' . Admin_Page::MENU_SLUG ),
124            )
125        );
126    }
127}