Completed
Push — use/yarn ( 651f39...32c263 )
by
unknown
11:30
created

Jetpack_SSO::error_msg_identity_crisis()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php' );
3
require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-notices.php' );
4
5
/**
6
 * Module Name: Single Sign On
7
 * Module Description: Secure user authentication with WordPress.com.
8
 * Jumpstart Description: Lets you log in to all your Jetpack-enabled sites with one click using your WordPress.com account.
9
 * Sort Order: 30
10
 * Recommendation Order: 5
11
 * First Introduced: 2.6
12
 * Requires Connection: Yes
13
 * Auto Activate: No
14
 * Module Tags: Developers
15
 * Feature: Security, Jumpstart
16
 * Additional Search Queries: sso, single sign on, login, log in
17
 */
18
19
class Jetpack_SSO {
20
	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...
21
22
	private function __construct() {
23
24
		self::$instance = $this;
25
26
		add_action( 'admin_init',             array( $this, 'maybe_authorize_user_after_sso' ), 1 );
27
		add_action( 'admin_init',             array( $this, 'register_settings' ) );
28
		add_action( 'login_init',             array( $this, 'login_init' ) );
29
		add_action( 'delete_user',            array( $this, 'delete_connection_for_user' ) );
30
		add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
31
		add_action( 'init',                   array( $this, 'maybe_logout_user' ), 5 );
32
		add_action( 'jetpack_modules_loaded', array( $this, 'module_configure_button' ) );
33
		add_action( 'login_form_logout',      array( $this, 'store_wpcom_profile_cookies_on_logout' ) );
34
		add_action( 'wp_login',               array( 'Jetpack_SSO', 'clear_wpcom_profile_cookies' ) );
35
		add_action( 'jetpack_unlinked_user',  array( $this, 'delete_connection_for_user') );
36
37
		// Adding this action so that on login_init, the action won't be sanitized out of the $action global.
38
		add_action( 'login_form_jetpack-sso', '__return_true' );
39
	}
40
41
	/**
42
	 * Returns the single instance of the Jetpack_SSO object
43
	 *
44
	 * @since 2.8
45
	 * @return Jetpack_SSO
46
	 **/
47
	public static function get_instance() {
48
		if ( ! is_null( self::$instance ) ) {
49
			return self::$instance;
50
		}
51
52
		return self::$instance = new Jetpack_SSO;
53
	}
54
55
	/**
56
	 * Add configure button and functionality to the module card on the Jetpack screen
57
	 **/
58
	public static function module_configure_button() {
59
		Jetpack::enable_module_configurable( __FILE__ );
60
		Jetpack::module_configuration_load( __FILE__, array( __CLASS__, 'module_configuration_load' ) );
61
		Jetpack::module_configuration_head( __FILE__, array( __CLASS__, 'module_configuration_head' ) );
62
		Jetpack::module_configuration_screen( __FILE__, array( __CLASS__, 'module_configuration_screen' ) );
63
	}
64
65
	public static function module_configuration_load() {}
66
67
	public static function module_configuration_head() {}
68
69
	public static function module_configuration_screen() {
70
		?>
71
		<form method="post" action="options.php">
72
			<?php settings_fields( 'jetpack-sso' ); ?>
73
			<?php do_settings_sections( 'jetpack-sso' ); ?>
74
			<?php submit_button(); ?>
75
		</form>
76
		<?php
77
	}
78
79
	/**
80
	 * If jetpack_force_logout == 1 in current user meta the user will be forced
81
	 * to logout and reauthenticate with the site.
82
	 **/
83
	public function maybe_logout_user() {
84
		global $current_user;
85
86
		if ( 1 == $current_user->jetpack_force_logout ) {
87
			delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
88
			self::delete_connection_for_user( $current_user->ID );
89
			wp_logout();
90
			wp_safe_redirect( wp_login_url() );
91
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method maybe_logout_user() 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...
92
		}
93
	}
94
95
	/**
96
	 * Adds additional methods the WordPress xmlrpc API for handling SSO specific features
97
	 *
98
	 * @param array $methods
99
	 * @return array
100
	 **/
101
	public function xmlrpc_methods( $methods ) {
102
		$methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' );
103
		return $methods;
104
	}
105
106
	/**
107
	 * Marks a user's profile for disconnect from WordPress.com and forces a logout
108
	 * the next time the user visits the site.
109
	 **/
110
	public function xmlrpc_user_disconnect( $user_id ) {
111
		$user_query = new WP_User_Query(
112
			array(
113
				'meta_key' => 'wpcom_user_id',
114
				'meta_value' => $user_id,
115
			)
116
		);
117
		$user = $user_query->get_results();
118
		$user = $user[0];
119
120
		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...
121
			$user = wp_set_current_user( $user->ID );
122
			update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
123
			self::delete_connection_for_user( $user->ID );
124
			return true;
125
		}
126
		return false;
127
	}
128
129
	/**
130
	 * Enqueues scripts and styles necessary for SSO login.
131
	 */
132
	public function login_enqueue_scripts() {
133
		global $action;
134
135
		if ( ! in_array( $action, array( 'jetpack-sso', 'login' ) ) ) {
136
			return;
137
		}
138
139
		if ( is_rtl() ) {
140
			wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login-rtl.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
141
		} else {
142
			wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
143
		}
144
145
		wp_enqueue_script( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION );
146
	}
147
148
	/**
149
	 * Adds Jetpack SSO classes to login body
150
	 *
151
	 * @param  array $classes Array of classes to add to body tag
152
	 * @return array          Array of classes to add to body tag
153
	 */
154
	public function login_body_class( $classes ) {
155
		global $action;
156
157
		if ( ! in_array( $action, array( 'jetpack-sso', 'login' ) ) ) {
158
			return $classes;
159
		}
160
161
		// Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed.
162
		$classes[] = 'jetpack-sso';
163
164
		/**
165
		 * Should we show the SSO login form?
166
		 *
167
		 * $_GET['jetpack-sso-default-form'] is used to provide a fallback in case JavaScript is not enabled.
168
		 *
169
		 * The default_to_sso_login() method allows us to dynamically decide whether we show the SSO login form or not.
170
		 * The SSO module uses the method to display the default login form if we can not find a user to log in via SSO.
171
		 * But, the method could be filtered by a site admin to always show the default login form if that is preferred.
172
		 */
173
		if ( empty( $_GET['jetpack-sso-show-default-form'] ) && Jetpack_SSO_Helpers::show_sso_login() ) {
174
			$classes[] = 'jetpack-sso-form-display';
175
		}
176
177
		return $classes;
178
	}
179
180
	/**
181
	 * Adds settings fields to Settings > General > Single Sign On that allows users to
182
	 * turn off the login form on wp-login.php
183
	 *
184
	 * @since 2.7
185
	 **/
186
	public function register_settings() {
187
188
		add_settings_section(
189
			'jetpack_sso_settings',
190
			__( 'Single Sign On' , 'jetpack' ),
191
			'__return_false',
192
			'jetpack-sso'
193
		);
194
195
		/*
196
		 * Settings > General > Single Sign On
197
		 * Require two step authentication
198
		 */
199
		register_setting(
200
			'jetpack-sso',
201
			'jetpack_sso_require_two_step',
202
			array( $this, 'validate_jetpack_sso_require_two_step' )
203
		);
204
205
		add_settings_field(
206
			'jetpack_sso_require_two_step',
207
			'', // __( 'Require Two-Step Authentication' , 'jetpack' ),
208
			array( $this, 'render_require_two_step' ),
209
			'jetpack-sso',
210
			'jetpack_sso_settings'
211
		);
212
213
		/*
214
		 * Settings > General > Single Sign On
215
		 */
216
		register_setting(
217
			'jetpack-sso',
218
			'jetpack_sso_match_by_email',
219
			array( $this, 'validate_jetpack_sso_match_by_email' )
220
		);
221
222
		add_settings_field(
223
			'jetpack_sso_match_by_email',
224
			'', // __( 'Match by Email' , 'jetpack' ),
225
			array( $this, 'render_match_by_email' ),
226
			'jetpack-sso',
227
			'jetpack_sso_settings'
228
		);
229
	}
230
231
	/**
232
	 * Builds the display for the checkbox allowing user to require two step
233
	 * auth be enabled on WordPress.com accounts before login. Displays in Settings > General
234
	 *
235
	 * @since 2.7
236
	 **/
237
	public function render_require_two_step() {
238
		?>
239
		<label>
240
			<input
241
				type="checkbox"
242
				name="jetpack_sso_require_two_step"
243
				<?php checked( Jetpack_SSO_Helpers::is_two_step_required() ); ?>
244
				<?php disabled( Jetpack_SSO_Helpers::is_require_two_step_checkbox_disabled() ); ?>
245
			>
246
			<?php esc_html_e( 'Require Two-Step Authentication' , 'jetpack' ); ?>
247
		</label>
248
		<?php
249
	}
250
251
	/**
252
	 * Validate the require  two step checkbox in Settings > General
253
	 *
254
	 * @since 2.7
255
	 * @return boolean
256
	 **/
257
	public function validate_jetpack_sso_require_two_step( $input ) {
258
		return ( ! empty( $input ) ) ? 1 : 0;
259
	}
260
261
	/**
262
	 * Builds the display for the checkbox allowing the user to allow matching logins by email
263
	 * Displays in Settings > General
264
	 *
265
	 * @since 2.9
266
	 **/
267
	public function render_match_by_email() {
268
		?>
269
			<label>
270
				<input
271
					type="checkbox"
272
					name="jetpack_sso_match_by_email"
273
					<?php checked( Jetpack_SSO_Helpers::match_by_email() ); ?>
274
					<?php disabled( Jetpack_SSO_Helpers::is_match_by_email_checkbox_disabled() ); ?>
275
				>
276
				<?php esc_html_e( 'Match by Email', 'jetpack' ); ?>
277
			</label>
278
		<?php
279
	}
280
281
	/**
282
	 * Validate the match by email check in Settings > General
283
	 *
284
	 * @since 2.9
285
	 * @return boolean
286
	 **/
287
	public function validate_jetpack_sso_match_by_email( $input ) {
288
		return ( ! empty( $input ) ) ? 1 : 0;
289
	}
290
291
	/**
292
	 * Checks to determine if the user wants to login on wp-login
293
	 *
294
	 * This function mostly exists to cover the exceptions to login
295
	 * that may exist as other parameters to $_GET[action] as $_GET[action]
296
	 * does not have to exist. By default WordPress assumes login if an action
297
	 * is not set, however this may not be true, as in the case of logout
298
	 * where $_GET[loggedout] is instead set
299
	 *
300
	 * @return boolean
301
	 **/
302
	private function wants_to_login() {
303
		$wants_to_login = false;
304
305
		// Cover default WordPress behavior
306
		$action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login';
307
308
		// And now the exceptions
309
		$action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;
310
311
		if ( 'login' == $action ) {
312
			$wants_to_login = true;
313
		}
314
315
		return $wants_to_login;
316
	}
317
318
	function login_init() {
319
		global $action;
320
321
		if ( Jetpack_SSO_Helpers::should_hide_login_form() ) {
322
			/**
323
			 * Since the default authenticate filters fire at priority 20 for checking username and password,
324
			 * let's fire at priority 30. wp_authenticate_spam_check is fired at priority 99, but since we return a
325
			 * WP_Error in disable_default_login_form, then we won't trigger spam processing logic.
326
			 */
327
			add_filter( 'authenticate', array( 'Jetpack_SSO_Notices', 'disable_default_login_form' ), 30 );
328
329
			/**
330
			 * Filter the display of the disclaimer message appearing when default WordPress login form is disabled.
331
			 *
332
			 * @module sso
333
			 *
334
			 * @since 2.8.0
335
			 *
336
			 * @param bool true Should the disclaimer be displayed. Default to true.
337
			 */
338
			$display_sso_disclaimer = apply_filters( 'jetpack_sso_display_disclaimer', true );
339
			if ( $display_sso_disclaimer ) {
340
				add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'msg_login_by_jetpack' ) );
341
			}
342
		}
343
344
		/**
345
		 * If the user is attempting to logout AND the auto-forward to WordPress.com
346
		 * login is set then we need to ensure we do not auto-forward the user and get
347
		 * them stuck in an infinite logout loop.
348
		 */
349
		if ( isset( $_GET['loggedout'] ) && Jetpack_SSO_Helpers::bypass_login_forward_wpcom() ) {
350
			add_filter( 'jetpack_remove_login_form', '__return_true' );
351
		}
352
353
		/**
354
		 * Check to see if the site admin wants to automagically forward the user
355
		 * to the WordPress.com login page AND  that the request to wp-login.php
356
		 * is not something other than login (Like logout!)
357
		 */
358 View Code Duplication
		if (
359
			$this->wants_to_login()
360
			&& Jetpack_SSO_Helpers::bypass_login_forward_wpcom()
361
		) {
362
			add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
363
			$this->maybe_save_cookie_redirect();
364
			$reauth = ! empty( $_GET['force_reauth'] );
365
			$sso_url = $this->get_sso_url_or_die( $reauth );
366
			JetpackTracking::record_user_event( 'sso_login_redirect_bypass_success' );
367
			wp_safe_redirect( $sso_url );
368
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method login_init() 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...
369
		}
370
371
		if ( 'login' === $action ) {
372
			$this->display_sso_login_form();
373
		} elseif ( 'jetpack-sso' === $action ) {
374
			if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
375
				$this->handle_login();
376
				$this->display_sso_login_form();
377
			} else {
378
				if ( Jetpack::check_identity_crisis() ) {
379
					JetpackTracking::record_user_event( 'sso_login_redirect_failed', array(
380
						'error_message' => 'identity_crisis'
381
					) );
382
					add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_identity_crisis' ) );
383 View Code Duplication
				} else {
384
					$this->maybe_save_cookie_redirect();
385
					// Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
386
					add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
387
					$reauth = ! empty( $_GET['force_reauth'] );
388
					$sso_url = $this->get_sso_url_or_die( $reauth );
389
					JetpackTracking::record_user_event( 'sso_login_redirect_success' );
390
					wp_safe_redirect( $sso_url );
391
					exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method login_init() 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...
392
				}
393
			}
394
		}
395
	}
396
397
	/**
398
	 * Ensures that we can get a nonce from WordPress.com via XML-RPC before setting
399
	 * up the hooks required to display the SSO form.
400
	 */
401
	public function display_sso_login_form() {
402
		if ( Jetpack::check_identity_crisis() ) {
403
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_identity_crisis' ) );
404
			return;
405
		}
406
407
		$sso_nonce = self::request_initial_nonce();
408
		if ( is_wp_error( $sso_nonce ) ) {
409
			return;
410
		}
411
412
		add_action( 'login_form',            array( $this, 'login_form' ) );
413
		add_filter( 'login_body_class',      array( $this, 'login_body_class' ) );
414
		add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) );
415
	}
416
417
	/**
418
	 * Conditionally save the redirect_to url as a cookie.
419
	 */
420
	public static function maybe_save_cookie_redirect() {
421
		if ( headers_sent() ) {
422
			return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
423
		}
424
425
		if ( ! empty( $_GET['redirect_to'] ) ) {
426
			// If we have something to redirect to
427
			$url = esc_url_raw( $_GET['redirect_to'] );
428
			setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
429
430
		} elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
431
			// Otherwise, if it's already set, purge it.
432
			setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
433
		}
434
	}
435
436
	/**
437
	 * Outputs the Jetpack SSO button and description as well as the toggle link
438
	 * for switching between Jetpack SSO and default login.
439
	 */
440
	function login_form() {
441
		$site_name = get_bloginfo( 'name' );
442
		if ( ! $site_name ) {
443
			$site_name = get_bloginfo( 'url' );
444
		}
445
446
		$display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] )
447
			? $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ]
448
			: false;
449
		$gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] )
450
			? $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ]
451
			: false;
452
453
		?>
454
		<div id="jetpack-sso-wrap">
455
			<?php if ( $display_name && $gravatar ) : ?>
456
				<div id="jetpack-sso-wrap__user">
457
					<img width="72" height="72" src="<?php echo esc_html( $gravatar ); ?>" />
458
459
					<h2>
460
						<?php
461
							echo wp_kses(
462
								sprintf( __( 'Log in as <span>%s</span>', 'jetpack' ), esc_html( $display_name ) ),
463
								array( 'span' => true )
464
							);
465
						?>
466
					</h2>
467
				</div>
468
469
			<?php endif; ?>
470
471
472
			<div id="jetpack-sso-wrap__action">
473
				<?php echo $this->build_sso_button( array(), 'is_primary' ); ?>
0 ignored issues
show
Documentation introduced by
'is_primary' is of type string, but the function expects a boolean.

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...
474
475
				<?php if ( $display_name && $gravatar ) : ?>
476
					<a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>">
477
						<?php esc_html_e( 'Log in as a different WordPress.com user', 'jetpack' ); ?>
478
					</a>
479
				<?php else : ?>
480
					<p>
481
						<?php
482
							echo esc_html(
483
								sprintf(
484
									__( 'You can now save time spent logging in by connecting your WordPress.com account to %s.', 'jetpack' ),
485
									esc_html( $site_name )
486
								)
487
							);
488
						?>
489
					</p>
490
				<?php endif; ?>
491
			</div>
492
493
			<?php if ( ! Jetpack_SSO_Helpers::should_hide_login_form() ) : ?>
494
				<div class="jetpack-sso-or">
495
					<span><?php esc_html_e( 'Or', 'jetpack' ); ?></span>
496
				</div>
497
498
				<a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '1' ) ); ?>" class="jetpack-sso-toggle wpcom">
499
					<?php
500
						esc_html_e( 'Log in with username and password', 'jetpack' )
501
					?>
502
				</a>
503
504
				<a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '0' ) ); ?>" class="jetpack-sso-toggle default">
505
					<?php
506
						esc_html_e( 'Log in with WordPress.com', 'jetpack' )
507
					?>
508
				</a>
509
			<?php endif; ?>
510
		</div>
511
		<?php
512
	}
513
514
	/**
515
	 * Clear the cookies that store the profile information for the last
516
	 * WPCOM user to connect.
517
	 */
518
	static function clear_wpcom_profile_cookies() {
519 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
520
			setcookie(
521
				'jetpack_sso_wpcom_name_' . COOKIEHASH,
522
				' ',
523
				time() - YEAR_IN_SECONDS,
524
				COOKIEPATH,
525
				COOKIE_DOMAIN
526
			);
527
		}
528
529 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
530
			setcookie(
531
				'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
532
				' ',
533
				time() - YEAR_IN_SECONDS,
534
				COOKIEPATH,
535
				COOKIE_DOMAIN
536
			);
537
		}
538
	}
539
540
	static function delete_connection_for_user( $user_id ) {
541
		if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
542
			return;
543
		}
544
		Jetpack::load_xml_rpc_client();
545
		$xml = new Jetpack_IXR_Client( array(
546
			'wpcom_user_id' => $user_id,
547
		) );
548
		$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
549
550
		if ( $xml->isError() ) {
551
			return false;
552
		}
553
554
		// Clean up local data stored for SSO
555
		delete_user_meta( $user_id, 'wpcom_user_id' );
556
		delete_user_meta( $user_id, 'wpcom_user_data'  );
557
		self::clear_wpcom_profile_cookies();
558
559
		return $xml->getResponse();
560
	}
561
562 View Code Duplication
	static function request_initial_nonce() {
563
		Jetpack::load_xml_rpc_client();
564
		$xml = new Jetpack_IXR_Client( array(
565
			'user_id' => get_current_user_id(),
566
		) );
567
		$xml->query( 'jetpack.sso.requestNonce' );
568
569
		if ( $xml->isError() ) {
570
			return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() );
571
		}
572
573
		return $xml->getResponse();
574
	}
575
576
	/**
577
	 * The function that actually handles the login!
578
	 */
579
	function handle_login() {
580
		$wpcom_nonce   = sanitize_key( $_GET['sso_nonce'] );
581
		$wpcom_user_id = (int) $_GET['user_id'];
582
583
		Jetpack::load_xml_rpc_client();
584
		$xml = new Jetpack_IXR_Client( array(
585
			'user_id' => get_current_user_id(),
586
		) );
587
		$xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id );
588
589
		$user_data = $xml->isError() ? false : $xml->getResponse();
590
		if ( empty( $user_data ) ) {
591
			add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
592
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_invalid_response_data' ) );
593
			return;
594
		}
595
596
		$user_data = (object) $user_data;
597
		$user = null;
598
599
		/**
600
		 * Fires before Jetpack's SSO modifies the log in form.
601
		 *
602
		 * @module sso
603
		 *
604
		 * @since 2.6.0
605
		 *
606
		 * @param object $user_data WordPress.com User information.
607
		 */
608
		do_action( 'jetpack_sso_pre_handle_login', $user_data );
609
610
		if ( Jetpack_SSO_Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) {
611
			$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...
612
613
			JetpackTracking::record_user_event( 'sso_login_failed', array(
614
				'error_message' => 'error_msg_enable_two_step'
615
			) );
616
617
			/** This filter is documented in core/src/wp-includes/pluggable.php */
618
			do_action( 'wp_login_failed', $user_data->login );
619
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_enable_two_step' ) );
620
			return;
621
		}
622
623
		$user_found_with = '';
624
		if ( empty( $user ) && isset( $user_data->external_user_id ) ) {
625
			$user_found_with = 'external_user_id';
626
			$user = get_user_by( 'id', intval( $user_data->external_user_id ) );
627
			if ( $user ) {
628
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
629
			}
630
		}
631
632
		// If we don't have one by wpcom_user_id, try by the email?
633
		if ( empty( $user ) && Jetpack_SSO_Helpers::match_by_email() ) {
634
			$user_found_with = 'match_by_email';
635
			$user = get_user_by( 'email', $user_data->email );
636
			if ( $user ) {
637
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
638
			}
639
		}
640
641
		// If we've still got nothing, create the user.
642
		if ( empty( $user ) && ( get_option( 'users_can_register' ) || Jetpack_SSO_Helpers::new_user_override() ) ) {
643
			/**
644
			 * If not matching by email we still need to verify the email does not exist
645
			 * or this blows up
646
			 *
647
			 * If match_by_email is true, we know the email doesn't exist, as it would have
648
			 * been found in the first pass.  If get_user_by( 'email' ) doesn't find the
649
			 * user, then we know that email is unused, so it's safe to add.
650
			 */
651
			if ( Jetpack_SSO_Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
652
				$user = Jetpack_SSO_Helpers::generate_user( $user_data );
653
				if ( ! $user ) {
654
					JetpackTracking::record_user_event( 'sso_login_failed', array(
655
						'error_message' => 'could_not_create_username'
656
					) );
657
					add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_unable_to_create_user' ) );
658
					return;
659
				}
660
661
				$user_found_with = Jetpack_SSO_Helpers::new_user_override()
662
					? 'user_created_new_user_override'
663
					: 'user_created_users_can_register';
664
			} else {
665
				JetpackTracking::record_user_event( 'sso_login_failed', array(
666
					'error_message' => 'error_msg_email_already_exists'
667
				) );
668
669
				$this->user_data = $user_data;
670
				add_action( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_email_already_exists' ) );
671
				return;
672
			}
673
		}
674
675
		/**
676
		 * Fires after we got login information from WordPress.com.
677
		 *
678
		 * @module sso
679
		 *
680
		 * @since 2.6.0
681
		 *
682
		 * @param array  $user      Local User information.
683
		 * @param object $user_data WordPress.com User Login information.
684
		 */
685
		do_action( 'jetpack_sso_handle_login', $user, $user_data );
686
687
		if ( $user ) {
688
			// Cache the user's details, so we can present it back to them on their user screen
689
			update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
690
691
			add_filter( 'auth_cookie_expiration',    array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
692
			wp_set_auth_cookie( $user->ID, true );
693
			remove_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
694
695
			/** This filter is documented in core/src/wp-includes/user.php */
696
			do_action( 'wp_login', $user->user_login, $user );
697
698
			wp_set_current_user( $user->ID );
699
700
			$_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( $_REQUEST['redirect_to'] ) : '';
701
			$redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
702
703
			// If we have a saved redirect to request in a cookie
704
			if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
705
				// Set that as the requested redirect to
706
				$redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
707
				// And then purge it
708
				setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
709
			}
710
711
			$is_user_connected = Jetpack::is_user_connected( $user->ID );
712
			JetpackTracking::record_user_event( 'sso_user_logged_in', array(
713
				'user_found_with' => $user_found_with,
714
				'user_connected'  => (bool) $is_user_connected,
715
				'user_role'       => Jetpack::translate_current_user_to_role()
716
			) );
717
718
			if ( ! $is_user_connected ) {
719
				$calypso_env = ! empty( $_GET['calypso_env'] )
720
					? sanitize_key( $_GET['calypso_env'] )
721
					: '';
722
723
				wp_safe_redirect(
724
					add_query_arg(
725
						array(
726
							'redirect_to'               => $redirect_to,
727
							'request_redirect_to'       => $_request_redirect_to,
728
							'calypso_env'               => $calypso_env,
729
							'jetpack-sso-auth-redirect' => '1',
730
						),
731
						admin_url()
732
					)
733
				);
734
				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...
735
			}
736
737
			wp_safe_redirect(
738
				/** This filter is documented in core/src/wp-login.php */
739
				apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
740
			);
741
			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...
742
		}
743
744
		add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
745
746
		JetpackTracking::record_user_event( 'sso_login_failed', array(
747
			'error_message' => 'cant_find_user'
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_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'cant_find_user' ) );
754
	}
755
756
	static function profile_page_url() {
757
		return admin_url( 'profile.php' );
758
	}
759
760
	/**
761
	 * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page.
762
	 *
763
	 * @param  array   $args       An array of arguments to add to the SSO URL.
764
	 * @param  boolean $is_primary Should the button have the `button-primary` class?
765
	 * @return string              Returns the HTML markup for the button.
766
	 */
767
	function build_sso_button( $args = array(), $is_primary = false ) {
768
		$url = $this->build_sso_button_url( $args );
769
		$classes = $is_primary
770
			? 'jetpack-sso button button-primary'
771
			: 'jetpack-sso button';
772
773
		return sprintf(
774
			'<a rel="nofollow" href="%1$s" class="%2$s"><span>%3$s %4$s</span></a>',
775
			esc_url( $url ),
776
			$classes,
777
			'<span class="genericon genericon-wordpress"></span>',
778
			esc_html__( 'Log in with WordPress.com', 'jetpack' )
779
		);
780
	}
781
782
	/**
783
	 * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO.
784
	 *
785
	 * @param  array  $args An array of arguments to add to the SSO URL.
786
	 * @return string       The URL used for SSO.
787
	 */
788
	function build_sso_button_url( $args = array() ) {
789
		$defaults = array(
790
			'action'  => 'jetpack-sso',
791
		);
792
793
		$args = wp_parse_args( $args, $defaults );
794
795
		if ( ! empty( $_GET['redirect_to'] ) ) {
796
			$args['redirect_to'] = urlencode( esc_url_raw( $_GET['redirect_to'] ) );
797
		}
798
799
		return add_query_arg( $args, wp_login_url() );
800
	}
801
802
	/**
803
	 * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies.
804
	 *
805
	 * @param  boolean  $reauth  Should the user be forced to reauthenticate on WordPress.com?
806
	 * @param  array    $args    Optional query parameters.
807
	 * @return string            The WordPress.com SSO URL.
808
	 */
809
	function get_sso_url_or_die( $reauth = false, $args = array() ) {
810
		if ( empty( $reauth ) ) {
811
			$sso_redirect = $this->build_sso_url( $args );
812
		} else {
813
			self::clear_wpcom_profile_cookies();
814
			$sso_redirect = $this->build_reauth_and_sso_url( $args );
815
		}
816
817
		// If there was an error retrieving the SSO URL, then error.
818
		if ( is_wp_error( $sso_redirect ) ) {
819
			$error_message = sanitize_text_field(
820
				sprintf( '%s: %s', $sso_redirect->get_error_code(), $sso_redirect->get_error_message() )
0 ignored issues
show
Bug introduced by
The method get_error_code cannot be called on $sso_redirect (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
Bug introduced by
The method get_error_message cannot be called on $sso_redirect (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
821
			);
822
			JetpackTracking::record_user_event( 'sso_login_redirect_failed', array(
823
				'error_message' => $error_message
824
			) );
825
			wp_die( $error_message );
826
		}
827
828
		return $sso_redirect;
829
	}
830
831
	/**
832
	 * Build WordPress.com SSO URL with appropriate query parameters.
833
	 *
834
	 * @param  array  $args Optional query parameters.
835
	 * @return string       WordPress.com SSO URL
836
	 */
837
	function build_sso_url( $args = array() ) {
838
		$sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
839
		$defaults = array(
840
			'action'       => 'jetpack-sso',
841
			'site_id'      => Jetpack_Options::get_option( 'id' ),
842
			'sso_nonce'    => $sso_nonce,
843
			'calypso_auth' => '1',
844
		);
845
846
		$args = wp_parse_args( $args, $defaults );
847
848
		if ( is_wp_error( $args['sso_nonce'] ) ) {
849
			return $args['sso_nonce'];
850
		}
851
852
		return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
853
	}
854
855
	/**
856
	 * Build WordPress.com SSO URL with appropriate query parameters,
857
	 * including the parameters necessary to force the user to reauthenticate
858
	 * on WordPress.com.
859
	 *
860
	 * @param  array  $args Optional query parameters.
861
	 * @return string       WordPress.com SSO URL
862
	 */
863
	function build_reauth_and_sso_url( $args = array() ) {
864
		$sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
865
		$redirect = $this->build_sso_url( array( 'force_auth' => '1', 'sso_nonce' => $sso_nonce ) );
866
867
		if ( is_wp_error( $redirect ) ) {
868
			return $redirect;
869
		}
870
871
		$defaults = array(
872
			'action'       => 'jetpack-sso',
873
			'site_id'      => Jetpack_Options::get_option( 'id' ),
874
			'sso_nonce'    => $sso_nonce,
875
			'reauth'       => '1',
876
			'redirect_to'  => urlencode( $redirect ),
877
			'calypso_auth' => '1',
878
		);
879
880
		$args = wp_parse_args( $args, $defaults );
881
882
		if ( is_wp_error( $args['sso_nonce'] ) ) {
883
			return $args['sso_nonce'];
884
		}
885
886
		return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
887
	}
888
889
	/**
890
	 * Determines local user associated with a given WordPress.com user ID.
891
	 *
892
	 * @since 2.6.0
893
	 *
894
	 * @param int $wpcom_user_id User ID from WordPress.com
895
	 * @return object Local user object if found, null if not.
896
	 */
897
	static function get_user_by_wpcom_id( $wpcom_user_id ) {
898
		$user_query = new WP_User_Query( array(
899
			'meta_key'   => 'wpcom_user_id',
900
			'meta_value' => intval( $wpcom_user_id ),
901
			'number'     => 1,
902
		) );
903
904
		$users = $user_query->get_results();
905
		return $users ? array_shift( $users ) : null;
906
	}
907
908
	/**
909
	 * When jetpack-sso-auth-redirect query parameter is set, will redirect user to
910
	 * WordPress.com authorization flow.
911
	 *
912
	 * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url
913
	 * calls menu_page_url() which doesn't work properly until admin menus are registered.
914
	 */
915
	function maybe_authorize_user_after_sso() {
916
		if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) {
917
			return;
918
		}
919
920
		$redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( $_GET['redirect_to'] ) : admin_url();
921
		$request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( $_GET['request_redirect_to'] ) : $redirect_to;
922
923
		/** This filter is documented in core/src/wp-login.php */
924
		$redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() );
925
926
		/**
927
		 * Since we are passing this redirect to WordPress.com and therefore can not use wp_safe_redirect(),
928
		 * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url().
929
		 */
930
		$redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth );
931
		$redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() );
932
933
		/**
934
		 * Return the raw connect URL with our redirect and attribute connection to SSO.
935
		 */
936
		$connect_url = Jetpack::init()->build_connect_url( true, $redirect_after_auth, 'sso' );
937
938
		add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
939
		wp_safe_redirect( $connect_url );
940
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method maybe_authorize_user_after_sso() 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...
941
	}
942
943
	/**
944
	 * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are
945
	 * stored when the user logs out, and then deleted when the user logs in.
946
	 */
947
	function store_wpcom_profile_cookies_on_logout() {
948
		if ( ! Jetpack::is_user_connected( get_current_user_id() ) ) {
949
			return;
950
		}
951
952
		$user_data = $this->get_user_data( get_current_user_id() );
953
		if ( ! $user_data ) {
954
			return;
955
		}
956
957
		setcookie(
958
			'jetpack_sso_wpcom_name_' . COOKIEHASH,
959
			$user_data->display_name,
960
			time() + WEEK_IN_SECONDS,
961
			COOKIEPATH,
962
			COOKIE_DOMAIN
963
		);
964
965
		setcookie(
966
			'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
967
			get_avatar_url(
968
				$user_data->email,
969
				array( 'size' => 144, 'default' => 'mystery' )
970
			),
971
			time() + WEEK_IN_SECONDS,
972
			COOKIEPATH,
973
			COOKIE_DOMAIN
974
		);
975
	}
976
977
	/**
978
	 * Determines if a local user is connected to WordPress.com
979
	 *
980
	 * @since 2.8
981
	 * @param integer $user_id - Local user id
982
	 * @return boolean
983
	 **/
984
	public function is_user_connected( $user_id ) {
985
		return $this->get_user_data( $user_id );
986
	}
987
988
	/**
989
	 * Retrieves a user's WordPress.com data
990
	 *
991
	 * @since 2.8
992
	 * @param integer $user_id - Local user id
993
	 * @return mixed null or stdClass
994
	 **/
995
	public function get_user_data( $user_id ) {
996
		return get_user_meta( $user_id, 'wpcom_user_data', true );
997
	}
998
}
999
1000
Jetpack_SSO::get_instance();
1001