Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.85% covered (success)
97.85%
182 / 186
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Debug_Data
97.85% covered (success)
97.85%
182 / 186
83.33% covered (warning)
83.33%
5 / 6
35
0.00% covered (danger)
0.00%
0 / 1
 add_section
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_section
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 get_fields
100.00% covered (success)
100.00%
148 / 148
100.00% covered (success)
100.00%
1 / 1
26
 token_key
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 human_readable_master_user
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 human_readable_user
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * The Jetpack Connection section of the Site Health Info tab.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Automattic\Jetpack\Constants;
11use Automattic\Jetpack\Identity_Crisis;
12use Automattic\Jetpack\Redirect;
13use Automattic\Jetpack\Status;
14use Jetpack_Options;
15use WP_User;
16
17/**
18 * Adds connection details to Tools › Site Health › Info, and so to "Copy site info to clipboard".
19 *
20 * @since 9.17.0
21 */
22class Debug_Data {
23
24    /**
25     * The callback older Jetpack versions add the whole section with.
26     *
27     * @var array
28     */
29    const LEGACY_CALLBACK = array( 'Jetpack_Debug_Data', 'core_debug_data' );
30
31    /**
32     * Add the Jetpack Connection section, unless an older Jetpack plugin adds its own.
33     *
34     * @since 9.17.0
35     *
36     * @param array $debug The debug information already compiled by Core.
37     *
38     * @return array The debug information with the Jetpack Connection section.
39     */
40    public static function add_section( $debug ) {
41        if ( has_filter( 'debug_information', self::LEGACY_CALLBACK ) ) {
42            return $debug;
43        }
44
45        $debug['jetpack'] = self::get_section();
46
47        return $debug;
48    }
49
50    /**
51     * Build the Jetpack Connection section.
52     *
53     * @since 9.17.0
54     *
55     * @return array The section, in the shape Core's `debug_information` filter expects.
56     */
57    public static function get_section() {
58        /** This filter is documented in src/health/class-connection-health-test-base.php */
59        $support_url = apply_filters( 'jetpack_connection_support_url', Redirect::get_url( 'jetpack-contact-support' ) );
60
61        return array(
62            'label'       => __( 'Jetpack Connection', 'jetpack-connection' ),
63            // Core prints the description unescaped.
64            'description' => wp_kses(
65                sprintf(
66                    /* translators: %1$s is URL to jetpack.com's contact support page. %2$s accessibility text */
67                    __(
68                        'Diagnostic information helpful to <a href="%1$s" target="_blank" rel="noopener noreferrer">your Jetpack Happiness team<span class="screen-reader-text">%2$s</span></a>',
69                        'jetpack-connection'
70                    ),
71                    esc_url( $support_url ),
72                    __( '(opens in a new tab)', 'jetpack-connection' )
73                ),
74                array(
75                    'a'    => array(
76                        'href'   => array(),
77                        'target' => array(),
78                        'rel'    => array(),
79                    ),
80                    'span' => array( 'class' => array() ),
81                )
82            ),
83            'fields'      => self::get_fields(),
84        );
85    }
86
87    /**
88     * Compile the section's fields.
89     *
90     * @since 9.17.0
91     *
92     * @return array Fields keyed by ID, each with a `label`, a `value` and a `private` flag.
93     */
94    public static function get_fields() {
95        $fields = array(
96            'site_id'                  => array(
97                'label'   => 'Jetpack Site ID',
98                'value'   => Jetpack_Options::get_option( 'id' ),
99                'private' => false,
100            ),
101            'ssl_cert'                 => array(
102                'label'   => 'Jetpack SSL Verification Bypass',
103                'value'   => Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ) ? 'Yes' : 'No',
104                'private' => false,
105            ),
106            'time_diff'                => array(
107                'label'   => "Offset between Jetpack server's time and this server's time.",
108                'value'   => Jetpack_Options::get_option( 'time_diff' ),
109                'private' => false,
110            ),
111            'public'                   => array(
112                'label'   => 'Jetpack Site Public',
113                'value'   => Jetpack_Options::get_option( 'public' ) ? 'Public' : 'Private',
114                'private' => false,
115            ),
116            'master_user'              => array(
117                'label'   => 'Jetpack Master User',
118                'value'   => self::human_readable_master_user(),
119                'private' => false,
120            ),
121            'is_offline_mode'          => array(
122                'label'   => 'Jetpack Offline Mode',
123                'value'   => ( new Status() )->is_offline_mode() ? 'on' : 'off',
124                'private' => false,
125            ),
126            'is_offline_mode_constant' => array(
127                'label'   => 'JETPACK_DEV_DEBUG Constant',
128                'value'   => ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) ? 'on' : 'off',
129                'private' => false,
130            ),
131        );
132
133        $user_id    = get_current_user_id();
134        $tokens     = new Tokens();
135        $blog_token = $tokens->get_access_token();
136        $user_token = $tokens->get_access_token( $user_id );
137
138        $tokenset = trim( ( $blog_token ? 'Blog ' : '' ) . ( $user_token ? 'User' : '' ) );
139
140        $fields['current_user'] = array(
141            'label'   => 'Current User',
142            'value'   => self::human_readable_user( $user_id ),
143            'private' => false,
144        );
145        $fields['tokens_set']   = array(
146            'label'   => 'Tokens defined',
147            'value'   => $tokenset ? $tokenset : 'None',
148            'private' => false,
149        );
150        $fields['blog_token']   = array(
151            'label'   => 'Blog Public Key',
152            'value'   => $blog_token ? self::token_key( $blog_token->secret ) : 'Not set.',
153            'private' => false,
154        );
155        $fields['user_token']   = array(
156            'label'   => 'User Public Key',
157            'value'   => $user_token ? self::token_key( $user_token->secret ) : 'Not set.',
158            'private' => false,
159        );
160
161        // Without the Jetpack plugin, these are what say which plugins are on the connection.
162        $connected_plugins           = Plugin_Storage::get_all();
163        $fields['connected_plugins'] = array(
164            'label'   => 'Connected Plugins',
165            'value'   => is_array( $connected_plugins ) ? implode( ', ', array_keys( $connected_plugins ) ) : 'Unknown',
166            'private' => false,
167        );
168        $package_versions            = get_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION, array() );
169        $package_versions            = is_array( $package_versions ) ? array_filter( $package_versions, 'is_string' ) : array();
170        $fields['package_versions']  = array(
171            'label'   => 'Package Versions',
172            'value'   => $package_versions ? $package_versions : 'None',
173            'private' => false,
174        );
175
176        foreach ( array(
177            'HTTP_HOST',
178            'SERVER_PORT',
179            'HTTPS',
180            'GD_PHP_HANDLER',
181            'HTTP_AKAMAI_ORIGIN_HOP',
182            'HTTP_CF_CONNECTING_IP',
183            'HTTP_CLIENT_IP',
184            'HTTP_FASTLY_CLIENT_IP',
185            'HTTP_FORWARDED',
186            'HTTP_FORWARDED_FOR',
187            'HTTP_INCAP_CLIENT_IP',
188            'HTTP_TRUE_CLIENT_IP',
189            'HTTP_X_CLIENTIP',
190            'HTTP_X_CLUSTER_CLIENT_IP',
191            'HTTP_X_FORWARDED',
192            'HTTP_X_FORWARDED_FOR',
193            'HTTP_X_IP_TRAIL',
194            'HTTP_X_REAL_IP',
195            'HTTP_X_VARNISH',
196            'REMOTE_ADDR',
197        ) as $header ) {
198            if ( isset( $_SERVER[ $header ] ) ) {
199                $fields[ $header ] = array(
200                    'label'   => 'Server Variable ' . $header,
201                    'value'   => empty( $_SERVER[ $header ] ) ? 'false' : filter_var( wp_unslash( $_SERVER[ $header ] ) ),
202                    // Not secret, but kept out of the copied report so it is not pasted into public forums.
203                    'private' => true,
204                );
205            }
206        }
207
208        $fields['protect_header'] = array(
209            'label'   => 'Trusted IP',
210            'value'   => wp_json_encode( get_site_option( 'trusted_ip_header' ), JSON_UNESCAPED_SLASHES ),
211            'private' => false,
212        );
213
214        /**
215         * Filters the fields of the Jetpack Connection section in Site Health › Info.
216         *
217         * For connection-related fields; a product's own status belongs in its own `debug_information` section.
218         * The IDC and connection test fields are added after this runs, so they stay last and can't be overwritten.
219         *
220         * @since 9.17.0
221         *
222         * @param array $fields Fields keyed by ID, each with a `label`, a `value` and a `private` flag.
223         */
224        $filtered = apply_filters( 'jetpack_connection_debug_data', $fields );
225        $fields   = is_array( $filtered ) ? $filtered : $fields;
226
227        $idc_urls = array(
228            'home'       => Urls::home_url(),
229            'siteurl'    => Urls::site_url(),
230            'WP_HOME'    => Constants::is_defined( 'WP_HOME' ) ? Constants::get_constant( 'WP_HOME' ) : '',
231            'WP_SITEURL' => Constants::is_defined( 'WP_SITEURL' ) ? Constants::get_constant( 'WP_SITEURL' ) : '',
232        );
233
234        $fields['idc_urls']         = array(
235            'label'   => 'IDC URLs',
236            'value'   => $idc_urls,
237            'private' => false,
238        );
239        $fields['idc_error_option'] = array(
240            'label'   => 'IDC Error Option',
241            'value'   => wp_json_encode( Jetpack_Options::get_option( 'sync_error_idc' ), JSON_UNESCAPED_SLASHES ),
242            'private' => false,
243        );
244        $fields['idc_optin']        = array(
245            'label'   => 'IDC Opt-in',
246            'value'   => Identity_Crisis::should_handle_idc(),
247            'private' => false,
248        );
249
250        $cxn_tests = new Connection_Health_Tests();
251        $cxn_value = 'All Pass.';
252        if ( ! $cxn_tests->pass() ) {
253            $cxn_value = array();
254            foreach ( $cxn_tests->list_fails() as $fail ) {
255                // Third-party tests can return anything, and Core casts each value to a string.
256                if ( ! isset( $fail['name'] ) || ! is_scalar( $fail['name'] ) || ! is_scalar( $fail['short_description'] ) ) {
257                    continue;
258                }
259                $name = (string) $fail['name'];
260                for ( $i = 2; isset( $cxn_value[ $name ] ); $i++ ) {
261                    $name = sprintf( '%s (%d)', $fail['name'], $i );
262                }
263                $cxn_value[ $name ] = (string) $fail['short_description'];
264            }
265        }
266
267        $fields['cxn_tests'] = array(
268            'label'   => 'Connection Tests',
269            'value'   => $cxn_value,
270            'private' => false,
271        );
272
273        return $fields;
274    }
275
276    /**
277     * The part of a token before its period, which identifies it without revealing it.
278     *
279     * @param string $secret The token secret.
280     *
281     * @return string The key, or a note that the token is malformed.
282     */
283    private static function token_key( $secret ) {
284        $key = substr( $secret, 0, (int) strpos( $secret, '.' ) );
285
286        return $key ? $key : 'Potentially Malformed Token.';
287    }
288
289    /**
290     * Describe the connection owner.
291     *
292     * @return string
293     */
294    private static function human_readable_master_user() {
295        $master_user = Jetpack_Options::get_option( 'master_user' );
296
297        if ( ! $master_user ) {
298            return __( 'No master user set.', 'jetpack-connection' );
299        }
300
301        $user = new WP_User( $master_user );
302
303        if ( ! $user->exists() ) {
304            return __( 'Master user no longer exists. Please disconnect and reconnect Jetpack.', 'jetpack-connection' );
305        }
306
307        return self::human_readable_user( $user );
308    }
309
310    /**
311     * Describe a user as "#1 username".
312     *
313     * @param WP_User|int $user Object or ID.
314     *
315     * @return string
316     */
317    private static function human_readable_user( $user ) {
318        $user = new WP_User( $user );
319
320        return sprintf( '#%1$d %2$s', $user->ID, $user->user_login );
321    }
322}