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