summaryrefslogtreecommitdiff
blob: bf2c0c72c94b86fce4bc3b2a4fe543349f080b7d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
 * Build the Jetpack admin menu as a whole.
 *
 * @package automattic/jetpack
 */

use Automattic\Jetpack\Assets\Logo as Jetpack_Logo;
use Automattic\Jetpack\Partner_Coupon as Jetpack_Partner_Coupon;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;

/**
 * Build the Jetpack admin menu as a whole.
 */
class Jetpack_Admin {

	/**
	 * Static instance.
	 *
	 * @var Jetpack_Admin
	 */
	private static $instance = null;

	/**
	 * Initialize and fetch the static instance.
	 *
	 * @return self
	 */
	public static function init() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['page'] ) && 'jetpack' === $_GET['page'] ) {
			add_filter( 'nocache_headers', array( 'Jetpack_Admin', 'add_no_store_header' ), 100 );
		}

		if ( self::$instance === null ) {
			self::$instance = new Jetpack_Admin();
		}
		return self::$instance;
	}

	/**
	 * Filter callback to add `no-store` to the `Cache-Control` header.
	 *
	 * @param array $headers Headers array.
	 * @return array Modified headers array.
	 */
	public static function add_no_store_header( $headers ) {
		$headers['Cache-Control'] .= ', no-store';
		return $headers;
	}

	/** Constructor. */
	private function __construct() {
		jetpack_require_lib( 'admin-pages/class.jetpack-react-page' );
		$this->jetpack_react = new Jetpack_React_Page();

		jetpack_require_lib( 'admin-pages/class.jetpack-settings-page' );
		$this->fallback_page = new Jetpack_Settings_Page();

		jetpack_require_lib( 'admin-pages/class-jetpack-about-page' );
		$this->jetpack_about = new Jetpack_About_Page();

		add_action( 'admin_init', array( $this->jetpack_react, 'react_redirects' ), 0 );
		add_action( 'admin_menu', array( $this->jetpack_react, 'add_actions' ), 998 );
		add_action( 'jetpack_admin_menu', array( $this->jetpack_react, 'jetpack_add_dashboard_sub_nav_item' ) );
		add_action( 'jetpack_admin_menu', array( $this->jetpack_react, 'jetpack_add_settings_sub_nav_item' ) );
		add_action( 'jetpack_admin_menu', array( $this, 'admin_menu_debugger' ) );
		add_action( 'jetpack_admin_menu', array( $this->fallback_page, 'add_actions' ) );
		add_action( 'jetpack_admin_menu', array( $this->jetpack_about, 'add_actions' ) );

		// Add redirect to current page for activation/deactivation of modules.
		add_action( 'jetpack_pre_activate_module', array( $this, 'fix_redirect' ), 10, 2 );
		add_action( 'jetpack_pre_deactivate_module', array( $this, 'fix_redirect' ), 10, 2 );

		// Add module bulk actions handler.
		add_action( 'jetpack_unrecognized_action', array( $this, 'handle_unrecognized_action' ) );

		if ( class_exists( 'Akismet_Admin' ) ) {
			// If the site has Jetpack Anti-Spam, change the Akismet menu label accordingly.
			$site_products      = Jetpack_Plan::get_products();
			$anti_spam_products = array( 'jetpack_anti_spam_monthly', 'jetpack_anti_spam' );
			if ( ! empty( array_intersect( $anti_spam_products, array_column( $site_products, 'product_slug' ) ) ) ) {
				// Prevent Akismet from adding a menu item.
				add_action(
					'admin_menu',
					function () {
						remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
					},
					4
				);

				// Add an Anti-spam menu item for Jetpack.
				add_action(
					'jetpack_admin_menu',
					function () {
						add_submenu_page( 'jetpack', __( 'Anti-Spam', 'jetpack' ), __( 'Anti-Spam', 'jetpack' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
					}
				);
				add_action( 'admin_enqueue_scripts', array( $this, 'akismet_logo_replacement_styles' ) );
			}
		}

		// Ensure an Additional CSS menu item is added to the Appearance menu whenever Jetpack is connected.
		add_action( 'admin_menu', array( $this, 'additional_css_menu' ) );

		add_filter( 'jetpack_display_jitms_on_screen', array( $this, 'should_display_jitms_on_screen' ), 10, 2 );

		// Register Jetpack partner coupon hooks.
		Jetpack_Partner_Coupon::register_coupon_admin_hooks( 'jetpack', Jetpack::admin_url() );
	}

	/**
	 * Generate styles to replace Akismet logo for the Jetpack logo. It's a workaround until we create a proper settings page for
	 * Jetpack Anti-Spam. Without this, we would have to change the logo from Akismet codebase and we want to avoid that.
	 */
	public function akismet_logo_replacement_styles() {
		$logo = new Jetpack_Logo();
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		$logo_base64     = base64_encode( $logo->get_jp_emblem_larger() );
		$logo_base64_url = "data:image/svg+xml;base64,{$logo_base64}";
		$style           = ".akismet-masthead__logo-container { background: url({$logo_base64_url}) no-repeat .25rem; height: 1.8125rem; } .akismet-masthead__logo { display: none; }";
		wp_add_inline_style( 'admin-bar', $style );
	}

	/**
	 * Handle our Additional CSS menu item and legacy page declaration.
	 *
	 * @since 11.0 . Prior to that, this function was located in custom-css-4.7.php.
	 */
	public static function additional_css_menu() {

		// If the site is a WoA site and the custom-css feature is not available, return.
		// See https://github.com/Automattic/jetpack/pull/19965 for more on how this menu item is dealt with on WoA sites.
		if ( ( new Host() )->is_woa_site() && ! ( in_array( 'custom-css', Jetpack::get_available_modules(), true ) ) ) {
			return;
		} elseif ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'custom-css' ) ) { // If the Custom CSS module is enabled, add the Additional CSS menu item and link to the Customizer.
			// Add in our legacy page to support old bookmarks and such.
			add_submenu_page( null, __( 'CSS', 'jetpack' ), __( 'Additional CSS', 'jetpack' ), 'edit_theme_options', 'editcss', array( __CLASS__, 'customizer_redirect' ) );

			// Add in our new page slug that will redirect to the customizer.
			$hook = add_theme_page( __( 'CSS', 'jetpack' ), __( 'Additional CSS', 'jetpack' ), 'edit_theme_options', 'editcss-customizer-redirect', array( __CLASS__, 'customizer_redirect' ) );
			add_action( "load-{$hook}", array( __CLASS__, 'customizer_redirect' ) );
		} else { // Link to the Jetpack Settings > Writing page, highlighting the Custom CSS setting.
			add_submenu_page( null, __( 'CSS', 'jetpack' ), __( 'Additional CSS', 'jetpack' ), 'edit_theme_options', 'editcss', array( __CLASS__, 'theme_enhancements_redirect' ) );

			$hook = add_theme_page( __( 'CSS', 'jetpack' ), __( 'Additional CSS', 'jetpack' ), 'edit_theme_options', 'editcss-theme-enhancements-redirect', array( __CLASS__, 'theme_enhancements_redirect' ) );
			add_action( "load-{$hook}", array( __CLASS__, 'theme_enhancements_redirect' ) );
		}

	}

	/**
	 * Handle the redirect for the customizer.  This is necessary because
	 * we can't directly add customizer links to the admin menu.
	 *
	 * @since 11.0 . Prior to that, this function was located in custom-css-4.7.php.
	 *
	 * There is a core patch in trac that would make this unnecessary.
	 *
	 * @link https://core.trac.wordpress.org/ticket/39050
	 */
	public static function customizer_redirect() {
		wp_safe_redirect(
			self::customizer_link(
				array(
					'return_url' => wp_get_referer(),
				)
			)
		);
		exit;
	}

	/**
	 * Handle the Additional CSS redirect to the Jetpack settings Theme Enhancements section.
	 *
	 * @since 11.0
	 */
	public static function theme_enhancements_redirect() {
		wp_safe_redirect(
			'admin.php?page=jetpack#/writing?term=Custom%20CSS'
		);
		exit;
	}

	/**
	 * Build the URL to deep link to the Customizer.
	 *
	 * You can modify the return url via $args.
	 *
	 * @since 11.0 in this file. This method is also located in custom-css-4.7.php to cover legacy scenarios.
	 *
	 * @param array $args Array of parameters.
	 * @return string
	 */
	public static function customizer_link( $args = array() ) {
		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$args = wp_parse_args(
				$args,
				array(
					'return_url' => rawurlencode( wp_unslash( $_SERVER['REQUEST_URI'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				)
			);
		}

		return add_query_arg(
			array(
				array(
					'autofocus' => array(
						'section' => 'custom_css',
					),
				),
				'return' => $args['return_url'],
			),
			admin_url( 'customize.php' )
		);
	}

	/**
	 * Sort callback to put modules with `requires_connection` last.
	 *
	 * @param array $module1 Module data.
	 * @param array $module2 Module data.
	 * @return int Indicating the relative ordering of module1 and module2.
	 */
	public static function sort_requires_connection_last( $module1, $module2 ) {
		if ( (bool) $module1['requires_connection'] === (bool) $module2['requires_connection'] ) {
			return 0;
		} elseif ( $module1['requires_connection'] ) {
			return 1;
		} elseif ( $module2['requires_connection'] ) {
			return -1;
		}

		return 0;
	}

	/**
	 * Produce JS understandable objects of modules containing information for
	 * presentation like description, name, configuration url, etc.
	 */
	public function get_modules() {
		include_once JETPACK__PLUGIN_DIR . 'modules/module-info.php';
		$available_modules = Jetpack::get_available_modules();
		$active_modules    = Jetpack::get_active_modules();
		$modules           = array();
		$jetpack_active    = Jetpack::is_connection_ready() || ( new Status() )->is_offline_mode();
		$overrides         = Jetpack_Modules_Overrides::instance();
		foreach ( $available_modules as $module ) {
			$module_array = Jetpack::get_module( $module );
			if ( $module_array ) {
				/**
				 * Filters each module's short description.
				 *
				 * @since 3.0.0
				 *
				 * @param string $module_array['description'] Module description.
				 * @param string $module Module slug.
				 */
				$short_desc = apply_filters( 'jetpack_short_module_description', $module_array['description'], $module );
				// Fix: correct multibyte strings truncate with checking for mbstring extension.
				$short_desc_trunc = ( function_exists( 'mb_strlen' ) )
							? ( ( mb_strlen( $short_desc ) > 143 )
								? mb_substr( $short_desc, 0, 140 ) . '...'
								: $short_desc )
							: ( ( strlen( $short_desc ) > 143 )
								? substr( $short_desc, 0, 140 ) . '...'
								: $short_desc );

				$module_array['module'] = $module;

				$is_available = self::is_module_available( $module_array );

				$module_array['activated']          = ( $jetpack_active ? in_array( $module, $active_modules, true ) : false );
				$module_array['deactivate_nonce']   = wp_create_nonce( 'jetpack_deactivate-' . $module );
				$module_array['activate_nonce']     = wp_create_nonce( 'jetpack_activate-' . $module );
				$module_array['available']          = $is_available;
				$module_array['unavailable_reason'] = $is_available ? false : self::get_module_unavailable_reason( $module_array );
				$module_array['short_description']  = $short_desc_trunc;
				$module_array['configure_url']      = Jetpack::module_configuration_url( $module );
				$module_array['override']           = $overrides->get_module_override( $module );

				ob_start();
				/**
				 * Allow the display of a "Learn More" button.
				 * The dynamic part of the action, $module, is the module slug.
				 *
				 * @since 3.0.0
				 */
				do_action( 'jetpack_learn_more_button_' . $module );
				$module_array['learn_more_button'] = ob_get_clean();

				ob_start();
				/**
				 * Allow the display of information text when Jetpack is connected to WordPress.com.
				 * The dynamic part of the action, $module, is the module slug.
				 *
				 * @since 3.0.0
				 */
				do_action( 'jetpack_module_more_info_' . $module );

				/**
				* Filter the long description of a module.
				*
				* @since 3.5.0
				*
				* @param string ob_get_clean() The module long description.
				* @param string $module The module name.
				*/
				$module_array['long_description'] = apply_filters( 'jetpack_long_module_description', ob_get_clean(), $module );

				ob_start();
				/**
				 * Filter the search terms for a module
				 *
				 * Search terms are typically added to the module headers, under "Additional Search Queries".
				 *
				 * Use syntax:
				 * function jetpack_$module_search_terms( $terms ) {
				 *  $terms = _x( 'term 1, term 2', 'search terms', 'jetpack' );
				 *  return $terms;
				 * }
				 * add_filter( 'jetpack_search_terms_$module', 'jetpack_$module_search_terms' );
				 *
				 * @since 3.5.0
				 *
				 * @param string The search terms (comma separated).
				 */
				echo apply_filters( 'jetpack_search_terms_' . $module, $module_array['additional_search_queries'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				$module_array['search_terms'] = ob_get_clean();

				$module_array['configurable'] = false;
				if (
					current_user_can( 'manage_options' ) &&
					/**
					 * Allow the display of a configuration link in the Jetpack Settings screen.
					 *
					 * @since 3.0.0
					 *
					 * @param string $module Module name.
					 * @param bool false Should the Configure module link be displayed? Default to false.
					 */
					apply_filters( 'jetpack_module_configurable_' . $module, false )
				) {
					$module_array['configurable'] = sprintf( '<a href="%1$s">%2$s</a>', esc_url( $module_array['configure_url'] ), __( 'Configure', 'jetpack' ) );
				}

				$modules[ $module ] = $module_array;
			}
		}

		uasort( $modules, array( 'Jetpack', 'sort_modules' ) );

		if ( ! Jetpack::is_connection_ready() ) {
			uasort( $modules, array( __CLASS__, 'sort_requires_connection_last' ) );
		}

		return $modules;
	}

	/**
	 * Check if a module is available.
	 *
	 * @param array $module Module data.
	 */
	public static function is_module_available( $module ) {
		if ( ! is_array( $module ) || empty( $module ) ) {
			return false;
		}

		/**
		 * We never want to show VaultPress as activatable through Jetpack.
		 */
		if ( 'vaultpress' === $module['module'] ) {
			return false;
		}

		/*
		 * WooCommerce Analytics should only be available
		 * when running WooCommerce 3+
		 */
		if (
			'woocommerce-analytics' === $module['module']
			&& (
				! class_exists( 'WooCommerce' )
				|| version_compare( WC_VERSION, '3.0', '<' )
			)
		) {
			return false;
		}

		/*
		 * In Offline mode, modules that require a site or user
		 * level connection should be unavailable.
		 */
		if ( ( new Status() )->is_offline_mode() ) {
			return ! ( $module['requires_connection'] || $module['requires_user_connection'] );
		}

		/*
		 * Jetpack not connected.
		 */
		if ( ! Jetpack::is_connection_ready() ) {
			return false;
		}

		/*
		 * Jetpack connected at a site level only. Make sure to make
		 * modules that require a user connection unavailable.
		 */
		if ( ! Jetpack::connection()->has_connected_owner() && $module['requires_user_connection'] ) {
			return false;
		}

		return Jetpack_Plan::supports( $module['module'] );

	}

	/**
	 * Returns why a module is unavailable.
	 *
	 * @param  array $module The module.
	 * @return string|false A string stating why the module is not available or false if the module is available.
	 */
	public static function get_module_unavailable_reason( $module ) {
		if ( ! is_array( $module ) || empty( $module ) ) {
			return false;
		}

		if ( self::is_module_available( $module ) ) {
			return false;
		}

		/**
		 * We never want to show VaultPress as activatable through Jetpack so return an empty string.
		 */
		if ( 'vaultpress' === $module['module'] ) {
			return '';
		}

		/*
		 * WooCommerce Analytics should only be available
		 * when running WooCommerce 3+
		 */
		if (
			'woocommerce-analytics' === $module['module']
			&& (
					! class_exists( 'WooCommerce' )
					|| version_compare( WC_VERSION, '3.0', '<' )
				)
			) {
			return __( 'Requires WooCommerce 3+ plugin', 'jetpack' );
		}

		/*
		 * In Offline mode, modules that require a site or user
		 * level connection should be unavailable.
		 */
		if ( ( new Status() )->is_offline_mode() ) {
			if ( $module['requires_connection'] || $module['requires_user_connection'] ) {
				return __( 'Offline mode', 'jetpack' );
			}
		}

		/*
		 * Jetpack not connected.
		 */
		if ( ! Jetpack::is_connection_ready() ) {
			return __( 'Jetpack is not connected', 'jetpack' );
		}

		/*
		 * Jetpack connected at a site level only and module requires a user connection.
		 */
		if ( ! Jetpack::connection()->has_connected_owner() && $module['requires_user_connection'] ) {
			return __( 'Requires a connected WordPress.com account', 'jetpack' );
		}

		/*
		 * Plan restrictions.
		 */
		if ( ! Jetpack_Plan::supports( $module['module'] ) ) {
			return __( 'Not supported by current plan', 'jetpack' );
		}

		return '';
	}

	/**
	 * Handle an unrecognized action.
	 *
	 * @param string $action Action.
	 */
	public function handle_unrecognized_action( $action ) {
		switch ( $action ) {
			case 'bulk-activate':
				check_admin_referer( 'bulk-jetpack_page_jetpack_modules' );
				if ( ! current_user_can( 'jetpack_activate_modules' ) ) {
					break;
				}

				$modules = isset( $_GET['modules'] ) ? array_map( 'sanitize_key', wp_unslash( (array) $_GET['modules'] ) ) : array();
				foreach ( $modules as $module ) {
					Jetpack::log( 'activate', $module );
					Jetpack::activate_module( $module, false );
				}
				// The following two lines will rarely happen, as Jetpack::activate_module normally exits at the end.
				wp_safe_redirect( wp_get_referer() );
				exit;
			case 'bulk-deactivate':
				check_admin_referer( 'bulk-jetpack_page_jetpack_modules' );
				if ( ! current_user_can( 'jetpack_deactivate_modules' ) ) {
					break;
				}

				$modules = isset( $_GET['modules'] ) ? array_map( 'sanitize_key', wp_unslash( (array) $_GET['modules'] ) ) : array();
				foreach ( $modules as $module ) {
					Jetpack::log( 'deactivate', $module );
					Jetpack::deactivate_module( $module );
					Jetpack::state( 'message', 'module_deactivated' );
				}
				Jetpack::state( 'module', $modules );
				wp_safe_redirect( wp_get_referer() );
				exit;
			default:
				return;
		}
	}

	/**
	 * Fix redirect.
	 *
	 * Apparently we redirect to the referrer instead of whatever WordPress
	 * wants to redirect to when activating and deactivating modules.
	 *
	 * @param string $module Module slug.
	 * @param bool   $redirect Should we exit after the module has been activated. Default to true.
	 */
	public function fix_redirect( $module, $redirect = true ) {
		if ( ! $redirect ) {
			return;
		}
		if ( wp_get_referer() ) {
			add_filter( 'wp_redirect', 'wp_get_referer' );
		}
	}

	/**
	 * Add debugger admin menu.
	 */
	public function admin_menu_debugger() {
		jetpack_require_lib( 'debugger' );
		Jetpack_Debugger::disconnect_and_redirect();
		$debugger_hook = add_submenu_page(
			null,
			__( 'Debugging Center', 'jetpack' ),
			'',
			'manage_options',
			'jetpack-debugger',
			array( $this, 'wrap_debugger_page' )
		);
		add_action( "admin_head-$debugger_hook", array( 'Jetpack_Debugger', 'jetpack_debug_admin_head' ) );
	}

	/**
	 * Wrap debugger page.
	 */
	public function wrap_debugger_page() {
		nocache_headers();
		if ( ! current_user_can( 'manage_options' ) ) {
			die( '-1' );
		}
		Jetpack_Admin_Page::wrap_ui( array( $this, 'debugger_page' ) );
	}

	/**
	 * Display debugger page.
	 */
	public function debugger_page() {
		jetpack_require_lib( 'debugger' );
		Jetpack_Debugger::jetpack_debug_display_handler();
	}

	/**
	 * Determines if JITMs should display on a particular screen.
	 *
	 * @param bool   $value The default value of the filter.
	 * @param string $screen_id The ID of the screen being tested for JITM display.
	 *
	 * @return bool True if JITMs should display, false otherwise.
	 */
	public function should_display_jitms_on_screen( $value, $screen_id ) {
		// Disable all JITMs on these pages.
		if (
		in_array(
			$screen_id,
			array(
				'jetpack_page_akismet-key-config',
				'admin_page_jetpack_modules',
			),
			true
		) ) {
			return false;
		}

		// Disable all JITMs on pages where the recommendations banner is displaying.
		if (
			in_array(
				$screen_id,
				array(
					'dashboard',
					'plugins',
					'jetpack_page_stats',
				),
				true
			)
			&& \Jetpack_Recommendations_Banner::can_be_displayed()
		) {
			return false;
		}

		return $value;
	}
}
Jetpack_Admin::init();