PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | _inc/lib/admin-pages/class.jetpack-react-page.php +341 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,341 @@
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 +}