PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.4.3
Jetpack – WP Security, Backup, Speed, & Growth v7.4.3
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / modules / protect.php
protect.php
883 lines 25.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Module Name: Protect
4 * Module Description: Protect yourself from brute force and distributed brute force attacks, which are the most common way for hackers to get into your site.
5 * Sort Order: 1
6 * Recommendation Order: 4
7 * First Introduced: 3.4
8 * Requires Connection: Yes
9 * Auto Activate: Yes
10 * Module Tags: Recommended
11 * Feature: Security
12 * Additional Search Queries: security, jetpack protect, secure, protection, botnet, brute force, protect, login, bot, password, passwords, strong passwords, strong password, wp-login.php, protect admin
13 */
14
15 include_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php';
16
17 class Jetpack_Protect_Module {
18
19 private static $__instance = null;
20 public $api_key;
21 public $api_key_error;
22 public $whitelist;
23 public $whitelist_error;
24 public $whitelist_saved;
25 private $user_ip;
26 private $local_host;
27 private $api_endpoint;
28 public $last_request;
29 public $last_response_raw;
30 public $last_response;
31 private $block_login_with_math;
32
33 /**
34 * Singleton implementation
35 *
36 * @return object
37 */
38 public static function instance() {
39 if ( ! is_a( self::$__instance, 'Jetpack_Protect_Module' ) ) {
40 self::$__instance = new Jetpack_Protect_Module();
41 }
42
43 return self::$__instance;
44 }
45
46 /**
47 * Registers actions
48 */
49 private function __construct() {
50 add_action( 'jetpack_activate_module_protect', array ( $this, 'on_activation' ) );
51 add_action( 'jetpack_deactivate_module_protect', array ( $this, 'on_deactivation' ) );
52 add_action( 'jetpack_modules_loaded', array ( $this, 'modules_loaded' ) );
53 add_action( 'login_form', array ( $this, 'check_use_math' ), 0 );
54 add_filter( 'authenticate', array ( $this, 'check_preauth' ), 10, 3 );
55 add_action( 'wp_login', array ( $this, 'log_successful_login' ), 10, 2 );
56 add_action( 'wp_login_failed', array ( $this, 'log_failed_attempt' ) );
57 add_action( 'admin_init', array ( $this, 'maybe_update_headers' ) );
58 add_action( 'admin_init', array ( $this, 'maybe_display_security_warning' ) );
59
60 // This is a backup in case $pagenow fails for some reason
61 add_action( 'login_form', array ( $this, 'check_login_ability' ), 1 );
62
63 // Load math fallback after math page form submission
64 if ( isset( $_POST[ 'jetpack_protect_process_math_form' ] ) ) {
65 include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
66 new Jetpack_Protect_Math_Authenticate;
67 }
68
69 // Runs a script every day to clean up expired transients so they don't
70 // clog up our users' databases
71 require_once( JETPACK__PLUGIN_DIR . '/modules/protect/transient-cleanup.php' );
72 }
73
74 /**
75 * On module activation, try to get an api key
76 */
77 public function on_activation() {
78 if ( is_multisite() && is_main_site() && get_site_option( 'jetpack_protect_active', 0 ) == 0 ) {
79 update_site_option( 'jetpack_protect_active', 1 );
80 }
81
82 update_site_option( 'jetpack_protect_activating', 'activating' );
83
84 // Get BruteProtect's counter number
85 Jetpack_Protect_Module::protect_call( 'check_key' );
86 }
87
88 /**
89 * On module deactivation, unset protect_active
90 */
91 public function on_deactivation() {
92 if ( is_multisite() && is_main_site() ) {
93 update_site_option( 'jetpack_protect_active', 0 );
94 }
95 }
96
97 public function maybe_get_protect_key() {
98 if ( get_site_option( 'jetpack_protect_activating', false ) && ! get_site_option( 'jetpack_protect_key', false ) ) {
99 $key = $this->get_protect_key();
100 delete_site_option( 'jetpack_protect_activating' );
101 return $key;
102 }
103
104 return get_site_option( 'jetpack_protect_key' );
105 }
106
107 /**
108 * Sends a "check_key" API call once a day. This call allows us to track IP-related
109 * headers for this server via the Protect API, in order to better identify the source
110 * IP for login attempts
111 */
112 public function maybe_update_headers( $force = false ) {
113 $updated_recently = $this->get_transient( 'jpp_headers_updated_recently' );
114
115 if ( ! $force ) {
116 if ( isset( $_GET['protect_update_headers'] ) ) {
117 $force = true;
118 }
119 }
120
121 // check that current user is admin so we prevent a lower level user from adding
122 // a trusted header, allowing them to brute force an admin account
123 if ( ( $updated_recently && ! $force ) || ! current_user_can( 'update_plugins' ) ) {
124 return;
125 }
126
127 $response = Jetpack_Protect_Module::protect_call( 'check_key' );
128 $this->set_transient( 'jpp_headers_updated_recently', 1, DAY_IN_SECONDS );
129
130 if ( isset( $response['msg'] ) && $response['msg'] ) {
131 update_site_option( 'trusted_ip_header', json_decode( $response['msg'] ) );
132 }
133
134 }
135
136 public function maybe_display_security_warning() {
137 if ( is_multisite() && current_user_can( 'manage_network' ) ) {
138 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
139 require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
140 }
141
142 if ( ! ( is_plugin_active_for_network( 'jetpack/jetpack.php' ) || is_plugin_active_for_network( 'jetpack-dev/jetpack.php' ) ) ) {
143 add_action( 'load-index.php', array ( $this, 'prepare_jetpack_protect_multisite_notice' ) );
144 }
145 }
146 }
147
148 public function prepare_jetpack_protect_multisite_notice() {
149 add_action( 'admin_print_styles', array ( $this, 'admin_banner_styles' ) );
150 add_action( 'admin_notices', array ( $this, 'admin_jetpack_manage_notice' ) );
151 }
152
153 public function admin_banner_styles() {
154 global $wp_styles;
155
156 $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
157
158 wp_enqueue_style( 'jetpack', plugins_url( "css/jetpack-banners{$min}.css", JETPACK__PLUGIN_FILE ), false, JETPACK__VERSION );
159 $wp_styles->add_data( 'jetpack', 'rtl', true );
160 }
161
162 public function admin_jetpack_manage_notice() {
163
164 $dismissed = get_site_option( 'jetpack_dismissed_protect_multisite_banner' );
165
166 if ( $dismissed ) {
167 return;
168 }
169
170 $referer = '&_wp_http_referer=' . add_query_arg( '_wp_http_referer', null );
171 $opt_out_url = wp_nonce_url( Jetpack::admin_url( 'jetpack-notice=jetpack-protect-multisite-opt-out' . $referer ), 'jetpack_protect_multisite_banner_opt_out' );
172
173 ?>
174 <div id="message" class="updated jetpack-message jp-banner is-opt-in protect-error"
175 style="display:block !important;">
176 <a class="jp-banner__dismiss" href="<?php echo esc_url( $opt_out_url ); ?>"
177 title="<?php esc_attr_e( 'Dismiss this notice.', 'jetpack' ); ?>"></a>
178
179 <div class="jp-banner__content">
180 <h2><?php esc_html_e( 'Protect cannot keep your site secure.', 'jetpack' ); ?></h2>
181
182 <p><?php printf( __( 'Thanks for activating Protect! To start protecting your site, please network activate Jetpack on your Multisite installation and activate Protect on your primary site. Due to the way logins are handled on WordPress Multisite, Jetpack must be network-enabled in order for Protect to work properly. <a href="%s" target="_blank">Learn More</a>', 'jetpack' ), 'http://jetpack.com/support/multisite-protect' ); ?></p>
183 </div>
184 <div class="jp-banner__action-container is-opt-in">
185 <a href="<?php echo esc_url( network_admin_url( 'plugins.php' ) ); ?>" class="jp-banner__button"
186 id="wpcom-connect"><?php _e( 'View Network Admin', 'jetpack' ); ?></a>
187 </div>
188 </div>
189 <?php
190 }
191
192 /**
193 * Request an api key from wordpress.com
194 *
195 * @return bool | string
196 */
197 public function get_protect_key() {
198
199 $protect_blog_id = Jetpack_Protect_Module::get_main_blog_jetpack_id();
200
201 // If we can't find the the blog id, that means we are on multisite, and the main site never connected
202 // the protect api key is linked to the main blog id - instruct the user to connect their main blog
203 if ( ! $protect_blog_id ) {
204 $this->api_key_error = __( 'Your main blog is not connected to WordPress.com. Please connect to get an API key.', 'jetpack' );
205
206 return false;
207 }
208
209 $request = array (
210 'jetpack_blog_id' => $protect_blog_id,
211 'bruteprotect_api_key' => get_site_option( 'bruteprotect_api_key' ),
212 'multisite' => '0',
213 );
214
215 // Send the number of blogs on the network if we are on multisite
216 if ( is_multisite() ) {
217 $request['multisite'] = get_blog_count();
218 if ( ! $request['multisite'] ) {
219 global $wpdb;
220 $request['multisite'] = $wpdb->get_var( "SELECT COUNT(blog_id) as c FROM $wpdb->blogs WHERE spam = '0' AND deleted = '0' and archived = '0'" );
221 }
222 }
223
224 // Request the key
225 Jetpack::load_xml_rpc_client();
226 $xml = new Jetpack_IXR_Client( array (
227 'user_id' => get_current_user_id()
228 ) );
229 $xml->query( 'jetpack.protect.requestKey', $request );
230
231 // Hmm, can't talk to wordpress.com
232 if ( $xml->isError() ) {
233 $code = $xml->getErrorCode();
234 $message = $xml->getErrorMessage();
235 $this->api_key_error = sprintf( __( 'Error connecting to WordPress.com. Code: %1$s, %2$s', 'jetpack' ), $code, $message );
236
237 return false;
238 }
239
240 $response = $xml->getResponse();
241
242 // Hmm. Can't talk to the protect servers ( api.bruteprotect.com )
243 if ( ! isset( $response['data'] ) ) {
244 $this->api_key_error = __( 'No reply from Jetpack servers', 'jetpack' );
245
246 return false;
247 }
248
249 // There was an issue generating the key
250 if ( empty( $response['success'] ) ) {
251 $this->api_key_error = $response['data'];
252
253 return false;
254 }
255
256 // Key generation successful!
257 $active_plugins = Jetpack::get_active_plugins();
258
259 // We only want to deactivate BruteProtect if we successfully get a key
260 if ( in_array( 'bruteprotect/bruteprotect.php', $active_plugins ) ) {
261 Jetpack_Client_Server::deactivate_plugin( 'bruteprotect/bruteprotect.php', 'BruteProtect' );
262 }
263
264 $key = $response['data'];
265 update_site_option( 'jetpack_protect_key', $key );
266
267 return $key;
268 }
269
270 /**
271 * Called via WP action wp_login_failed to log failed attempt with the api
272 *
273 * Fires custom, plugable action jpp_log_failed_attempt with the IP
274 *
275 * @return void
276 */
277 function log_failed_attempt( $login_user = null ) {
278
279 /**
280 * Fires before every failed login attempt.
281 *
282 * @module protect
283 *
284 * @since 3.4.0
285 *
286 * @param array Information about failed login attempt
287 * [
288 * 'login' => (string) Username or email used in failed login attempt
289 * ]
290 */
291 do_action( 'jpp_log_failed_attempt', array( 'login' => $login_user ) );
292
293 if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
294
295 $transient = $this->get_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
296 $transient--;
297
298 if ( ! $transient || $transient < 1 ) {
299 $this->delete_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
300 setcookie( 'jpp_math_pass', 0, time() - DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false );
301 } else {
302 $this->set_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'], $transient, DAY_IN_SECONDS );
303 }
304
305 }
306 $this->protect_call( 'failed_attempt' );
307 }
308
309 /**
310 * Set up the Protect configuration page
311 */
312 public function modules_loaded() {
313 Jetpack::enable_module_configurable( __FILE__ );
314 }
315
316 /**
317 * Logs a successful login back to our servers, this allows us to make sure we're not blocking
318 * a busy IP that has a lot of good logins along with some forgotten passwords. Also saves current user's ip
319 * to the ip address whitelist
320 */
321 public function log_successful_login( $user_login, $user = null ) {
322 if ( ! $user ) { // For do_action( 'wp_login' ) calls that lacked passing the 2nd arg.
323 $user = get_user_by( 'login', $user_login );
324 }
325
326 $this->protect_call( 'successful_login', array ( 'roles' => $user->roles ) );
327 }
328
329
330 /**
331 * Checks for loginability BEFORE authentication so that bots don't get to go around the log in form.
332 *
333 * If we are using our math fallback, authenticate via math-fallback.php
334 *
335 * @param string $user
336 * @param string $username
337 * @param string $password
338 *
339 * @return string $user
340 */
341 function check_preauth( $user = 'Not Used By Protect', $username = 'Not Used By Protect', $password = 'Not Used By Protect' ) {
342 $allow_login = $this->check_login_ability( true );
343 $use_math = $this->get_transient( 'brute_use_math' );
344
345 if ( ! $allow_login ) {
346 $this->block_with_math();
347 }
348
349 if ( ( 1 == $use_math || 1 == $this->block_login_with_math ) && isset( $_POST['log'] ) ) {
350 include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
351 Jetpack_Protect_Math_Authenticate::math_authenticate();
352 }
353
354 return $user;
355 }
356
357 /**
358 * Get all IP headers so that we can process on our server...
359 *
360 * @return string
361 */
362 function get_headers() {
363 $ip_related_headers = array (
364 'GD_PHP_HANDLER',
365 'HTTP_AKAMAI_ORIGIN_HOP',
366 'HTTP_CF_CONNECTING_IP',
367 'HTTP_CLIENT_IP',
368 'HTTP_FASTLY_CLIENT_IP',
369 'HTTP_FORWARDED',
370 'HTTP_FORWARDED_FOR',
371 'HTTP_INCAP_CLIENT_IP',
372 'HTTP_TRUE_CLIENT_IP',
373 'HTTP_X_CLIENTIP',
374 'HTTP_X_CLUSTER_CLIENT_IP',
375 'HTTP_X_FORWARDED',
376 'HTTP_X_FORWARDED_FOR',
377 'HTTP_X_IP_TRAIL',
378 'HTTP_X_REAL_IP',
379 'HTTP_X_VARNISH',
380 'REMOTE_ADDR'
381 );
382
383 foreach ( $ip_related_headers as $header ) {
384 if ( isset( $_SERVER[ $header ] ) ) {
385 $output[ $header ] = $_SERVER[ $header ];
386 }
387 }
388
389 return $output;
390 }
391
392 /*
393 * Checks if the IP address has been whitelisted
394 *
395 * @param string $ip
396 *
397 * @return bool
398 */
399 function ip_is_whitelisted( $ip ) {
400 // If we found an exact match in wp-config
401 if ( defined( 'JETPACK_IP_ADDRESS_OK' ) && JETPACK_IP_ADDRESS_OK == $ip ) {
402 return true;
403 }
404
405 $whitelist = jetpack_protect_get_local_whitelist();
406
407 if ( is_multisite() ) {
408 $whitelist = array_merge( $whitelist, get_site_option( 'jetpack_protect_global_whitelist', array () ) );
409 }
410
411 if ( ! empty( $whitelist ) ) :
412 foreach ( $whitelist as $item ) :
413 // If the IPs are an exact match
414 if ( ! $item->range && isset( $item->ip_address ) && $item->ip_address == $ip ) {
415 return true;
416 }
417
418 if ( $item->range && isset( $item->range_low ) && isset( $item->range_high ) ) {
419 if ( jetpack_protect_ip_address_is_in_range( $ip, $item->range_low, $item->range_high ) ) {
420 return true;
421 }
422 }
423 endforeach;
424 endif;
425
426 return false;
427 }
428
429 /**
430 * Checks the status for a given IP. API results are cached as transients
431 *
432 * @param bool $preauth Whether or not we are checking prior to authorization
433 *
434 * @return bool Either returns true, fires $this->kill_login, or includes a math fallback and returns false
435 */
436 function check_login_ability( $preauth = false ) {
437
438 /**
439 * JETPACK_ALWAYS_PROTECT_LOGIN will always disable the login page, and use a page provided by Jetpack.
440 */
441 if ( Jetpack_Constants::is_true( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) ) {
442 $this->kill_login();
443 }
444
445 if ( $this->is_current_ip_whitelisted() ) {
446 return true;
447 }
448
449 $status = $this->get_cached_status();
450
451 if ( empty( $status ) ) {
452 // If we've reached this point, this means that the IP isn't cached.
453 // Now we check with the Protect API to see if we should allow login
454 $response = $this->protect_call( $action = 'check_ip' );
455
456 if ( isset( $response['math'] ) && ! function_exists( 'brute_math_authenticate' ) ) {
457 include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
458 new Jetpack_Protect_Math_Authenticate;
459
460 return false;
461 }
462
463 $status = $response['status'];
464 }
465
466 if ( 'blocked' == $status ) {
467 $this->block_with_math();
468 }
469
470 if ( 'blocked-hard' == $status ) {
471 $this->kill_login();
472 }
473
474 return true;
475 }
476
477 function is_current_ip_whitelisted() {
478 $ip = jetpack_protect_get_ip();
479
480 // Server is misconfigured and we can't get an IP
481 if ( ! $ip && class_exists( 'Jetpack' ) ) {
482 Jetpack::deactivate_module( 'protect' );
483 ob_start();
484 Jetpack::state( 'message', 'protect_misconfigured_ip' );
485 ob_end_clean();
486 return true;
487 }
488
489 /**
490 * Short-circuit check_login_ability.
491 *
492 * If there is an alternate way to validate the current IP such as
493 * a hard-coded list of IP addresses, we can short-circuit the rest
494 * of the login ability checks and return true here.
495 *
496 * @module protect
497 *
498 * @since 4.4.0
499 *
500 * @param bool false Should we allow all logins for the current ip? Default: false
501 */
502 if ( apply_filters( 'jpp_allow_login', false, $ip ) ) {
503 return true;
504 }
505
506 if ( jetpack_protect_ip_is_private( $ip ) ) {
507 return true;
508 }
509
510 if ( $this->ip_is_whitelisted( $ip ) ) {
511 return true;
512 }
513 }
514
515 function has_login_ability() {
516 if ( $this->is_current_ip_whitelisted() ) {
517 return true;
518 }
519 $status = $this->get_cached_status();
520 if ( empty( $status ) || $status === 'ok' ) {
521 return true;
522 }
523 return false;
524 }
525
526 function get_cached_status() {
527 $transient_name = $this->get_transient_name();
528 $value = $this->get_transient( $transient_name );
529 if ( isset( $value['status'] ) ) {
530 return $value['status'];
531 }
532 return '';
533 }
534
535 function block_with_math() {
536 /**
537 * By default, Protect will allow a user who has been blocked for too
538 * many failed logins to start answering math questions to continue logging in
539 *
540 * For added security, you can disable this.
541 *
542 * @module protect
543 *
544 * @since 3.6.0
545 *
546 * @param bool Whether to allow math for blocked users or not.
547 */
548
549 $this->block_login_with_math = 1;
550 /**
551 * Allow Math fallback for blocked IPs.
552 *
553 * @module protect
554 *
555 * @since 3.6.0
556 *
557 * @param bool true Should we fallback to the Math questions when an IP is blocked. Default to true.
558 */
559 $allow_math_fallback_on_fail = apply_filters( 'jpp_use_captcha_when_blocked', true );
560 if ( ! $allow_math_fallback_on_fail ) {
561 $this->kill_login();
562 }
563 include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
564 new Jetpack_Protect_Math_Authenticate;
565
566 return false;
567 }
568
569 /*
570 * Kill a login attempt
571 */
572 function kill_login() {
573 if (
574 isset( $_GET['action'], $_GET['_wpnonce'] ) &&
575 'logout' === $_GET['action'] &&
576 wp_verify_nonce( $_GET['_wpnonce'], 'log-out' ) &&
577 wp_get_current_user()
578
579 ) {
580 // Allow users to logout
581 return;
582 }
583
584 $ip = jetpack_protect_get_ip();
585 /**
586 * Fires before every killed login.
587 *
588 * @module protect
589 *
590 * @since 3.4.0
591 *
592 * @param string $ip IP flagged by Protect.
593 */
594 do_action( 'jpp_kill_login', $ip );
595
596 if( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
597 $die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
598 wp_die(
599 $die_string,
600 __( 'Login Blocked by Jetpack', 'jetpack' ),
601 array ( 'response' => 403 )
602 );
603 }
604
605 require_once dirname( __FILE__ ) . '/protect/blocked-login-page.php';
606 $blocked_login_page = Jetpack_Protect_Blocked_Login_Page::instance( $ip );
607
608 if ( $blocked_login_page->is_blocked_user_valid() ) {
609 return;
610 }
611
612 $blocked_login_page->render_and_die();
613 }
614
615 /*
616 * Checks if the protect API call has failed, and if so initiates the math captcha fallback.
617 */
618 public function check_use_math() {
619 $use_math = $this->get_transient( 'brute_use_math' );
620 if ( $use_math ) {
621 include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
622 new Jetpack_Protect_Math_Authenticate;
623 }
624 }
625
626 /**
627 * If we're in a multisite network, return the blog ID of the primary blog
628 *
629 * @return int
630 */
631 public function get_main_blog_id() {
632 if ( ! is_multisite() ) {
633 return false;
634 }
635
636 global $current_site;
637 $primary_blog_id = $current_site->blog_id;
638
639 return $primary_blog_id;
640 }
641
642 /**
643 * Get jetpack blog id, or the jetpack blog id of the main blog in the main network
644 *
645 * @return int
646 */
647 public function get_main_blog_jetpack_id() {
648 if ( ! is_main_site() ) {
649 switch_to_blog( $this->get_main_blog_id() );
650 $id = Jetpack::get_option( 'id', false );
651 restore_current_blog();
652 } else {
653 $id = Jetpack::get_option( 'id' );
654 }
655
656 return $id;
657 }
658
659 public function check_api_key() {
660 $response = $this->protect_call( 'check_key' );
661
662 if ( isset( $response['ckval'] ) ) {
663 return true;
664 }
665
666 if ( isset( $response['error'] ) ) {
667
668 if ( $response['error'] == 'Invalid API Key' ) {
669 $this->api_key_error = __( 'Your API key is invalid', 'jetpack' );
670 }
671
672 if ( $response['error'] == 'API Key Required' ) {
673 $this->api_key_error = __( 'No API key', 'jetpack' );
674 }
675 }
676
677 $this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack' );
678
679 return false;
680 }
681
682 /**
683 * Calls over to the api using wp_remote_post
684 *
685 * @param string $action 'check_ip', 'check_key', or 'failed_attempt'
686 * @param array $request Any custom data to post to the api
687 *
688 * @return array
689 */
690 function protect_call( $action = 'check_ip', $request = array () ) {
691 global $wp_version;
692
693 $api_key = $this->maybe_get_protect_key();
694
695 $user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' );
696
697 $request['action'] = $action;
698 $request['ip'] = jetpack_protect_get_ip();
699 $request['host'] = $this->get_local_host();
700 $request['headers'] = json_encode( $this->get_headers() );
701 $request['jetpack_version'] = constant( 'JETPACK__VERSION' );
702 $request['wordpress_version'] = strval( $wp_version );
703 $request['api_key'] = $api_key;
704 $request['multisite'] = "0";
705
706 if ( is_multisite() ) {
707 $request['multisite'] = get_blog_count();
708 }
709
710
711 /**
712 * Filter controls maximum timeout in waiting for reponse from Protect servers.
713 *
714 * @module protect
715 *
716 * @since 4.0.4
717 *
718 * @param int $timeout Max time (in seconds) to wait for a response.
719 */
720 $timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 );
721
722 $args = array (
723 'body' => $request,
724 'user-agent' => $user_agent,
725 'httpversion' => '1.0',
726 'timeout' => absint( $timeout )
727 );
728
729 $response_json = wp_remote_post( $this->get_api_host(), $args );
730 $this->last_response_raw = $response_json;
731
732 $transient_name = $this->get_transient_name();
733 $this->delete_transient( $transient_name );
734
735 if ( is_array( $response_json ) ) {
736 $response = json_decode( $response_json['body'], true );
737 }
738
739 if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) {
740 update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] );
741 }
742
743 if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) {
744 $response['expire'] = time() + $response['seconds_remaining'];
745 $this->set_transient( $transient_name, $response, $response['seconds_remaining'] );
746 $this->delete_transient( 'brute_use_math' );
747 } else { // Fallback to Math Captcha if no response from API host
748 $this->set_transient( 'brute_use_math', 1, 600 );
749 $response['status'] = 'ok';
750 $response['math'] = true;
751 }
752
753 if ( isset( $response['error'] ) ) {
754 update_site_option( 'jetpack_protect_error', $response['error'] );
755 } else {
756 delete_site_option( 'jetpack_protect_error' );
757 }
758
759 return $response;
760 }
761
762 function get_transient_name() {
763 $headers = $this->get_headers();
764 $header_hash = md5( json_encode( $headers ) );
765
766 return 'jpp_li_' . $header_hash;
767 }
768
769 /**
770 * Wrapper for WordPress set_transient function, our version sets
771 * the transient on the main site in the network if this is a multisite network
772 *
773 * We do it this way (instead of set_site_transient) because of an issue where
774 * sitewide transients are always autoloaded
775 * https://core.trac.wordpress.org/ticket/22846
776 *
777 * @param string $transient Transient name. Expected to not be SQL-escaped. Must be
778 * 45 characters or fewer in length.
779 * @param mixed $value Transient value. Must be serializable if non-scalar.
780 * Expected to not be SQL-escaped.
781 * @param int $expiration Optional. Time until expiration in seconds. Default 0.
782 *
783 * @return bool False if value was not set and true if value was set.
784 */
785 function set_transient( $transient, $value, $expiration ) {
786 if ( is_multisite() && ! is_main_site() ) {
787 switch_to_blog( $this->get_main_blog_id() );
788 $return = set_transient( $transient, $value, $expiration );
789 restore_current_blog();
790
791 return $return;
792 }
793
794 return set_transient( $transient, $value, $expiration );
795 }
796
797 /**
798 * Wrapper for WordPress delete_transient function, our version deletes
799 * the transient on the main site in the network if this is a multisite network
800 *
801 * @param string $transient Transient name. Expected to not be SQL-escaped.
802 *
803 * @return bool true if successful, false otherwise
804 */
805 function delete_transient( $transient ) {
806 if ( is_multisite() && ! is_main_site() ) {
807 switch_to_blog( $this->get_main_blog_id() );
808 $return = delete_transient( $transient );
809 restore_current_blog();
810
811 return $return;
812 }
813
814 return delete_transient( $transient );
815 }
816
817 /**
818 * Wrapper for WordPress get_transient function, our version gets
819 * the transient on the main site in the network if this is a multisite network
820 *
821 * @param string $transient Transient name. Expected to not be SQL-escaped.
822 *
823 * @return mixed Value of transient.
824 */
825 function get_transient( $transient ) {
826 if ( is_multisite() && ! is_main_site() ) {
827 switch_to_blog( $this->get_main_blog_id() );
828 $return = get_transient( $transient );
829 restore_current_blog();
830
831 return $return;
832 }
833
834 return get_transient( $transient );
835 }
836
837 function get_api_host() {
838 if ( isset( $this->api_endpoint ) ) {
839 return $this->api_endpoint;
840 }
841
842 //Check to see if we can use SSL
843 $this->api_endpoint = Jetpack::fix_url_for_bad_hosts( JETPACK_PROTECT__API_HOST );
844
845 return $this->api_endpoint;
846 }
847
848 function get_local_host() {
849 if ( isset( $this->local_host ) ) {
850 return $this->local_host;
851 }
852
853 $uri = 'http://' . strtolower( $_SERVER['HTTP_HOST'] );
854
855 if ( is_multisite() ) {
856 $uri = network_home_url();
857 }
858
859 $uridata = parse_url( $uri );
860
861 $domain = $uridata['host'];
862
863 // If we still don't have the site_url, get it
864 if ( ! $domain ) {
865 $uri = get_site_url( 1 );
866 $uridata = parse_url( $uri );
867 $domain = $uridata['host'];
868 }
869
870 $this->local_host = $domain;
871
872 return $this->local_host;
873 }
874
875 }
876
877 $jetpack_protect = Jetpack_Protect_Module::instance();
878
879 global $pagenow;
880 if ( isset( $pagenow ) && 'wp-login.php' == $pagenow ) {
881 $jetpack_protect->check_login_ability();
882 }
883