Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.20% covered (warning)
87.20%
109 / 125
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connection_Notice
87.20% covered (warning)
87.20%
109 / 125
60.00% covered (warning)
60.00%
3 / 5
20.84
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initialize_notices
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 delete_user_update_connection_owner_notice
95.24% covered (success)
95.24%
80 / 84
0.00% covered (danger)
0.00%
0 / 1
10
 enqueue_connection_owner_script
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_connection_owner_script
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Admin connection notices.
4 *
5 * @package automattic/jetpack-admin-ui
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Automattic\Jetpack\Redirect;
11use Automattic\Jetpack\Tracking;
12
13/**
14 * Admin connection notices.
15 *
16 * @phan-constructor-used-for-side-effects
17 */
18class Connection_Notice {
19
20    /**
21     * Whether the class has been initialized.
22     *
23     * @var bool
24     */
25    private static $is_initialized = false;
26
27    /**
28     * The constructor.
29     */
30    public function __construct() {
31        if ( ! static::$is_initialized ) {
32            add_action( 'current_screen', array( $this, 'initialize_notices' ) );
33            static::$is_initialized = true;
34        }
35    }
36
37    /**
38     * Initialize the notices if needed.
39     *
40     * @param \WP_Screen $screen WP Core's screen object.
41     *
42     * @return void
43     */
44    public function initialize_notices( $screen ) {
45        if ( in_array(
46            $screen->id,
47            array(
48                'jetpack_page_akismet-key-config',
49                'admin_page_jetpack_modules',
50            ),
51            true
52        ) ) {
53            return;
54        }
55
56        /*
57         * phpcs:disable WordPress.Security.NonceVerification.Recommended
58         *
59         * This function is firing within wp-admin and checks (below) if it is in the midst of a deletion on the users
60         * page. Nonce will be already checked by WordPress, so we do not need to check ourselves.
61         */
62
63        if ( isset( $screen->base ) && 'users' === $screen->base
64            && isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action']
65        ) {
66            add_action( 'admin_notices', array( $this, 'delete_user_update_connection_owner_notice' ) );
67        }
68    }
69
70    /**
71     * This is an entire admin notice dedicated to messaging and handling of the case where a user is trying to delete
72     * the connection owner.
73     */
74    public function delete_user_update_connection_owner_notice() {
75        // Get connection owner or bail.
76        $connection_manager  = new Manager();
77        $connection_owner_id = $connection_manager->get_connection_owner_id();
78        if ( ! $connection_owner_id ) {
79            return;
80        }
81        $connection_owner_userdata = get_userdata( $connection_owner_id );
82
83        // Bail if we're not trying to delete connection owner.
84        $user_ids_to_delete = array();
85        if ( isset( $_REQUEST['users'] ) ) {
86            $user_ids_to_delete = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['users'] ) );
87        } elseif ( isset( $_REQUEST['user'] ) ) {
88            $user_ids_to_delete[] = sanitize_text_field( wp_unslash( $_REQUEST['user'] ) );
89        }
90
91        // phpcs:enable
92        $user_ids_to_delete        = array_map( 'absint', $user_ids_to_delete );
93        $deleting_connection_owner = in_array( $connection_owner_id, (array) $user_ids_to_delete, true );
94        if ( ! $deleting_connection_owner ) {
95            return;
96        }
97
98        // Bail if they're trying to delete themselves to avoid confusion.
99        if ( get_current_user_id() === $connection_owner_id ) {
100            return;
101        }
102
103        $tracking = new Tracking();
104
105        // Track it!
106        if ( method_exists( $tracking, 'record_user_event' ) ) {
107            $tracking->record_user_event( 'delete_connection_owner_notice_view' );
108        }
109
110        $connected_admins = $connection_manager->get_connected_users( 'jetpack_disconnect' );
111        $user             = is_a( $connection_owner_userdata, 'WP_User' ) ? esc_html( $connection_owner_userdata->data->user_login ) : '';
112
113        echo "<div class='notice notice-warning' id='jetpack-notice-switch-connection-owner'>";
114        echo '<h2>' . esc_html__( 'Important notice about your Jetpack connection:', 'jetpack-connection' ) . '</h2>';
115        echo '<p>' . sprintf(
116            /* translators: WordPress User, if available. */
117            esc_html__( 'Warning! You are about to delete the Jetpack connection owner (%s) for this site, which may cause some of your Jetpack features to stop working.', 'jetpack-connection' ),
118            esc_html( $user )
119        ) . '</p>';
120
121        if ( ! empty( $connected_admins ) && count( $connected_admins ) > 1 ) {
122            echo '<form id="jp-switch-connection-owner" action="" method="post">';
123            echo "<label for='owner'>" . esc_html__( 'You can choose to transfer connection ownership to one of these already-connected admins:', 'jetpack-connection' ) . ' </label>';
124
125            $connected_admin_ids = array_map(
126                function ( $connected_admin ) {
127                    return $connected_admin->ID;
128                },
129                $connected_admins
130            );
131
132            wp_dropdown_users(
133                array(
134                    'name'    => 'owner',
135                    'include' => array_diff( $connected_admin_ids, array( $connection_owner_id ) ),
136                    'show'    => 'display_name_with_login',
137                )
138            );
139
140            echo '<p>';
141            submit_button( esc_html__( 'Set new connection owner', 'jetpack-connection' ), 'primary', 'jp-switch-connection-owner-submit', false );
142            echo '</p>';
143
144            echo "<div id='jp-switch-user-results'></div>";
145            echo '</form>';
146
147            $this->enqueue_connection_owner_script();
148        } else {
149            echo '<p>' . esc_html__( 'Every Jetpack site needs at least one connected admin for the features to work properly. Please connect to your WordPress.com account via the button below. Once you connect, you may refresh this page to see an option to change the connection owner.', 'jetpack-connection' ) . '</p>';
150            $connect_url = $connection_manager->get_authorization_url();
151            $connect_url = add_query_arg( 'from', 'delete_connection_owner_notice', $connect_url );
152            echo "<a href='" . esc_url( $connect_url ) . "' target='_blank' rel='noopener noreferrer' class='button-primary'>" . esc_html__( 'Connect to WordPress.com', 'jetpack-connection' ) . '</a>';
153        }
154
155        echo '<p>';
156        printf(
157            wp_kses(
158            /* translators: URL to Jetpack support doc regarding the primary user. */
159                __( "<a href='%s' target='_blank' rel='noopener noreferrer'>Learn more</a> about the connection owner and what will break if you do not have one.", 'jetpack-connection' ),
160                array(
161                    'a' => array(
162                        'href'   => true,
163                        'target' => true,
164                        'rel'    => true,
165                    ),
166                )
167            ),
168            esc_url( Redirect::get_url( 'jetpack-support-primary-user' ) )
169        );
170        echo '</p>';
171        echo '<p>';
172        printf(
173            wp_kses(
174            /* translators: URL to contact Jetpack support. */
175                __( 'As always, feel free to <a href="%s" target="_blank" rel="noopener noreferrer">contact our support team</a> if you have any questions.', 'jetpack-connection' ),
176                array(
177                    'a' => array(
178                        'href'   => true,
179                        'target' => true,
180                        'rel'    => true,
181                    ),
182                )
183            ),
184            esc_url( Redirect::get_url( 'jetpack-contact-support' ) )
185        );
186        echo '</p>';
187        echo '</div>';
188    }
189
190    /**
191     * Enqueue the script that hands the chosen owner to the connection owner REST endpoint.
192     *
193     * The notice prints on `admin_notices`, after `admin_enqueue_scripts` has run, so the handle
194     * has no source of its own and goes in the footer, where the form it binds to already exists.
195     *
196     * @return void
197     */
198    private function enqueue_connection_owner_script() {
199        $handle = 'jetpack-connection-owner-notice';
200
201        wp_register_script( $handle, false, array(), Package_Version::PACKAGE_VERSION, true );
202        wp_enqueue_script( $handle );
203        wp_add_inline_script( $handle, $this->get_connection_owner_script() );
204    }
205
206    /**
207     * Build the script that hands the chosen owner to the connection owner REST endpoint.
208     *
209     * @return string
210     */
211    private function get_connection_owner_script() {
212        $json_flags = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP;
213
214        ob_start();
215        ?>
216( function() {
217    const switchOwnerButton = document.getElementById('jp-switch-connection-owner');
218    if ( ! switchOwnerButton ) {
219        return;
220    }
221
222    switchOwnerButton.addEventListener( 'submit', function ( e ) {
223        e.preventDefault();
224
225        const submitBtn = document.getElementById('jp-switch-connection-owner-submit');
226        submitBtn.disabled = true;
227
228        const results = document.getElementById('jp-switch-user-results');
229        results.innerHTML = '';
230        results.classList.remove( 'error-message' );
231
232        const handleAPIError = ( message ) => {
233            submitBtn.disabled = false;
234
235            results.classList.add( 'error-message' );
236            results.innerHTML = message || <?php echo wp_json_encode( esc_html__( 'Something went wrong. Please try again.', 'jetpack-connection' ), $json_flags ); ?>;
237        }
238
239        fetch(
240            <?php echo wp_json_encode( esc_url_raw( get_rest_url() . 'jetpack/v4/connection/owner' ), $json_flags ); ?>,
241            {
242                method: 'POST',
243                headers: {
244                    'X-WP-Nonce': <?php echo wp_json_encode( wp_create_nonce( 'wp_rest' ), $json_flags ); ?>,
245                },
246                body: new URLSearchParams( new FormData( this ) ),
247            }
248        )
249            .then( response => response.json() )
250            .then( data => {
251                if ( data.hasOwnProperty( 'code' ) && data.code === 'success' ) {
252                    // Owner successfully changed.
253                    results.innerHTML = <?php echo wp_json_encode( esc_html__( 'Success!', 'jetpack-connection' ), $json_flags ); ?>;
254                    setTimeout(function () {
255                        document.getElementById( 'jetpack-notice-switch-connection-owner' ).style.display = 'none';
256                    }, 1000);
257
258                    return;
259                }
260
261                handleAPIError( data?.message );
262            } )
263            .catch( () => handleAPIError() );
264    });
265} )();
266        <?php
267        return ob_get_clean();
268    }
269}