PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 10.5.3
Jetpack – WP Security, Backup, Speed, & Growth v10.5.3
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / class.jetpack-network.php
class.jetpack-network.php
744 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFilename
2 /**
3 * Jetpack Network Manager class file.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Connection\Manager;
9 use Automattic\Jetpack\Connection\Tokens;
10 use Automattic\Jetpack\Status;
11
12 /**
13 * Used to manage Jetpack installation on Multisite Network installs
14 *
15 * SINGLETON: To use call Jetpack_Network::init()
16 *
17 * DO NOT USE ANY STATIC METHODS IN THIS CLASS!!!!!!
18 *
19 * @since 2.9
20 */
21 class Jetpack_Network {
22
23 /**
24 * Holds a static copy of Jetpack_Network for the singleton
25 *
26 * @since 2.9
27 * @var Jetpack_Network
28 */
29 private static $instance = null;
30
31 /**
32 * An instance of the connection manager object.
33 *
34 * @since 7.7
35 * @var Automattic\Jetpack\Connection\Manager
36 */
37 private $connection;
38
39 /**
40 * Name of the network wide settings
41 *
42 * @since 2.9
43 * @var string
44 */
45 private $settings_name = 'jetpack-network-settings';
46
47 /**
48 * Defaults for settings found on the Jetpack > Settings page
49 *
50 * @since 2.9
51 * @var array
52 */
53 private $setting_defaults = array(
54 'auto-connect' => 0,
55 'sub-site-connection-override' => 1,
56 );
57
58 /**
59 * Constructor
60 *
61 * @since 2.9
62 */
63 private function __construct() {
64 require_once ABSPATH . '/wp-admin/includes/plugin.php'; // For the is_plugin... check.
65 require_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php'; // For managing the global whitelist.
66
67 /**
68 * Sanity check to ensure the install is Multisite and we
69 * are in Network Admin
70 */
71 if ( is_multisite() && is_network_admin() ) {
72 add_action( 'network_admin_menu', array( $this, 'add_network_admin_menu' ) );
73 add_action( 'network_admin_edit_jetpack-network-settings', array( $this, 'save_network_settings_page' ), 10, 0 );
74 add_filter( 'admin_body_class', array( $this, 'body_class' ) );
75
76 if ( isset( $_GET['page'] ) && 'jetpack' == $_GET['page'] ) {
77 add_action( 'admin_init', array( $this, 'jetpack_sites_list' ) );
78 }
79 }
80
81 /*
82 * Things that should only run on multisite
83 */
84 if ( is_multisite() && is_plugin_active_for_network( 'jetpack/jetpack.php' ) ) {
85 add_action( 'wp_before_admin_bar_render', array( $this, 'add_to_menubar' ) );
86 add_filter( 'jetpack_disconnect_cap', array( $this, 'set_multisite_disconnect_cap' ) );
87
88 /*
89 * If admin wants to automagically register new sites set the hook here
90 *
91 * This is a hacky way because xmlrpc is not available on wp_initialize_site
92 */
93 if ( 1 === $this->get_option( 'auto-connect' ) ) {
94 add_action( 'wp_initialize_site', array( $this, 'do_automatically_add_new_site' ) );
95 }
96 }
97 }
98
99 /**
100 * Sets a connection object.
101 *
102 * @param Automattic\Jetpack\Connection\Manager $connection the connection manager object.
103 */
104 public function set_connection( Manager $connection ) {
105 $this->connection = $connection;
106 }
107
108 /**
109 * Registers new sites upon creation
110 *
111 * @since 2.9
112 * @since 7.4.0 Uses a WP_Site object.
113 * @uses wp_initialize_site
114 *
115 * @param WP_Site $site the WordPress site object.
116 **/
117 public function do_automatically_add_new_site( $site ) {
118 if ( is_a( $site, 'WP_Site' ) ) {
119 $this->do_subsiteregister( $site->id );
120 }
121 }
122
123 /**
124 * Adds .network-admin class to the body tag
125 * Helps distinguish network admin JP styles from regular site JP styles
126 *
127 * @since 2.9
128 *
129 * @param String $classes current assigned body classes.
130 * @return String amended class string.
131 */
132 public function body_class( $classes ) {
133 return trim( $classes ) . ' network-admin ';
134 }
135
136 /**
137 * Provides access to an instance of Jetpack_Network
138 *
139 * This is how the Jetpack_Network object should *always* be accessed
140 *
141 * @since 2.9
142 * @return Jetpack_Network
143 */
144 public static function init() {
145 if ( ! self::$instance || ! is_a( self::$instance, 'Jetpack_Network' ) ) {
146 self::$instance = new Jetpack_Network();
147 }
148
149 return self::$instance;
150 }
151
152 /**
153 * Registers the Multisite admin bar menu item shortcut.
154 * This shortcut helps users quickly and easily navigate to the Jetpack Network Admin
155 * menu from anywhere in their network.
156 *
157 * @since 2.9
158 */
159 public function register_menubar() {
160 add_action( 'wp_before_admin_bar_render', array( $this, 'add_to_menubar' ) );
161 }
162
163 /**
164 * Runs when Jetpack is deactivated from the network admin plugins menu.
165 * Each individual site will need to have Jetpack::disconnect called on it.
166 * Site that had Jetpack individually enabled will not be disconnected as
167 * on Multisite individually activated plugins are still activated when
168 * a plugin is deactivated network wide.
169 *
170 * @since 2.9
171 **/
172 public function deactivate() {
173 // Only fire if in network admin.
174 if ( ! is_network_admin() ) {
175 return;
176 }
177
178 $sites = get_sites();
179
180 foreach ( $sites as $s ) {
181 switch_to_blog( $s->blog_id );
182 $active_plugins = get_option( 'active_plugins' );
183
184 /*
185 * If this plugin was activated in the subsite individually
186 * we do not want to call disconnect. Plugins activated
187 * individually (before network activation) stay activated
188 * when the network deactivation occurs
189 */
190 if ( ! in_array( 'jetpack/jetpack.php', $active_plugins, true ) ) {
191 Jetpack::disconnect();
192 Jetpack_Options::delete_option( 'version' );
193 }
194 restore_current_blog();
195 }
196
197 }
198
199 /**
200 * Adds a link to the Jetpack Network Admin page in the network admin menu bar.
201 *
202 * @since 2.9
203 **/
204 public function add_to_menubar() {
205 global $wp_admin_bar;
206 // Don't show for logged out users or single site mode.
207 if ( ! is_user_logged_in() || ! is_multisite() ) {
208 return;
209 }
210
211 $wp_admin_bar->add_node(
212 array(
213 'parent' => 'network-admin',
214 'id' => 'network-admin-jetpack',
215 'title' => 'Jetpack',
216 'href' => $this->get_url( 'network_admin_page' ),
217 )
218 );
219 }
220
221 /**
222 * Returns various URL strings. Factory like
223 *
224 * $args can be a string or an array.
225 * If $args is an array there must be an element called name for the switch statement
226 *
227 * Currently supports:
228 * - subsiteregister: Pass array( 'name' => 'subsiteregister', 'site_id' => SITE_ID )
229 * - network_admin_page: Provides link to /wp-admin/network/JETPACK
230 * - subsitedisconnect: Pass array( 'name' => 'subsitedisconnect', 'site_id' => SITE_ID )
231 *
232 * @since 2.9
233 *
234 * @param Mixed $args URL parameters.
235 *
236 * @return String
237 **/
238 public function get_url( $args ) {
239 $url = null; // Default url value.
240
241 if ( is_string( $args ) ) {
242 $name = $args;
243 } else if ( is_array( $args ) ) {
244 $name = $args['name'];
245 } else {
246 return $url;
247 }
248
249 switch ( $name ) {
250 case 'subsiteregister':
251 if ( ! isset( $args['site_id'] ) ) {
252 break; // If there is not a site id present we cannot go further.
253 }
254 $url = network_admin_url(
255 'admin.php?page=jetpack&action=subsiteregister&site_id='
256 . $args['site_id']
257 );
258 break;
259
260 case 'network_admin_page':
261 $url = network_admin_url( 'admin.php?page=jetpack' );
262 break;
263
264 case 'subsitedisconnect':
265 if ( ! isset( $args['site_id'] ) ) {
266 break; // If there is not a site id present we cannot go further.
267 }
268 $url = network_admin_url(
269 'admin.php?page=jetpack&action=subsitedisconnect&site_id='
270 . $args['site_id']
271 );
272 break;
273 }
274
275 return $url;
276 }
277
278 /**
279 * Adds the Jetpack menu item to the Network Admin area
280 *
281 * @since 2.9
282 */
283 public function add_network_admin_menu() {
284 add_menu_page( 'Jetpack', 'Jetpack', 'jetpack_network_admin_page', 'jetpack', array( $this, 'wrap_network_admin_page' ), 'div', 3 );
285 $jetpack_sites_page_hook = add_submenu_page( 'jetpack', __( 'Jetpack Sites', 'jetpack' ), __( 'Sites', 'jetpack' ), 'jetpack_network_sites_page', 'jetpack', array( $this, 'wrap_network_admin_page' ) );
286 $jetpack_settings_page_hook = add_submenu_page( 'jetpack', __( 'Settings', 'jetpack' ), __( 'Settings', 'jetpack' ), 'jetpack_network_settings_page', 'jetpack-settings', array( $this, 'wrap_render_network_admin_settings_page' ) );
287 add_action( "admin_print_styles-$jetpack_sites_page_hook", array( 'Jetpack_Admin_Page', 'load_wrapper_styles' ) );
288 add_action( "admin_print_styles-$jetpack_settings_page_hook", array( 'Jetpack_Admin_Page', 'load_wrapper_styles' ) );
289 /**
290 * As jetpack_register_genericons is by default fired off a hook,
291 * the hook may have already fired by this point.
292 * So, let's just trigger it manually.
293 */
294 require_once JETPACK__PLUGIN_DIR . '_inc/genericons.php';
295 jetpack_register_genericons();
296
297 if ( ! wp_style_is( 'jetpack-icons', 'registered' ) ) {
298 wp_register_style( 'jetpack-icons', plugins_url( 'css/jetpack-icons.min.css', JETPACK__PLUGIN_FILE ), false, JETPACK__VERSION );
299 }
300
301 add_action( 'admin_enqueue_scripts', array( $this, 'admin_menu_css' ) );
302 }
303
304 /**
305 * Adds JP menu icon
306 *
307 * @since 2.9
308 **/
309 public function admin_menu_css() {
310 wp_enqueue_style( 'jetpack-icons' );
311 }
312
313 /**
314 * Provides functionality for the Jetpack > Sites page.
315 * Does not do the display!
316 *
317 * @since 2.9
318 */
319 public function jetpack_sites_list() {
320 Jetpack::init();
321
322 if ( isset( $_GET['action'] ) ) {
323 switch ( $_GET['action'] ) {
324 case 'subsiteregister':
325 /**
326 * Add actual referrer checking.
327 *
328 * @todo check_admin_referer( 'jetpack-subsite-register' );
329 */
330 Jetpack::log( 'subsiteregister' );
331
332 // If !$_GET['site_id'] stop registration and error.
333 if ( ! isset( $_GET['site_id'] ) || empty( $_GET['site_id'] ) ) {
334 /**
335 * Log error to state cookie for display later.
336 *
337 * @todo Make state messages show on Jetpack NA pages
338 */
339 Jetpack::state( 'missing_site_id', esc_html__( 'Site ID must be provided to register a sub-site.', 'jetpack' ) );
340 break;
341 }
342
343 // Send data to register endpoint and retrieve shadow blog details.
344 $result = $this->do_subsiteregister();
345 $url = $this->get_url( 'network_admin_page' );
346
347 if ( is_wp_error( $result ) ) {
348 $url = add_query_arg( 'action', 'connection_failed', $url );
349 } else {
350 $url = add_query_arg( 'action', 'connected', $url );
351 }
352
353 wp_safe_redirect( $url );
354 exit;
355
356 case 'subsitedisconnect':
357 Jetpack::log( 'subsitedisconnect' );
358
359 if ( ! isset( $_GET['site_id'] ) || empty( $_GET['site_id'] ) ) {
360 Jetpack::state( 'missing_site_id', esc_html__( 'Site ID must be provided to disconnect a sub-site.', 'jetpack' ) );
361 break;
362 }
363
364 $this->do_subsitedisconnect();
365 break;
366
367 case 'connected':
368 case 'connection_failed':
369 add_action( 'jetpack_notices', array( $this, 'show_jetpack_notice' ) );
370 break;
371 }
372 }
373 }
374
375 /**
376 * Set the disconnect capability for multisite.
377 *
378 * @param array $caps The capabilities array.
379 */
380 public function set_multisite_disconnect_cap( $caps ) {
381 // Can individual site admins manage their own connection?
382 if ( ! is_super_admin() && ! $this->get_option( 'sub-site-connection-override' ) ) {
383 /*
384 * We need to update the option name -- it's terribly unclear which
385 * direction the override goes.
386 *
387 * @todo: Update the option name to `sub-sites-can-manage-own-connections`
388 */
389 return array( 'do_not_allow' );
390 }
391
392 return $caps;
393 }
394
395 /**
396 * Shows the Jetpack plugin notices.
397 */
398 public function show_jetpack_notice() {
399 if ( isset( $_GET['action'] ) && 'connected' == $_GET['action'] ) {
400 $notice = __( 'Site successfully connected.', 'jetpack' );
401 $classname = 'updated';
402 } elseif ( isset( $_GET['action'] ) && 'connection_failed' == $_GET['action'] ) {
403 $notice = __( 'Site connection failed!', 'jetpack' );
404 $classname = 'error';
405 }
406 ?>
407 <div id="message" class="<?php echo esc_attr( $classname ); ?> jetpack-message jp-connect" style="display:block !important;">
408 <p><?php echo esc_html( $notice ); ?></p>
409 </div>
410 <?php
411 }
412
413 /**
414 * Disconnect functionality for an individual site
415 *
416 * @since 2.9
417 * @see Jetpack_Network::jetpack_sites_list()
418 *
419 * @param int $site_id the site identifier.
420 */
421 public function do_subsitedisconnect( $site_id = null ) {
422 if ( ! current_user_can( 'jetpack_disconnect' ) ) {
423 return;
424 }
425 $site_id = ( is_null( $site_id ) ) ? $_GET['site_id'] : $site_id;
426 switch_to_blog( $site_id );
427 Jetpack::disconnect();
428 restore_current_blog();
429 }
430
431 /**
432 * Registers a subsite with the Jetpack servers
433 *
434 * @since 2.9
435 * @todo Break apart into easier to manage chunks that can be unit tested
436 * @see Jetpack_Network::jetpack_sites_list();
437 *
438 * @param int $site_id the site identifier.
439 */
440 public function do_subsiteregister( $site_id = null ) {
441 if ( ! current_user_can( 'jetpack_disconnect' ) ) {
442 return;
443 }
444
445 if ( ( new Status() )->is_offline_mode() ) {
446 return;
447 }
448
449 // Figure out what site we are working on.
450 $site_id = ( is_null( $site_id ) ) ? $_GET['site_id'] : $site_id;
451
452 /*
453 * Here we need to switch to the subsite
454 * For the registration process we really only hijack how it
455 * works for an individual site and pass in some extra data here
456 */
457 switch_to_blog( $site_id );
458
459 add_filter( 'jetpack_register_request_body', array( $this, 'filter_register_request_body' ) );
460 add_action( 'jetpack_site_registered_user_token', array( $this, 'filter_register_user_token' ) );
461
462 // Save the secrets in the subsite so when the wpcom server does a pingback it
463 // will be able to validate the connection.
464 $result = $this->connection->register( 'subsiteregister' );
465
466 if ( is_wp_error( $result ) || ! $result ) {
467 restore_current_blog();
468 return $result;
469 }
470
471 Jetpack::activate_default_modules( false, false, array(), false );
472
473 restore_current_blog();
474 }
475
476 /**
477 * Receives the registration response token.
478 *
479 * @param Object $token the received token.
480 */
481 public function filter_register_user_token( $token ) {
482 $is_connection_owner = ! $this->connection->has_connected_owner();
483 ( new Tokens() )->update_user_token(
484 get_current_user_id(),
485 sprintf( '%s.%d', $token->secret, get_current_user_id() ),
486 $is_connection_owner
487 );
488 }
489
490 /**
491 * Filters the registration request body to include additional properties.
492 *
493 * @param array $properties standard register request body properties.
494 * @return array amended properties.
495 */
496 public function filter_register_request_body( $properties ) {
497 $blog_details = get_blog_details();
498
499 $network = get_network();
500
501 switch_to_blog( $network->blog_id );
502 // The blog id on WordPress.com of the primary network site.
503 $network_wpcom_blog_id = Jetpack_Options::get_option( 'id' );
504 restore_current_blog();
505
506 /**
507 * Both `state` and `user_id` need to be sent in the request, even though they are the same value.
508 * Connecting via the network admin combines `register()` and `authorize()` methods into one step,
509 * because we assume the main site is already authorized. `state` is used to verify the `register()`
510 * request, while `user_id()` is used to create the token in the `authorize()` request.
511 */
512 return array_merge(
513 $properties,
514 array(
515 'network_url' => $this->get_url( 'network_admin_page' ),
516 'network_wpcom_blog_id' => $network_wpcom_blog_id,
517 'user_id' => get_current_user_id(),
518
519 /*
520 * Use the subsite's registration date as the site creation date.
521 *
522 * This is in contrast to regular standalone sites, where we use the helper
523 * `Jetpack::get_assumed_site_creation_date()` to assume the site's creation date.
524 */
525 'site_created' => $blog_details->registered,
526 )
527 );
528 }
529
530 /**
531 * A hook handler for adding admin pages and subpages.
532 */
533 public function wrap_network_admin_page() {
534 Jetpack_Admin_Page::wrap_ui( array( $this, 'network_admin_page' ) );
535 }
536
537 /**
538 * Handles the displaying of all sites on the network that are
539 * dis/connected to Jetpack
540 *
541 * @since 2.9
542 * @see Jetpack_Network::jetpack_sites_list()
543 */
544 public function network_admin_page() {
545 global $current_site;
546 $this->network_admin_page_header();
547
548 $jp = Jetpack::init();
549
550 // We should be, but ensure we are on the main blog.
551 switch_to_blog( $current_site->blog_id );
552 $main_active = $jp->is_connection_ready();
553 restore_current_blog();
554
555 // If we are in dev mode, just show the notice and bail.
556 if ( ( new Status() )->is_offline_mode() ) {
557 Jetpack::show_development_mode_notice();
558 return;
559 }
560
561 /*
562 * Ensure the main blog is connected as all other subsite blog
563 * connections will feed off this one
564 */
565 if ( ! $main_active ) {
566 $url = $this->get_url(
567 array(
568 'name' => 'subsiteregister',
569 'site_id' => 1,
570 )
571 );
572 $data = array( 'url' => $jp->build_connect_url() );
573 Jetpack::init()->load_view( 'admin/must-connect-main-blog.php', $data );
574
575 return;
576 }
577
578 require_once 'class.jetpack-network-sites-list-table.php';
579
580 $network_sites_table = new Jetpack_Network_Sites_List_Table();
581 echo '<div class="wrap"><h2>' . esc_html__( 'Sites', 'jetpack' ) . '</h2>';
582 echo '<form method="post">';
583 $network_sites_table->prepare_items();
584 $network_sites_table->display();
585 echo '</form></div>';
586
587 }
588
589 /**
590 * Stylized JP header formatting
591 *
592 * @since 2.9
593 */
594 public function network_admin_page_header() {
595 $is_connected = Jetpack::is_connection_ready();
596
597 $data = array(
598 'is_connected' => $is_connected,
599 );
600 Jetpack::init()->load_view( 'admin/network-admin-header.php', $data );
601 }
602
603 /**
604 * Fires when the Jetpack > Settings page is saved.
605 *
606 * @since 2.9
607 */
608 public function save_network_settings_page() {
609
610 if ( ! wp_verify_nonce( $_POST['_wpnonce'], 'jetpack-network-settings' ) ) {
611 // No nonce, push back to settings page.
612 wp_safe_redirect(
613 add_query_arg(
614 array( 'page' => 'jetpack-settings' ),
615 network_admin_url( 'admin.php' )
616 )
617 );
618 exit();
619 }
620
621 // Try to save the Protect whitelist before anything else, since that action can result in errors.
622 $whitelist = str_replace( ' ', '', $_POST['global-whitelist'] );
623 $whitelist = explode( PHP_EOL, $whitelist );
624 $result = jetpack_protect_save_whitelist( $whitelist, true );
625 if ( is_wp_error( $result ) ) {
626 wp_safe_redirect(
627 add_query_arg(
628 array(
629 'page' => 'jetpack-settings',
630 'error' => 'jetpack_protect_whitelist',
631 ),
632 network_admin_url( 'admin.php' )
633 )
634 );
635 exit();
636 }
637
638 /*
639 * Fields
640 *
641 * auto-connect - Checkbox for global Jetpack connection
642 * sub-site-connection-override - Allow sub-site admins to (dis)reconnect with their own Jetpack account
643 */
644 $auto_connect = 0;
645 if ( isset( $_POST['auto-connect'] ) ) {
646 $auto_connect = 1;
647 }
648
649 $sub_site_connection_override = 0;
650 if ( isset( $_POST['sub-site-connection-override'] ) ) {
651 $sub_site_connection_override = 1;
652 }
653
654 $data = array(
655 'auto-connect' => $auto_connect,
656 'sub-site-connection-override' => $sub_site_connection_override,
657 );
658
659 update_site_option( $this->settings_name, $data );
660 wp_safe_redirect(
661 add_query_arg(
662 array(
663 'page' => 'jetpack-settings',
664 'updated' => 'true',
665 ),
666 network_admin_url( 'admin.php' )
667 )
668 );
669 exit();
670 }
671
672 /**
673 * A hook handler for adding admin pages and subpages.
674 */
675 public function wrap_render_network_admin_settings_page() {
676 Jetpack_Admin_Page::wrap_ui( array( $this, 'render_network_admin_settings_page' ) );
677 }
678
679 /**
680 * A hook rendering the admin settings page.
681 */
682 public function render_network_admin_settings_page() {
683 $this->network_admin_page_header();
684 $options = wp_parse_args( get_site_option( $this->settings_name ), $this->setting_defaults );
685
686 $modules = array();
687 $module_slugs = Jetpack::get_available_modules();
688 foreach ( $module_slugs as $slug ) {
689 $module = Jetpack::get_module( $slug );
690 $module['module'] = $slug;
691 $modules[] = $module;
692 }
693
694 usort( $modules, array( 'Jetpack', 'sort_modules' ) );
695
696 if ( ! isset( $options['modules'] ) ) {
697 $options['modules'] = $modules;
698 }
699
700 $data = array(
701 'modules' => $modules,
702 'options' => $options,
703 'jetpack_protect_whitelist' => jetpack_protect_format_whitelist(),
704 );
705
706 Jetpack::init()->load_view( 'admin/network-settings.php', $data );
707 }
708
709 /**
710 * Updates a site wide option
711 *
712 * @since 2.9
713 *
714 * @param string $key option name.
715 * @param mixed $value option value.
716 *
717 * @return boolean
718 **/
719 public function update_option( $key, $value ) {
720 $options = get_site_option( $this->settings_name, $this->setting_defaults );
721 $options[ $key ] = $value;
722
723 return update_site_option( $this->settings_name, $options );
724 }
725
726 /**
727 * Retrieves a site wide option
728 *
729 * @since 2.9
730 *
731 * @param string $name - Name of the option in the database.
732 **/
733 public function get_option( $name ) {
734 $options = get_site_option( $this->settings_name, $this->setting_defaults );
735 $options = wp_parse_args( $options, $this->setting_defaults );
736
737 if ( ! isset( $options[ $name ] ) ) {
738 $options[ $name ] = null;
739 }
740
741 return $options[ $name ];
742 }
743 }
744