Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.44% covered (warning)
69.44%
75 / 108
46.15% covered (danger)
46.15%
6 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_React_Page
69.16% covered (warning)
69.16%
74 / 107
46.15% covered (danger)
46.15%
6 / 13
90.31
0.00% covered (danger)
0.00%
0 / 1
 get_page_hook
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 add_page_actions
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
8.83
 remove_jetpack_menu
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 get_legacy_route_redirects
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
6
 should_redirect_legacy_routes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 print_legacy_route_redirect
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 render_redirect_document
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 print_redirect_document
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 can_use_my_jetpack
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 get_partner_coupon_redirect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 jetpack_add_settings_sub_nav_item
n/a
0 / 0
n/a
0 / 0
1
 page_render
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 react_redirects
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 page_admin_scripts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3use Automattic\Jetpack\Assets\Logo;
4use Automattic\Jetpack\Redirect;
5use Automattic\Jetpack\Status;
6
7require_once __DIR__ . '/class.jetpack-admin-page.php';
8
9/**
10 * Registers the Jetpack menu parent, whose page only redirects.
11 */
12class Jetpack_React_Page extends Jetpack_Admin_Page {
13    /**
14     * Register the menu parent before the site connects too.
15     *
16     * @var bool
17     */
18    protected $dont_show_if_not_active = false;
19
20    /**
21     * Legacy hashes the Settings page renders; they forward there with their hash.
22     *
23     * Mirrors `settingsRoutes` in `_inc/client/main.jsx`, plus the connection screens.
24     *
25     * @since 16.3
26     * @var string[]
27     */
28    const SETTINGS_ROUTES = array(
29        '/settings',
30        '/security',
31        '/performance',
32        '/writing',
33        '/sharing',
34        '/discussion',
35        '/earn',
36        '/reader',
37        '/traffic',
38        '/privacy',
39        '/setup',
40        '/connect-user',
41        '/connect-user-setup',
42    );
43
44    /**
45     * Forwards Settings hashes with their hash, maps known routes, and falls back for the rest.
46     *
47     * @var string
48     */
49    const LEGACY_ROUTE_REDIRECT_SCRIPT = <<<'JS'
50function ( settings, forward, routes, fallback ) {
51    var hash = window.location.hash;
52    var path = hash.replace( /^#\/?/, '/' ).split( '?' )[ 0 ] || '/';
53    var target = fallback;
54    if ( forward.indexOf( path ) !== -1 ) {
55        target = settings + hash;
56    } else if ( Object.prototype.hasOwnProperty.call( routes, path ) ) {
57        target = routes[ path ];
58    }
59    window.location.replace( target );
60}
61JS;
62
63    /**
64     * Add the main admin Jetpack menu.
65     *
66     * @return string|false Return value from WordPress's `add_menu_page()`.
67     */
68    public function get_page_hook() {
69        $logo = new Logo();
70        // Keep this fallback in sync with Jetpack_Network::add_network_admin_menu().
71        $icon = method_exists( $logo, 'get_base64_admin_menu_logo' ) ? $logo->get_base64_admin_menu_logo() : $logo->get_base64_logo();
72        return add_menu_page( 'Jetpack', 'Jetpack', 'jetpack_admin_page', 'jetpack', array( $this, 'render' ), $icon, 3 );
73    }
74
75    /**
76     * Add page action.
77     *
78     * @param string $hook Hook of current page.
79     * @return void
80     */
81    public function add_page_actions( $hook ) {
82        /** This action is documented in class.jetpack-admin.php */
83        do_action( 'jetpack_admin_menu', $hook );
84
85        if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
86            return;
87        }
88        $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
89        if ( 'jetpack' !== $page ) {
90            if ( strpos( $page, 'jetpack/' ) === 0 ) {
91                $section = substr( $page, 8 );
92                wp_safe_redirect( admin_url( 'admin.php?page=jetpack#/' . $section ) );
93                exit( 0 );
94            }
95            return; // No need to handle the fallback redirection if we are not on the Jetpack page.
96        }
97
98        // After the action handlers and the connection controller, which exit when they act.
99        add_action( "load-$hook", array( $this, 'render_redirect_document' ), PHP_INT_MAX );
100
101        // If this is the first time the user is viewing the admin, don't show JITMs.
102        // This filter is added just in time because this function is called on admin_menu
103        // and JITMs are initialized on admin_init.
104        if ( Jetpack::is_connection_ready() && ! Jetpack_Options::get_option( 'first_admin_view', false ) ) {
105            Jetpack_Options::update_option( 'first_admin_view', true );
106            add_filter( 'jetpack_just_in_time_msgs', '__return_false' );
107        }
108    }
109
110    /**
111     * Remove the main Jetpack submenu if a site is in offline mode or connected
112     * or if My Jetpack is available.
113     * At that point, admins can access the Jetpack Dashboard instead.
114     *
115     * @since 13.8
116     */
117    public function remove_jetpack_menu() {
118        $is_offline_mode = ( new Status() )->is_offline_mode();
119        $has_my_jetpack  = (
120            class_exists( 'Automattic\Jetpack\My_Jetpack\Initializer' ) &&
121            method_exists( 'Automattic\Jetpack\My_Jetpack\Initializer', 'should_initialize' ) &&
122            \Automattic\Jetpack\My_Jetpack\Initializer::should_initialize()
123        );
124
125        if ( $is_offline_mode || $has_my_jetpack || Jetpack::is_connection_ready() ) {
126            remove_submenu_page( 'jetpack', 'jetpack' );
127        }
128    }
129
130    /**
131     * Where links into page=jetpack land.
132     *
133     * Settings hashes keep their hash on the Settings page. Admins go to My Jetpack
134     * wherever it runs; everyone else, and a request with a pending error, lands on
135     * Settings. While the partner coupon screen applies, every route goes there.
136     *
137     * @return array{settings: string, forward: string[], routes: array<string, string>, fallback: string}
138     */
139    public static function get_legacy_route_redirects() {
140        $settings_url  = admin_url( 'admin.php?page=jetpack-settings' );
141        $coupon_screen = self::get_partner_coupon_redirect();
142        if ( $coupon_screen ) {
143            return array(
144                'settings' => $settings_url,
145                'forward'  => array(),
146                'routes'   => array(),
147                'fallback' => $coupon_screen,
148            );
149        }
150
151        $table = array(
152            'settings' => $settings_url,
153            'forward'  => self::SETTINGS_ROUTES,
154            'routes'   => array(),
155            'fallback' => $settings_url,
156        );
157
158        if ( ! self::should_redirect_legacy_routes() ) {
159            return $table;
160        }
161
162        $pricing_url     = Redirect::get_url( 'jetpack-plans' );
163        $table['routes'] = array(
164            '/plans'        => $pricing_url,
165            '/plans-prompt' => $pricing_url,
166            '/newsletter'   => admin_url( 'admin.php?page=jetpack-newsletter' ),
167        );
168
169        if ( self::can_use_my_jetpack() ) {
170            $my_jetpack = admin_url( 'admin.php?page=my-jetpack' );
171
172            foreach ( array( 'akismet', 'backup', 'scan', 'search', 'security', 'videopress' ) as $product ) {
173                $table['routes'][ '/product/' . $product ] = $my_jetpack . '#/add-' . $product;
174            }
175
176            $table['routes']['/license/activation'] = $my_jetpack . '#/add-license';
177
178            foreach ( array( '/reconnect', '/disconnect', '/woo-setup' ) as $route ) {
179                $table['routes'][ $route ] = $my_jetpack . '#/connection';
180            }
181
182            $table['fallback'] = $my_jetpack;
183        }
184
185        return $table;
186    }
187
188    /**
189     * Whether this request may leave Settings.
190     *
191     * @return bool
192     */
193    public static function should_redirect_legacy_routes() {
194        // A pending error only renders via the Settings app's state notices.
195        return ! Jetpack::state( 'error' );
196    }
197
198    /**
199     * Print the legacy route redirect; it runs in the browser because the server never sees the hash.
200     */
201    public function print_legacy_route_redirect() {
202        $table = self::get_legacy_route_redirects();
203        $flags = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP;
204
205        wp_print_inline_script_tag(
206            sprintf(
207                '( %s )( %s, %s, %s, %s );',
208                self::LEGACY_ROUTE_REDIRECT_SCRIPT,
209                wp_json_encode( $table['settings'], $flags ),
210                wp_json_encode( $table['forward'], $flags ),
211                wp_json_encode( (object) $table['routes'], $flags ),
212                wp_json_encode( $table['fallback'], $flags )
213            )
214        );
215    }
216
217    /**
218     * Replace page=jetpack with the redirect document; nothing else renders here.
219     *
220     * @since 16.3
221     *
222     * @return never
223     */
224    public function render_redirect_document() {
225        $table = self::get_legacy_route_redirects();
226        if ( $table['settings'] === $table['fallback'] ) {
227            // Settings renders this request's notices, so its state must survive the hop.
228            Jetpack::restate();
229        }
230
231        $this->print_redirect_document();
232        exit( 0 );
233    }
234
235    /**
236     * Print a bare document that only redirects.
237     *
238     * @since 16.3
239     */
240    public function print_redirect_document() {
241        ?>
242<!DOCTYPE html>
243<html <?php language_attributes(); ?>>
244<head>
245<meta charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>">
246<title>Jetpack</title><?php // "Jetpack" is a product name, do not translate. ?>
247        <?php
248        if ( $this->is_rest_api_enabled() ) {
249            $this->print_legacy_route_redirect();
250            $this->add_noscript_head_meta();
251        } else {
252            $this->add_fallback_head_meta();
253        }
254        ?>
255</head>
256<body></body>
257</html>
258        <?php
259    }
260
261    /**
262     * Whether My Jetpack can take over for the current user.
263     *
264     * @return bool
265     */
266    private static function can_use_my_jetpack() {
267        return current_user_can( 'manage_options' )
268            && class_exists( 'Automattic\Jetpack\My_Jetpack\Initializer' )
269            && method_exists( 'Automattic\Jetpack\My_Jetpack\Initializer', 'should_initialize' )
270            && \Automattic\Jetpack\My_Jetpack\Initializer::should_initialize();
271    }
272
273    /**
274     * The My Jetpack coupon screen, while it should replace this page.
275     *
276     * An older My Jetpack bounces showCouponRedemption back here, so only forward to one that renders it.
277     *
278     * @return string|null
279     */
280    private static function get_partner_coupon_redirect() {
281        if (
282            ! class_exists( 'Automattic\Jetpack\My_Jetpack\Initializer' )
283            || ! method_exists( 'Automattic\Jetpack\My_Jetpack\Initializer', 'get_partner_coupon_screen' )
284            || null === \Automattic\Jetpack\My_Jetpack\Initializer::get_partner_coupon_screen()
285        ) {
286            return null;
287        }
288
289        return admin_url( 'admin.php?page=my-jetpack&showCouponRedemption=1' );
290    }
291
292    /**
293     * Formerly added the Settings sub-link.
294     *
295     * @since 4.3.0
296     * @deprecated 16.3 Jetpack_Settings_React_Page registers the Settings page.
297     */
298    public function jetpack_add_settings_sub_nav_item() {
299        _deprecated_function( __METHOD__, 'jetpack-16.3' );
300    }
301
302    /**
303     * Nothing renders here: render_redirect_document() exits on load.
304     *
305     * @return void
306     */
307    public function page_render() {}
308    /**
309     * Allow robust deep links to React.
310     *
311     * The Jetpack dashboard requires fragments/hash values to make
312     * a deep link to it but passing fragments as part of a return URL
313     * will most often be discarded throughout the process.
314     * This logic aims to bridge this gap and reduce the chance of React
315     * specific links being broken while passing them along.
316     */
317    public function react_redirects() {
318        global $pagenow;
319
320        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
321        if ( 'admin.php' !== $pagenow || ! isset( $_GET['jp-react-redirect'] ) ) {
322            return;
323        }
324
325        $allowed_paths = array(
326            'product-purchased' => admin_url( 'admin.php?page=jetpack' ),
327        );
328
329        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
330        $target = sanitize_text_field( wp_unslash( $_GET['jp-react-redirect'] ) );
331        if ( isset( $allowed_paths[ $target ] ) ) {
332            wp_safe_redirect( $allowed_paths[ $target ] );
333            exit( 0 );
334        }
335    }
336
337    /**
338     * Nothing loads here: render_redirect_document() exits on load.
339     */
340    public function page_admin_scripts() {}
341}