Completed
Push — add/debugger-package ( ddaa92...efda91 )
by
unknown
31:32 queued 23:06
created

packages/debugger/src/class-base.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Base class for Jetpack's debugging tests.
4
 *
5
 * @package Jetpack.
6
 */
7
8
use 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-tests.php file.
16
 *
17
 * @author Brandon Kraft
18
 * @package Automattic/jetpack-debugger
19
 */
20
21
namespace Automattic\Jetpack\Debugger;
0 ignored issues
show
This code did not parse for me. Apparently, there is an error somewhere around this line:

Namespace declaration statement has to be the very first statement in the script
Loading history...
22
23
/**
24
 * "Unit Tests" for the Jetpack connection.
25
 *
26
 * @since Jetpack 7.1.0
27
 */
28
class Base {
29
30
	const PUBLIC_KEY =
31
		"\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n"
32
		. 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n"
33
		. '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n"
34
		. '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n"
35
		. 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n"
36
		. 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n"
37
		. 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n"
38
		. 'EwIDAQAB' . "\r\n"
39
		. '-----END PUBLIC KEY-----' . "\r\n";
40
41
	/**
42
	 * Tests to run on the Jetpack connection.
43
	 *
44
	 * @var array $tests
45
	 */
46
	protected $tests = array();
47
48
	/**
49
	 * Results of the Jetpack connection tests.
50
	 *
51
	 * @var array $results
52
	 */
53
	protected $results = array();
54
55
	/**
56
	 * Status of the testing suite.
57
	 *
58
	 * Used internally to determine if a test should be skipped since the tests are already failing. Assume passing.
59
	 *
60
	 * @var bool $pass
61
	 */
62
	protected $pass = true;
63
64
	/**
65
	 * Debugger constructor.
66
	 */
67
	public function __construct() {
68
		$this->tests   = array();
69
		$this->results = array();
70
	}
71
72
	/**
73
	 * Adds a new test to the Jetpack Connection Testing suite.
74
	 *
75
	 * @since 7.1.0
76
	 * @since 7.3.0 Adds name parameter and returns WP_Error on failure.
77
	 *
78
	 * @param callable $callable Test to add to queue.
79
	 * @param string   $name Unique name for the test.
80
	 * @param string   $type   Optional. Core Site Health type: 'direct' if test can be run during initial load or 'async' if test should run async.
81
	 * @param array    $groups Optional. Testing groups to add test to.
82
	 *
83
	 * @return mixed True if successfully added. WP_Error on failure.
84
	 */
85
	public function add_test( $callable, $name, $type = 'direct', $groups = array( 'default' ) ) {
86
		if ( is_array( $name ) ) {
87
			// Pre-7.3.0 method passed the $groups parameter here.
88
			return new WP_Error( __( 'add_test arguments changed in 7.3.0. Please reference inline documentation.', 'jetpack' ) );
89
		}
90
		if ( array_key_exists( $name, $this->tests ) ) {
91
			return new WP_Error( __( 'Test names must be unique.', 'jetpack' ) );
92
		}
93
		if ( ! is_callable( $callable ) ) {
94
			return new WP_Error( __( 'Tests must be valid PHP callables.', 'jetpack' ) );
95
		}
96
97
		$this->tests[ $name ] = array(
98
			'name'  => $name,
99
			'test'  => $callable,
100
			'group' => $groups,
101
			'type'  => $type,
102
		);
103
		return true;
104
	}
105
106
	/**
107
	 * Lists all tests to run.
108
	 *
109
	 * @since 7.3.0
110
	 *
111
	 * @param string $type Optional. Core Site Health type: 'direct' or 'async'. All by default.
112
	 * @param string $group Optional. A specific testing group. All by default.
113
	 *
114
	 * @return array $tests Array of tests with test information.
115
	 */
116
	public function list_tests( $type = 'all', $group = 'all' ) {
117
		if ( ! ( 'all' === $type || 'direct' === $type || 'async' === $type ) ) {
118
			_doing_it_wrong( 'Debugger_Base->list_tests', 'Type must be all, direct, or async', '7.3.0' );
119
		}
120
121
		$tests = array();
122
		foreach ( $this->tests as $name => $value ) {
123
			// Get all valid tests by group staged.
124
			if ( 'all' === $group || $group === $value['group'] ) {
125
				$tests[ $name ] = $value;
126
			}
127
128
			// Next filter out any that do not match the type.
129
			if ( 'all' !== $type && $type !== $value['type'] ) {
130
				unset( $tests[ $name ] );
131
			}
132
		}
133
134
		return $tests;
135
	}
136
137
	/**
138
	 * Run a specific test.
139
	 *
140
	 * @since 7.3.0
141
	 *
142
	 * @param string $name Name of test.
143
	 *
144
	 * @return mixed $result Test result array or WP_Error if invalid name. {
145
	 * @type string $name Test name
146
	 * @type mixed  $pass True if passed, false if failed, 'skipped' if skipped.
147
	 * @type string $message Human-readable test result message.
148
	 * @type string $resolution Human-readable resolution steps.
149
	 * }
150
	 */
151
	public function run_test( $name ) {
152
		if ( array_key_exists( $name, $this->tests ) ) {
153
			return call_user_func( $this->tests[ $name ]['test'], $this );
154
		}
155
		return new WP_Error( __( 'There is no test by that name: ', 'jetpack' ) . $name );
156
	}
157
158
	/**
159
	 * Runs the Jetpack connection suite.
160
	 */
161
	public function run_tests() {
162
		foreach ( $this->tests as $test ) {
163
			$result          = call_user_func( $test['test'], $this );
164
			$result['group'] = $test['group'];
165
			$result['type']  = $test['type'];
166
			$this->results[] = $result;
167
			if ( false === $result['pass'] ) {
168
				$this->pass = false;
169
			}
170
		}
171
	}
172
173
	/**
174
	 * Returns the full results array.
175
	 *
176
	 * @since 7.1.0
177
	 * @since 7.3.0 Add 'type'
178
	 *
179
	 * @param string $type  Test type, async or direct.
180
	 * @param string $group Testing group whose results we want. Defaults to all tests.
181
	 * @return array Array of test results.
182
	 */
183
	public function raw_results( $type = 'all', $group = 'all' ) {
184
		if ( ! $this->results ) {
185
			$this->run_tests();
186
		}
187
188
		$results = $this->results;
189
190
		if ( 'all' !== $group ) {
191
			foreach ( $results as $test => $result ) {
192
				if ( ! in_array( $group, $result['group'], true ) ) {
193
					unset( $results[ $test ] );
194
				}
195
			}
196
		}
197
198
		if ( 'all' !== $type ) {
199
			foreach ( $results as $test => $result ) {
200
				if ( $type !== $result['type'] ) {
201
					unset( $results[ $test ] );
202
				}
203
			}
204
		}
205
206
		return $results;
207
	}
208
209
	/**
210
	 * Returns the status of the connection suite.
211
	 *
212
	 * @since 7.1.0
213
	 * @since 7.3.0 Add 'type'
214
	 *
215
	 * @param string $type  Test type, async or direct. Optional, direct all tests.
216
	 * @param string $group Testing group to check status of. Optional, default all tests.
217
	 *
218
	 * @return true|array True if all tests pass. Array of failed tests.
219
	 */
220
	public function pass( $type = 'all', $group = 'all' ) {
221
		$results = $this->raw_results( $type, $group );
222
223
		foreach ( $results as $result ) {
224
			// 'pass' could be true, false, or 'skipped'. We only want false.
225
			if ( isset( $result['pass'] ) && false === $result['pass'] ) {
226
				return false;
227
			}
228
		}
229
230
		return true;
231
232
	}
233
234
	/**
235
	 * Return array of failed test messages.
236
	 *
237
	 * @since 7.1.0
238
	 * @since 7.3.0 Add 'type'
239
	 *
240
	 * @param string $type  Test type, direct or async.
241
	 * @param string $group Testing group whose failures we want. Defaults to "all".
242
	 *
243
	 * @return false|array False if no failed tests. Otherwise, array of failed tests.
244
	 */
245
	public function list_fails( $type = 'all', $group = 'all' ) {
246
		$results = $this->raw_results( $type, $group );
247
248
		foreach ( $results as $test => $result ) {
249
			// We do not want tests that passed or ones that are misconfigured (no pass status or no failure message).
250
			if ( ! isset( $result['pass'] ) || false !== $result['pass'] || ! isset( $result['message'] ) ) {
251
				unset( $results[ $test ] );
252
			}
253
		}
254
255
		return $results;
256
	}
257
258
	/**
259
	 * Helper function to return consistent responses for a passing test.
260
	 *
261
	 * @param string      $name Test name.
262
	 * @param string|bool $message Plain text message to show when test passed.
263
	 * @param string|bool $label Label to be used on Site Health card.
264
	 * @param string|bool $description HTML description to be used in Site Health card.
265
	 *
266
	 * @return array Test results.
267
	 */
268
	public static function passing_test( $name = 'Unnamed', $message = false, $label = false, $description = false ) {
269
		if ( ! $message ) {
270
			$message = __( 'Test Passed!', 'jetpack' );
271
		}
272
		return array(
273
			'name'        => $name,
274
			'pass'        => true,
275
			'message'     => $message,
276
			'description' => $description,
277
			'resolution'  => false,
278
			'severity'    => false,
279
			'label'       => $label,
280
		);
281
	}
282
283
	/**
284
	 * Helper function to return consistent responses for a skipped test.
285
	 *
286
	 * @param string $name Test name.
287
	 * @param string $message Reason for skipping the test. Optional.
288
	 *
289
	 * @return array Test results.
290
	 */
291
	public static function skipped_test( $name = 'Unnamed', $message = false ) {
292
		return array(
293
			'name'       => $name,
294
			'pass'       => 'skipped',
295
			'message'    => $message,
296
			'resolution' => false,
297
			'severity'   => false,
298
		);
299
	}
300
301
	/**
302
	 * Helper function to return consistent responses for a failing test.
303
	 *
304
	 * @since 7.1.0
305
	 * @since 7.3.0 Added $action for resolution action link, $severity for issue severity.
306
	 *
307
	 * @param string      $name Test name.
308
	 * @param string      $message Message detailing the failure.
309
	 * @param string      $resolution Optional. Steps to resolve.
310
	 * @param string      $action Optional. URL to direct users to self-resolve.
311
	 * @param string      $severity Optional. "critical" or "recommended" for failure stats. "good" for passing.
312
	 * @param string      $label Optional. The label to use instead of the test name.
313
	 * @param string|bool $action_label Optional. The label for the action url instead of default 'Resolve'.
314
	 * @param string|bool $description Optional. An HTML description to override resolution.
315
	 *
316
	 * @return array Test results.
317
	 */
318
	public static function failing_test( $name, $message, $resolution = false, $action = false, $severity = 'critical', $label = false, $action_label = false, $description = false ) {
319
		if ( ! $action_label ) {
320
			/* translators: Resolve is used as a verb, a command that when invoked will lead to a problem's solution. */
321
			$action_label = __( 'Resolve', 'jetpack' );
322
		}
323
		// Provide standard resolutions steps, but allow pass-through of non-standard ones.
324
		switch ( $resolution ) {
325
			case 'cycle_connection':
326
				$resolution = __( 'Please disconnect and reconnect Jetpack.', 'jetpack' ); // @todo: Link.
327
				break;
328
			case 'outbound_requests':
329
				$resolution = __( 'Please ask your hosting provider to confirm your server can make outbound requests to jetpack.com.', 'jetpack' );
330
				break;
331
			case 'support':
332
			case false:
333
				$resolution = __( 'Please contact Jetpack support.', 'jetpack' ); // @todo: Link to support.
334
				break;
335
		}
336
337
		return array(
338
			'name'         => $name,
339
			'pass'         => false,
340
			'message'      => $message,
341
			'resolution'   => $resolution,
342
			'action'       => $action,
343
			'severity'     => $severity,
344
			'label'        => $label,
345
			'action_label' => $action_label,
346
			'description'  => $description,
347
		);
348
	}
349
350
	/**
351
	 * Provide WP_CLI friendly testing results.
352
	 *
353
	 * @since 7.1.0
354
	 * @since 7.3.0 Add 'type'
355
	 *
356
	 * @param string $type  Test type, direct or async.
357
	 * @param string $group Testing group whose results we are outputting. Default all tests.
358
	 */
359
	public function output_results_for_cli( $type = 'all', $group = 'all' ) {
360
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
361
			if ( ( new Status() )->is_development_mode() ) {
362
				WP_CLI::line( __( 'Jetpack is in Development Mode:', 'jetpack' ) );
363
				WP_CLI::line( Jetpack::development_mode_trigger_text() );
364
			}
365
			WP_CLI::line( __( 'TEST RESULTS:', 'jetpack' ) );
366
			foreach ( $this->raw_results( $group ) as $test ) {
367
				if ( true === $test['pass'] ) {
368
					WP_CLI::log( WP_CLI::colorize( '%gPassed:%n  ' . $test['name'] ) );
369
				} elseif ( 'skipped' === $test['pass'] ) {
370
					WP_CLI::log( WP_CLI::colorize( '%ySkipped:%n ' . $test['name'] ) );
371
					if ( $test['message'] ) {
372
						WP_CLI::log( '         ' . $test['message'] ); // Number of spaces to "tab indent" the reason.
373
					}
374
				} else { // Failed.
375
					WP_CLI::log( WP_CLI::colorize( '%rFailed:%n  ' . $test['name'] ) );
376
					WP_CLI::log( '         ' . $test['message'] ); // Number of spaces to "tab indent" the reason.
377
				}
378
			}
379
		}
380
	}
381
382
	/**
383
	 * Output results of failures in format expected by Core's Site Health tool for async tests.
384
	 *
385
	 * Specifically not asking for a testing group since we're opinionated that Site Heath should see all.
386
	 *
387
	 * @since 7.3.0
388
	 *
389
	 * @return array Array of test results
390
	 */
391
	public function output_results_for_core_async_site_health() {
392
		$result = array(
393
			'label'       => __( 'Jetpack passed all async tests.', 'jetpack' ),
394
			'status'      => 'good',
395
			'badge'       => array(
396
				'label' => __( 'Jetpack', 'jetpack' ),
397
				'color' => 'green',
398
			),
399
			'description' => sprintf(
400
				'<p>%s</p>',
401
				__( "Jetpack's async local testing suite passed all tests!", 'jetpack' )
402
			),
403
			'actions'     => '',
404
			'test'        => 'jetpack_debugger_local_testing_suite_core',
405
		);
406
407
		if ( $this->pass() ) {
408
			return $result;
409
		}
410
411
		$fails = $this->list_fails( 'async' );
412
		$error = false;
413
		foreach ( $fails as $fail ) {
414
			if ( ! $error ) {
415
				$error                 = true;
416
				$result['label']       = $fail['message'];
417
				$result['status']      = $fail['severity'];
418
				$result['description'] = sprintf(
419
					'<p>%s</p>',
420
					$fail['resolution']
421
				);
422
				if ( ! empty( $fail['action'] ) ) {
423
					$result['actions'] = sprintf(
424
						'<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>',
425
						esc_url( $fail['action'] ),
426
						__( 'Resolve', 'jetpack' ),
427
						/* translators: accessibility text */
428
						__( '(opens in a new tab)', 'jetpack' )
429
					);
430
				}
431
			} else {
432
				$result['description'] .= sprintf(
433
					'<p>%s</p>',
434
					__( 'There was another problem:', 'jetpack' )
435
				) . ' ' . $fail['message'] . ': ' . $fail['resolution'];
436
				if ( 'critical' === $fail['severity'] ) { // In case the initial failure is only "recommended".
437
					$result['status'] = 'critical';
438
				}
439
			}
440
		}
441
442
		return $result;
443
444
	}
445
446
	/**
447
	 * Provide single WP Error instance of all failures.
448
	 *
449
	 * @since 7.1.0
450
	 * @since 7.3.0 Add 'type'
451
	 *
452
	 * @param string $type  Test type, direct or async.
453
	 * @param string $group Testing group whose failures we want converted. Default all tests.
454
	 *
455
	 * @return WP_Error|false WP_Error with all failed tests or false if there were no failures.
456
	 */
457
	public function output_fails_as_wp_error( $type = 'all', $group = 'all' ) {
458
		if ( $this->pass( $group ) ) {
459
			return false;
460
		}
461
		$fails = $this->list_fails( $type, $group );
462
		$error = false;
463
464
		foreach ( $fails as $result ) {
465
			$code    = 'failed_' . $result['name'];
466
			$message = $result['message'];
467
			$data    = array(
468
				'resolution' => $result['resolution'],
469
			);
470
			if ( ! $error ) {
471
				$error = new WP_Error( $code, $message, $data );
472
			} else {
473
				$error->add( $code, $message, $data );
474
			}
475
		}
476
477
		return $error;
478
	}
479
480
	/**
481
	 * Encrypt data for sending to WordPress.com.
482
	 *
483
	 * @todo When PHP minimum is 5.3+, add cipher detection to use an agreed better cipher than RC4. RC4 should be the last resort.
484
	 *
485
	 * @param string $data Data to encrypt with the WP.com Public Key.
486
	 *
487
	 * @return false|array False if functionality not available. Array of encrypted data, encryption key.
488
	 */
489
	public function encrypt_string_for_wpcom( $data ) {
490
		$return = false;
491
		if ( ! function_exists( 'openssl_get_publickey' ) || ! function_exists( 'openssl_seal' ) ) {
492
			return $return;
493
		}
494
495
		$public_key = openssl_get_publickey( self::PUBLIC_KEY );
496
497
		if ( $public_key && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ) ) ) {
498
			// We are returning base64-encoded values to ensure they're characters we can use in JSON responses without issue.
499
			$return = array(
500
				'data'   => base64_encode( $encrypted_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
501
				'key'    => base64_encode( $env_key[0] ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
502
				'cipher' => 'RC4', // When Jetpack's minimum WP version is at PHP 5.3+, we will add in detecting and using a stronger one.
503
			);
504
		}
505
506
		openssl_free_key( $public_key );
507
508
		return $return;
509
	}
510
}
511