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