https-detection.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. /**
  3. * @group https-detection
  4. */
  5. class Tests_HTTPS_Detection extends WP_UnitTestCase {
  6. private $last_request_url;
  7. public function setUp() {
  8. parent::setUp();
  9. remove_all_filters( 'option_home' );
  10. remove_all_filters( 'option_siteurl' );
  11. remove_all_filters( 'home_url' );
  12. remove_all_filters( 'site_url' );
  13. }
  14. /**
  15. * @ticket 47577
  16. */
  17. public function test_wp_is_using_https() {
  18. update_option( 'home', 'http://example.com/' );
  19. update_option( 'siteurl', 'http://example.com/' );
  20. $this->assertFalse( wp_is_using_https() );
  21. // Expect false if only one of the two relevant URLs is HTTPS.
  22. update_option( 'siteurl', 'https://example.com/' );
  23. $this->assertFalse( wp_is_using_https() );
  24. update_option( 'home', 'https://example.com/' );
  25. $this->assertTrue( wp_is_using_https() );
  26. // Test that the manually included 'site_url' filter works as expected
  27. // by using it to set the URL to use HTTP.
  28. add_filter( 'site_url', $this->filter_set_url_scheme( 'http' ) );
  29. $this->assertFalse( wp_is_using_https() );
  30. }
  31. /**
  32. * @ticket 47577
  33. */
  34. public function test_wp_is_https_supported() {
  35. // The function works with cached errors, so only test that here.
  36. $wp_error = new WP_Error();
  37. // No errors, so HTTPS is supported.
  38. update_option( 'https_detection_errors', $wp_error->errors );
  39. $this->assertTrue( wp_is_https_supported() );
  40. // Errors, so HTTPS is not supported.
  41. $wp_error->add( 'ssl_verification_failed', 'SSL verification failed.' );
  42. update_option( 'https_detection_errors', $wp_error->errors );
  43. $this->assertFalse( wp_is_https_supported() );
  44. }
  45. /**
  46. * @ticket 47577
  47. * @ticket 52484
  48. */
  49. public function test_wp_update_https_detection_errors() {
  50. // Set HTTP URL, the request below should use its HTTPS version.
  51. update_option( 'home', 'http://example.com/' );
  52. add_filter( 'pre_http_request', array( $this, 'record_request_url' ), 10, 3 );
  53. // If initial request succeeds, all good.
  54. add_filter( 'pre_http_request', array( $this, 'mock_success_with_sslverify' ), 10, 2 );
  55. wp_update_https_detection_errors();
  56. $this->assertSame( array(), get_option( 'https_detection_errors' ) );
  57. // If initial request fails and request without SSL verification succeeds,
  58. // return 'ssl_verification_failed' error.
  59. add_filter( 'pre_http_request', array( $this, 'mock_error_with_sslverify' ), 10, 2 );
  60. add_filter( 'pre_http_request', array( $this, 'mock_success_without_sslverify' ), 10, 2 );
  61. wp_update_https_detection_errors();
  62. $this->assertSame(
  63. array( 'ssl_verification_failed' => array( __( 'SSL verification failed.' ) ) ),
  64. get_option( 'https_detection_errors' )
  65. );
  66. // If both initial request and request without SSL verification fail,
  67. // return 'https_request_failed' error.
  68. add_filter( 'pre_http_request', array( $this, 'mock_error_with_sslverify' ), 10, 2 );
  69. add_filter( 'pre_http_request', array( $this, 'mock_error_without_sslverify' ), 10, 2 );
  70. wp_update_https_detection_errors();
  71. $this->assertSame(
  72. array( 'https_request_failed' => array( __( 'HTTPS request failed.' ) ) ),
  73. get_option( 'https_detection_errors' )
  74. );
  75. // If request succeeds, but response is not 200, return error with
  76. // 'bad_response_code' error code.
  77. add_filter( 'pre_http_request', array( $this, 'mock_not_found' ), 10, 2 );
  78. wp_update_https_detection_errors();
  79. $this->assertSame(
  80. array( 'bad_response_code' => array( 'Not Found' ) ),
  81. get_option( 'https_detection_errors' )
  82. );
  83. // If request succeeds, but response was not generated by this
  84. // WordPress site, return error with 'bad_response_source' error code.
  85. add_filter( 'pre_http_request', array( $this, 'mock_bad_source' ), 10, 2 );
  86. wp_update_https_detection_errors();
  87. $this->assertSame(
  88. array( 'bad_response_source' => array( 'It looks like the response did not come from this site.' ) ),
  89. get_option( 'https_detection_errors' )
  90. );
  91. // Check that the requests are made to the correct URL.
  92. $this->assertSame( 'https://example.com/', $this->last_request_url );
  93. }
  94. /**
  95. * @ticket 47577
  96. */
  97. public function test_pre_wp_update_https_detection_errors() {
  98. // Override to enforce no errors being detected.
  99. add_filter(
  100. 'pre_wp_update_https_detection_errors',
  101. function() {
  102. return new WP_Error();
  103. }
  104. );
  105. wp_update_https_detection_errors();
  106. $this->assertSame( array(), get_option( 'https_detection_errors' ) );
  107. // Override to enforce an error being detected.
  108. add_filter(
  109. 'pre_wp_update_https_detection_errors',
  110. function() {
  111. return new WP_Error(
  112. 'ssl_verification_failed',
  113. 'Bad SSL certificate.'
  114. );
  115. }
  116. );
  117. wp_update_https_detection_errors();
  118. $this->assertSame(
  119. array( 'ssl_verification_failed' => array( 'Bad SSL certificate.' ) ),
  120. get_option( 'https_detection_errors' )
  121. );
  122. }
  123. /**
  124. * @ticket 47577
  125. */
  126. public function test_wp_schedule_https_detection() {
  127. wp_schedule_https_detection();
  128. $this->assertSame( 'twicedaily', wp_get_schedule( 'wp_https_detection' ) );
  129. }
  130. /**
  131. * @ticket 47577
  132. */
  133. public function test_wp_cron_conditionally_prevent_sslverify() {
  134. // If URL is not using HTTPS, don't set 'sslverify' to false.
  135. $request = array(
  136. 'url' => 'http://example.com/',
  137. 'args' => array( 'sslverify' => true ),
  138. );
  139. $this->assertSame( $request, wp_cron_conditionally_prevent_sslverify( $request ) );
  140. // If URL is using HTTPS, set 'sslverify' to false.
  141. $request = array(
  142. 'url' => 'https://example.com/',
  143. 'args' => array( 'sslverify' => true ),
  144. );
  145. $expected = $request;
  146. $expected['args']['sslverify'] = false;
  147. $this->assertSame( $expected, wp_cron_conditionally_prevent_sslverify( $request ) );
  148. }
  149. /**
  150. * @ticket 47577
  151. * @ticket 52542
  152. */
  153. public function test_wp_is_local_html_output_via_rsd_link() {
  154. // HTML includes RSD link.
  155. $head_tag = get_echo( 'rsd_link' );
  156. $html = $this->get_sample_html_string( $head_tag );
  157. $this->assertTrue( wp_is_local_html_output( $html ) );
  158. // HTML includes modified RSD link but same URL.
  159. $head_tag = str_replace( ' />', '>', get_echo( 'rsd_link' ) );
  160. $html = $this->get_sample_html_string( $head_tag );
  161. $this->assertTrue( wp_is_local_html_output( $html ) );
  162. // HTML includes RSD link with alternative URL scheme.
  163. $head_tag = get_echo( 'rsd_link' );
  164. $head_tag = false !== strpos( $head_tag, 'https://' ) ? str_replace( 'https://', 'http://', $head_tag ) : str_replace( 'http://', 'https://', $head_tag );
  165. $html = $this->get_sample_html_string( $head_tag );
  166. $this->assertTrue( wp_is_local_html_output( $html ) );
  167. // HTML does not include RSD link.
  168. $html = $this->get_sample_html_string();
  169. $this->assertFalse( wp_is_local_html_output( $html ) );
  170. }
  171. /**
  172. * @ticket 47577
  173. */
  174. public function test_wp_is_local_html_output_via_wlwmanifest_link() {
  175. remove_action( 'wp_head', 'rsd_link' );
  176. // HTML includes WLW manifest link.
  177. $head_tag = get_echo( 'wlwmanifest_link' );
  178. $html = $this->get_sample_html_string( $head_tag );
  179. $this->assertTrue( wp_is_local_html_output( $html ) );
  180. // HTML includes modified WLW manifest link but same URL.
  181. $head_tag = str_replace( ' />', '>', get_echo( 'wlwmanifest_link' ) );
  182. $html = $this->get_sample_html_string( $head_tag );
  183. $this->assertTrue( wp_is_local_html_output( $html ) );
  184. // HTML includes WLW manifest link with alternative URL scheme.
  185. $head_tag = get_echo( 'wlwmanifest_link' );
  186. $head_tag = false !== strpos( $head_tag, 'https://' ) ? str_replace( 'https://', 'http://', $head_tag ) : str_replace( 'http://', 'https://', $head_tag );
  187. $html = $this->get_sample_html_string( $head_tag );
  188. $this->assertTrue( wp_is_local_html_output( $html ) );
  189. // HTML does not include WLW manifest link.
  190. $html = $this->get_sample_html_string();
  191. $this->assertFalse( wp_is_local_html_output( $html ) );
  192. }
  193. /**
  194. * @ticket 47577
  195. */
  196. public function test_wp_is_local_html_output_via_rest_link() {
  197. remove_action( 'wp_head', 'rsd_link' );
  198. remove_action( 'wp_head', 'wlwmanifest_link' );
  199. // HTML includes REST API link.
  200. $head_tag = get_echo( 'rest_output_link_wp_head' );
  201. $html = $this->get_sample_html_string( $head_tag );
  202. $this->assertTrue( wp_is_local_html_output( $html ) );
  203. // HTML includes modified REST API link but same URL.
  204. $head_tag = str_replace( ' />', '>', get_echo( 'rest_output_link_wp_head' ) );
  205. $html = $this->get_sample_html_string( $head_tag );
  206. $this->assertTrue( wp_is_local_html_output( $html ) );
  207. // HTML includes REST API link with alternative URL scheme.
  208. $head_tag = get_echo( 'rest_output_link_wp_head' );
  209. $head_tag = false !== strpos( $head_tag, 'https://' ) ? str_replace( 'https://', 'http://', $head_tag ) : str_replace( 'http://', 'https://', $head_tag );
  210. $html = $this->get_sample_html_string( $head_tag );
  211. $this->assertTrue( wp_is_local_html_output( $html ) );
  212. // HTML does not include REST API link.
  213. $html = $this->get_sample_html_string();
  214. $this->assertFalse( wp_is_local_html_output( $html ) );
  215. }
  216. /**
  217. * @ticket 47577
  218. */
  219. public function test_wp_is_local_html_output_cannot_determine() {
  220. remove_action( 'wp_head', 'rsd_link' );
  221. remove_action( 'wp_head', 'wlwmanifest_link' );
  222. remove_action( 'wp_head', 'rest_output_link_wp_head' );
  223. // The HTML here doesn't matter because all hooks are removed.
  224. $html = $this->get_sample_html_string();
  225. $this->assertNull( wp_is_local_html_output( $html ) );
  226. }
  227. public function record_request_url( $preempt, $parsed_args, $url ) {
  228. $this->last_request_url = $url;
  229. return $preempt;
  230. }
  231. public function mock_success_with_sslverify( $preempt, $parsed_args ) {
  232. if ( ! empty( $parsed_args['sslverify'] ) ) {
  233. return $this->mock_success();
  234. }
  235. return $preempt;
  236. }
  237. public function mock_error_with_sslverify( $preempt, $parsed_args ) {
  238. if ( ! empty( $parsed_args['sslverify'] ) ) {
  239. return $this->mock_error();
  240. }
  241. return $preempt;
  242. }
  243. public function mock_success_without_sslverify( $preempt, $parsed_args ) {
  244. if ( empty( $parsed_args['sslverify'] ) ) {
  245. return $this->mock_success();
  246. }
  247. return $preempt;
  248. }
  249. public function mock_error_without_sslverify( $preempt, $parsed_args ) {
  250. if ( empty( $parsed_args['sslverify'] ) ) {
  251. return $this->mock_error();
  252. }
  253. return $preempt;
  254. }
  255. public function mock_not_found() {
  256. return array(
  257. 'body' => '<!DOCTYPE html><html><head><title>404</title></head><body>Not Found</body></html>',
  258. 'response' => array(
  259. 'code' => 404,
  260. 'message' => 'Not Found',
  261. ),
  262. );
  263. }
  264. public function mock_bad_source() {
  265. // Looks like a success response, but is not generated by WordPress (e.g. missing RSD link).
  266. return array(
  267. 'body' => $this->get_sample_html_string(),
  268. 'response' => array(
  269. 'code' => 200,
  270. 'message' => 'OK',
  271. ),
  272. );
  273. }
  274. private function mock_success() {
  275. // Success response containing RSD link.
  276. return array(
  277. 'body' => $this->get_sample_html_string( get_echo( 'rsd_link' ) ),
  278. 'response' => array(
  279. 'code' => 200,
  280. 'message' => 'OK',
  281. ),
  282. );
  283. }
  284. private function mock_error() {
  285. return new WP_Error( 'bad_ssl_certificate', 'Bad SSL certificate.' );
  286. }
  287. private function get_sample_html_string( $head_tag = '' ) {
  288. return '<!DOCTYPE html><html><head><title>Page Title</title>' . $head_tag . '</head><body>Page Content.</body></html>';
  289. }
  290. /**
  291. * Returns a filter callback that expects a URL and will set the URL scheme
  292. * to the provided $scheme.
  293. *
  294. * @param string $scheme URL scheme to set.
  295. * @return callable Filter callback.
  296. */
  297. private function filter_set_url_scheme( $scheme ) {
  298. return function( $url ) use ( $scheme ) {
  299. return set_url_scheme( $url, $scheme );
  300. };
  301. }
  302. }