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