Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
69.44% |
75 / 108 |
|
46.15% |
6 / 13 |
CRAP | |
0.00% |
0 / 1 |
| Jetpack_React_Page | |
69.16% |
74 / 107 |
|
46.15% |
6 / 13 |
90.31 | |
0.00% |
0 / 1 |
| get_page_hook | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| add_page_actions | |
57.14% |
8 / 14 |
|
0.00% |
0 / 1 |
8.83 | |||
| remove_jetpack_menu | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
| get_legacy_route_redirects | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
6 | |||
| should_redirect_legacy_routes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| print_legacy_route_redirect | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
| render_redirect_document | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| print_redirect_document | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
| can_use_my_jetpack | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| get_partner_coupon_redirect | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| jetpack_add_settings_sub_nav_item | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
| page_render | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| react_redirects | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| page_admin_scripts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName |
| 2 | |
| 3 | use Automattic\Jetpack\Assets\Logo; |
| 4 | use Automattic\Jetpack\Redirect; |
| 5 | use Automattic\Jetpack\Status; |
| 6 | |
| 7 | require_once __DIR__ . '/class.jetpack-admin-page.php'; |
| 8 | |
| 9 | /** |
| 10 | * Registers the Jetpack menu parent, whose page only redirects. |
| 11 | */ |
| 12 | class 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' |
| 50 | function ( 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 | } |
| 61 | JS; |
| 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 | } |