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

Base::list_fails()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 2
dl 0
loc 12
rs 9.5555
c 0
b 0
f 0
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
Bug introduced by
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