Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 243
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Debug_Data
0.00% covered (danger)
0.00%
0 / 243
0.00% covered (danger)
0.00%
0 / 6
1122
0.00% covered (danger)
0.00%
0 / 1
 what_jetpack_plan
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 seconds_to_time
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 core_debug_data
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 debug_data
0.00% covered (danger)
0.00%
0 / 195
0.00% covered (danger)
0.00%
0 / 1
420
 human_readable_master_user
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 human_readable_user
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jetpack Debug Data for the Site Health sections.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Connection\Tokens;
9use Automattic\Jetpack\Connection\Urls;
10use Automattic\Jetpack\Constants;
11use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
12use Automattic\Jetpack\Identity_Crisis;
13use Automattic\Jetpack\Redirect;
14use Automattic\Jetpack\Status;
15use Automattic\Jetpack\Sync\Modules;
16use Automattic\Jetpack\Sync\Sender;
17
18/**
19 * Class Jetpack_Debug_Data
20 *
21 * Collect and return debug data for Jetpack.
22 *
23 * @since 7.3.0
24 */
25class Jetpack_Debug_Data {
26    /**
27     * Determine the active plan and normalize it for the debugger results.
28     *
29     * @since 7.3.0
30     *
31     * @return string The plan slug.
32     */
33    public static function what_jetpack_plan() {
34        $plan = Jetpack_Plan::get();
35        return ! empty( $plan['class'] ) ? $plan['class'] : 'undefined';
36    }
37
38    /**
39     * Convert seconds to human readable time.
40     *
41     * A dedication function instead of using Core functionality to allow for output in seconds.
42     *
43     * @since 7.3.0
44     *
45     * @param int $seconds Number of seconds to convert to human time.
46     *
47     * @return string Human readable time.
48     */
49    public static function seconds_to_time( $seconds ) {
50        $seconds = (int) $seconds;
51        $units   = array(
52            'week'   => WEEK_IN_SECONDS,
53            'day'    => DAY_IN_SECONDS,
54            'hour'   => HOUR_IN_SECONDS,
55            'minute' => MINUTE_IN_SECONDS,
56            'second' => 1,
57        );
58        // specifically handle zero.
59        if ( 0 === $seconds ) {
60            return '0 seconds';
61        }
62        $human_readable = '';
63        foreach ( $units as $name => $divisor ) {
64            $quot = (int) ( $seconds / $divisor );
65            if ( $quot ) {
66                $human_readable .= "$quot $name";
67                $human_readable .= ( abs( $quot ) > 1 ? 's' : '' ) . ', ';
68                $seconds        -= $quot * $divisor;
69            }
70        }
71        return substr( $human_readable, 0, -2 );
72    }
73
74    /**
75     * Return debug data in the format expected by Core's Site Health Info tab.
76     *
77     * @since 7.3.0
78     *
79     * @param array $debug {
80     *     The debug information already compiled by Core.
81     *
82     *     @type string  $label        The title for this section of the debug output.
83     *     @type string  $description  Optional. A description for your information section which may contain basic HTML
84     *                                 markup: `em`, `strong` and `a` for linking to documentation or putting emphasis.
85     *     @type boolean $show_count   Optional. If set to `true` the amount of fields will be included in the title for
86     *                                 this section.
87     *     @type boolean $private      Optional. If set to `true` the section and all associated fields will be excluded
88     *                                 from the copy-paste text area.
89     *     @type array   $fields {
90     *         An associative array containing the data to be displayed.
91     *
92     *         @type string  $label    The label for this piece of information.
93     *         @type string  $value    The output that is of interest for this field.
94     *         @type boolean $private  Optional. If set to `true` the field will not be included in the copy-paste text area
95     *                                 on top of the page, allowing you to show, for example, API keys here.
96     *     }
97     * }
98     *
99     * @return array $args Debug information in the same format as the initial argument.
100     */
101    public static function core_debug_data( $debug ) {
102        $support_url = Jetpack::is_development_version()
103            ? Redirect::get_url( 'jetpack-contact-support-beta-group' )
104            : Redirect::get_url( 'jetpack-contact-support' );
105
106        $jetpack = array(
107            'jetpack' => array(
108                'label'       => __( 'Jetpack', 'jetpack' ),
109                'description' => sprintf(
110                    /* translators: %1$s is URL to jetpack.com's contact support page. %2$s accessibility text */
111                    __(
112                        '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>',
113                        'jetpack'
114                    ),
115                    esc_url( $support_url ),
116                    __( '(opens in a new tab)', 'jetpack' )
117                ),
118                'fields'      => self::debug_data(),
119            ),
120        );
121        $debug   = array_merge( $debug, $jetpack );
122        return $debug;
123    }
124
125    /**
126     * Compile and return array of debug information.
127     *
128     * @since 7.3.0
129     *
130     * @return array $args {
131     *          Associated array of arrays with the following.
132     *         @type string  $label    The label for this piece of information.
133     *         @type string  $value    The output that is of interest for this field.
134     *         @type boolean $private  Optional. Set to true if data is sensitive (API keys, etc).
135     * }
136     */
137    public static function debug_data() {
138        $debug_info = array();
139
140        /* Add various important Jetpack options */
141        $debug_info['site_id']                  = array(
142            'label'   => 'Jetpack Site ID',
143            'value'   => Jetpack_Options::get_option( 'id' ),
144            'private' => false,
145        );
146        $debug_info['ssl_cert']                 = array(
147            'label'   => 'Jetpack SSL Verfication Bypass',
148            'value'   => ( Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ) ) ? 'Yes' : 'No',
149            'private' => false,
150        );
151        $debug_info['time_diff']                = array(
152            'label'   => "Offset between Jetpack server's time and this server's time.",
153            'value'   => Jetpack_Options::get_option( 'time_diff' ),
154            'private' => false,
155        );
156        $debug_info['version_option']           = array(
157            'label'   => 'Current Jetpack Version Option',
158            'value'   => Jetpack_Options::get_option( 'version' ),
159            'private' => false,
160        );
161        $debug_info['old_version']              = array(
162            'label'   => 'Previous Jetpack Version',
163            'value'   => Jetpack_Options::get_option( 'old_version' ),
164            'private' => false,
165        );
166        $debug_info['public']                   = array(
167            'label'   => 'Jetpack Site Public',
168            'value'   => ( Jetpack_Options::get_option( 'public' ) ) ? 'Public' : 'Private',
169            'private' => false,
170        );
171        $debug_info['master_user']              = array(
172            'label'   => 'Jetpack Master User',
173            'value'   => self::human_readable_master_user(), // Only ID number and user name.
174            'private' => false,
175        );
176        $debug_info['is_offline_mode']          = array(
177            'label'   => 'Jetpack Offline Mode',
178            'value'   => ( new Status() )->is_offline_mode() ? 'on' : 'off',
179            'private' => false,
180        );
181        $debug_info['is_offline_mode_constant'] = array(
182            'label'   => 'JETPACK_DEV_DEBUG Constant',
183            'value'   => ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) ? 'on' : 'off',
184            'private' => false,
185        );
186
187        /**
188         * Token information is private, but awareness if there one is set is helpful.
189         *
190         * To balance out information vs privacy, we only display and include the "key",
191         * which is a segment of the token prior to a period within the token and is
192         * technically not private.
193         *
194         * If a token does not contain a period, then it is malformed and we report it as such.
195         */
196        $user_id    = get_current_user_id();
197        $blog_token = ( new Tokens() )->get_access_token();
198        $user_token = ( new Tokens() )->get_access_token( $user_id );
199
200        $tokenset = '';
201        if ( $blog_token ) {
202            $tokenset = 'Blog ';
203            $blog_key = substr( $blog_token->secret, 0, strpos( $blog_token->secret, '.' ) );
204            // Intentionally not translated since this is helpful when sent to Happiness.
205            $blog_key = ( $blog_key ) ? $blog_key : 'Potentially Malformed Token.';
206        }
207        if ( $user_token ) {
208            $tokenset .= 'User';
209            $user_key  = substr( $user_token->secret, 0, strpos( $user_token->secret, '.' ) );
210            // Intentionally not translated since this is helpful when sent to Happiness.
211            $user_key = ( $user_key ) ? $user_key : 'Potentially Malformed Token.';
212        }
213        if ( ! $tokenset ) {
214            $tokenset = 'None';
215        }
216
217        $debug_info['current_user'] = array(
218            'label'   => 'Current User',
219            'value'   => self::human_readable_user( $user_id ),
220            'private' => false,
221        );
222        $debug_info['tokens_set']   = array(
223            'label'   => 'Tokens defined',
224            'value'   => $tokenset,
225            'private' => false,
226        );
227        $debug_info['blog_token']   = array(
228            'label'   => 'Blog Public Key',
229            'value'   => $blog_key ?? 'Not set.',
230            'private' => false,
231        );
232        $debug_info['user_token']   = array(
233            'label'   => 'User Public Key',
234            'value'   => $user_key ?? 'Not set.',
235            'private' => false,
236        );
237
238        /** Jetpack Environmental Information */
239        $debug_info['version']       = array(
240            'label'   => 'Jetpack Version',
241            'value'   => JETPACK__VERSION,
242            'private' => false,
243        );
244        $debug_info['jp_plugin_dir'] = array(
245            'label'   => 'Jetpack Directory',
246            'value'   => JETPACK__PLUGIN_DIR,
247            'private' => false,
248        );
249        $debug_info['plan']          = array(
250            'label'   => 'Plan Type',
251            'value'   => self::what_jetpack_plan(),
252            'private' => false,
253        );
254
255        foreach ( array(
256            'HTTP_HOST',
257            'SERVER_PORT',
258            'HTTPS',
259            'GD_PHP_HANDLER',
260            'HTTP_AKAMAI_ORIGIN_HOP',
261            'HTTP_CF_CONNECTING_IP',
262            'HTTP_CLIENT_IP',
263            'HTTP_FASTLY_CLIENT_IP',
264            'HTTP_FORWARDED',
265            'HTTP_FORWARDED_FOR',
266            'HTTP_INCAP_CLIENT_IP',
267            'HTTP_TRUE_CLIENT_IP',
268            'HTTP_X_CLIENTIP',
269            'HTTP_X_CLUSTER_CLIENT_IP',
270            'HTTP_X_FORWARDED',
271            'HTTP_X_FORWARDED_FOR',
272            'HTTP_X_IP_TRAIL',
273            'HTTP_X_REAL_IP',
274            'HTTP_X_VARNISH',
275            'REMOTE_ADDR',
276        ) as $header ) {
277            if ( isset( $_SERVER[ $header ] ) ) {
278                $debug_info[ $header ] = array(
279                    'label'   => 'Server Variable ' . $header,
280                    'value'   => empty( $_SERVER[ $header ] ) ? 'false' : filter_var( wp_unslash( $_SERVER[ $header ] ) ),
281                    'private' => true, // This isn't really 'private' information, but we don't want folks to easily paste these into public forums.
282                );
283            }
284        }
285
286        $debug_info['protect_header'] = array(
287            'label'   => 'Trusted IP',
288            'value'   => wp_json_encode( get_site_option( 'trusted_ip_header' ), JSON_UNESCAPED_SLASHES ),
289            'private' => false,
290        );
291
292        /** Sync Debug Information */
293        $sync_module = Modules::get_module( 'full-sync' );
294        '@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $sync_module';
295        if ( $sync_module ) {
296            $sync_statuses              = $sync_module->get_status();
297            $human_readable_sync_status = array();
298            foreach ( $sync_statuses as $sync_status => $sync_status_value ) {
299                $human_readable_sync_status[ $sync_status ] =
300                    in_array( $sync_status, array( 'started', 'queue_finished', 'send_started', 'finished' ), true )
301                        ? gmdate( 'r', $sync_status_value ) : $sync_status_value;
302            }
303            $debug_info['full_sync'] = array(
304                'label'   => 'Full Sync Status',
305                'value'   => wp_json_encode( $human_readable_sync_status, JSON_UNESCAPED_SLASHES ),
306                'private' => false,
307            );
308        }
309
310        $queue = Sender::get_instance()->get_sync_queue();
311
312        $debug_info['sync_size'] = array(
313            'label'   => 'Sync Queue Size',
314            'value'   => $queue->size(),
315            'private' => false,
316        );
317        $debug_info['sync_lag']  = array(
318            'label'   => 'Sync Queue Lag',
319            'value'   => self::seconds_to_time( $queue->lag() ),
320            'private' => false,
321        );
322
323        $full_sync_queue = Sender::get_instance()->get_full_sync_queue();
324
325        $debug_info['full_sync_size'] = array(
326            'label'   => 'Full Sync Queue Size',
327            'value'   => $full_sync_queue->size(),
328            'private' => false,
329        );
330        $debug_info['full_sync_lag']  = array(
331            'label'   => 'Full Sync Queue Lag',
332            'value'   => self::seconds_to_time( $full_sync_queue->lag() ),
333            'private' => false,
334        );
335
336        /**
337         * IDC Information
338         *
339         * Must follow sync debug since it depends on sync functionality.
340         */
341        $idc_urls = array(
342            'home'       => Urls::home_url(),
343            'siteurl'    => Urls::site_url(),
344            'WP_HOME'    => Constants::is_defined( 'WP_HOME' ) ? Constants::get_constant( 'WP_HOME' ) : '',
345            'WP_SITEURL' => Constants::is_defined( 'WP_SITEURL' ) ? Constants::get_constant( 'WP_SITEURL' ) : '',
346        );
347
348        $debug_info['idc_urls']         = array(
349            'label'   => 'IDC URLs',
350            'value'   => wp_json_encode( $idc_urls, JSON_UNESCAPED_SLASHES ),
351            'private' => false,
352        );
353        $debug_info['idc_error_option'] = array(
354            'label'   => 'IDC Error Option',
355            'value'   => wp_json_encode( Jetpack_Options::get_option( 'sync_error_idc' ), JSON_UNESCAPED_SLASHES ),
356            'private' => false,
357        );
358        $debug_info['idc_optin']        = array(
359            'label'   => 'IDC Opt-in',
360            'value'   => Identity_Crisis::should_handle_idc(),
361            'private' => false,
362        );
363
364        // @todo -- Add testing results?
365        $cxn_tests               = new Jetpack_Cxn_Tests();
366        $debug_info['cxn_tests'] = array(
367            'label'   => 'Connection Tests',
368            'value'   => '',
369            'private' => false,
370        );
371        if ( $cxn_tests->pass() ) {
372            $debug_info['cxn_tests']['value'] = 'All Pass.';
373        } else {
374            $debug_info['cxn_tests']['value'] = wp_json_encode( $cxn_tests->list_fails(), JSON_UNESCAPED_SLASHES );
375        }
376
377        return $debug_info;
378    }
379
380    /**
381     * Returns a human readable string for which user is the master user.
382     *
383     * @return string
384     */
385    private static function human_readable_master_user() {
386        $master_user = Jetpack_Options::get_option( 'master_user' );
387
388        if ( ! $master_user ) {
389            return __( 'No master user set.', 'jetpack' );
390        }
391
392        $user = new WP_User( $master_user );
393
394        if ( ! $user->exists() ) {
395            return __( 'Master user no longer exists. Please disconnect and reconnect Jetpack.', 'jetpack' );
396        }
397
398        return self::human_readable_user( $user );
399    }
400
401    /**
402     * Return human readable string for a given user object.
403     *
404     * @param WP_User|int $user Object or ID.
405     *
406     * @return string
407     */
408    private static function human_readable_user( $user ) {
409        $user = new WP_User( $user );
410
411        return sprintf( '#%1$d %2$s', $user->ID, $user->user_login ); // Format: "#1 username".
412    }
413}