Completed
Push — update/sync-protect-failed-log... ( dc0d1b...9df7cc )
by
unknown
49:56 queued 42:13
created

Jetpack_Protect_Module::get_transient_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
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_init', array ( $this, 'check_use_math' ) );
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_head', array ( $this, 'check_login_ability' ) );
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() {
272
		/**
273
		 * Fires before every failed login attempt.
274
		 *
275
		 * @module protect
276
		 *
277
		 * @since 3.4.0
278
		 *
279
		 * @param string jetpack_protect_get_ip IP stored by Protect.
280
		 */
281
		do_action( 'jpp_log_failed_attempt', jetpack_protect_get_ip() );
282
283
		if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
284
285
			$transient = $this->get_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
286
			$transient--;
287
288
			if ( ! $transient || $transient < 1 ) {
289
				$this->delete_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
290
				setcookie( 'jpp_math_pass', 0, time() - DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false );
291
			} else {
292
				$this->set_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'], $transient, DAY_IN_SECONDS );
293
			}
294
295
		}
296
		$this->protect_call( 'failed_attempt' );
297
	}
298
299
	/**
300
	 * Set up the Protect configuration page
301
	 */
302
	public function modules_loaded() {
303
		Jetpack::enable_module_configurable( __FILE__ );
304
		Jetpack::module_configuration_load( __FILE__, array ( $this, 'configuration_load' ) );
305
		Jetpack::module_configuration_head( __FILE__, array ( $this, 'configuration_head' ) );
306
		Jetpack::module_configuration_screen( __FILE__, array ( $this, 'configuration_screen' ) );
307
	}
308
309
	/**
310
	 * Logs a successful login back to our servers, this allows us to make sure we're not blocking
311
	 * a busy IP that has a lot of good logins along with some forgotten passwords. Also saves current user's ip
312
	 * to the ip address whitelist
313
	 */
314
	public function log_successful_login( $user_login, $user = null ) {
315
		if ( ! $user ) { // For do_action( 'wp_login' ) calls that lacked passing the 2nd arg.
316
			$user = get_user_by( 'login', $user_login );
317
		}
318
319
		$this->protect_call( 'successful_login', array ( 'roles' => $user->roles ) );
320
	}
321
322
323
	/**
324
	 * Checks for loginability BEFORE authentication so that bots don't get to go around the log in form.
325
	 *
326
	 * If we are using our math fallback, authenticate via math-fallback.php
327
	 *
328
	 * @param string $user
329
	 * @param string $username
330
	 * @param string $password
331
	 *
332
	 * @return string $user
333
	 */
334
	function check_preauth( $user = 'Not Used By Protect', $username = 'Not Used By Protect', $password = 'Not Used By Protect' ) {
335
		$allow_login = $this->check_login_ability( true );
336
		$use_math    = $this->get_transient( 'brute_use_math' );
337
338
		if ( ! $allow_login ) {
339
			$this->block_with_math();
340
		}
341
342
		if ( ( 1 == $use_math || 1 == $this->block_login_with_math ) && isset( $_POST['log'] ) ) {
343
			include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
344
			Jetpack_Protect_Math_Authenticate::math_authenticate();
345
		}
346
347
		return $user;
348
	}
349
350
	/**
351
	 * Get all IP headers so that we can process on our server...
352
	 *
353
	 * @return string
354
	 */
355
	function get_headers() {
356
		$ip_related_headers = array (
357
			'GD_PHP_HANDLER',
358
			'HTTP_AKAMAI_ORIGIN_HOP',
359
			'HTTP_CF_CONNECTING_IP',
360
			'HTTP_CLIENT_IP',
361
			'HTTP_FASTLY_CLIENT_IP',
362
			'HTTP_FORWARDED',
363
			'HTTP_FORWARDED_FOR',
364
			'HTTP_INCAP_CLIENT_IP',
365
			'HTTP_TRUE_CLIENT_IP',
366
			'HTTP_X_CLIENTIP',
367
			'HTTP_X_CLUSTER_CLIENT_IP',
368
			'HTTP_X_FORWARDED',
369
			'HTTP_X_FORWARDED_FOR',
370
			'HTTP_X_IP_TRAIL',
371
			'HTTP_X_REAL_IP',
372
			'HTTP_X_VARNISH',
373
			'REMOTE_ADDR'
374
		);
375
376
		foreach ( $ip_related_headers as $header ) {
377
			if ( isset( $_SERVER[ $header ] ) ) {
378
				$output[ $header ] = $_SERVER[ $header ];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$output was never initialized. Although not strictly required by PHP, it is generally a good practice to add $output = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
379
			}
380
		}
381
382
		return $output;
0 ignored issues
show
Bug introduced by
The variable $output does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
383
	}
384
385
	/*
386
	 * Checks if the IP address has been whitelisted
387
	 *
388
	 * @param string $ip
389
	 *
390
	 * @return bool
391
	 */
392
	function ip_is_whitelisted( $ip ) {
393
		// If we found an exact match in wp-config
394
		if ( defined( 'JETPACK_IP_ADDRESS_OK' ) && JETPACK_IP_ADDRESS_OK == $ip ) {
395
			return true;
396
		}
397
398
		$whitelist = jetpack_protect_get_local_whitelist();
399
400
		if ( is_multisite() ) {
401
			$whitelist = array_merge( $whitelist, get_site_option( 'jetpack_protect_global_whitelist', array () ) );
402
		}
403
404
		if ( ! empty( $whitelist ) ) :
405
			foreach ( $whitelist as $item ) :
406
				// If the IPs are an exact match
407
				if ( ! $item->range && isset( $item->ip_address ) && $item->ip_address == $ip ) {
408
					return true;
409
				}
410
411
				if ( $item->range && isset( $item->range_low ) && isset( $item->range_high ) ) {
412
					if ( jetpack_protect_ip_address_is_in_range( $ip, $item->range_low, $item->range_high ) ) {
413
						return true;
414
					}
415
				}
416
			endforeach;
417
		endif;
418
419
		return false;
420
	}
421
422
	/**
423
	 * Checks the status for a given IP. API results are cached as transients
424
	 *
425
	 * @param bool $preauth Whether or not we are checking prior to authorization
426
	 *
427
	 * @return bool Either returns true, fires $this->kill_login, or includes a math fallback and returns false
428
	 */
429
	function check_login_ability( $preauth = false ) {
430
		if ( $this->is_current_ip_whitelisted() ) {
431
		    return true;
432
        }
433
434
		$status = $this->get_cached_status();
435
436
		if ( empty( $status ) ) {
437
			// If we've reached this point, this means that the IP isn't cached.
438
			// Now we check with the Protect API to see if we should allow login
439
			$response = $this->protect_call( $action = 'check_ip' );
440
441
			if ( isset( $response['math'] ) && ! function_exists( 'brute_math_authenticate' ) ) {
442
				include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
443
				new Jetpack_Protect_Math_Authenticate;
444
445
				return false;
446
			}
447
448
			$status = $response['status'];
449
		}
450
451
		if ( 'blocked' == $status ) {
452
			$this->block_with_math();
453
		}
454
455
		if ( 'blocked-hard' == $status ) {
456
			$this->kill_login();
457
		}
458
459
		return true;
460
	}
461
462
	function is_current_ip_whitelisted() {
463
		$ip = jetpack_protect_get_ip();
464
465
		// Server is misconfigured and we can't get an IP
466
		if ( ! $ip && class_exists( 'Jetpack' ) ) {
467
			Jetpack::deactivate_module( 'protect' );
468
			ob_start();
469
			Jetpack::state( 'message', 'protect_misconfigured_ip' );
470
			ob_end_clean();
471
			return true;
472
		}
473
474
		/**
475
		 * Short-circuit check_login_ability.
476
		 *
477
		 * If there is an alternate way to validate the current IP such as
478
		 * a hard-coded list of IP addresses, we can short-circuit the rest
479
		 * of the login ability checks and return true here.
480
		 *
481
		 * @module protect
482
		 *
483
		 * @since 4.4.0
484
		 *
485
		 * @param bool false Should we allow all logins for the current ip? Default: false
486
		 */
487
		if ( apply_filters( 'jpp_allow_login', false, $ip ) ) {
488
			return true;
489
		}
490
491
		if ( jetpack_protect_ip_is_private( $ip ) ) {
492
			return true;
493
		}
494
495
		if ( $this->ip_is_whitelisted( $ip ) ) {
496
			return true;
497
		}
498
    }
499
500
    function has_login_ability() {
501
	    if ( $this->is_current_ip_whitelisted() ) {
502
		    return true;
503
	    }
504
	    $status = $this->get_cached_status();
505
	    if ( empty( $status ) || $status === 'ok' ) {
506
	        return true;
507
        }
508
        return false;
509
    }
510
511
	function get_cached_status() {
512
		$transient_name  = $this->get_transient_name();
513
		$value = $this->get_transient( $transient_name );
514
		if ( isset( $value['status'] ) ) {
515
		    return $value['status'];
516
        }
517
        return '';
518
	}
519
520
	function block_with_math() {
521
		/**
522
		 * By default, Protect will allow a user who has been blocked for too
523
		 * many failed logins to start answering math questions to continue logging in
524
		 *
525
		 * For added security, you can disable this.
526
		 *
527
		 * @module protect
528
		 *
529
		 * @since 3.6.0
530
		 *
531
		 * @param bool Whether to allow math for blocked users or not.
532
		 */
533
534
		$this->block_login_with_math = 1;
535
		/**
536
		 * Allow Math fallback for blocked IPs.
537
		 *
538
		 * @module protect
539
		 *
540
		 * @since 3.6.0
541
		 *
542
		 * @param bool true Should we fallback to the Math questions when an IP is blocked. Default to true.
543
		 */
544
		$allow_math_fallback_on_fail = apply_filters( 'jpp_use_captcha_when_blocked', true );
545
		if ( ! $allow_math_fallback_on_fail ) {
546
			$this->kill_login();
547
		}
548
		include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
549
		new Jetpack_Protect_Math_Authenticate;
550
551
		return false;
552
	}
553
554
	/*
555
	 * Kill a login attempt
556
	 */
557
	function kill_login() {
558
		$ip = jetpack_protect_get_ip();
559
		/**
560
		 * Fires before every killed login.
561
		 *
562
		 * @module protect
563
		 *
564
		 * @since 3.4.0
565
		 *
566
		 * @param string $ip IP flagged by Protect.
567
		 */
568
		do_action( 'jpp_kill_login', $ip );
569
		$help_url = 'https://jetpack.com/support/security-features/#unblock';
570
571
		$die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.  <a href="%2$s">Find out more...</a>', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ), esc_url( $help_url ) );
572
573
		if( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
574
			$die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
575
		}
576
577
		wp_die(
578
			$die_string,
579
			__( 'Login Blocked by Jetpack', 'jetpack' ),
580
			array ( 'response' => 403 )
581
		);
582
	}
583
584
	/*
585
	 * Checks if the protect API call has failed, and if so initiates the math captcha fallback.
586
	 */
587
	public function check_use_math() {
588
		$use_math = $this->get_transient( 'brute_use_math' );
589
		if ( $use_math ) {
590
			include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
591
			new Jetpack_Protect_Math_Authenticate;
592
		}
593
	}
594
595
	/**
596
	 * Get or delete API key
597
	 */
598
	public function configuration_load() {
599
600
		if ( isset( $_POST['action'] ) && $_POST['action'] == 'jetpack_protect_save_whitelist' && wp_verify_nonce( $_POST['_wpnonce'], 'jetpack-protect' ) ) {
601
			$whitelist             = str_replace( ' ', '', $_POST['whitelist'] );
602
			$whitelist             = explode( PHP_EOL, $whitelist );
603
			$result                = jetpack_protect_save_whitelist( $whitelist );
604
			$this->whitelist_saved = ! is_wp_error( $result );
605
			$this->whitelist_error = is_wp_error( $result );
606
		}
607
608
		if ( isset( $_POST['action'] ) && 'get_protect_key' == $_POST['action'] && wp_verify_nonce( $_POST['_wpnonce'], 'jetpack-protect' ) ) {
609
			$result = $this->get_protect_key();
610
			// Only redirect on success
611
			// If it fails we need access to $this->api_key_error
612
			if ( $result ) {
613
				wp_safe_redirect( Jetpack::module_configuration_url( 'protect' ) );
614
				exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method configuration_load() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
615
			}
616
		}
617
618
		$this->api_key = get_site_option( 'jetpack_protect_key', false );
619
		$this->user_ip = jetpack_protect_get_ip();
620
	}
621
622
	public function configuration_head() {
623
		wp_enqueue_style( 'jetpack-protect' );
624
	}
625
626
	/**
627
	 * Prints the configuration screen
628
	 */
629
	public function configuration_screen() {
630
		require_once dirname( __FILE__ ) . '/protect/config-ui.php';
631
	}
632
633
	/**
634
	 * If we're in a multisite network, return the blog ID of the primary blog
635
	 *
636
	 * @return int
637
	 */
638
	public function get_main_blog_id() {
639
		if ( ! is_multisite() ) {
640
			return false;
641
		}
642
643
		global $current_site;
644
		$primary_blog_id = $current_site->blog_id;
645
646
		return $primary_blog_id;
647
	}
648
649
	/**
650
	 * Get jetpack blog id, or the jetpack blog id of the main blog in the main network
651
	 *
652
	 * @return int
653
	 */
654
	public function get_main_blog_jetpack_id() {
655
		if ( ! is_main_site() ) {
656
			switch_to_blog( $this->get_main_blog_id() );
657
			$id = Jetpack::get_option( 'id', false );
658
			restore_current_blog();
659
		} else {
660
			$id = Jetpack::get_option( 'id' );
661
		}
662
663
		return $id;
664
	}
665
666
	public function check_api_key() {
667
		$response = $this->protect_call( 'check_key' );
668
669
		if ( isset( $response['ckval'] ) ) {
670
			return true;
671
		}
672
673
		if ( isset( $response['error'] ) ) {
674
675
			if ( $response['error'] == 'Invalid API Key' ) {
676
				$this->api_key_error = __( 'Your API key is invalid', 'jetpack' );
677
			}
678
679
			if ( $response['error'] == 'API Key Required' ) {
680
				$this->api_key_error = __( 'No API key', 'jetpack' );
681
			}
682
		}
683
684
		$this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack' );
685
686
		return false;
687
	}
688
689
	/**
690
	 * Calls over to the api using wp_remote_post
691
	 *
692
	 * @param string $action 'check_ip', 'check_key', or 'failed_attempt'
693
	 * @param array $request Any custom data to post to the api
694
	 *
695
	 * @return array
696
	 */
697
	function protect_call( $action = 'check_ip', $request = array () ) {
698
		global $wp_version;
699
700
		$api_key = $this->maybe_get_protect_key();
701
702
		$user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' );
703
704
		$request['action']            = $action;
705
		$request['ip']                = jetpack_protect_get_ip();
706
		$request['host']              = $this->get_local_host();
707
		$request['headers']           = json_encode( $this->get_headers() );
708
		$request['jetpack_version']   = constant( 'JETPACK__VERSION' );
709
		$request['wordpress_version'] = strval( $wp_version );
710
		$request['api_key']           = $api_key;
711
		$request['multisite']         = "0";
712
713
		if ( is_multisite() ) {
714
			$request['multisite'] = get_blog_count();
715
		}
716
717
718
		/**
719
		 * Filter controls maximum timeout in waiting for reponse from Protect servers.
720
		 *
721
		 * @module protect
722
		 *
723
		 * @since 4.0.4
724
		 *
725
		 * @param int $timeout Max time (in seconds) to wait for a response.
726
		 */
727
		$timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 );
728
729
		$args = array (
730
			'body'        => $request,
731
			'user-agent'  => $user_agent,
732
			'httpversion' => '1.0',
733
			'timeout'     => absint( $timeout )
734
		);
735
736
		$response_json           = wp_remote_post( $this->get_api_host(), $args );
737
		$this->last_response_raw = $response_json;
738
739
		$transient_name = $this->get_transien_name();
0 ignored issues
show
Bug introduced by
The method get_transien_name() does not exist on Jetpack_Protect_Module. Did you maybe mean get_transient_name()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
740
		$this->delete_transient( $transient_name );
741
742
		if ( is_array( $response_json ) ) {
743
			$response = json_decode( $response_json['body'], true );
744
		}
745
746
		if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) {
747
			update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] );
748
		}
749
750
		if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) {
751
			$response['expire'] = time() + $response['seconds_remaining'];
752
			$this->set_transient( $transient_name, $response, $response['seconds_remaining'] );
753
			$this->delete_transient( 'brute_use_math' );
754
		} else { // Fallback to Math Captcha if no response from API host
755
			$this->set_transient( 'brute_use_math', 1, 600 );
756
			$response['status'] = 'ok';
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
757
			$response['math']   = true;
758
		}
759
760
		if ( isset( $response['error'] ) ) {
761
			update_site_option( 'jetpack_protect_error', $response['error'] );
762
		} else {
763
			delete_site_option( 'jetpack_protect_error' );
764
		}
765
766
		return $response;
767
	}
768
769
	function get_transient_name() {
770
		$headers     = $this->get_headers();
771
		$header_hash = md5( json_encode( $headers ) );
772
773
		return 'jpp_li_' . $header_hash;
774
	}
775
776
	/**
777
	 * Wrapper for WordPress set_transient function, our version sets
778
	 * the transient on the main site in the network if this is a multisite network
779
	 *
780
	 * We do it this way (instead of set_site_transient) because of an issue where
781
	 * sitewide transients are always autoloaded
782
	 * https://core.trac.wordpress.org/ticket/22846
783
	 *
784
	 * @param string $transient Transient name. Expected to not be SQL-escaped. Must be
785
	 *                           45 characters or fewer in length.
786
	 * @param mixed $value Transient value. Must be serializable if non-scalar.
787
	 *                           Expected to not be SQL-escaped.
788
	 * @param int $expiration Optional. Time until expiration in seconds. Default 0.
789
	 *
790
	 * @return bool False if value was not set and true if value was set.
791
	 */
792
	function set_transient( $transient, $value, $expiration ) {
793
		if ( is_multisite() && ! is_main_site() ) {
794
			switch_to_blog( $this->get_main_blog_id() );
795
			$return = set_transient( $transient, $value, $expiration );
796
			restore_current_blog();
797
798
			return $return;
799
		}
800
801
		return set_transient( $transient, $value, $expiration );
802
	}
803
804
	/**
805
	 * Wrapper for WordPress delete_transient function, our version deletes
806
	 * the transient on the main site in the network if this is a multisite network
807
	 *
808
	 * @param string $transient Transient name. Expected to not be SQL-escaped.
809
	 *
810
	 * @return bool true if successful, false otherwise
811
	 */
812 View Code Duplication
	function delete_transient( $transient ) {
813
		if ( is_multisite() && ! is_main_site() ) {
814
			switch_to_blog( $this->get_main_blog_id() );
815
			$return = delete_transient( $transient );
816
			restore_current_blog();
817
818
			return $return;
819
		}
820
821
		return delete_transient( $transient );
822
	}
823
824
	/**
825
	 * Wrapper for WordPress get_transient function, our version gets
826
	 * the transient on the main site in the network if this is a multisite network
827
	 *
828
	 * @param string $transient Transient name. Expected to not be SQL-escaped.
829
	 *
830
	 * @return mixed Value of transient.
831
	 */
832 View Code Duplication
	function get_transient( $transient ) {
833
		if ( is_multisite() && ! is_main_site() ) {
834
			switch_to_blog( $this->get_main_blog_id() );
835
			$return = get_transient( $transient );
836
			restore_current_blog();
837
838
			return $return;
839
		}
840
841
		return get_transient( $transient );
842
	}
843
844
	function get_api_host() {
845
		if ( isset( $this->api_endpoint ) ) {
846
			return $this->api_endpoint;
847
		}
848
849
		//Check to see if we can use SSL
850
		$this->api_endpoint = Jetpack::fix_url_for_bad_hosts( JETPACK_PROTECT__API_HOST );
851
852
		return $this->api_endpoint;
853
	}
854
855
	function get_local_host() {
856
		if ( isset( $this->local_host ) ) {
857
			return $this->local_host;
858
		}
859
860
		$uri = 'http://' . strtolower( $_SERVER['HTTP_HOST'] );
861
862
		if ( is_multisite() ) {
863
			$uri = network_home_url();
864
		}
865
866
		$uridata = parse_url( $uri );
867
868
		$domain = $uridata['host'];
869
870
		// If we still don't have the site_url, get it
871
		if ( ! $domain ) {
872
			$uri     = get_site_url( 1 );
873
			$uridata = parse_url( $uri );
874
			$domain  = $uridata['host'];
875
		}
876
877
		$this->local_host = $domain;
878
879
		return $this->local_host;
880
	}
881
882
}
883
884
Jetpack_Protect_Module::instance();
885
886
if ( isset( $pagenow ) && 'wp-login.php' == $pagenow ) {
887
	Jetpack_Protect_Module::check_login_ability();
888
}
889