Completed
Push — update/jetpack-sso-mercury ( 5416cc )
by
unknown
09:43
created

Jetpack_SSO::display_sso_button()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
rs 9.4285
cc 3
eloc 13
nc 4
nop 1
1
<?php
2
3
/**
4
 * Module Name: Single Sign On
5
 * Module Description: Secure user authentication.
6
 * Jumpstart Description: Lets you log in to all your Jetpack-enabled sites with one click using your WordPress.com account.
7
 * Sort Order: 30
8
 * Recommendation Order: 5
9
 * First Introduced: 2.6
10
 * Requires Connection: Yes
11
 * Auto Activate: No
12
 * Module Tags: Developers
13
 * Feature: Jumpstart, Performance-Security
14
 * Additional Search Queries: sso, single sign on, login, log in
15
 */
16
17
class Jetpack_SSO {
18
	static $instance = null;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $instance.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
19
20
	private function __construct() {
21
22
		self::$instance = $this;
23
24
		add_action( 'admin_init',  array( $this, 'admin_init' ) );
25
		add_action( 'admin_init',  array( $this, 'register_settings' ) );
26
		add_action( 'login_init',  array( $this, 'login_init' ) );
27
		add_action( 'delete_user', array( $this, 'delete_connection_for_user' ) );
28
		add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
29
		add_action( 'init', array( $this, 'maybe_logout_user' ), 5 );
30
		add_action( 'jetpack_modules_loaded', array( $this, 'module_configure_button' ) );
31
32
		// Adding this action so that on login_init, the action won't be sanitized out of the $action global.
33
		add_action( 'login_form_jetpack-sso', '__return_true' );
34
35
		if (
36
			$this->should_hide_login_form() &&
37
			/**
38
			 * Filter the display of the disclaimer message appearing when default WordPress login form is disabled.
39
			 *
40
			 * @module sso
41
			 *
42
			 * @since 2.8.0
43
			 *
44
			 * @param bool true Should the disclaimer be displayed. Default to true.
45
			 */
46
			apply_filters( 'jetpack_sso_display_disclaimer', true )
47
		) {
48
			add_action( 'login_message', array( $this, 'msg_login_by_jetpack' ) );
49
		}
50
	}
51
52
	/**
53
	 * Returns the single instance of the Jetpack_SSO object
54
	 *
55
	 * @since 2.8
56
	 * @return Jetpack_SSO
57
	 **/
58
	public static function get_instance() {
59
		if ( ! is_null( self::$instance ) ) {
60
			return self::$instance;
61
		}
62
63
		return self::$instance = new Jetpack_SSO;
64
	}
65
66
	/**
67
	 * Add configure button and functionality to the module card on the Jetpack screen
68
	 **/
69
	public static function module_configure_button() {
70
		Jetpack::enable_module_configurable( __FILE__ );
71
		Jetpack::module_configuration_load( __FILE__, array( __CLASS__, 'module_configuration_load' ) );
72
		Jetpack::module_configuration_head( __FILE__, array( __CLASS__, 'module_configuration_head' ) );
73
		Jetpack::module_configuration_screen( __FILE__, array( __CLASS__, 'module_configuration_screen' ) );
74
	}
75
76
	public static function module_configuration_load() {
77
		// wp_safe_redirect( admin_url( 'options-general.php#configure-sso' ) );
78
		// exit;
79
	}
80
81
	public static function module_configuration_head() {}
82
83
	public static function module_configuration_screen() {
84
		?>
85
		<form method="post" action="options.php">
86
			<?php settings_fields( 'jetpack-sso' ); ?>
87
			<?php do_settings_sections( 'jetpack-sso' ); ?>
88
			<?php submit_button(); ?>
89
		</form>
90
		<?php
91
	}
92
93
	/**
94
	 * If jetpack_force_logout == 1 in current user meta the user will be forced
95
	 * to logout and reauthenticate with the site.
96
	 **/
97
	public function maybe_logout_user() {
98
		global $current_user;
99
100
		if ( 1 == $current_user->jetpack_force_logout ) {
101
			delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
102
			self::delete_connection_for_user( $current_user->ID );
103
			wp_logout();
104
			wp_safe_redirect( wp_login_url() );
105
		}
106
	}
107
108
109
	/**
110
	 * Adds additional methods the WordPress xmlrpc API for handling SSO specific features
111
	 *
112
	 * @param array $methods
113
	 * @return array
114
	 **/
115
	public function xmlrpc_methods( $methods ) {
116
		$methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' );
117
		return $methods;
118
	}
119
120
	/**
121
	 * Marks a user's profile for disconnect from WordPress.com and forces a logout
122
	 * the next time the user visits the site.
123
	 **/
124
	public function xmlrpc_user_disconnect( $user_id ) {
125
		$user_query = new WP_User_Query(
126
			array(
127
				'meta_key' => 'wpcom_user_id',
128
				'meta_value' => $user_id,
129
			)
130
		);
131
		$user = $user_query->get_results();
132
		$user = $user[0];
133
134
		if ( $user instanceof WP_User ) {
0 ignored issues
show
Bug introduced by
The class WP_User does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
135
			$user = wp_set_current_user( $user->ID );
136
			update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
137
			self::delete_connection_for_user( $user->ID );
138
			return true;
139
		}
140
		return false;
141
	}
142
143
	/**
144
	 * Adds settings fields to Settings > General > Single Sign On that allows users to
145
	 * turn off the login form on wp-login.php
146
	 *
147
	 * @since 2.7
148
	 **/
149
	public function register_settings() {
150
151
		add_settings_section(
152
			'jetpack_sso_settings',
153
			__( 'Single Sign On' , 'jetpack' ),
154
			'__return_false',
155
			'jetpack-sso'
156
		);
157
158
		/*
159
		 * Settings > General > Single Sign On
160
		 * Checkbox for Remove default login form
161
		 */
162
		 /* Hide in 2.9
163
		register_setting(
164
			'general',
165
			'jetpack_sso_remove_login_form',
166
			array( $this, 'validate_settings_remove_login_form_checkbox' )
167
		);
168
169
		add_settings_field(
170
			'jetpack_sso_remove_login_form',
171
			__( 'Remove default login form?' , 'jetpack' ),
172
			array( $this, 'render_remove_login_form_checkbox' ),
173
			'general',
174
			'jetpack_sso_settings'
175
		);
176
		*/
177
178
		/*
179
		 * Settings > General > Single Sign On
180
		 * Require two step authentication
181
		 */
182
		register_setting(
183
			'jetpack-sso',
184
			'jetpack_sso_require_two_step',
185
			array( $this, 'validate_jetpack_sso_require_two_step' )
186
		);
187
188
		add_settings_field(
189
			'jetpack_sso_require_two_step',
190
			'', // __( 'Require Two-Step Authentication' , 'jetpack' ),
191
			array( $this, 'render_require_two_step' ),
192
			'jetpack-sso',
193
			'jetpack_sso_settings'
194
		);
195
196
		/*
197
		 * Settings > General > Single Sign On
198
		 */
199
		register_setting(
200
			'jetpack-sso',
201
			'jetpack_sso_match_by_email',
202
			array( $this, 'validate_jetpack_sso_match_by_email' )
203
		);
204
205
		add_settings_field(
206
			'jetpack_sso_match_by_email',
207
			'', // __( 'Match by Email' , 'jetpack' ),
208
			array( $this, 'render_match_by_email' ),
209
			'jetpack-sso',
210
			'jetpack_sso_settings'
211
		);
212
	}
213
214
	/**
215
	 * Builds the display for the checkbox allowing user to require two step
216
	 * auth be enabled on WordPress.com accounts before login. Displays in Settings > General
217
	 *
218
	 * @since 2.7
219
	 **/
220
	public function render_require_two_step() {
221
		/** This filter is documented in modules/sso.php */
222
		$require_two_step = ( 1 == apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step' ) ) );
223
		$disabled = $require_two_step ? ' disabled="disabled"' : '';
224
		echo '<label>';
225
		echo '<input type="checkbox" name="jetpack_sso_require_two_step" ' . checked( $require_two_step, true, false ) . "$disabled>";
226
		esc_html_e( 'Require Two-Step Authentication' , 'jetpack' );
227
		echo '</label>';
228
	}
229
230
	/**
231
	 * Validate the require  two step checkbox in Settings > General
232
	 *
233
	 * @since 2.7
234
	 * @return boolean
235
	 **/
236
	public function validate_jetpack_sso_require_two_step( $input ) {
237
		return ( ! empty( $input ) ) ? 1 : 0;
238
	}
239
240
	/**
241
	 * Builds the display for the checkbox allowing the user to allow matching logins by email
242
	 * Displays in Settings > General
243
	 *
244
	 * @since 2.9
245
	 **/
246
	public function render_match_by_email() {
247
		$match_by_email = 1 == $this->match_by_email();
248
		$disabled = $match_by_email ? ' disabled="disabled"' : '';
249
		echo '<label>';
250
		echo '<input type="checkbox" name="jetpack_sso_match_by_email"' . checked( $match_by_email, true, false ) . "$disabled>";
251
		esc_html_e( 'Match by Email', 'jetpack' );
252
		echo '</label>';
253
	}
254
255
	/**
256
	 * Validate the match by email check in Settings > General
257
	 *
258
	 * @since 2.9
259
	 * @return boolean
260
	 **/
261
	public function validate_jetpack_sso_match_by_email( $input ) {
262
		return ( ! empty( $input ) ) ? 1 : 0;
263
	}
264
265
	/**
266
	 * Builds the display for the checkbox allowing users to remove the default
267
	 * WordPress login form from wp-login.php. Displays in Settings > General
268
	 *
269
	 * @since 2.7
270
	 **/
271
	public function render_remove_login_form_checkbox() {
272
		if ( $this->is_user_connected( get_current_user_id() ) ) {
273
			echo '<a name="configure-sso"></a>';
274
			echo '<input type="checkbox" name="jetpack_sso_remove_login_form[remove_login_form]" ' . checked( 1 == get_option( 'jetpack_sso_remove_login_form' ), true, false ) . '>';
275
			echo '<p class="description">Removes default login form and disallows login via POST</p>';
276
		} else {
277
			echo 'Your account must be connected to WordPress.com before disabling the login form.';
278
			echo '<br/>' . $this->button();
279
		}
280
	}
281
282
	/**
283
	 * Validate settings input from Settings > General
284
	 *
285
	 * @since 2.7
286
	 * @return boolean
287
	 **/
288
	public function validate_settings_remove_login_form_checkbox( $input ) {
289
		return ( isset( $input['remove_login_form'] ) )? 1: 0;
290
	}
291
292
	/**
293
	 * Removes 'Lost your password?' text from the login form if user
294
	 * does not want to show the login form
295
	 *
296
	 * @since 2.7
297
	 * @return string
298
	 **/
299
	public function remove_lost_password_text( $text ) {
300
		if ( 'Lost your password?' == $text ) {
301
			$text = '';
302
		}
303
		return $text;
304
	}
305
306
	/**
307
	 * Checks to determine if the user wants to login on wp-login
308
	 *
309
	 * This function mostly exists to cover the exceptions to login
310
	 * that may exist as other parameters to $_GET[action] as $_GET[action]
311
	 * does not have to exist. By default WordPress assumes login if an action
312
	 * is not set, however this may not be true, as in the case of logout
313
	 * where $_GET[loggedout] is instead set
314
	 *
315
	 * @return boolean
316
	 **/
317
	private function wants_to_login() {
318
		$wants_to_login = false;
319
320
		// Cover default WordPress behavior
321
		$action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login';
322
323
		// And now the exceptions
324
		$action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;
325
326
		if ( 'login' == $action ) {
327
			$wants_to_login = true;
328
		}
329
330
		return $wants_to_login;
331
	}
332
333
	private function bypass_login_forward_wpcom() {
334
		/**
335
		 * Redirect the site's log in form to WordPress.com's log in form.
336
		 *
337
		 * @module sso
338
		 *
339
		 * @since 3.1.0
340
		 *
341
		 * @param bool false Should the site's log in form be automatically forwarded to WordPress.com's log in form.
342
		 */
343
		return apply_filters( 'jetpack_sso_bypass_login_forward_wpcom', false );
344
	}
345
346
	function login_init() {
347
		global $action;
348
349
		/**
350
		 * If the user is attempting to logout AND the auto-forward to WordPress.com
351
		 * login is set then we need to ensure we do not auto-forward the user and get
352
		 * them stuck in an infinite logout loop.
353
		 */
354
		if ( isset( $_GET['loggedout'] ) && $this->bypass_login_forward_wpcom() ) {
355
			add_filter( 'jetpack_remove_login_form', '__return_true' );
356
			add_filter( 'gettext', array( $this, 'remove_lost_password_text' ) );
357
		}
358
359
		/**
360
		 * Check to see if the site admin wants to automagically forward the user
361
		 * to the WordPress.com login page AND  that the request to wp-login.php
362
		 * is not something other than login (Like logout!)
363
		 */
364
		if (
365
			$this->wants_to_login()
366
			&& $this->bypass_login_forward_wpcom()
367
		) {
368
			add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) );
369
			$this->maybe_save_cookie_redirect();
370
			wp_safe_redirect( $this->build_sso_url() );
371
		}
372
373
		if ( 'login' === $action ) {
374
			wp_enqueue_script( 'jquery' );
375
			wp_enqueue_style( 'genericons' );
376
			add_action( 'login_message', array( $this, 'display_sso_button' ), 101 );
377
			add_action( 'login_footer', array( $this, 'login_footer' ) );
378
379
			/*
380
			if ( get_option( 'jetpack_sso_remove_login_form' ) ) {
381
				// Check to see if the user is attempting to login via the default login form.
382
				// If so we need to deny it and forward elsewhere.
383
				if( isset( $_REQUEST['wp-submit'] ) && 'Log In' == $_REQUEST['wp-submit'] ) {
384
					wp_die( 'Login not permitted by this method. ');
385
				}
386
				add_filter( 'gettext', array( $this, 'remove_lost_password_text' ) );
387
			}
388
			*/
389
		} elseif ( 'jetpack-sso' === $action ) {
390
			if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
391
				$this->handle_login();
392
				wp_enqueue_script( 'jquery' );
393
				wp_enqueue_style( 'genericons' );
394
				add_action( 'login_message', array( $this, 'display_sso_button' ), 101 );
395
				add_action( 'login_footer', array( $this, 'login_footer' ) );
396
			} else {
397
				if ( Jetpack::check_identity_crisis() ) {
398
					wp_die( __( "Error: This site's Jetpack connection is currently experiencing problems.", 'jetpack' ) );
399
				} else {
400
					$this->maybe_save_cookie_redirect();
401
					// Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
402
					add_action( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) );
403
					wp_safe_redirect( $this->build_sso_url() );
404
				}
405
			}
406
		}
407
	}
408
409
	/**
410
	 * Conditionally save the redirect_to url as a cookie.
411
	 */
412
	public static function maybe_save_cookie_redirect() {
413
		if ( headers_sent() ) {
414
			return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
415
		}
416
417
		if ( ! empty( $_GET['redirect_to'] ) ) {
418
			// If we have something to redirect to
419
			$url = esc_url_raw( $_GET['redirect_to'] );
420
			setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
421
422
		} elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
423
			// Otherwise, if it's already set, purge it.
424
			setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
425
		}
426
427
		if ( ! empty( $_GET['rememberme'] ) ) {
428
			setcookie( 'jetpack_sso_remember_me', '1', time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
429
		} elseif ( ! empty( $_COOKIE['jetpack_sso_remember_me'] ) ) {
430
			setcookie( 'jetpack_sso_remember_me', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
431
		}
432
	}
433
434
	/**
435
	 * Determine if the login form should be hidden or not
436
	 *
437
	 * Method is private only because it is only used in this class so far.
438
	 * Feel free to change it later
439
	 *
440
	 * @return bool
441
	 **/
442
	private function should_hide_login_form() {
443
		/**
444
		 * Remove the default log in form, only leave the WordPress.com log in button.
445
		 *
446
		 * @module sso
447
		 *
448
		 * @since 3.1.0
449
		 *
450
		 * @param bool get_option( 'jetpack_sso_remove_login_form', false ) Should the default log in form be removed. Default to false.
451
		 */
452
		return apply_filters( 'jetpack_remove_login_form', get_option( 'jetpack_sso_remove_login_form', false ) );
453
	}
454
455
	function display_sso_button( $message ) {
456
		$classes = '';
457
		$hide_login_form = $this->should_hide_login_form();
458
459
		if ( $hide_login_form ) {
460
			$classes .= ' forced-sso';
461
		}
462
463
		$sso_button = '<div class="jetpack-sso-wrap' . $classes . '">' . $this->button() . '</div>';
464
465
		if ( $hide_login_form ) {
466
			$sso_or = '';
467
		} else {
468
			$sso_or = '<div class="jetpack-sso-or"><span class="jetpack-sso-or__text">';
469
				$sso_or .= _x( 'Or', 'Displayed when a user has a choice between login options.', 'jetpack' );
470
			$sso_or .= '</span></div>';
471
		}
472
473
		return $message . $sso_button . $sso_or;
474
	}
475
476
	function login_footer() {
477
		$hide_login_form = $this->should_hide_login_form();
478
		?>
479
		<style>
480
			.jetpack-sso-wrap {
481
				background: #fff;
482
				box-shadow: 0 1px 3px rgba(0,0,0,.13);
483
				margin-top: 20px;
484
				margin-left: 0;
485
				padding: 26px 24px;
486
				text-align: center;
487
			}
488
489
			.jetpack-sso-or {
490
				margin-top: 20px;
491
				position: relative;
492
				text-align: center;
493
			}
494
495
			.jetpack-sso-or:before {
496
				background: #ccc;
497
				content: '';
498
				height: 1px;
499
				position: absolute;
500
					left: 0;
501
					top: 50%;
502
				width: 100%;
503
			}
504
505
			.jetpack-sso-or__text {
506
				background: #f1f1f1;
507
				color: #ccc;
508
				position: relative;
509
				padding: 0 8px;
510
				text-transform: uppercase
511
			}
512
		</style>
513
		<script>
514
			jQuery(document).ready(function($){
515
			<?php if ( $hide_login_form ) : ?>
516
				$( '#loginform' ).empty();
517
			<?php endif; ?>
518
				var $rememberme = $( '#rememberme' ),
519
					$ssoButton  = $( 'a.jetpack-sso.button' );
520
521
				$rememberme.on( 'change', function() {
522
					var url       = $ssoButton.prop( 'href' ),
523
						isChecked = $rememberme.prop( 'checked' ) ? 1 : 0;
524
525
					if ( url.match( /&rememberme=\d/ ) ) {
526
						url = url.replace( /&rememberme=\d/, '&rememberme=' + isChecked );
527
					} else {
528
						url += '&rememberme=' + isChecked;
529
					}
530
531
					$ssoButton.prop( 'href', url );
532
				} ).change();
533
			});
534
		</script>
535
		<?php
536
	}
537
538 View Code Duplication
	static function delete_connection_for_user( $user_id ) {
539
		if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
540
			return;
541
		}
542
		Jetpack::load_xml_rpc_client();
543
		$xml = new Jetpack_IXR_Client( array(
544
			'user_id' => $user_id,
545
		) );
546
		$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
547
548
		if ( $xml->isError() ) {
549
			return false;
550
		}
551
552
		return $xml->getResponse();
553
	}
554
555 View Code Duplication
	static function request_initial_nonce() {
556
		Jetpack::load_xml_rpc_client();
557
		$xml = new Jetpack_IXR_Client( array(
558
			'user_id' => get_current_user_id(),
559
		) );
560
		$xml->query( 'jetpack.sso.requestNonce' );
561
562
		if ( $xml->isError() ) {
563
			wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
564
		}
565
566
		return $xml->getResponse();
567
	}
568
569
	/**
570
	 * The function that actually handles the login!
571
	 */
572
	function handle_login() {
573
		$wpcom_nonce   = sanitize_key( $_GET['sso_nonce'] );
574
		$wpcom_user_id = (int) $_GET['user_id'];
575
		$result        = sanitize_key( $_GET['result'] );
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
576
577
		Jetpack::load_xml_rpc_client();
578
		$xml = new Jetpack_IXR_Client( array(
579
			'user_id' => get_current_user_id(),
580
		) );
581
		$xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id );
582
583
		if ( $xml->isError() ) {
584
			wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
585
		}
586
587
		$user_data = $xml->getResponse();
588
589
		if ( empty( $user_data ) ) {
590
			wp_die( __( 'Error, invalid response data.', 'jetpack' ) );
591
		}
592
593
		$user_data = (object) $user_data;
594
		$user = null;
595
596
		/**
597
		 * Fires before Jetpack's SSO modifies the log in form.
598
		 *
599
		 * @module sso
600
		 *
601
		 * @since 2.6.0
602
		 *
603
		 * @param object $user_data User login information.
604
		 */
605
		do_action( 'jetpack_sso_pre_handle_login', $user_data );
606
607
		/**
608
		 * Is it required to have 2-step authentication enabled on WordPress.com to use SSO?
609
		 *
610
		 * @module sso
611
		 *
612
		 * @since 2.8.0
613
		 *
614
		 * @param bool get_option( 'jetpack_sso_require_two_step' ) Does SSO require 2-step authentication?
615
		 */
616
		$require_two_step = apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step' ) );
617
		if ( $require_two_step && 0 == (int) $user_data->two_step_enabled ) {
618
			$this->user_data = $user_data;
0 ignored issues
show
Bug introduced by
The property user_data does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
619
			/** This filter is documented in core/src/wp-includes/pluggable.php */
620
			do_action( 'wp_login_failed', $user_data->login );
621
			add_action( 'login_message', array( $this, 'error_msg_enable_two_step' ) );
622
			return;
623
		}
624
625
		if ( isset( $_GET['state'] ) && ( 0 < strpos( $_GET['state'], '|' ) ) ) {
626
			list( $state, $nonce ) = explode( '|', $_GET['state'] );
627
628
			if ( wp_verify_nonce( $nonce, $state ) ) {
629
				if ( 'sso-link-user' == $state ) {
630
					$user = wp_get_current_user();
631
					update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
632
					add_filter( 'login_redirect', array( __CLASS__, 'profile_page_url' ) );
633
				}
634
			} else {
635
				wp_nonce_ays();
636
			}
637
		}
638
639
		if ( empty( $user ) ) {
640
			$user = $this->get_user_by_wpcom_id( $user_data->ID );
641
		}
642
643
		// If we don't have one by wpcom_user_id, try by the email?
644
		if ( empty( $user ) && self::match_by_email() ) {
645
			$user = get_user_by( 'email', $user_data->email );
646
			if ( $user ) {
647
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
648
			}
649
		}
650
651
		// If we've still got nothing, create the user.
652
		if ( empty( $user ) && ( get_option( 'users_can_register' ) || self::new_user_override() ) ) {
653
			// If not matching by email we still need to verify the email does not exist
654
			// or this blows up
655
			/**
656
			 * If match_by_email is true, we know the email doesn't exist, as it would have
657
			 * been found in the first pass.  If get_user_by( 'email' ) doesn't find the
658
			 * user, then we know that email is unused, so it's safe to add.
659
			 */
660
			if ( self::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
661
				$username = $user_data->login;
662
663
				if ( username_exists( $username ) ) {
664
					$username = $user_data->login . '_' . $user_data->ID;
665
				}
666
667
				$tries = 0;
668
				while ( username_exists( $username ) ) {
669
					$username = $user_data->login . '_' . $user_data->ID . '_' . mt_rand();
670
					if ( $tries++ >= 5 ) {
671
						wp_die( __( "Error: Couldn't create suitable username.", 'jetpack' ) );
672
					}
673
				}
674
675
				$password = wp_generate_password( 20 );
676
				$user_id  = wp_create_user( $username, $password, $user_data->email );
677
				$user     = get_userdata( $user_id );
678
679
				$user->display_name = $user_data->display_name;
680
				$user->first_name   = $user_data->first_name;
681
				$user->last_name    = $user_data->last_name;
682
				$user->url          = $user_data->url;
683
				$user->description  = $user_data->description;
684
				wp_update_user( $user );
685
686
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
687
			} else {
688
				$this->user_data = $user_data;
689
				// do_action( 'wp_login_failed', $user_data->login );
690
				add_action( 'login_message', array( $this, 'error_msg_email_already_exists' ) );
691
				return;
692
			}
693
		}
694
695
		/**
696
		 * Fires after we got login information from WordPress.com.
697
		 *
698
		 * @module sso
699
		 *
700
		 * @since 2.6.0
701
		 *
702
		 * @param array $user WordPress.com User information.
703
		 * @param object $user_data User Login information.
704
		 */
705
		do_action( 'jetpack_sso_handle_login', $user, $user_data );
706
707
		if ( $user ) {
708
			// Cache the user's details, so we can present it back to them on their user screen.
709
			update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
710
711
			$remember = false;
712 View Code Duplication
			if ( ! empty( $_COOKIE['jetpack_sso_remember_me'] ) ) {
713
				$remember = true;
714
				// And then purge it
715
				setcookie( 'jetpack_sso_remember_me', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
716
			}
717
			/**
718
			 * Filter the remember me value.
719
			 *
720
			 * @module sso
721
			 *
722
			 * @since 2.8.0
723
			 *
724
			 * @param bool $remember Is the remember me option checked?
725
			 */
726
			$remember = apply_filters( 'jetpack_remember_login', $remember );
727
			wp_set_auth_cookie( $user->ID, $remember );
728
729
			/** This filter is documented in core/src/wp-includes/user.php */
730
			do_action( 'wp_login', $user->user_login, $user );
731
732
			$_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
733
			$redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
734
735
			// If we have a saved redirect to request in a cookie
736 View Code Duplication
			if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
737
				// Set that as the requested redirect to
738
				$redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
739
				// And then purge it
740
				setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
741
			}
742
743
			wp_safe_redirect(
744
				/** This filter is documented in core/src/wp-login.php */
745
				apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
746
			);
747
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method handle_login() 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...
748
		}
749
750
		$this->user_data = $user_data;
751
		/** This filter is documented in core/src/wp-includes/pluggable.php */
752
		do_action( 'wp_login_failed', $user_data->login );
753
		add_action( 'login_message', array( $this, 'cant_find_user' ) );
754
	}
755
756
	static function profile_page_url() {
757
		return admin_url( 'profile.php' );
758
	}
759
760
	static function match_by_email() {
761
		$match_by_email = ( 1 == get_option( 'jetpack_sso_match_by_email', true ) ) ? true: false;
762
		$match_by_email = defined( 'WPCC_MATCH_BY_EMAIL' ) ? WPCC_MATCH_BY_EMAIL : $match_by_email;
763
764
		/**
765
		 * Link the local account to an account on WordPress.com using the same email address.
766
		 *
767
		 * @module sso
768
		 *
769
		 * @since 2.6.0
770
		 *
771
		 * @param bool $match_by_email Should we link the local account to an account on WordPress.com using the same email address. Default to false.
772
		 */
773
		return apply_filters( 'jetpack_sso_match_by_email', $match_by_email );
774
	}
775
776
	static function new_user_override() {
777
		$new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? WPCC_NEW_USER_OVERRIDE : false;
778
779
		/**
780
		 * Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations.
781
		 *
782
		 * @module sso
783
		 *
784
		 * @since 2.6.0
785
		 *
786
		 * @param bool $new_user_override Allow users to register on your site with a WordPress.com account. Default to false.
787
		 */
788
		return apply_filters( 'jetpack_sso_new_user_override', $new_user_override );
789
	}
790
791
	function allowed_redirect_hosts( $hosts ) {
792
		if ( empty( $hosts ) ) {
793
			$hosts = array();
794
		}
795
		$hosts[] = 'wordpress.com';
796
797
		return array_unique( $hosts );
798
	}
799
800
	function button( $args = array() ) {
801
		$defaults = array(
802
			'action'  => 'jetpack-sso',
803
		);
804
805
		$args = wp_parse_args( $args, $defaults );
806
807
		if ( ! empty( $_GET['redirect_to'] ) ) {
808
			$args['redirect_to'] = esc_url_raw( $_GET['redirect_to'] );
809
		}
810
811
		$url  = add_query_arg( $args, wp_login_url() );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
812
813
		$css = "<style>
814
		.jetpack-sso.button {
815
			position: relative;
816
			padding-left: 37px;
817
		}
818
		.jetpack-sso.button:before {
819
			display: block;
820
			box-sizing: border-box;
821
			padding: 7px 0 0;
822
			text-align: center;
823
			position: absolute;
824
			top: -1px;
825
			left: -1px;
826
			border-radius: 2px 0 0 2px;
827
			content: '\\f205';
828
			background: #0074a2;
829
			color: #fff;
830
			-webkit-font-smoothing: antialiased;
831
			width: 30px;
832
			height: 107%;
833
			height: calc( 100% + 2px );
834
			font: normal 22px/1 Genericons !important;
835
			text-shadow: none;
836
		}
837
		@media screen and (min-width: 783px) {
838
			.jetpack-sso.button:before {
839
				padding-top: 3px;
840
			}
841
		}
842
		.jetpack-sso.button:hover {
843
			border: 1px solid #aaa;
844
		}";
845
846
		if ( version_compare( $GLOBALS['wp_version'], '3.8-alpha', '<' ) ) {
847
			$css .= '
848
			.jetpack-sso.button:before {
849
				width: 25px;
850
				font-size: 18px !important;
851
			}
852
			';
853
		}
854
855
		$css .= '</style>';
856
857
		$button = sprintf( '<a href="%1$s" class="jetpack-sso button">%2$s</a>', esc_url( $url ), esc_html__( 'Log in with WordPress.com', 'jetpack' ) );
858
		return $button . $css;
859
	}
860
861
	function build_sso_url( $args = array() ) {
862
		$defaults = array(
863
			'action'    => 'jetpack-sso',
864
			'site_id'   => Jetpack_Options::get_option( 'id' ),
865
			'sso_nonce' => self::request_initial_nonce(),
866
		);
867
868
		if ( isset( $_GET['state'] ) && check_admin_referer( $_GET['state'] ) ) {
869
			$defaults['state'] = rawurlencode( $_GET['state'] . '|' . $_GET['_wpnonce'] );
870
		}
871
872
		$args = wp_parse_args( $args, $defaults );
873
		$url  = add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
874
875
		return $url;
876
	}
877
878
	/**
879
	 * Determines local user associated with a given WordPress.com user ID.
880
	 *
881
	 * @since 2.6.0
882
	 *
883
	 * @param int $wpcom_user_id User ID from WordPress.com
884
	 * @return object Local user object if found, null if not.
885
	 */
886
	static function get_user_by_wpcom_id( $wpcom_user_id ) {
887
		$user_query = new WP_User_Query( array(
888
			'meta_key'   => 'wpcom_user_id',
889
			'meta_value' => intval( $wpcom_user_id ),
890
			'number'     => 1,
891
		) );
892
893
		$users = $user_query->get_results();
894
		return $users ? array_shift( $users ) : null;
895
	}
896
897
	/**
898
	 * Error message displayed on the login form when two step is required and
899
	 * the user's account on WordPress.com does not have two step enabled.
900
	 *
901
	 * @since 2.7
902
	 * @param string $message
903
	 * @return string
904
	 **/
905
	public function error_msg_enable_two_step( $message ) {
906
		$err = __( sprintf( 'This site requires two step authentication be enabled for your user account on WordPress.com. Please visit the <a href="%1$s" target="_blank"> Security Settings</a> page to enable two step', 'https://wordpress.com/me/security/two-step' ) , 'jetpack' );
907
908
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
909
910
		return $message;
911
	}
912
913
	/**
914
	 * Error message displayed when the user tries to SSO, but match by email
915
	 * is off and they already have an account with their email address on
916
	 * this site.
917
	 *
918
	 * @param string $message
919
	 * @return string
920
	 */
921
	public function error_msg_email_already_exists( $message ) {
922
		$err = __( sprintf( 'You already have an account on this site. Please visit your <a href="%1$s">profile page</a> page to link your account to WordPress.com!', admin_url( 'profile.php' ) ) , 'jetpack' );
923
924
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
925
926
		return $message;
927
	}
928
929
	/**
930
	 * Message displayed when the site admin has disabled the default WordPress
931
	 * login form in Settings > General > Single Sign On
932
	 *
933
	 * @since 2.7
934
	 * @param string $message
935
	 * @return string
936
	 **/
937
	public function msg_login_by_jetpack( $message ) {
938
939
		$msg = __( sprintf( 'Jetpack authenticates through WordPress.com — to log in, enter your WordPress.com username and password, or <a href="%1$s" target="_blank">visit WordPress.com</a> to create a free account now.', 'http://wordpress.com/signup' ) , 'jetpack' );
940
941
		/**
942
		 * Filter the message displayed when the default WordPress login form is disabled.
943
		 *
944
		 * @module sso
945
		 *
946
		 * @since 2.8.0
947
		 *
948
		 * @param string $msg Disclaimer when default WordPress login form is disabled.
949
		 */
950
		$msg = apply_filters( 'jetpack_sso_disclaimer_message', $msg );
951
952
		$message .= sprintf( '<p class="message">%s</p>', $msg );
953
		return $message;
954
	}
955
956
	/**
957
	 * Error message displayed on the login form when the user attempts
958
	 * to post to the login form and it is disabled.
959
	 *
960
	 * @since 2.8
961
	 * @param string $message
962
	 * @param string
963
	 **/
964
	public function error_msg_login_method_not_allowed( $message ) {
965
		$err = __( 'Login method not allowed' , 'jetpack' );
966
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
967
968
		return $message;
969
	}
970
	function cant_find_user( $message ) {
971
		if ( self::match_by_email() ) {
972
			$err_format = __( 'We couldn\'t find an account with the email <strong><code>%1$s</code></strong> to log you in with.  If you already have an account on <strong>%2$s</strong>, please make sure that <strong><code>%1$s</code></strong> is configured as the email address, or that you have connected to WordPress.com on your profile page.', 'jetpack' );
973
		} else {
974
			$err_format = __( 'We couldn\'t find any account on <strong>%2$s</strong> that is linked to your WordPress.com account to log you in with.  If you already have an account on <strong>%2$s</strong>, please make sure that you have connected to WordPress.com on your profile page.', 'jetpack' );
975
		}
976
		$err = sprintf( $err_format, $this->user_data->email, get_bloginfo( 'name' ) );
977
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
978
		return $message;
979
	}
980
981
	/**
982
	 * Deal with user connections...
983
	 */
984
	function admin_init() {
985
		add_action( 'show_user_profile', array( $this, 'edit_profile_fields' ) ); // For their own profile
986
		add_action( 'edit_user_profile', array( $this, 'edit_profile_fields' ) ); // For folks editing others profiles
987
988
		if ( isset( $_GET['jetpack_sso'] ) && 'purge' == $_GET['jetpack_sso'] && check_admin_referer( 'jetpack_sso_purge' ) ) {
989
			$user = wp_get_current_user();
990
			// Remove the connection on the wpcom end.
991
			self::delete_connection_for_user( $user->ID );
992
			// Clear it locally.
993
			delete_user_meta( $user->ID, 'wpcom_user_id' );
994
			delete_user_meta( $user->ID, 'wpcom_user_data' );
995
			// Forward back to the profile page.
996
			wp_safe_redirect( remove_query_arg( array( 'jetpack_sso', '_wpnonce' ) ) );
997
		}
998
	}
999
1000
	/**
1001
	 * Determines if a local user is connected to WordPress.com
1002
	 *
1003
	 * @since 2.8
1004
	 * @param integer $user_id - Local user id
1005
	 * @return boolean
1006
	 **/
1007
	public function is_user_connected( $user_id ) {
1008
		return $this->get_user_data( $user_id );
1009
	}
1010
1011
	/**
1012
	 * Retrieves a user's WordPress.com data
1013
	 *
1014
	 * @since 2.8
1015
	 * @param integer $user_id - Local user id
1016
	 * @return mixed null or stdClass
1017
	 **/
1018
	public function get_user_data( $user_id ) {
1019
		return get_user_meta( $user_id, 'wpcom_user_data', true );
1020
	}
1021
1022
	function edit_profile_fields( $user ) {
1023
		wp_enqueue_style( 'genericons' );
1024
		?>
1025
1026
		<h3 id="single-sign-on"><?php _e( 'Single Sign On', 'jetpack' ); ?></h3>
1027
		<p><?php _e( 'Connecting with Single Sign On enables you to log in via your WordPress.com account.', 'jetpack' ); ?></p>
1028
1029
		<?php if ( $this->is_user_connected( $user->ID ) ) : /* If the user is currently connected... */ ?>
1030
			<?php $user_data = $this->get_user_data( $user->ID ); ?>
1031
			<table class="form-table jetpack-sso-form-table">
1032
				<tbody>
1033
					<tr>
1034
						<td>
1035
							<div class="profile-card">
1036
								<?php echo get_avatar( $user_data->email ); ?>
1037
								<p class="connected"><strong><?php _e( 'Connected', 'jetpack' ); ?></strong></p>
1038
								<p><?php echo esc_html( $user_data->login ); ?></p>
1039
								<span class="two_step">
1040
									<?php
1041
										if ( $user_data->two_step_enabled ) {
1042
											?> <p class="enabled"><a href="https://wordpress.com/me/security/two-step" target="_blank"><?php _e( 'Two-Step Authentication Enabled', 'jetpack' ); ?></a></p> <?php
1043
										} else {
1044
											?> <p class="disabled"><a href="https://wordpress.com/me/security/two-step" target="_blank"><?php _e( 'Two-Step Authentication Disabled', 'jetpack' ); ?></a></p> <?php
1045
										}
1046
									?>
1047
								</span>
1048
1049
							</div>
1050
							<p><a class="button button-secondary" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'jetpack_sso', 'purge' ), 'jetpack_sso_purge' ) ); ?>"><?php _e( 'Unlink This Account', 'jetpack' ); ?></a></p>
1051
						</td>
1052
					</tr>
1053
				</tbody>
1054
			</table>
1055
1056
			<style>
1057
			.jetpack-sso-form-table td {
1058
				padding-left: 0;
1059
			}
1060
1061
			.jetpack-sso-form-table .profile-card {
1062
				padding: 10px;
1063
				background: #fff;
1064
				overflow: hidden;
1065
				max-width: 400px;
1066
				box-shadow: 0 1px 2px rgba( 0, 0, 0, 0.1 );
1067
				margin-bottom: 1em;
1068
			}
1069
1070
			.jetpack-sso-form-table .profile-card img {
1071
				float: left;
1072
				margin-right: 1em;
1073
				width: 48px;
1074
				height: 48px;
1075
			}
1076
1077
			.jetpack-sso-form-table .profile-card .connected {
1078
				float: right;
1079
				margin-right: 0.5em;
1080
				color: #0a0;
1081
			}
1082
1083
			.jetpack-sso-form-table .profile-card p {
1084
				margin-top: 0.7em;
1085
				font-size: 1.2em;
1086
			}
1087
1088
			.jetpack-sso-form-table .profile-card .two_step .enabled a {
1089
				float: right;
1090
				color: #0a0;
1091
			}
1092
1093
			.jetpack-sso-form-table .profile-card .two_step .disabled a {
1094
				float: right;
1095
				color: red;
1096
			}
1097
			</style>
1098
1099
		<?php elseif ( get_current_user_id() == $user->ID && Jetpack::is_user_connected( $user->ID ) ) : ?>
1100
1101
			<?php echo $this->button( 'state=sso-link-user&_wpnonce=' . wp_create_nonce( 'sso-link-user' ) ); // update ?>
0 ignored issues
show
Documentation introduced by
'state=sso-link-user&_wp..._nonce('sso-link-user') is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1102
1103
		<?php else : ?>
1104
1105
			<p><?php esc_html_e( wptexturize( __( "If you don't have a WordPress.com account yet, you can sign up for free in just a few seconds.", 'jetpack' ) ) ); ?></p>
1106
			<a href="<?php echo Jetpack::init()->build_connect_url( false, get_edit_profile_url( get_current_user_id() ) . '#single-sign-on' ); ?>" class="button button-connector" id="wpcom-connect"><?php esc_html_e( 'Link account with WordPress.com', 'jetpack' ); ?></a>
1107
1108
		<?php endif;
1109
	}
1110
}
1111
1112
Jetpack_SSO::get_instance();
1113