Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
12.95% covered (danger)
12.95%
36 / 278
11.11% covered (danger)
11.11%
3 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Network
12.95% covered (danger)
12.95%
36 / 278
11.11% covered (danger)
11.11%
3 / 27
4964.75
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 / 6
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
 admin_init_network_page
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_network_admin_scripts
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 wrap_network_admin_page
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 network_admin_page
0.00% covered (danger)
0.00%
0 / 18
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 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 render_network_admin_settings_page
0.00% covered (danger)
0.00%
0 / 16
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( "load-$jetpack_sites_page_hook", array( $this, 'admin_init_network_page' ) );
289        add_action( "load-$jetpack_settings_page_hook", array( $this, 'admin_init_network_page' ) );
290    }
291
292    /**
293     * Provides functionality for the Jetpack > Sites page.
294     * Does not do the display!
295     *
296     * @since 2.9
297     */
298    public function jetpack_sites_list() {
299        Jetpack::init();
300
301        if ( isset( $_GET['action'] ) ) {
302            switch ( $_GET['action'] ) {
303                case 'subsiteregister':
304                    check_admin_referer( 'jetpack-subsite-register' );
305                    Jetpack::log( 'subsiteregister' );
306
307                    // If no site_id, stop registration and error.
308                    if ( ! isset( $_GET['site_id'] ) || empty( $_GET['site_id'] ) ) {
309                        /**
310                         * Log error to state cookie for display later.
311                         *
312                         * @todo Make state messages show on Jetpack NA pages
313                         */
314                        Jetpack::state( 'missing_site_id', esc_html__( 'Site ID must be provided to register a sub-site.', 'jetpack' ) );
315                        break;
316                    }
317
318                    // Send data to register endpoint and retrieve shadow blog details.
319                    $result = $this->do_subsiteregister();
320                    $url    = $this->get_url( 'network_admin_page' );
321
322                    if ( is_wp_error( $result ) ) {
323                        $url = add_query_arg( 'action', 'connection_failed', $url );
324                    } else {
325                        $url = add_query_arg( 'action', 'connected', $url );
326                    }
327
328                    wp_safe_redirect( $url );
329                    exit( 0 );
330
331                case 'subsitedisconnect':
332                    check_admin_referer( 'jetpack-subsite-disconnect' );
333                    Jetpack::log( 'subsitedisconnect' );
334
335                    if ( ! isset( $_GET['site_id'] ) || empty( $_GET['site_id'] ) ) {
336                        Jetpack::state( 'missing_site_id', esc_html__( 'Site ID must be provided to disconnect a sub-site.', 'jetpack' ) );
337                        break;
338                    }
339
340                    $this->do_subsitedisconnect();
341                    break;
342
343                case 'connected':
344                case 'connection_failed':
345                    add_action( 'jetpack_notices', array( $this, 'show_jetpack_notice' ) );
346                    break;
347            }
348        }
349    }
350
351    /**
352     * Set the disconnect capability for multisite.
353     *
354     * @param array $caps The capabilities array.
355     */
356    public function set_multisite_disconnect_cap( $caps ) {
357        // Can individual site admins manage their own connection?
358        if ( ! is_super_admin() && ! $this->get_option( 'sub-site-connection-override' ) ) {
359            /*
360             * We need to update the option name -- it's terribly unclear which
361             * direction the override goes.
362             *
363             * @todo: Update the option name to `sub-sites-can-manage-own-connections`
364             */
365            return array( 'do_not_allow' );
366        }
367
368        return $caps;
369    }
370
371    /**
372     * Shows the Jetpack plugin notices.
373     */
374    public function show_jetpack_notice() {
375        if ( isset( $_GET['action'] ) && 'connected' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
376            $notice    = __( 'Site successfully connected.', 'jetpack' );
377            $classname = 'updated';
378        } elseif ( isset( $_GET['action'] ) && 'connection_failed' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
379            $notice    = __( 'Site connection failed!', 'jetpack' );
380            $classname = 'error';
381        }
382        ?>
383        <div id="message" class="<?php echo esc_attr( $classname ?? '' ); ?> jetpack-message jp-connect" style="display:block !important;">
384            <p><?php echo esc_html( $notice ?? '' ); ?></p>
385        </div>
386        <?php
387    }
388
389    /**
390     * Disconnect functionality for an individual site
391     *
392     * @since 2.9
393     * @see   Jetpack_Network::jetpack_sites_list()
394     *
395     * @param int $site_id the site identifier.
396     */
397    public function do_subsitedisconnect( $site_id = null ) {
398        if ( ! current_user_can( 'jetpack_disconnect' ) ) {
399            return;
400        }
401        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Caller (i.e. `$this->jetpack_sites_list()`) should check.
402        $site_id = ( $site_id === null ) ? ( isset( $_GET['site_id'] ) ? (int) $_GET['site_id'] : null ) : $site_id;
403        switch_to_blog( $site_id );
404        Jetpack::disconnect();
405        restore_current_blog();
406    }
407
408    /**
409     * Registers a subsite with the Jetpack servers
410     *
411     * @since 2.9
412     * @todo  Break apart into easier to manage chunks that can be unit tested
413     * @see   Jetpack_Network::jetpack_sites_list();
414     *
415     * @param int $site_id the site identifier.
416     */
417    public function do_subsiteregister( $site_id = null ) {
418        if ( ! current_user_can( 'jetpack_disconnect' ) ) {
419            return;
420        }
421
422        if ( ( new Status() )->is_offline_mode() ) {
423            return;
424        }
425
426        // Figure out what site we are working on.
427        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Caller (i.e. `$this->jetpack_sites_list()`) should check.
428        $site_id = ( $site_id === null ) ? ( isset( $_GET['site_id'] ) ? (int) $_GET['site_id'] : null ) : $site_id;
429
430        /*
431         * Here we need to switch to the subsite
432         * For the registration process we really only hijack how it
433         * works for an individual site and pass in some extra data here
434         */
435        switch_to_blog( $site_id );
436
437        add_filter( 'jetpack_register_request_body', array( $this, 'filter_register_request_body' ) );
438        add_action( 'jetpack_site_registered_user_token', array( $this, 'filter_register_user_token' ) );
439
440        // Save the secrets in the subsite so when the wpcom server does a pingback it
441        // will be able to validate the connection.
442        $result = $this->connection->register( 'subsiteregister' );
443
444        if ( is_wp_error( $result ) || ! $result ) {
445            restore_current_blog();
446            return $result;
447        }
448
449        Jetpack::activate_default_modules( false, false, array(), false );
450
451        restore_current_blog();
452    }
453
454    /**
455     * Receives the registration response token.
456     *
457     * @param Object $token the received token.
458     */
459    public function filter_register_user_token( $token ) {
460        $is_connection_owner = ! $this->connection->has_connected_owner();
461        ( new Tokens() )->update_user_token(
462            get_current_user_id(),
463            sprintf( '%s.%d', $token->secret, get_current_user_id() ),
464            $is_connection_owner
465        );
466    }
467
468    /**
469     * Filters the registration request body to include additional properties.
470     *
471     * @param array $properties standard register request body properties.
472     * @return array amended properties.
473     */
474    public function filter_register_request_body( $properties ) {
475        $blog_details = get_blog_details();
476
477        $network = get_network();
478
479        switch_to_blog( $network->blog_id );
480        // The blog id on WordPress.com of the primary network site.
481        $network_wpcom_blog_id = Jetpack_Options::get_option( 'id' );
482        restore_current_blog();
483
484        /**
485         * Both `state` and `user_id` need to be sent in the request, even though they are the same value.
486         * Connecting via the network admin combines `register()` and `authorize()` methods into one step,
487         * because we assume the main site is already authorized. `state` is used to verify the `register()`
488         * request, while `user_id()` is used to create the token in the `authorize()` request.
489         */
490        return array_merge(
491            $properties,
492            array(
493                'network_url'           => $this->get_url( 'network_admin_page' ),
494                'network_wpcom_blog_id' => $network_wpcom_blog_id,
495                'user_id'               => get_current_user_id(),
496
497                /*
498                 * Use the subsite's registration date as the site creation date.
499                 *
500                 * This is in contrast to regular standalone sites, where we use the helper
501                 * `Jetpack::get_assumed_site_creation_date()` to assume the site's creation date.
502                 */
503                'site_created'          => $blog_details->registered,
504            )
505        );
506    }
507
508    /**
509     * Initializes assets for network admin pages.
510     *
511     * @since 15.7
512     */
513    public function admin_init_network_page() {
514        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_network_admin_scripts' ) );
515    }
516
517    /**
518     * Enqueues the JS and CSS for the unified network admin header.
519     *
520     * @since 15.7
521     */
522    public function enqueue_network_admin_scripts() {
523        $build_dir         = JETPACK__PLUGIN_DIR . '_inc/build/';
524        $script_asset_path = $build_dir . 'network-admin.asset.php';
525
526        if ( ! file_exists( $script_asset_path ) ) {
527            return;
528        }
529
530        $script_asset = require $script_asset_path;
531
532        wp_enqueue_script(
533            'jetpack-network-admin',
534            plugins_url( '_inc/build/network-admin.js', JETPACK__PLUGIN_FILE ),
535            $script_asset['dependencies'],
536            $script_asset['version'],
537            true
538        );
539
540        wp_enqueue_style(
541            'jetpack-network-admin',
542            plugins_url( '_inc/build/network-admin.css', JETPACK__PLUGIN_FILE ),
543            array(),
544            $script_asset['version']
545        );
546
547        wp_set_script_translations( 'jetpack-network-admin', 'jetpack' );
548
549        wp_localize_script(
550            'jetpack-network-admin',
551            'JetpackNetworkAdminData',
552            array(
553                'sitesUrl'    => network_admin_url( 'admin.php?page=jetpack' ),
554                'settingsUrl' => network_admin_url( 'admin.php?page=jetpack-settings' ),
555            )
556        );
557    }
558
559    /**
560     * Renders the Network Sites page with the unified admin header.
561     */
562    public function wrap_network_admin_page() {
563        echo '<div id="jp-network-admin-root" data-page="sites"></div>';
564        echo '<div id="jp-network-admin-content" style="display:none">';
565        $this->network_admin_page();
566        echo '</div>';
567    }
568
569    /**
570     * Handles the displaying of all sites on the network that are
571     * dis/connected to Jetpack
572     *
573     * @since 2.9
574     * @see   Jetpack_Network::jetpack_sites_list()
575     */
576    public function network_admin_page() {
577        global $current_site;
578
579        $jp = Jetpack::init();
580
581        // We should be, but ensure we are on the main blog.
582        switch_to_blog( $current_site->blog_id );
583        $main_active = $jp->is_connection_ready();
584        restore_current_blog();
585
586        // If we are in dev mode, just show the notice and bail.
587        if ( ( new Status() )->is_offline_mode() ) {
588            Jetpack::show_development_mode_notice();
589            return;
590        }
591
592        /*
593         * Ensure the main blog is connected as all other subsite blog
594         * connections will feed off this one
595         */
596        if ( ! $main_active ) {
597            $data = array( 'url' => $jp->build_connect_url() );
598            Jetpack::init()->load_view( 'admin/must-connect-main-blog.php', $data );
599
600            return;
601        }
602
603        require_once __DIR__ . '/class.jetpack-network-sites-list-table.php';
604
605        $network_sites_table = new Jetpack_Network_Sites_List_Table();
606        echo '<div class="wrap"><h2>' . esc_html__( 'Sites', 'jetpack' ) . '</h2>';
607        echo '<form method="post">';
608        $network_sites_table->prepare_items();
609        $network_sites_table->display();
610        echo '</form></div>';
611    }
612
613    /**
614     * Stylized JP header formatting
615     *
616     * @since 2.9
617     */
618    public function network_admin_page_header() {
619        $is_connected = Jetpack::is_connection_ready();
620
621        $data = array(
622            'is_connected' => $is_connected,
623        );
624        Jetpack::init()->load_view( 'admin/network-admin-header.php', $data );
625    }
626
627    /**
628     * Fires when the Jetpack > Settings page is saved.
629     *
630     * @since 2.9
631     * @return never
632     */
633    public function save_network_settings_page() {
634
635        if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'jetpack-network-settings' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
636            // No nonce, push back to settings page.
637            wp_safe_redirect(
638                add_query_arg(
639                    array( 'page' => 'jetpack-settings' ),
640                    network_admin_url( 'admin.php' )
641                )
642            );
643            exit( 0 );
644        }
645
646        // Try to save the Protect allow list before anything else, since that action can result in errors.
647        $allow_list = isset( $_POST['global-allow-list'] ) ? filter_var( wp_unslash( $_POST['global-allow-list'] ) ) : '';
648        $allow_list = str_replace( ' ', '', $allow_list );
649        $allow_list = explode( PHP_EOL, $allow_list );
650        $result     = Brute_Force_Protection_Shared_Functions::save_allow_list( $allow_list, true );
651        if ( is_wp_error( $result ) ) {
652            wp_safe_redirect(
653                add_query_arg(
654                    array(
655                        'page'  => 'jetpack-settings',
656                        'error' => 'jetpack_protect_whitelist',
657                    ),
658                    network_admin_url( 'admin.php' )
659                )
660            );
661            exit( 0 );
662        }
663
664        /*
665         * Fields
666         *
667         * auto-connect - Checkbox for global Jetpack connection
668         * sub-site-connection-override - Allow sub-site admins to (dis)reconnect with their own Jetpack account
669         */
670        $auto_connect = 0;
671        if ( isset( $_POST['auto-connect'] ) ) {
672            $auto_connect = 1;
673        }
674
675        $sub_site_connection_override = 0;
676        if ( isset( $_POST['sub-site-connection-override'] ) ) {
677            $sub_site_connection_override = 1;
678        }
679
680        $data = array(
681            'auto-connect'                 => $auto_connect,
682            'sub-site-connection-override' => $sub_site_connection_override,
683        );
684
685        update_site_option( $this->settings_name, $data );
686        wp_safe_redirect(
687            add_query_arg(
688                array(
689                    'page'    => 'jetpack-settings',
690                    'updated' => 'true',
691                ),
692                network_admin_url( 'admin.php' )
693            )
694        );
695        exit( 0 );
696    }
697
698    /**
699     * Renders the Network Settings page with the unified admin header.
700     */
701    public function wrap_render_network_admin_settings_page() {
702        echo '<div id="jp-network-admin-root" data-page="settings"></div>';
703        echo '<div id="jp-network-admin-content" style="display:none">';
704        $this->render_network_admin_settings_page();
705        echo '</div>';
706    }
707
708    /**
709     * A hook rendering the admin settings page.
710     */
711    public function render_network_admin_settings_page() {
712        $options = wp_parse_args( get_site_option( $this->settings_name ), $this->setting_defaults );
713
714        $modules      = array();
715        $module_slugs = Jetpack::get_available_modules();
716        foreach ( $module_slugs as $slug ) {
717            $module           = Jetpack::get_module( $slug );
718            $module['module'] = $slug;
719            $modules[]        = $module;
720        }
721
722        usort( $modules, array( 'Jetpack', 'sort_modules' ) );
723
724        if ( ! isset( $options['modules'] ) ) {
725            $options['modules'] = $modules;
726        }
727
728        $data = array(
729            'modules'                   => $modules,
730            'options'                   => $options,
731            'jetpack_protect_whitelist' => Brute_Force_Protection_Shared_Functions::format_allow_list(),
732        );
733
734        Jetpack::init()->load_view( 'admin/network-settings.php', $data );
735    }
736
737    /**
738     * Updates a site wide option
739     *
740     * @since 2.9
741     *
742     * @param string $key option name.
743     * @param mixed  $value option value.
744     *
745     * @return boolean
746     **/
747    public function update_option( $key, $value ) {
748        $options         = get_site_option( $this->settings_name, $this->setting_defaults );
749        $options[ $key ] = $value;
750
751        return update_site_option( $this->settings_name, $options );
752    }
753
754    /**
755     * Retrieves a site wide option
756     *
757     * @since 2.9
758     *
759     * @param string $name - Name of the option in the database.
760     **/
761    public function get_option( $name ) {
762        $options = get_site_option( $this->settings_name, $this->setting_defaults );
763        $options = wp_parse_args( $options, $this->setting_defaults );
764
765        if ( ! isset( $options[ $name ] ) ) {
766            $options[ $name ] = null;
767        }
768
769        return $options[ $name ];
770    }
771}