| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- <?php
- /**
- * Base class for Jetpack's debugging tests.
- *
- * @package automattic/jetpack
- */
- use Automattic\Jetpack\Status;
- /**
- * Jetpack Connection Testing
- *
- * Framework for various "unit tests" against the Jetpack connection.
- *
- * Individual tests should be added to the class-jetpack-cxn-tests.php file.
- *
- * @author Brandon Kraft
- * @package automattic/jetpack
- */
- /**
- * "Unit Tests" for the Jetpack connection.
- *
- * @since 7.1.0
- */
- class Jetpack_Cxn_Test_Base {
- /**
- * Tests to run on the Jetpack connection.
- *
- * @var array $tests
- */
- protected $tests = array();
- /**
- * Results of the Jetpack connection tests.
- *
- * @var array $results
- */
- protected $results = array();
- /**
- * Status of the testing suite.
- *
- * Used internally to determine if a test should be skipped since the tests are already failing. Assume passing.
- *
- * @var bool $pass
- */
- protected $pass = true;
- /**
- * Jetpack_Cxn_Test constructor.
- */
- public function __construct() {
- $this->tests = array();
- $this->results = array();
- }
- /**
- * Adds a new test to the Jetpack Connection Testing suite.
- *
- * @since 7.1.0
- * @since 7.3.0 Adds name parameter and returns WP_Error on failure.
- *
- * @param callable $callable Test to add to queue.
- * @param string $name Unique name for the test.
- * @param string $type Optional. Core Site Health type: 'direct' if test can be run during initial load or 'async' if test should run async.
- * @param array $groups Optional. Testing groups to add test to.
- *
- * @return mixed True if successfully added. WP_Error on failure.
- */
- public function add_test( $callable, $name, $type = 'direct', $groups = array( 'default' ) ) {
- if ( is_array( $name ) ) {
- // Pre-7.3.0 method passed the $groups parameter here.
- return new WP_Error( __( 'add_test arguments changed in 7.3.0. Please reference inline documentation.', 'jetpack' ) );
- }
- if ( array_key_exists( $name, $this->tests ) ) {
- return new WP_Error( __( 'Test names must be unique.', 'jetpack' ) );
- }
- if ( ! is_callable( $callable ) ) {
- return new WP_Error( __( 'Tests must be valid PHP callables.', 'jetpack' ) );
- }
- $this->tests[ $name ] = array(
- 'name' => $name,
- 'test' => $callable,
- 'group' => $groups,
- 'type' => $type,
- );
- return true;
- }
- /**
- * Lists all tests to run.
- *
- * @since 7.3.0
- *
- * @param string $type Optional. Core Site Health type: 'direct' or 'async'. All by default.
- * @param string $group Optional. A specific testing group. All by default.
- *
- * @return array $tests Array of tests with test information.
- */
- public function list_tests( $type = 'all', $group = 'all' ) {
- if ( ! ( 'all' === $type || 'direct' === $type || 'async' === $type ) ) {
- _doing_it_wrong( 'Jetpack_Cxn_Test_Base->list_tests', 'Type must be all, direct, or async', '7.3.0' );
- }
- $tests = array();
- foreach ( $this->tests as $name => $value ) {
- // Get all valid tests by group staged.
- if ( 'all' === $group || $group === $value['group'] ) {
- $tests[ $name ] = $value;
- }
- // Next filter out any that do not match the type.
- if ( 'all' !== $type && $type !== $value['type'] ) {
- unset( $tests[ $name ] );
- }
- }
- return $tests;
- }
- /**
- * Run a specific test.
- *
- * @since 7.3.0
- *
- * @param string $name Name of test.
- *
- * @return mixed $result Test result array or WP_Error if invalid name. {
- * @type string $name Test name
- * @type mixed $pass True if passed, false if failed, 'skipped' if skipped.
- * @type string $message Human-readable test result message.
- * @type string $resolution Human-readable resolution steps.
- * }
- */
- public function run_test( $name ) {
- if ( array_key_exists( $name, $this->tests ) ) {
- return call_user_func( $this->tests[ $name ]['test'] );
- }
- return new WP_Error( __( 'There is no test by that name: ', 'jetpack' ) . $name );
- }
- /**
- * Runs the Jetpack connection suite.
- */
- public function run_tests() {
- foreach ( $this->tests as $test ) {
- $result = call_user_func( $test['test'] );
- $result['group'] = $test['group'];
- $result['type'] = $test['type'];
- $this->results[] = $result;
- if ( false === $result['pass'] ) {
- $this->pass = false;
- }
- }
- }
- /**
- * Returns the full results array.
- *
- * @since 7.1.0
- * @since 7.3.0 Add 'type'
- *
- * @param string $type Test type, async or direct.
- * @param string $group Testing group whose results we want. Defaults to all tests.
- * @return array Array of test results.
- */
- public function raw_results( $type = 'all', $group = 'all' ) {
- if ( ! $this->results ) {
- $this->run_tests();
- }
- $results = $this->results;
- if ( 'all' !== $group ) {
- foreach ( $results as $test => $result ) {
- if ( ! in_array( $group, $result['group'], true ) ) {
- unset( $results[ $test ] );
- }
- }
- }
- if ( 'all' !== $type ) {
- foreach ( $results as $test => $result ) {
- if ( $type !== $result['type'] ) {
- unset( $results[ $test ] );
- }
- }
- }
- return $results;
- }
- /**
- * Returns the status of the connection suite.
- *
- * @since 7.1.0
- * @since 7.3.0 Add 'type'
- *
- * @param string $type Test type, async or direct. Optional, direct all tests.
- * @param string $group Testing group to check status of. Optional, default all tests.
- *
- * @return true|array True if all tests pass. Array of failed tests.
- */
- public function pass( $type = 'all', $group = 'all' ) {
- $results = $this->raw_results( $type, $group );
- foreach ( $results as $result ) {
- // 'pass' could be true, false, or 'skipped'. We only want false.
- if ( isset( $result['pass'] ) && false === $result['pass'] ) {
- return false;
- }
- }
- return true;
- }
- /**
- * Return array of failed test messages.
- *
- * @since 7.1.0
- * @since 7.3.0 Add 'type'
- *
- * @param string $type Test type, direct or async.
- * @param string $group Testing group whose failures we want. Defaults to "all".
- *
- * @return false|array False if no failed tests. Otherwise, array of failed tests.
- */
- public function list_fails( $type = 'all', $group = 'all' ) {
- $results = $this->raw_results( $type, $group );
- foreach ( $results as $test => $result ) {
- // We do not want tests that passed or ones that are misconfigured (no pass status or no failure message).
- if ( ! isset( $result['pass'] ) || false !== $result['pass'] || ! isset( $result['short_description'] ) ) {
- unset( $results[ $test ] );
- }
- }
- return $results;
- }
- /**
- * Helper function to return consistent responses for a passing test.
- * Possible Args:
- * - name: string The raw method name that runs the test. Default 'unnamed_test'.
- * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
- * - short_description: bool|string A brief, non-html description that will appear in CLI results. Default 'Test passed!'.
- * - long_description: bool|string An html description that will appear in the site health page. Default false.
- * - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
- * - action: bool|string A URL for the recommended action. Default: false
- * - action_label: bool|string The label for the recommended action. Default: false
- * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
- *
- * @param array $args Arguments to override defaults.
- *
- * @return array Test results.
- */
- public static function passing_test( $args ) {
- $defaults = self::test_result_defaults();
- $defaults['short_description'] = __( 'Test passed!', 'jetpack' );
- $args = wp_parse_args( $args, $defaults );
- $args['pass'] = true;
- return $args;
- }
- /**
- * Helper function to return consistent responses for a skipped test.
- * Possible Args:
- * - name: string The raw method name that runs the test. Default unnamed_test.
- * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
- * - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default false.
- * - long_description: bool|string An html description that will appear in the site health page. Default false.
- * - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
- * - action: bool|string A URL for the recommended action. Default: false
- * - action_label: bool|string The label for the recommended action. Default: false
- * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
- *
- * @param array $args Arguments to override defaults.
- *
- * @return array Test results.
- */
- public static function skipped_test( $args = array() ) {
- $args = wp_parse_args(
- $args,
- self::test_result_defaults()
- );
- $args['pass'] = 'skipped';
- return $args;
- }
- /**
- * Helper function to return consistent responses for an informational test.
- * Possible Args:
- * - name: string The raw method name that runs the test. Default unnamed_test.
- * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
- * - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default false.
- * - long_description: bool|string An html description that will appear in the site health page. Default false.
- * - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
- * - action: bool|string A URL for the recommended action. Default: false
- * - action_label: bool|string The label for the recommended action. Default: false
- * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
- *
- * @param array $args Arguments to override defaults.
- *
- * @return array Test results.
- */
- public static function informational_test( $args = array() ) {
- $args = wp_parse_args(
- $args,
- self::test_result_defaults()
- );
- $args['pass'] = 'informational';
- return $args;
- }
- /**
- * Helper function to return consistent responses for a failing test.
- * Possible Args:
- * - name: string The raw method name that runs the test. Default unnamed_test.
- * - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
- * - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default 'Test failed!'.
- * - long_description: bool|string An html description that will appear in the site health page. Default false.
- * - severity: bool|string 'critical', 'recommended', or 'good'. Default: 'critical'.
- * - action: bool|string A URL for the recommended action. Default: false.
- * - action_label: bool|string The label for the recommended action. Default: false.
- * - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
- *
- * @since 7.1.0
- *
- * @param array $args Arguments to override defaults.
- *
- * @return array Test results.
- */
- public static function failing_test( $args ) {
- $defaults = self::test_result_defaults();
- $defaults['short_description'] = __( 'Test failed!', 'jetpack' );
- $defaults['severity'] = 'critical';
- $args = wp_parse_args( $args, $defaults );
- $args['pass'] = false;
- return $args;
- }
- /**
- * Provides defaults for test arguments.
- *
- * @since 8.5.0
- *
- * @return array Result defaults.
- */
- private static function test_result_defaults() {
- return array(
- 'name' => 'unnamed_test',
- 'label' => false,
- 'short_description' => false,
- 'long_description' => false,
- 'severity' => false,
- 'action' => false,
- 'action_label' => false,
- 'show_in_site_health' => true,
- );
- }
- /**
- * Provide WP_CLI friendly testing results.
- *
- * @since 7.1.0
- * @since 7.3.0 Add 'type'
- *
- * @param string $type Test type, direct or async.
- * @param string $group Testing group whose results we are outputting. Default all tests.
- */
- public function output_results_for_cli( $type = 'all', $group = 'all' ) {
- if ( defined( 'WP_CLI' ) && WP_CLI ) {
- if ( ( new Status() )->is_offline_mode() ) {
- WP_CLI::line( __( 'Jetpack is in Offline Mode:', 'jetpack' ) );
- WP_CLI::line( Jetpack::development_mode_trigger_text() );
- }
- WP_CLI::line( __( 'TEST RESULTS:', 'jetpack' ) );
- foreach ( $this->raw_results( $group ) as $test ) {
- if ( true === $test['pass'] ) {
- WP_CLI::log( WP_CLI::colorize( '%gPassed:%n ' . $test['name'] ) );
- } elseif ( 'skipped' === $test['pass'] ) {
- WP_CLI::log( WP_CLI::colorize( '%ySkipped:%n ' . $test['name'] ) );
- if ( $test['short_description'] ) {
- WP_CLI::log( ' ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
- }
- } elseif ( 'informational' === $test['pass'] ) {
- WP_CLI::log( WP_CLI::colorize( '%yInfo:%n ' . $test['name'] ) );
- if ( $test['short_description'] ) {
- WP_CLI::log( ' ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
- }
- } else { // Failed.
- WP_CLI::log( WP_CLI::colorize( '%rFailed:%n ' . $test['name'] ) );
- WP_CLI::log( ' ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
- }
- }
- }
- }
- /**
- * Output results of failures in format expected by Core's Site Health tool for async tests.
- *
- * Specifically not asking for a testing group since we're opinionated that Site Heath should see all.
- *
- * @since 7.3.0
- *
- * @return array Array of test results
- */
- public function output_results_for_core_async_site_health() {
- $result = array(
- 'label' => __( 'Jetpack passed all async tests.', 'jetpack' ),
- 'status' => 'good',
- 'badge' => array(
- 'label' => __( 'Jetpack', 'jetpack' ),
- 'color' => 'green',
- ),
- 'description' => sprintf(
- '<p>%s</p>',
- __( "Jetpack's async local testing suite passed all tests!", 'jetpack' )
- ),
- 'actions' => '',
- 'test' => 'jetpack_debugger_local_testing_suite_core',
- );
- if ( $this->pass() ) {
- return $result;
- }
- $fails = $this->list_fails( 'async' );
- $error = false;
- foreach ( $fails as $fail ) {
- if ( ! $error ) {
- $error = true;
- $result['label'] = $fail['message'];
- $result['status'] = $fail['severity'];
- $result['description'] = sprintf(
- '<p>%s</p>',
- $fail['resolution']
- );
- if ( ! empty( $fail['action'] ) ) {
- $result['actions'] = sprintf(
- '<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>',
- esc_url( $fail['action'] ),
- __( 'Resolve', 'jetpack' ),
- /* translators: accessibility text */
- __( '(opens in a new tab)', 'jetpack' )
- );
- }
- } else {
- $result['description'] .= sprintf(
- '<p>%s</p>',
- __( 'There was another problem:', 'jetpack' )
- ) . ' ' . $fail['message'] . ': ' . $fail['resolution'];
- if ( 'critical' === $fail['severity'] ) { // In case the initial failure is only "recommended".
- $result['status'] = 'critical';
- }
- }
- }
- return $result;
- }
- /**
- * Provide single WP Error instance of all failures.
- *
- * @since 7.1.0
- * @since 7.3.0 Add 'type'
- *
- * @param string $type Test type, direct or async.
- * @param string $group Testing group whose failures we want converted. Default all tests.
- *
- * @return WP_Error|false WP_Error with all failed tests or false if there were no failures.
- */
- public function output_fails_as_wp_error( $type = 'all', $group = 'all' ) {
- if ( $this->pass( $group ) ) {
- return false;
- }
- $fails = $this->list_fails( $type, $group );
- $error = false;
- foreach ( $fails as $result ) {
- $code = 'failed_' . $result['name'];
- $message = $result['short_description'];
- $data = array(
- 'resolution' => $result['action'] ?
- $result['action_label'] . ' :' . $result['action'] :
- '',
- );
- if ( ! $error ) {
- $error = new WP_Error( $code, $message, $data );
- } else {
- $error->add( $code, $message, $data );
- }
- }
- return $error;
- }
- /**
- * Encrypt data for sending to WordPress.com.
- *
- * @todo When PHP minimum is 5.3+, add cipher detection to use an agreed better cipher than RC4. RC4 should be the last resort.
- *
- * @param string $data Data to encrypt with the WP.com Public Key.
- *
- * @return false|array False if functionality not available. Array of encrypted data, encryption key.
- */
- public function encrypt_string_for_wpcom( $data ) {
- $return = false;
- if ( ! function_exists( 'openssl_get_publickey' ) || ! function_exists( 'openssl_seal' ) ) {
- return $return;
- }
- $public_key = openssl_get_publickey( JETPACK__DEBUGGER_PUBLIC_KEY );
- if ( $public_key && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ), 'RC4' ) ) {
- // We are returning base64-encoded values to ensure they're characters we can use in JSON responses without issue.
- $return = array(
- 'data' => base64_encode( $encrypted_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
- 'key' => base64_encode( $env_key[0] ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
- 'cipher' => 'RC4', // When Jetpack's minimum WP version is at PHP 5.3+, we will add in detecting and using a stronger one.
- );
- }
- // openssl_free_key was deprecated as no longer needed in PHP 8.0+. Can remove when PHP 8.0 is our minimum. (lol).
- if ( PHP_VERSION_ID < 80000 ) {
- openssl_free_key( $public_key ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.openssl_free_keyDeprecated
- }
- return $return;
- }
- }
|