Нет описания

class-jetpack-cxn-test-base.php 18KB

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