Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 182
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Cxn_Test_Base
0.00% covered (danger)
0.00%
0 / 182
0.00% covered (danger)
0.00%
0 / 17
4830
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_test
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 list_tests
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
90
 run_test
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 run_tests
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 raw_results
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 pass
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 list_fails
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 passing_test
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 skipped_test
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 informational_test
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 failing_test
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 test_result_defaults
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 output_results_for_cli
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 output_results_for_core_async_site_health
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
 output_fails_as_wp_error
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 encrypt_string_for_wpcom
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Base class for Jetpack's debugging tests.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Status;
9
10/**
11 * Jetpack Connection Testing
12 *
13 * Framework for various "unit tests" against the Jetpack connection.
14 *
15 * Individual tests should be added to the class-jetpack-cxn-tests.php file.
16 *
17 * @author Brandon Kraft
18 * @package automattic/jetpack
19 */
20
21/**
22 * "Unit Tests" for the Jetpack connection.
23 *
24 * @since 7.1.0
25 */
26class Jetpack_Cxn_Test_Base {
27
28    /**
29     * Tests to run on the Jetpack connection.
30     *
31     * @var array $tests
32     */
33    protected $tests = array();
34
35    /**
36     * Results of the Jetpack connection tests.
37     *
38     * @var array $results
39     */
40    protected $results = array();
41
42    /**
43     * Status of the testing suite.
44     *
45     * Used internally to determine if a test should be skipped since the tests are already failing. Assume passing.
46     *
47     * @var bool $pass
48     */
49    protected $pass = true;
50
51    /**
52     * Jetpack_Cxn_Test constructor.
53     */
54    public function __construct() {
55        $this->tests   = array();
56        $this->results = array();
57    }
58
59    /**
60     * Adds a new test to the Jetpack Connection Testing suite.
61     *
62     * @since 7.1.0
63     * @since 7.3.0 Adds name parameter and returns WP_Error on failure.
64     *
65     * @param callable $callable Test to add to queue.
66     * @param string   $name Unique name for the test.
67     * @param string   $type   Optional. Core Site Health type: 'direct' if test can be run during initial load or 'async' if test should run async.
68     * @param array    $groups Optional. Testing groups to add test to.
69     *
70     * @return mixed True if successfully added. WP_Error on failure.
71     */
72    public function add_test( $callable, $name, $type = 'direct', $groups = array( 'default' ) ) {
73        if ( is_array( $name ) ) {
74            // Pre-7.3.0 method passed the $groups parameter here.
75            return new WP_Error( __( 'add_test arguments changed in 7.3.0. Please reference inline documentation.', 'jetpack' ) );
76        }
77        if ( array_key_exists( $name, $this->tests ) ) {
78            return new WP_Error( __( 'Test names must be unique.', 'jetpack' ) );
79        }
80        if ( ! is_callable( $callable ) ) {
81            return new WP_Error( __( 'Tests must be valid PHP callables.', 'jetpack' ) );
82        }
83
84        $this->tests[ $name ] = array(
85            'name'  => $name,
86            'test'  => $callable,
87            'group' => $groups,
88            'type'  => $type,
89        );
90        return true;
91    }
92
93    /**
94     * Lists all tests to run.
95     *
96     * @since 7.3.0
97     *
98     * @param string $type Optional. Core Site Health type: 'direct' or 'async'. All by default.
99     * @param string $group Optional. A specific testing group. All by default.
100     *
101     * @return array $tests Array of tests with test information.
102     */
103    public function list_tests( $type = 'all', $group = 'all' ) {
104        if ( ! ( 'all' === $type || 'direct' === $type || 'async' === $type ) ) {
105            _doing_it_wrong( 'Jetpack_Cxn_Test_Base->list_tests', 'Type must be all, direct, or async', '7.3.0' );
106        }
107
108        $tests = array();
109        foreach ( $this->tests as $name => $value ) {
110            // Get all valid tests by group staged.
111            if ( 'all' === $group || $group === $value['group'] ) {
112                $tests[ $name ] = $value;
113            }
114
115            // Next filter out any that do not match the type.
116            if ( 'all' !== $type && $type !== $value['type'] ) {
117                unset( $tests[ $name ] );
118            }
119        }
120
121        return $tests;
122    }
123
124    /**
125     * Run a specific test.
126     *
127     * @since 7.3.0
128     *
129     * @param string $name Name of test.
130     *
131     * @return mixed $result Test result array or WP_Error if invalid name. {
132     * @type string $name Test name
133     * @type mixed  $pass True if passed, false if failed, 'skipped' if skipped.
134     * @type string $message Human-readable test result message.
135     * @type string $resolution Human-readable resolution steps.
136     * }
137     */
138    public function run_test( $name ) {
139        if ( array_key_exists( $name, $this->tests ) ) {
140            return call_user_func( $this->tests[ $name ]['test'] );
141        }
142        return new WP_Error( __( 'There is no test by that name: ', 'jetpack' ) . $name );
143    }
144
145    /**
146     * Runs the Jetpack connection suite.
147     */
148    public function run_tests() {
149        foreach ( $this->tests as $test ) {
150            $result          = call_user_func( $test['test'] );
151            $result['group'] = $test['group'];
152            $result['type']  = $test['type'];
153            $this->results[] = $result;
154            if ( false === $result['pass'] ) {
155                $this->pass = false;
156            }
157        }
158    }
159
160    /**
161     * Returns the full results array.
162     *
163     * @since 7.1.0
164     * @since 7.3.0 Add 'type'
165     *
166     * @param string $type  Test type, async or direct.
167     * @param string $group Testing group whose results we want. Defaults to all tests.
168     * @return array Array of test results.
169     */
170    public function raw_results( $type = 'all', $group = 'all' ) {
171        if ( ! $this->results ) {
172            $this->run_tests();
173        }
174
175        $results = $this->results;
176
177        if ( 'all' !== $group ) {
178            foreach ( $results as $test => $result ) {
179                if ( ! in_array( $group, $result['group'], true ) ) {
180                    unset( $results[ $test ] );
181                }
182            }
183        }
184
185        if ( 'all' !== $type ) {
186            foreach ( $results as $test => $result ) {
187                if ( $type !== $result['type'] ) {
188                    unset( $results[ $test ] );
189                }
190            }
191        }
192
193        return $results;
194    }
195
196    /**
197     * Returns the status of the connection suite.
198     *
199     * @since 7.1.0
200     * @since 7.3.0 Add 'type'
201     *
202     * @param string $type  Test type, async or direct. Optional, direct all tests.
203     * @param string $group Testing group to check status of. Optional, default all tests.
204     *
205     * @return true|array True if all tests pass. Array of failed tests.
206     */
207    public function pass( $type = 'all', $group = 'all' ) {
208        $results = $this->raw_results( $type, $group );
209
210        foreach ( $results as $result ) {
211            // 'pass' could be true, false, or 'skipped'. We only want false.
212            if ( isset( $result['pass'] ) && false === $result['pass'] ) {
213                return false;
214            }
215        }
216
217        return true;
218    }
219
220    /**
221     * Return array of failed test messages.
222     *
223     * @since 7.1.0
224     * @since 7.3.0 Add 'type'
225     *
226     * @param string $type  Test type, direct or async.
227     * @param string $group Testing group whose failures we want. Defaults to "all".
228     *
229     * @return false|array False if no failed tests. Otherwise, array of failed tests.
230     */
231    public function list_fails( $type = 'all', $group = 'all' ) {
232        $results = $this->raw_results( $type, $group );
233
234        foreach ( $results as $test => $result ) {
235            // We do not want tests that passed or ones that are misconfigured (no pass status or no failure message).
236            if ( ! isset( $result['pass'] ) || false !== $result['pass'] || ! isset( $result['short_description'] ) ) {
237                unset( $results[ $test ] );
238            }
239        }
240
241        return $results;
242    }
243
244    /**
245     * Helper function to return consistent responses for a passing test.
246     * Possible Args:
247     * - name: string The raw method name that runs the test. Default 'unnamed_test'.
248     * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
249     * - short_description: bool|string A brief, non-html description that will appear in CLI results. Default 'Test passed!'.
250     * - long_description: bool|string An html description that will appear in the site health page. Default false.
251     * - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
252     * - action: bool|string A URL for the recommended action. Default: false
253     * - action_label: bool|string The label for the recommended action. Default: false
254     * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
255     *
256     * @param array $args Arguments to override defaults.
257     *
258     * @return array Test results.
259     */
260    public static function passing_test( $args ) {
261        $defaults                      = self::test_result_defaults();
262        $defaults['short_description'] = __( 'Test passed!', 'jetpack' );
263
264        $args = wp_parse_args( $args, $defaults );
265
266        $args['pass'] = true;
267
268        return $args;
269    }
270
271    /**
272     * Helper function to return consistent responses for a skipped test.
273     * Possible Args:
274     * - name: string The raw method name that runs the test. Default unnamed_test.
275     * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
276     * - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default false.
277     * - long_description: bool|string An html description that will appear in the site health page. Default false.
278     * - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
279     * - action: bool|string A URL for the recommended action. Default: false
280     * - action_label: bool|string The label for the recommended action. Default: false
281     * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
282     *
283     * @param array $args Arguments to override defaults.
284     *
285     * @return array Test results.
286     */
287    public static function skipped_test( $args = array() ) {
288        $args = wp_parse_args(
289            $args,
290            self::test_result_defaults()
291        );
292
293        $args['pass'] = 'skipped';
294
295        return $args;
296    }
297
298    /**
299     * Helper function to return consistent responses for an informational test.
300     * Possible Args:
301     * - name: string The raw method name that runs the test. Default unnamed_test.
302     * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
303     * - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default false.
304     * - long_description: bool|string An html description that will appear in the site health page. Default false.
305     * - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
306     * - action: bool|string A URL for the recommended action. Default: false
307     * - action_label: bool|string The label for the recommended action. Default: false
308     * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
309     *
310     * @param array $args Arguments to override defaults.
311     *
312     * @return array Test results.
313     */
314    public static function informational_test( $args = array() ) {
315        $args = wp_parse_args(
316            $args,
317            self::test_result_defaults()
318        );
319
320        $args['pass'] = 'informational';
321
322        return $args;
323    }
324
325    /**
326     * Helper function to return consistent responses for a failing test.
327     * Possible Args:
328     * - name: string The raw method name that runs the test. Default unnamed_test.
329     * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
330     * - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default 'Test failed!'.
331     * - long_description: bool|string An html description that will appear in the site health page. Default false.
332     * - severity: bool|string 'critical', 'recommended', or 'good'. Default: 'critical'.
333     * - action: bool|string A URL for the recommended action. Default: false.
334     * - action_label: bool|string The label for the recommended action. Default: false.
335     * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
336     *
337     * @since 7.1.0
338     *
339     * @param array $args Arguments to override defaults.
340     *
341     * @return array Test results.
342     */
343    public static function failing_test( $args ) {
344        $defaults                      = self::test_result_defaults();
345        $defaults['short_description'] = __( 'Test failed!', 'jetpack' );
346        $defaults['severity']          = 'critical';
347
348        $args = wp_parse_args( $args, $defaults );
349
350        $args['pass'] = false;
351
352        return $args;
353    }
354
355    /**
356     * Provides defaults for test arguments.
357     *
358     * @since 8.5.0
359     *
360     * @return array Result defaults.
361     */
362    private static function test_result_defaults() {
363        return array(
364            'name'                => 'unnamed_test',
365            'label'               => false,
366            'short_description'   => false,
367            'long_description'    => false,
368            'severity'            => false,
369            'action'              => false,
370            'action_label'        => false,
371            'show_in_site_health' => true,
372        );
373    }
374
375    /**
376     * Provide WP_CLI friendly testing results.
377     *
378     * @since 7.1.0
379     * @since 7.3.0 Add 'type'
380     *
381     * @param string $type  Test type, direct or async.
382     * @param string $group Testing group whose results we are outputting. Default all tests.
383     */
384    public function output_results_for_cli( $type = 'all', $group = 'all' ) {
385        if ( defined( 'WP_CLI' ) && WP_CLI ) {
386            if ( ( new Status() )->is_offline_mode() ) {
387                WP_CLI::line( __( 'Jetpack is in Offline Mode:', 'jetpack' ) );
388                WP_CLI::line( Jetpack::development_mode_trigger_text() );
389            }
390            WP_CLI::line( __( 'TEST RESULTS:', 'jetpack' ) );
391            foreach ( $this->raw_results( $group ) as $test ) {
392                if ( true === $test['pass'] ) {
393                    WP_CLI::log( WP_CLI::colorize( '%gPassed:%n  ' . $test['name'] ) );
394                } elseif ( 'skipped' === $test['pass'] ) {
395                    WP_CLI::log( WP_CLI::colorize( '%ySkipped:%n ' . $test['name'] ) );
396                    if ( $test['short_description'] ) {
397                        WP_CLI::log( '         ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
398                    }
399                } elseif ( 'informational' === $test['pass'] ) {
400                    WP_CLI::log( WP_CLI::colorize( '%yInfo:%n    ' . $test['name'] ) );
401                    if ( $test['short_description'] ) {
402                        WP_CLI::log( '         ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
403                    }
404                } else { // Failed.
405                    WP_CLI::log( WP_CLI::colorize( '%rFailed:%n  ' . $test['name'] ) );
406                    WP_CLI::log( '         ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
407                }
408            }
409        }
410    }
411
412    /**
413     * Output results of failures in format expected by Core's Site Health tool for async tests.
414     *
415     * Specifically not asking for a testing group since we're opinionated that Site Heath should see all.
416     *
417     * @since 7.3.0
418     *
419     * @return array Array of test results
420     */
421    public function output_results_for_core_async_site_health() {
422        $result = array(
423            'label'       => __( 'Jetpack passed all async tests.', 'jetpack' ),
424            'status'      => 'good',
425            'badge'       => array(
426                'label' => __( 'Jetpack', 'jetpack' ),
427                'color' => 'green',
428            ),
429            'description' => sprintf(
430                '<p>%s</p>',
431                __( "Jetpack's async local testing suite passed all tests!", 'jetpack' )
432            ),
433            'actions'     => '',
434            'test'        => 'jetpack_debugger_local_testing_suite_core',
435        );
436
437        if ( $this->pass() ) {
438            return $result;
439        }
440
441        $fails = $this->list_fails( 'async' );
442        $error = false;
443        foreach ( $fails as $fail ) {
444            if ( ! $error ) {
445                $error                 = true;
446                $result['label']       = $fail['message'];
447                $result['status']      = $fail['severity'];
448                $result['description'] = sprintf(
449                    '<p>%s</p>',
450                    $fail['resolution']
451                );
452                if ( ! empty( $fail['action'] ) ) {
453                    $result['actions'] = sprintf(
454                        '<a class="button button-primary" href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
455                        esc_url( $fail['action'] ),
456                        __( 'Resolve', 'jetpack' ),
457                        /* translators: accessibility text */
458                        __( '(opens in a new tab)', 'jetpack' )
459                    );
460                }
461            } else {
462                $result['description'] .= sprintf(
463                    '<p>%s</p>',
464                    __( 'There was another problem:', 'jetpack' )
465                ) . ' ' . $fail['message'] . ': ' . $fail['resolution'];
466                if ( 'critical' === $fail['severity'] ) { // In case the initial failure is only "recommended".
467                    $result['status'] = 'critical';
468                }
469            }
470        }
471
472        return $result;
473    }
474
475    /**
476     * Provide single WP Error instance of all failures.
477     *
478     * @since 7.1.0
479     * @since 7.3.0 Add 'type'
480     *
481     * @param string $type  Test type, direct or async.
482     * @param string $group Testing group whose failures we want converted. Default all tests.
483     *
484     * @return WP_Error|false WP_Error with all failed tests or false if there were no failures.
485     */
486    public function output_fails_as_wp_error( $type = 'all', $group = 'all' ) {
487        if ( $this->pass( $group ) ) {
488            return false;
489        }
490        $fails = $this->list_fails( $type, $group );
491        $error = false;
492
493        foreach ( $fails as $result ) {
494            $code    = 'failed_' . $result['name'];
495            $message = $result['short_description'];
496            $data    = array(
497                'resolution' => $result['action'] ?
498                    $result['action_label'] . ' :' . $result['action'] :
499                    '',
500            );
501            if ( ! $error ) {
502                $error = new WP_Error( $code, $message, $data );
503            } else {
504                $error->add( $code, $message, $data );
505            }
506        }
507
508        return $error;
509    }
510
511    /**
512     * Encrypt data for sending to WordPress.com.
513     *
514     * @param string $data Data to encrypt with the WP.com Public Key.
515     *
516     * @return false|array False if functionality not available. Array of encrypted data, encryption key.
517     */
518    public function encrypt_string_for_wpcom( $data ) {
519        $return = false;
520        if ( ! function_exists( 'openssl_get_publickey' ) || ! function_exists( 'openssl_seal' ) ) {
521            return $return;
522        }
523
524        $public_key = openssl_get_publickey( JETPACK__DEBUGGER_PUBLIC_KEY );
525
526        // Select the first allowed cipher method.
527        $allowed_methods = array( 'aes-256-ctr', 'aes-256-cbc' );
528        $methods         = array_intersect( $allowed_methods, openssl_get_cipher_methods() );
529        $method          = array_shift( $methods );
530
531        $iv = '';
532        if ( $public_key && $method && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ), $method, $iv ) ) {
533            // We are returning base64-encoded values to ensure they're characters we can use in JSON responses without issue.
534            $return = array(
535                'data'   => base64_encode( $encrypted_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
536                'key'    => base64_encode( $env_key[0] ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
537                'iv'     => base64_encode( $iv ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
538                'cipher' => strtoupper( $method ),
539            );
540        }
541
542        // openssl_free_key was deprecated as no longer needed in PHP 8.0+. Can remove when PHP 8.0 is our minimum. (lol).
543        if ( PHP_VERSION_ID < 80000 ) {
544            openssl_free_key( $public_key ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.openssl_free_keyDeprecated, Generic.PHP.DeprecatedFunctions.Deprecated
545        }
546
547        return $return;
548    }
549}