This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,549 @@
<?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.
*
* @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 );
// Select the first allowed cipher method.
$allowed_methods = array( 'aes-256-ctr', 'aes-256-cbc' );
$methods = array_intersect( $allowed_methods, openssl_get_cipher_methods() );
$method = array_shift( $methods );
$iv = '';
if ( $public_key && $method && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ), $method, $iv ) ) {
// 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
'iv' => base64_encode( $iv ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
'cipher' => strtoupper( $method ),
);
}
// 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, Generic.PHP.DeprecatedFunctions.Deprecated
}
return $return;
}
}
@@ -0,0 +1,815 @@
<?php
/**
* Collection of tests to run on the Jetpack connection locally.
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Tokens;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Sync\Health as Sync_Health;
use Automattic\Jetpack\Sync\Settings as Sync_Settings;
/**
* Class Jetpack_Cxn_Tests contains all of the actual tests.
*/
class Jetpack_Cxn_Tests extends Jetpack_Cxn_Test_Base {
/**
* Jetpack_Cxn_Tests constructor.
*/
public function __construct() {
parent::__construct();
$methods = get_class_methods( 'Jetpack_Cxn_Tests' );
foreach ( $methods as $method ) {
if ( ! str_contains( $method, 'test__' ) ) {
continue;
}
$this->add_test( array( $this, $method ), $method, 'direct' );
}
/**
* Fires after loading default Jetpack Connection tests.
*
* @since 7.1.0
* @since 8.3.0 Passes the Jetpack_Cxn_Tests instance.
*/
do_action( 'jetpack_connection_tests_loaded', $this );
/**
* Determines if the WP.com testing suite should be included.
*
* @since 7.1.0
* @since 8.1.0 Default false.
*
* @param bool $run_test To run the WP.com testing suite. Default false.
*/
if ( apply_filters( 'jetpack_debugger_run_self_test', false ) ) {
/**
* Intentionally added last as it checks for an existing failure state before attempting.
* Generally, any failed location condition would result in the WP.com check to fail too, so
* we will skip it to avoid confusing error messages.
*
* Note: This really should be an 'async' test.
*/
$this->add_test( array( $this, 'last__wpcom_self_test' ), 'test__wpcom_self_test', 'direct' );
}
}
/**
* Helper function to look up the expected master user and return the local WP_User.
*
* @return WP_User Jetpack's expected master user.
*/
protected function helper_retrieve_local_master_user() {
$master_user = Jetpack_Options::get_option( 'master_user' );
return new WP_User( $master_user );
}
/**
* Is Jetpack even connected and supposed to be talking to WP.com?
*/
protected function helper_is_jetpack_connected() {
return Jetpack::is_connection_ready() && ! ( new Status() )->is_offline_mode();
}
/**
* Retrieve the `blog_token` if it exists.
*
* @return object|false
*/
protected function helper_get_blog_token() {
return ( new Tokens() )->get_access_token();
}
/**
* Returns a support url based on using a development version.
*/
protected function helper_get_support_url() {
return Jetpack::is_development_version()
? Redirect::get_url( 'jetpack-contact-support-beta-group' )
: Redirect::get_url( 'jetpack-contact-support' );
}
/**
* Returns the url to reconnect Jetpack.
*
* @return string The reconnect url.
*/
protected static function helper_get_reconnect_url() {
return admin_url( 'admin.php?page=jetpack#/reconnect' );
}
/**
* Gets translated support text.
*/
protected function helper_get_support_text() {
return __( 'Please contact Jetpack support.', 'jetpack' );
}
/**
* Returns the translated text to reconnect Jetpack.
*
* @return string The translated reconnect text.
*/
protected static function helper_get_reconnect_text() {
return __( 'Reconnect Jetpack now', 'jetpack' );
}
/**
* Returns the translated text for failing tests due to timeouts.
*
* @return string The translated timeout text.
*/
protected static function helper_get_timeout_text() {
return __( 'The test timed out which may sometimes indicate a failure or may be a false failure. Please relaunch tests.', 'jetpack' );
}
/**
* Gets translated reconnect long description.
*
* @param string $connection_error The connection specific error.
* @param string $recommendation The recommendation for resolving the connection error.
*
* @return string The translated long description for reconnection recommendations.
*/
protected static function helper_get_reconnect_long_description( $connection_error, $recommendation ) {
return sprintf(
'<p>%1$s</p>' .
'<p><span class="dashicons fail"><span class="screen-reader-text">%2$s</span></span> %3$s</p><p><strong>%4$s</strong></p>',
__( 'A healthy connection ensures Jetpack essential services are provided to your WordPress site, such as Stats and Site Security.', 'jetpack' ),
/* translators: screen reader text indicating a test failed */
__( 'Error', 'jetpack' ),
$connection_error,
$recommendation
);
}
/**
* Helper function to return consistent responses for a connection failing test.
*
* @param string $name The raw method name that runs the test. Default unnamed_test.
* @param string $connection_error The connection specific error. Default 'Your site is not connected to Jetpack.'.
* @param string $recommendation The recommendation for resolving the connection error. Default 'We recommend reconnecting Jetpack.'.
*
* @return array Test results.
*/
public static function connection_failing_test( $name, $connection_error = '', $recommendation = '' ) {
$connection_error = empty( $connection_error ) ? __( 'Your site is not connected to Jetpack.', 'jetpack' ) : $connection_error;
$recommendation = empty( $recommendation ) ? __( 'We recommend reconnecting Jetpack.', 'jetpack' ) : $recommendation;
$args = array(
'name' => $name,
'short_description' => $connection_error,
'action' => self::helper_get_reconnect_url(),
'action_label' => self::helper_get_reconnect_text(),
'long_description' => self::helper_get_reconnect_long_description( $connection_error, $recommendation ),
);
return self::failing_test( $args );
}
/**
* Gets translated text to enable outbound requests.
*
* @param string $protocol Either 'HTTP' or 'HTTPS'.
*
* @return string The translated text.
*/
protected function helper_enable_outbound_requests( $protocol ) {
return sprintf(
/* translators: %1$s - request protocol, either http or https */
__(
'Your server did not successfully connect to the Jetpack server using %1$s
Please ask your hosting provider to confirm your server can make outbound requests to jetpack.com.',
'jetpack'
),
$protocol
);
}
/**
* Returns 30 for use with a filter.
*
* To allow time for WP.com to run upstream testing, this function exists to increase the http_request_timeout value
* to 30.
*
* @return int 30
*/
public static function increase_timeout() {
return 30; // seconds.
}
/**
* The test verifies the blog token exists.
*
* @return array
*/
protected function test__blog_token_if_exists() {
$name = __FUNCTION__;
if ( ! $this->helper_is_jetpack_connected() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is not connected. No blog token to check.', 'jetpack' ),
)
);
}
$blog_token = $this->helper_get_blog_token();
if ( $blog_token ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
$connection_error = __( 'Blog token is missing.', 'jetpack' );
$result = self::connection_failing_test( $name, $connection_error );
}
return $result;
}
/**
* Test if Jetpack is connected.
*/
protected function test__check_if_connected() {
$name = __FUNCTION__;
if ( ! $this->helper_get_blog_token() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Blog token is missing.', 'jetpack' ),
)
);
}
if ( $this->helper_is_jetpack_connected() ) {
$result = self::passing_test(
array(
'name' => $name,
'label' => __( 'Your site is connected to Jetpack', 'jetpack' ),
'long_description' => sprintf(
'<p>%1$s</p>' .
'<p><span class="dashicons pass"><span class="screen-reader-text">%2$s</span></span> %3$s</p>',
__( 'A healthy connection ensures Jetpack essential services are provided to your WordPress site, such as Stats and Site Security.', 'jetpack' ),
/* translators: Screen reader text indicating a test has passed */
__( 'Passed', 'jetpack' ),
__( 'Your site is connected to Jetpack.', 'jetpack' )
),
)
);
} elseif ( ( new Status() )->is_offline_mode() ) {
$result = self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is in Offline Mode:', 'jetpack' ) . ' ' . Jetpack::development_mode_trigger_text(),
)
);
} else {
$connection_error = __( 'Your site is not connected to Jetpack', 'jetpack' );
$result = self::connection_failing_test( $name, $connection_error );
}
return $result;
}
/**
* Test that the master user still exists on this site.
*
* @return array Test results.
*/
protected function test__master_user_exists_on_site() {
$name = __FUNCTION__;
if ( ! $this->helper_is_jetpack_connected() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is not connected. No master user to check.', 'jetpack' ),
)
);
}
if ( ! ( new Connection_Manager() )->get_connection_owner_id() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is running without a connected user. No master user to check.', 'jetpack' ),
)
);
}
$local_user = $this->helper_retrieve_local_master_user();
if ( $local_user->exists() ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
$connection_error = __( 'The user who setup the Jetpack connection no longer exists on this site.', 'jetpack' );
$result = self::connection_failing_test( $name, $connection_error );
}
return $result;
}
/**
* Test that the master user has the manage options capability (e.g. is an admin).
*
* Generic calls from WP.com execute on Jetpack as the master user. If it isn't an admin, random things will fail.
*
* @return array Test results.
*/
protected function test__master_user_can_manage_options() {
$name = __FUNCTION__;
if ( ! $this->helper_is_jetpack_connected() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is not connected.', 'jetpack' ),
)
);
}
if ( ! ( new Connection_Manager() )->get_connection_owner_id() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is running without a connected user. No master user to check.', 'jetpack' ),
)
);
}
$master_user = $this->helper_retrieve_local_master_user();
if ( user_can( $master_user, 'manage_options' ) ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
/* translators: a WordPress username */
$connection_error = sprintf( __( 'The user (%s) who setup the Jetpack connection is not an administrator.', 'jetpack' ), $master_user->user_login );
/* translators: a WordPress username */
$recommendation = sprintf( __( 'We recommend either upgrading the user (%s) or reconnecting Jetpack.', 'jetpack' ), $master_user->user_login );
$result = self::connection_failing_test( $name, $connection_error, $recommendation );
}
return $result;
}
/**
* Test that the PHP's XML library is installed.
*
* While it should be installed by default, increasingly in PHP 7, some OSes require an additional php-xml package.
*
* @return array Test results.
*/
protected function test__xml_parser_available() {
$name = __FUNCTION__;
if ( function_exists( 'xml_parser_create' ) ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
$result = self::failing_test(
array(
'name' => $name,
'label' => __( 'PHP XML manipulation libraries are not available.', 'jetpack' ),
'short_description' => __( 'Please ask your hosting provider to refer to our server requirements and enable PHP\'s XML module.', 'jetpack' ),
'action_label' => __( 'View our server requirements', 'jetpack' ),
'action' => Redirect::get_url( 'jetpack-support-server-requirements' ),
)
);
}
return $result;
}
/**
* Test that the server is able to send an outbound http communication.
*
* @return array Test results.
*/
protected function test__outbound_http() {
$name = __FUNCTION__;
$request = wp_remote_get( preg_replace( '/^https:/', 'http:', JETPACK__API_BASE ) . 'test/1/' );
$code = wp_remote_retrieve_response_code( $request );
if ( 200 === (int) $code ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
$result = self::failing_test(
array(
'name' => $name,
'short_description' => $this->helper_enable_outbound_requests( 'HTTP' ),
)
);
}
return $result;
}
/**
* Test that the server is able to send an outbound https communication.
*
* @return array Test results.
*/
protected function test__outbound_https() {
$name = __FUNCTION__;
$request = wp_remote_get( preg_replace( '/^http:/', 'https:', JETPACK__API_BASE ) . 'test/1/' );
$code = wp_remote_retrieve_response_code( $request );
if ( 200 === (int) $code ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
$result = self::failing_test(
array(
'name' => $name,
'short_description' => $this->helper_enable_outbound_requests( 'HTTPS' ),
)
);
}
return $result;
}
/**
* Check for an IDC.
*
* @return array Test results.
*/
protected function test__identity_crisis() {
$name = __FUNCTION__;
if ( ! $this->helper_is_jetpack_connected() ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Jetpack is not connected.', 'jetpack' ),
)
);
}
$identity_crisis = Jetpack::check_identity_crisis();
if ( ! $identity_crisis ) {
$result = self::passing_test( array( 'name' => $name ) );
} else {
$result = self::failing_test(
array(
'name' => $name,
'short_description' => sprintf(
/* translators: Two URLs. The first is the locally-recorded value, the second is the value as recorded on WP.com. */
__( 'Your url is set as `%1$s`, but your WordPress.com connection lists it as `%2$s`!', 'jetpack' ),
$identity_crisis['home'],
$identity_crisis['wpcom_home']
),
'action_label' => $this->helper_get_support_text(),
'action' => $this->helper_get_support_url(),
)
);
}
return $result;
}
/**
* Tests the health of the Connection tokens.
*
* This will always check the blog token health. It will also check the user token health if
* a user is logged in and connected, or if there's a connected owner.
*
* @since 9.0.0
* @since 9.6.0 Checks only blog token if current user not connected or site does not have a connected owner.
*
* @return array Test results.
*/
protected function test__connection_token_health() {
$name = __FUNCTION__;
$m = new Connection_Manager();
$user_id = get_current_user_id();
// Check if there's a connected logged in user.
if ( $user_id && ! $m->is_user_connected( $user_id ) ) {
$user_id = false;
}
// If no logged in user to check, let's see if there's a master_user set.
if ( ! $user_id ) {
$user_id = Jetpack_Options::get_option( 'master_user' );
if ( $user_id && ! $m->is_user_connected( $user_id ) ) {
return self::connection_failing_test( $name, __( 'Missing token for the connection owner.', 'jetpack' ) );
}
}
if ( $user_id ) {
return $this->check_tokens_health( $user_id );
} else {
return $this->check_blog_token_health();
}
}
/**
* Tests blog and user's token against wp.com's check-token-health endpoint.
*
* @since 9.6.0
*
* @return array Test results.
*/
protected function check_blog_token_health() {
$name = 'test__connection_token_health';
$valid = ( new Tokens() )->validate_blog_token();
if ( ! $valid ) {
return self::connection_failing_test( $name, __( 'Blog token validation failed.', 'jetpack' ) );
} else {
return self::passing_test( array( 'name' => $name ) );
}
}
/**
* Tests blog token against wp.com's check-token-health endpoint.
*
* @since 9.6.0
*
* @param int $user_id The user ID to check the tokens for.
*
* @return array Test results.
*/
protected function check_tokens_health( $user_id ) {
$name = 'test__connection_token_health';
$validated_tokens = ( new Tokens() )->validate( $user_id );
if ( ! is_array( $validated_tokens ) || count( array_diff_key( array_flip( array( 'blog_token', 'user_token' ) ), $validated_tokens ) ) ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'Token health check failed to validate tokens.', 'jetpack' ),
)
);
}
$invalid_tokens_exist = false;
foreach ( $validated_tokens as $validated_token ) {
if ( ! $validated_token['is_healthy'] ) {
$invalid_tokens_exist = true;
break;
}
}
if ( false === $invalid_tokens_exist ) {
return self::passing_test( array( 'name' => $name ) );
}
$connection_error = __( 'Invalid Jetpack connection tokens.', 'jetpack' );
return self::connection_failing_test( $name, $connection_error );
}
/**
* Tests connection status against wp.com's test-connection endpoint.
*
* @todo: Compare with the wpcom_self_test. We only need one of these.
*
* @return array Test results.
*/
protected function test__wpcom_connection_test() {
$name = __FUNCTION__;
$status = new Status();
if ( ! Jetpack::is_connection_ready() || $status->is_offline_mode() || $status->in_safe_mode() || ! $this->pass ) {
return self::skipped_test( array( 'name' => $name ) );
}
add_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ) );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
Client::WPCOM_JSON_API_VERSION
);
remove_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ) );
if ( is_wp_error( $response ) ) {
if ( str_contains( $response->get_error_message(), 'cURL error 28' ) ) { // Timeout.
$result = self::skipped_test(
array(
'name' => $name,
'short_description' => self::helper_get_timeout_text(),
)
);
} else {
/* translators: %1$s is the error code, %2$s is the error message */
$message = sprintf( __( 'Connection test failed (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() );
$result = self::connection_failing_test( $name, $message );
}
return $result;
}
$body = wp_remote_retrieve_body( $response );
if ( ! $body ) {
return self::failing_test(
array(
'name' => $name,
'short_description' => __( 'Connection test failed (empty response body)', 'jetpack' ) . wp_remote_retrieve_response_code( $response ),
'action_label' => $this->helper_get_support_text(),
'action' => $this->helper_get_support_url(),
)
);
}
if ( 404 === wp_remote_retrieve_response_code( $response ) ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'The WordPress.com API returned a 404 error.', 'jetpack' ),
)
);
}
$result = json_decode( $body );
$is_connected = ! empty( $result->connected );
$message = $result->message . ': ' . wp_remote_retrieve_response_code( $response );
if ( $is_connected ) {
$res = self::passing_test( array( 'name' => $name ) );
} else {
$res = self::connection_failing_test( $name, $message );
}
return $res;
}
/**
* Tests the port number to ensure it is an expected value.
*
* We expect that sites on be on one of:
* port 80,
* port 443 (https sites only),
* the value of JETPACK_SIGNATURE__HTTP_PORT,
* unless the site is intentionally on a different port (e.g. example.com:8080 is the site's URL).
*
* If the value isn't one of those and the site's URL doesn't include a port, then the signature verification will fail.
*
* This happens most commonly on sites with reverse proxies, so the edge (e.g. Varnish) is running on 80/443, but nginx
* or Apache is responding internally on a different port (e.g. 81).
*
* @return array Test results
*/
protected function test__server_port_value() {
$name = __FUNCTION__;
if ( ! isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) && ! isset( $_SERVER['SERVER_PORT'] ) ) {
return self::skipped_test(
array(
'name' => $name,
'short_description' => __( 'The server port values are not defined. This is most common when running PHP via a CLI.', 'jetpack' ),
)
);
}
$site_port = wp_parse_url( home_url(), PHP_URL_PORT );
$server_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? (int) $_SERVER['HTTP_X_FORWARDED_PORT'] : (int) $_SERVER['SERVER_PORT'];
$http_ports = array( 80 );
$https_ports = array( 80, 443 );
if ( defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ) {
$http_ports[] = JETPACK_SIGNATURE__HTTP_PORT;
}
if ( defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ) {
$https_ports[] = JETPACK_SIGNATURE__HTTPS_PORT;
}
if ( $site_port ) {
return self::skipped_test( array( 'name' => $name ) ); // Not currently testing for this situation.
}
if ( is_ssl() && in_array( $server_port, $https_ports, true ) ) {
return self::passing_test( array( 'name' => $name ) );
} elseif ( in_array( $server_port, $http_ports, true ) ) {
return self::passing_test( array( 'name' => $name ) );
} else {
if ( is_ssl() ) {
$needed_constant = 'JETPACK_SIGNATURE__HTTPS_PORT';
} else {
$needed_constant = 'JETPACK_SIGNATURE__HTTP_PORT';
}
return self::failing_test(
array(
'name' => $name,
'short_description' => sprintf(
/* translators: %1$s - a PHP code snippet */
__(
'The server port value is unexpected.
Try adding the following to your wp-config.php file: %1$s',
'jetpack'
),
"define( '$needed_constant', $server_port )"
),
)
);
}
}
/**
* Sync Health Tests.
*
* Disabled: Results in a failing test (recommended)
* Delayed: Results in failing test (recommended)
* Error: Results in failing test (critical)
*/
protected function test__sync_health() {
$name = __FUNCTION__;
if ( ! $this->helper_is_jetpack_connected() ) {
// If the site is not connected, there is no point in testing Sync health.
return self::skipped_test(
array(
'name' => $name,
'show_in_site_health' => false,
)
);
}
// Sync is disabled.
if ( ! Sync_Settings::is_sync_enabled() ) {
return self::failing_test(
array(
'name' => $name,
'label' => __( 'Jetpack Sync has been disabled on your site.', 'jetpack' ),
'severity' => 'recommended',
'action' => 'https://github.com/Automattic/jetpack/blob/trunk/projects/packages/sync/src/class-settings.php',
'action_label' => __( 'See Github for more on Sync Settings', 'jetpack' ),
'short_description' => __( 'Jetpack Sync has been disabled on your site. This could be impacting some of your sites Jetpack-powered features. Developers may enable / disable syncing using the Sync Settings API.', 'jetpack' ),
)
);
}
// Sync has experienced Data Loss.
if ( Sync_Health::get_status() === Sync_Health::STATUS_OUT_OF_SYNC ) {
return self::failing_test(
array(
'name' => $name,
'label' => __( 'Jetpack has detected a problem with the communication between your site and WordPress.com', 'jetpack' ),
'severity' => 'critical',
'action' => Redirect::get_url( 'jetpack-contact-support' ),
'action_label' => __( 'Contact Jetpack Support', 'jetpack' ),
'short_description' => __( 'There is a problem with the communication between your site and WordPress.com. This could be impacting some of your sites Jetpack-powered features. If you continue to see this error, please contact support for assistance.', 'jetpack' ),
)
);
}
return self::passing_test( array( 'name' => $name ) );
}
/**
* Calls to WP.com to run the connection diagnostic testing suite.
*
* Intentionally added last as it will be skipped if any local failed conditions exist.
*
* @since 7.1.0
* @since 7.9.0 Timeout waiting for a WP.com response no longer fails the test. Test is marked skipped instead.
*
* @return array Test results.
*/
protected function last__wpcom_self_test() {
$name = 'test__wpcom_self_test';
$status = new Status();
if ( ! Jetpack::is_connection_ready() || $status->is_offline_mode() || $status->in_safe_mode() || ! $this->pass ) {
return self::skipped_test( array( 'name' => $name ) );
}
$self_xml_rpc_url = site_url( 'xmlrpc.php' );
$testsite_url = JETPACK__API_BASE . 'testsite/1/?url=';
// Using PHP_INT_MAX - 1 so that there is still a way to override this if needed and since it only impacts this one call.
add_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ), PHP_INT_MAX - 1 );
$response = wp_remote_get( $testsite_url . $self_xml_rpc_url );
remove_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ), PHP_INT_MAX - 1 );
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
$result = self::passing_test( array( 'name' => $name ) );
} elseif ( is_wp_error( $response ) && str_contains( $response->get_error_message(), 'cURL error 28' ) ) { // Timeout.
$result = self::skipped_test(
array(
'name' => $name,
'short_description' => self::helper_get_timeout_text(),
)
);
} else {
$result = self::failing_test(
array(
'name' => $name,
'short_description' => sprintf(
/* translators: %1$s - A debugging url */
__( 'Jetpack.com detected an error on the WP.com Self Test. Visit the Jetpack Debug page for more info: %1$s, or contact support.', 'jetpack' ),
Redirect::get_url( 'jetpack-support-debug', array( 'query' => 'url=' . rawurlencode( site_url() ) ) )
),
'action_label' => $this->helper_get_support_text(),
'action' => $this->helper_get_support_url(),
)
);
}
return $result;
}
}
@@ -0,0 +1,413 @@
<?php
/**
* Jetpack Debug Data for the Site Health sections.
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Connection\Tokens;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
use Automattic\Jetpack\Identity_Crisis;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Sender;
/**
* Class Jetpack_Debug_Data
*
* Collect and return debug data for Jetpack.
*
* @since 7.3.0
*/
class Jetpack_Debug_Data {
/**
* Determine the active plan and normalize it for the debugger results.
*
* @since 7.3.0
*
* @return string The plan slug.
*/
public static function what_jetpack_plan() {
$plan = Jetpack_Plan::get();
return ! empty( $plan['class'] ) ? $plan['class'] : 'undefined';
}
/**
* Convert seconds to human readable time.
*
* A dedication function instead of using Core functionality to allow for output in seconds.
*
* @since 7.3.0
*
* @param int $seconds Number of seconds to convert to human time.
*
* @return string Human readable time.
*/
public static function seconds_to_time( $seconds ) {
$seconds = (int) $seconds;
$units = array(
'week' => WEEK_IN_SECONDS,
'day' => DAY_IN_SECONDS,
'hour' => HOUR_IN_SECONDS,
'minute' => MINUTE_IN_SECONDS,
'second' => 1,
);
// specifically handle zero.
if ( 0 === $seconds ) {
return '0 seconds';
}
$human_readable = '';
foreach ( $units as $name => $divisor ) {
$quot = (int) ( $seconds / $divisor );
if ( $quot ) {
$human_readable .= "$quot $name";
$human_readable .= ( abs( $quot ) > 1 ? 's' : '' ) . ', ';
$seconds -= $quot * $divisor;
}
}
return substr( $human_readable, 0, -2 );
}
/**
* Return debug data in the format expected by Core's Site Health Info tab.
*
* @since 7.3.0
*
* @param array $debug {
* The debug information already compiled by Core.
*
* @type string $label The title for this section of the debug output.
* @type string $description Optional. A description for your information section which may contain basic HTML
* markup: `em`, `strong` and `a` for linking to documentation or putting emphasis.
* @type boolean $show_count Optional. If set to `true` the amount of fields will be included in the title for
* this section.
* @type boolean $private Optional. If set to `true` the section and all associated fields will be excluded
* from the copy-paste text area.
* @type array $fields {
* An associative array containing the data to be displayed.
*
* @type string $label The label for this piece of information.
* @type string $value The output that is of interest for this field.
* @type boolean $private Optional. If set to `true` the field will not be included in the copy-paste text area
* on top of the page, allowing you to show, for example, API keys here.
* }
* }
*
* @return array $args Debug information in the same format as the initial argument.
*/
public static function core_debug_data( $debug ) {
$support_url = Jetpack::is_development_version()
? Redirect::get_url( 'jetpack-contact-support-beta-group' )
: Redirect::get_url( 'jetpack-contact-support' );
$jetpack = array(
'jetpack' => array(
'label' => __( 'Jetpack', 'jetpack' ),
'description' => sprintf(
/* translators: %1$s is URL to jetpack.com's contact support page. %2$s accessibility text */
__(
'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>',
'jetpack'
),
esc_url( $support_url ),
__( '(opens in a new tab)', 'jetpack' )
),
'fields' => self::debug_data(),
),
);
$debug = array_merge( $debug, $jetpack );
return $debug;
}
/**
* Compile and return array of debug information.
*
* @since 7.3.0
*
* @return array $args {
* Associated array of arrays with the following.
* @type string $label The label for this piece of information.
* @type string $value The output that is of interest for this field.
* @type boolean $private Optional. Set to true if data is sensitive (API keys, etc).
* }
*/
public static function debug_data() {
$debug_info = array();
/* Add various important Jetpack options */
$debug_info['site_id'] = array(
'label' => 'Jetpack Site ID',
'value' => Jetpack_Options::get_option( 'id' ),
'private' => false,
);
$debug_info['ssl_cert'] = array(
'label' => 'Jetpack SSL Verfication Bypass',
'value' => ( Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ) ) ? 'Yes' : 'No',
'private' => false,
);
$debug_info['time_diff'] = array(
'label' => "Offset between Jetpack server's time and this server's time.",
'value' => Jetpack_Options::get_option( 'time_diff' ),
'private' => false,
);
$debug_info['version_option'] = array(
'label' => 'Current Jetpack Version Option',
'value' => Jetpack_Options::get_option( 'version' ),
'private' => false,
);
$debug_info['old_version'] = array(
'label' => 'Previous Jetpack Version',
'value' => Jetpack_Options::get_option( 'old_version' ),
'private' => false,
);
$debug_info['public'] = array(
'label' => 'Jetpack Site Public',
'value' => ( Jetpack_Options::get_option( 'public' ) ) ? 'Public' : 'Private',
'private' => false,
);
$debug_info['master_user'] = array(
'label' => 'Jetpack Master User',
'value' => self::human_readable_master_user(), // Only ID number and user name.
'private' => false,
);
$debug_info['is_offline_mode'] = array(
'label' => 'Jetpack Offline Mode',
'value' => ( new Status() )->is_offline_mode() ? 'on' : 'off',
'private' => false,
);
$debug_info['is_offline_mode_constant'] = array(
'label' => 'JETPACK_DEV_DEBUG Constant',
'value' => ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) ? 'on' : 'off',
'private' => false,
);
/**
* Token information is private, but awareness if there one is set is helpful.
*
* To balance out information vs privacy, we only display and include the "key",
* which is a segment of the token prior to a period within the token and is
* technically not private.
*
* If a token does not contain a period, then it is malformed and we report it as such.
*/
$user_id = get_current_user_id();
$blog_token = ( new Tokens() )->get_access_token();
$user_token = ( new Tokens() )->get_access_token( $user_id );
$tokenset = '';
if ( $blog_token ) {
$tokenset = 'Blog ';
$blog_key = substr( $blog_token->secret, 0, strpos( $blog_token->secret, '.' ) );
// Intentionally not translated since this is helpful when sent to Happiness.
$blog_key = ( $blog_key ) ? $blog_key : 'Potentially Malformed Token.';
}
if ( $user_token ) {
$tokenset .= 'User';
$user_key = substr( $user_token->secret, 0, strpos( $user_token->secret, '.' ) );
// Intentionally not translated since this is helpful when sent to Happiness.
$user_key = ( $user_key ) ? $user_key : 'Potentially Malformed Token.';
}
if ( ! $tokenset ) {
$tokenset = 'None';
}
$debug_info['current_user'] = array(
'label' => 'Current User',
'value' => self::human_readable_user( $user_id ),
'private' => false,
);
$debug_info['tokens_set'] = array(
'label' => 'Tokens defined',
'value' => $tokenset,
'private' => false,
);
$debug_info['blog_token'] = array(
'label' => 'Blog Public Key',
'value' => ( $blog_token ) ? $blog_key : 'Not set.',
'private' => false,
);
$debug_info['user_token'] = array(
'label' => 'User Public Key',
'value' => ( $user_token ) ? $user_key : 'Not set.',
'private' => false,
);
/** Jetpack Environmental Information */
$debug_info['version'] = array(
'label' => 'Jetpack Version',
'value' => JETPACK__VERSION,
'private' => false,
);
$debug_info['jp_plugin_dir'] = array(
'label' => 'Jetpack Directory',
'value' => JETPACK__PLUGIN_DIR,
'private' => false,
);
$debug_info['plan'] = array(
'label' => 'Plan Type',
'value' => self::what_jetpack_plan(),
'private' => false,
);
foreach ( array(
'HTTP_HOST',
'SERVER_PORT',
'HTTPS',
'GD_PHP_HANDLER',
'HTTP_AKAMAI_ORIGIN_HOP',
'HTTP_CF_CONNECTING_IP',
'HTTP_CLIENT_IP',
'HTTP_FASTLY_CLIENT_IP',
'HTTP_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_INCAP_CLIENT_IP',
'HTTP_TRUE_CLIENT_IP',
'HTTP_X_CLIENTIP',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_X_FORWARDED',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_IP_TRAIL',
'HTTP_X_REAL_IP',
'HTTP_X_VARNISH',
'REMOTE_ADDR',
) as $header ) {
if ( isset( $_SERVER[ $header ] ) ) {
$debug_info[ $header ] = array(
'label' => 'Server Variable ' . $header,
'value' => empty( $_SERVER[ $header ] ) ? 'false' : filter_var( wp_unslash( $_SERVER[ $header ] ) ),
'private' => true, // This isn't really 'private' information, but we don't want folks to easily paste these into public forums.
);
}
}
$debug_info['protect_header'] = array(
'label' => 'Trusted IP',
'value' => wp_json_encode( get_site_option( 'trusted_ip_header' ) ),
'private' => false,
);
/** Sync Debug Information */
$sync_module = Modules::get_module( 'full-sync' );
'@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $sync_module';
if ( $sync_module ) {
$sync_statuses = $sync_module->get_status();
$human_readable_sync_status = array();
foreach ( $sync_statuses as $sync_status => $sync_status_value ) {
$human_readable_sync_status[ $sync_status ] =
in_array( $sync_status, array( 'started', 'queue_finished', 'send_started', 'finished' ), true )
? gmdate( 'r', $sync_status_value ) : $sync_status_value;
}
$debug_info['full_sync'] = array(
'label' => 'Full Sync Status',
'value' => wp_json_encode( $human_readable_sync_status ),
'private' => false,
);
}
$queue = Sender::get_instance()->get_sync_queue();
$debug_info['sync_size'] = array(
'label' => 'Sync Queue Size',
'value' => $queue->size(),
'private' => false,
);
$debug_info['sync_lag'] = array(
'label' => 'Sync Queue Lag',
'value' => self::seconds_to_time( $queue->lag() ),
'private' => false,
);
$full_sync_queue = Sender::get_instance()->get_full_sync_queue();
$debug_info['full_sync_size'] = array(
'label' => 'Full Sync Queue Size',
'value' => $full_sync_queue->size(),
'private' => false,
);
$debug_info['full_sync_lag'] = array(
'label' => 'Full Sync Queue Lag',
'value' => self::seconds_to_time( $full_sync_queue->lag() ),
'private' => false,
);
/**
* IDC Information
*
* Must follow sync debug since it depends on sync functionality.
*/
$idc_urls = array(
'home' => Urls::home_url(),
'siteurl' => Urls::site_url(),
'WP_HOME' => Constants::is_defined( 'WP_HOME' ) ? Constants::get_constant( 'WP_HOME' ) : '',
'WP_SITEURL' => Constants::is_defined( 'WP_SITEURL' ) ? Constants::get_constant( 'WP_SITEURL' ) : '',
);
$debug_info['idc_urls'] = array(
'label' => 'IDC URLs',
'value' => wp_json_encode( $idc_urls ),
'private' => false,
);
$debug_info['idc_error_option'] = array(
'label' => 'IDC Error Option',
'value' => wp_json_encode( Jetpack_Options::get_option( 'sync_error_idc' ) ),
'private' => false,
);
$debug_info['idc_optin'] = array(
'label' => 'IDC Opt-in',
'value' => Identity_Crisis::should_handle_idc(),
'private' => false,
);
// @todo -- Add testing results?
$cxn_tests = new Jetpack_Cxn_Tests();
$debug_info['cxn_tests'] = array(
'label' => 'Connection Tests',
'value' => '',
'private' => false,
);
if ( $cxn_tests->pass() ) {
$debug_info['cxn_tests']['value'] = 'All Pass.';
} else {
$debug_info['cxn_tests']['value'] = wp_json_encode( $cxn_tests->list_fails() );
}
return $debug_info;
}
/**
* Returns a human readable string for which user is the master user.
*
* @return string
*/
private static function human_readable_master_user() {
$master_user = Jetpack_Options::get_option( 'master_user' );
if ( ! $master_user ) {
return __( 'No master user set.', 'jetpack' );
}
$user = new WP_User( $master_user );
if ( ! $user ) {
return __( 'Master user no longer exists. Please disconnect and reconnect Jetpack.', 'jetpack' );
}
return self::human_readable_user( $user );
}
/**
* Return human readable string for a given user object.
*
* @param WP_User|int $user Object or ID.
*
* @return string
*/
private static function human_readable_user( $user ) {
$user = new WP_User( $user );
return sprintf( '#%1$d %2$s', $user->ID, $user->user_login ); // Format: "#1 username".
}
}
@@ -0,0 +1,363 @@
<?php
/**
* Jetpack Debugger functionality allowing for self-service diagnostic information via the legacy jetpack debugger.
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
/**
* Class Jetpack_Debugger
*
* A namespacing class for functionality related to the legacy in-plugin diagnostic tooling.
*/
class Jetpack_Debugger {
/**
* Disconnect Jetpack and redirect user to connection flow.
*
* Used in class.jetpack-admin.php.
*/
public static function disconnect_and_redirect() {
if ( ! ( isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'], 'jp_disconnect' ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
return;
}
if ( ! empty( $_GET['disconnect'] ) ) {
if ( Jetpack::is_connection_ready() ) {
Jetpack::disconnect();
wp_safe_redirect( Jetpack::admin_url() );
exit( 0 );
}
}
}
/**
* Handles output to the browser for the in-plugin debugger.
*/
public static function jetpack_debug_display_handler() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'jetpack' ) );
}
$support_url = Jetpack::is_development_version()
? Redirect::get_url( 'jetpack-contact-support-beta-group' )
: Redirect::get_url( 'jetpack-contact-support' );
$cxntests = new Jetpack_Cxn_Tests();
?>
<div class="wrap">
<div class="jp-static-block">
<h2><?php esc_html_e( 'Debugging Center', 'jetpack' ); ?></h2>
<div class="jp-static-block-body">
<h3><?php esc_html_e( "Testing your site's compatibility with Jetpack...", 'jetpack' ); ?></h3>
<div class="jetpack-debug-test-container">
<?php
if ( $cxntests->pass() ) {
echo '<div class="jetpack-tests-succeed">' . esc_html__( 'Your Jetpack setup looks a-okay!', 'jetpack' ) . '</div>';
} else {
$failures = $cxntests->list_fails();
foreach ( $failures as $fail ) {
?>
<div class="notice notice-error inline">
<div class="notice-icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" width="24" height="24" class="y_IPyP1wIAOhyNaqvXJq" aria-hidden="true" focusable="false"><path d="M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zm1.13 9.38l.35-6.46H8.52l.35 6.46h2.26zm-.09 3.36c.24-.23.37-.55.37-.96 0-.42-.12-.74-.36-.97s-.59-.35-1.06-.35-.82.12-1.07.35-.37.55-.37.97c0 .41.13.73.38.96.26.23.61.34 1.06.34s.8-.11 1.05-.34z"></path></svg>
</div>
<div class="notice-main-content">
<div class="notice-title"><?php echo esc_html( $fail['short_description'] ); ?></div>
<div class="notice-action-bar">
<div>
<a href="<?php echo esc_attr( $fail['action'] ); ?>" aria-disabled="false" class="components-button is-primary"><span><?php echo esc_html( $fail['action_label'] ); ?></span></a>
</div>
</div>
</div>
</div>
<?php
}
}
?>
</div>
<div class="entry-content">
<h4><?php esc_html_e( 'Trouble with Jetpack?', 'jetpack' ); ?></h4>
<p><?php esc_html_e( 'It may be caused by one of these issues, which you can diagnose yourself:', 'jetpack' ); ?></p>
<ol>
<li><?php esc_html_e( 'A known issue.', 'jetpack' ); ?>
<?php
printf(
wp_kses(
/* translators: URLs to Jetpack support pages. */
__( 'Some themes and plugins have <a href="%1$s" target="_blank" rel="noopener noreferrer">known conflicts</a> with Jetpack check the list. (You can also browse the <a href="%2$s" target="_blank" rel="noopener noreferrer">Jetpack support pages</a> or <a href="%3$s" target="_blank" rel="noopener noreferrer">Jetpack support forum</a> to see if others have experienced and solved the problem.)', 'jetpack' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
)
),
esc_url( Redirect::get_url( 'jetpack-contact-support-known-issues' ) ),
esc_url( Redirect::get_url( 'jetpack-support' ) ),
esc_url( Redirect::get_url( 'wporg-support-plugin-jetpack' ) )
);
?>
</li>
<li>
<?php esc_html_e( 'An incompatible plugin.', 'jetpack' ); ?>
<?php esc_html_e( "Find out by disabling all plugins except Jetpack. If the problem persists, it's not a plugin issue. If the problem is solved, turn your plugins on one by one until the problem pops up again there's the culprit! Let us know, and we'll try to help.", 'jetpack' ); ?>
</li>
<li>
<?php esc_html_e( 'A theme conflict.', 'jetpack' ); ?>
<?php
$default_theme = wp_get_theme( WP_DEFAULT_THEME );
if ( $default_theme->exists() ) {
/* translators: %s is the name of a theme */
echo esc_html( sprintf( __( "If your problem isn't known or caused by a plugin, try activating %s (the default WordPress theme).", 'jetpack' ), $default_theme->get( 'Name' ) ) );
} else {
esc_html_e( "If your problem isn't known or caused by a plugin, try activating the default WordPress theme.", 'jetpack' );
}
?>
<?php esc_html_e( "If this solves the problem, something in your theme is probably broken let the theme's author know.", 'jetpack' ); ?>
</li>
<li>
<?php esc_html_e( 'A problem with your XML-RPC file.', 'jetpack' ); ?>
<?php
printf(
wp_kses(
/* translators: The URL to the site's xmlrpc.php file. */
__( 'Load your <a href="%s">XML-RPC file</a>. It should say “XML-RPC server accepts POST requests only.” on a line by itself.', 'jetpack' ),
array( 'a' => array( 'href' => array() ) )
),
esc_attr( site_url( 'xmlrpc.php' ) )
);
?>
<ul>
<li><?php esc_html_e( "If it's not by itself, a theme or plugin is displaying extra characters. Try steps 2 and 3.", 'jetpack' ); ?></li>
<li><?php esc_html_e( 'If you get a 404 message, contact your web host. Their security may block the XML-RPC file.', 'jetpack' ); ?></li>
</ul>
</li>
<?php if ( current_user_can( 'jetpack_disconnect' ) && Jetpack::is_connection_ready() ) : ?>
<li>
<?php esc_html_e( 'A connection problem with WordPress.com.', 'jetpack' ); ?>
<?php
printf(
wp_kses(
/* translators: URL to disconnect and reconnect Jetpack. */
__( 'Jetpack works by connecting to WordPress.com for a lot of features. Sometimes, when the connection gets messed up, you need to disconnect and reconnect to get things working properly. <a href="%s">Disconnect from WordPress.com</a>', 'jetpack' ),
array(
'a' => array(
'href' => array(),
'class' => array(),
),
)
),
esc_attr(
wp_nonce_url(
Jetpack::admin_url(
array(
'page' => 'jetpack-debugger',
'disconnect' => true,
)
),
'jp_disconnect',
'nonce'
)
)
);
?>
</li>
<?php endif; ?>
</ol>
<h4><?php esc_html_e( 'Still having trouble?', 'jetpack' ); ?></h4>
<p>
<?php esc_html_e( 'Ask us for help!', 'jetpack' ); ?>
<?php
/**
* Offload to new WordPress debug data.
*/
printf(
wp_kses(
/* translators: URL for Jetpack support. URL for WordPress's Site Health */
__( '<a href="%1$s">Contact our Happiness team</a>. When you do, please include the <a href="%2$s">full debug information from your site</a>.', 'jetpack' ),
array( 'a' => array( 'href' => array() ) )
),
esc_url( $support_url ),
esc_url( admin_url() . 'site-health.php?tab=debug' )
);
?>
</p>
</div>
</div>
</div>
<div class="jp-static-block">
<h2><?php esc_html_e( 'More details about your Jetpack settings', 'jetpack' ); ?></h2>
<div class="jp-static-block-body">
<?php if ( Jetpack::is_connection_ready() ) : ?>
<div id="connected-user-details">
<p>
<?php
printf(
wp_kses(
/* translators: %s is an e-mail address */
__( 'The primary connection is owned by <strong>%s</strong>\'s WordPress.com account.', 'jetpack' ),
array( 'strong' => array() )
),
esc_html( Jetpack::get_master_user_email() )
);
?>
</p>
</div>
<?php else : ?>
<div id="dev-mode-details">
<p>
<?php
printf(
wp_kses(
/* translators: Link to a Jetpack support page. */
__( 'Would you like to use Jetpack on your local development site? You can do so thanks to <a href="%s">Jetpack\'s offline mode</a>.', 'jetpack' ),
array( 'a' => array( 'href' => array() ) )
),
esc_url( Redirect::get_url( 'jetpack-support-development-mode' ) )
);
?>
</p>
</div>
<?php endif; ?>
<?php
if (
current_user_can( 'jetpack_manage_modules' )
&& ( ( new Status() )->is_offline_mode() || Jetpack::is_connection_ready() )
) {
printf(
wp_kses(
'<p><a href="%1$s">%2$s</a></p>',
array(
'a' => array( 'href' => array() ),
'p' => array(),
)
),
esc_attr( Jetpack::admin_url( 'page=jetpack_modules' ) ),
esc_html__( 'Access the full list of Jetpack modules available on your site.', 'jetpack' )
);
}
?>
</div>
</div>
</div>
<?php
}
/**
* Outputs html needed within the <head> for the in-plugin debugger page.
*/
public static function jetpack_debug_admin_head() {
Jetpack_Admin_Page::load_wrapper_styles();
?>
<style type="text/css">
.jetpack-debug-test-container {
margin: 8px 0;
}
#connected-user-details p strong {
word-break: break-all;
}
.jetpack-tests-succeed {
font-size: large;
color: #069E08;
}
.jetpack-test-details {
margin: 4px 6px;
padding: 10px;
overflow: auto;
display: none;
}
.formbox {
margin: 0 0 25px 0;
}
.formbox input[type="text"], .formbox input[type="email"], .formbox input[type="url"], .formbox textarea, #debug_info_div {
border: 1px solid #dcdcde;
border-radius: 11px;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.1);
color: #646970;
font-size: 14px;
padding: 10px;
width: 97%;
}
#debug_info_div {
border-radius: 0;
margin-top: 16px;
background: #FFF;
padding: 16px;
}
.formbox .contact-support input[type="submit"] {
float: right;
margin: 0 !important;
border-radius: 20px !important;
cursor: pointer;
font-size: 13pt !important;
height: auto !important;
margin: 0 0 2em 10px !important;
padding: 8px 16px !important;
background-color: #dcdcde;
border: 1px solid rgba(0,0,0,0.05);
border-top-color: rgba(255,255,255,0.1);
border-bottom-color: rgba(0,0,0,0.15);
color: #333;
font-weight: 400;
display: inline-block;
text-align: center;
text-decoration: none;
}
.formbox span.errormsg {
margin: 0 0 10px 10px;
color: #d00;
display: none;
}
.formbox.error span.errormsg {
display: block;
}
#debug_info_div, #toggle_debug_info, #debug_info_div p {
font-size: 12px;
}
#category_div ul li {
list-style-type: none;
}
</style>
<script type="text/javascript">
jQuery( document ).ready( function($) {
$( '#debug_info' ).prepend( 'jQuery version: ' + jQuery.fn.jquery + "\r\n" );
$( '#debug_form_info' ).prepend( 'jQuery version: ' + jQuery.fn.jquery + "\r\n" );
$( '.jetpack-test-error .jetpack-test-heading' ).on( 'click', function() {
$( this ).parents( '.jetpack-test-error' ).find( '.jetpack-test-details' ).slideToggle();
return false;
} );
} );
</script>
<?php
}
}
@@ -0,0 +1,118 @@
<?php
/**
* WP Site Health debugging functions.
*
* @package automattic/jetpack
*/
/**
* Test runner for Core's Site Health module.
*
* @since 7.3.0
*/
function jetpack_debugger_ajax_local_testing_suite() {
check_ajax_referer( 'health-check-site-status' );
if ( ! current_user_can( 'jetpack_manage_modules' ) ) {
wp_send_json_error();
}
$tests = new Jetpack_Cxn_Tests();
wp_send_json_success( $tests->output_results_for_core_async_site_health() );
}
/**
* Adds the Jetpack Local Testing Suite to the Core Site Health system.
*
* @since 7.3.0
*
* @param array $core_tests Array of tests from Core's Site Health.
*
* @return array $core_tests Array of tests for Core's Site Health.
*/
function jetpack_debugger_site_status_tests( $core_tests ) {
$cxn_tests = new Jetpack_Cxn_Tests();
$tests = $cxn_tests->list_tests( 'direct' );
foreach ( $tests as $test ) {
$core_tests['direct'][ $test['name'] ] = array(
'label' => __( 'Jetpack: ', 'jetpack' ) . $test['name'],
/**
* Callable for Core's Site Health system to execute.
*
* @var array $test A Jetpack Testing Suite test array.
* @var Jetpack_Cxn_Tests $cxn_tests An instance of the Jetpack Test Suite.
*
* @return array {
* A results array to match the format expected by WordPress Core.
*
* @type string $label Name for the test.
* @type string $status 'critical', 'recommended', or 'good'.
* @type array $badge Array for Site Health status. Keys label and color.
* @type string $description Description of the test result.
* @type string $action HTML to a link to resolve issue.
* @type string $test Unique test identifier.
* }
*/
'test' => function () use ( $test, $cxn_tests ) {
$results = $cxn_tests->run_test( $test['name'] );
if ( is_wp_error( $results ) ) {
return;
}
$label = $results['label'] ?
$results['label'] :
ucwords(
str_replace(
'_',
' ',
str_replace( 'test__', '', $test['name'] )
)
);
if ( $results['long_description'] ) {
$description = $results['long_description'];
} elseif ( $results['short_description'] ) {
$description = sprintf(
'<p>%s</p>',
$results['short_description']
);
} else {
$description = sprintf(
'<p>%s</p>',
__( 'This test successfully passed!', 'jetpack' )
);
}
$return = array(
'label' => $label,
'status' => 'good',
'badge' => array(
'label' => __( 'Jetpack', 'jetpack' ),
'color' => 'green',
),
'description' => $description,
'actions' => '',
'test' => 'jetpack_' . $test['name'],
);
if ( false === $results['pass'] ) {
$return['status'] = $results['severity'];
if ( ! empty( $results['action'] ) ) {
$return['actions'] = sprintf(
'<a 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( $results['action'] ),
$results['action_label'],
/* translators: accessibility text */
__( '(opens in a new tab)', 'jetpack' )
);
}
}
return $return;
},
);
}
$core_tests['async']['jetpack_test_suite'] = array(
'label' => __( 'Jetpack Tests', 'jetpack' ),
'test' => 'jetpack_local_testing_suite',
);
return $core_tests;
}