Completed
Push — update/sso-stash-nonce ( aeda31 )
by
unknown
09:42
created

Jetpack_SSO::render_match_by_email()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 13
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( 'jetpack_unlinked_user',  array( $this, 'delete_connection_for_user') );
35
36
		// Adding this action so that on login_init, the action won't be sanitized out of the $action global.
37
		add_action( 'login_form_jetpack-sso', '__return_true' );
38
	}
39
40
	/**
41
	 * Returns the single instance of the Jetpack_SSO object
42
	 *
43
	 * @since 2.8
44
	 * @return Jetpack_SSO
45
	 **/
46
	public static function get_instance() {
47
		if ( ! is_null( self::$instance ) ) {
48
			return self::$instance;
49
		}
50
51
		return self::$instance = new Jetpack_SSO;
52
	}
53
54
	/**
55
	 * Add configure button and functionality to the module card on the Jetpack screen
56
	 **/
57
	public static function module_configure_button() {
58
		Jetpack::enable_module_configurable( __FILE__ );
59
		Jetpack::module_configuration_load( __FILE__, array( __CLASS__, 'module_configuration_load' ) );
60
		Jetpack::module_configuration_head( __FILE__, array( __CLASS__, 'module_configuration_head' ) );
61
		Jetpack::module_configuration_screen( __FILE__, array( __CLASS__, 'module_configuration_screen' ) );
62
	}
63
64
	public static function module_configuration_load() {}
65
66
	public static function module_configuration_head() {}
67
68
	public static function module_configuration_screen() {
69
		?>
70
		<form method="post" action="options.php">
71
			<?php settings_fields( 'jetpack-sso' ); ?>
72
			<?php do_settings_sections( 'jetpack-sso' ); ?>
73
			<?php submit_button(); ?>
74
		</form>
75
		<?php
76
	}
77
78
	/**
79
	 * If jetpack_force_logout == 1 in current user meta the user will be forced
80
	 * to logout and reauthenticate with the site.
81
	 **/
82
	public function maybe_logout_user() {
83
		global $current_user;
84
85
		if ( 1 == $current_user->jetpack_force_logout ) {
86
			delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
87
			self::delete_connection_for_user( $current_user->ID );
88
			wp_logout();
89
			wp_safe_redirect( wp_login_url() );
90
			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...
91
		}
92
	}
93
94
	/**
95
	 * Adds additional methods the WordPress xmlrpc API for handling SSO specific features
96
	 *
97
	 * @param array $methods
98
	 * @return array
99
	 **/
100
	public function xmlrpc_methods( $methods ) {
101
		$methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' );
102
		return $methods;
103
	}
104
105
	/**
106
	 * Marks a user's profile for disconnect from WordPress.com and forces a logout
107
	 * the next time the user visits the site.
108
	 **/
109
	public function xmlrpc_user_disconnect( $user_id ) {
110
		$user_query = new WP_User_Query(
111
			array(
112
				'meta_key' => 'wpcom_user_id',
113
				'meta_value' => $user_id,
114
			)
115
		);
116
		$user = $user_query->get_results();
117
		$user = $user[0];
118
119
		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...
120
			$user = wp_set_current_user( $user->ID );
121
			update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
122
			self::delete_connection_for_user( $user->ID );
123
			return true;
124
		}
125
		return false;
126
	}
127
128
	/**
129
	 * Enqueues scripts and styles necessary for SSO login.
130
	 */
131
	public function login_enqueue_scripts() {
132
		global $action;
133
134
		if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
135
			return;
136
		}
137
138
		if ( is_rtl() ) {
139
			wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login-rtl.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
140
		} else {
141
			wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
142
		}
143
144
		wp_enqueue_script( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION );
145
	}
146
147
	/**
148
	 * Adds Jetpack SSO classes to login body
149
	 *
150
	 * @param  array $classes Array of classes to add to body tag
151
	 * @return array          Array of classes to add to body tag
152
	 */
153
	public function login_body_class( $classes ) {
154
		global $action;
155
156
		if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
157
			return $classes;
158
		}
159
160
		// Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed.
161
		$classes[] = 'jetpack-sso';
162
163
		if ( ! Jetpack::is_staging_site() ) {
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
178
		return $classes;
179
	}
180
181
	public function print_inline_admin_css() {
182
		?>
183
			<style>
184
				.jetpack-sso .message {
185
					margin-top: 20px;
186
				}
187
188
				.jetpack-sso #login .message:first-child,
189
				.jetpack-sso #login h1 + .message {
190
					margin-top: 0;
191
				}
192
			</style>
193
		<?php
194
	}
195
196
	/**
197
	 * Adds settings fields to Settings > General > Single Sign On that allows users to
198
	 * turn off the login form on wp-login.php
199
	 *
200
	 * @since 2.7
201
	 **/
202
	public function register_settings() {
203
204
		add_settings_section(
205
			'jetpack_sso_settings',
206
			__( 'Single Sign On' , 'jetpack' ),
207
			'__return_false',
208
			'jetpack-sso'
209
		);
210
211
		/*
212
		 * Settings > General > Single Sign On
213
		 * Require two step authentication
214
		 */
215
		register_setting(
216
			'jetpack-sso',
217
			'jetpack_sso_require_two_step',
218
			array( $this, 'validate_jetpack_sso_require_two_step' )
219
		);
220
221
		add_settings_field(
222
			'jetpack_sso_require_two_step',
223
			'', // __( 'Require Two-Step Authentication' , 'jetpack' ),
224
			array( $this, 'render_require_two_step' ),
225
			'jetpack-sso',
226
			'jetpack_sso_settings'
227
		);
228
229
		/*
230
		 * Settings > General > Single Sign On
231
		 */
232
		register_setting(
233
			'jetpack-sso',
234
			'jetpack_sso_match_by_email',
235
			array( $this, 'validate_jetpack_sso_match_by_email' )
236
		);
237
238
		add_settings_field(
239
			'jetpack_sso_match_by_email',
240
			'', // __( 'Match by Email' , 'jetpack' ),
241
			array( $this, 'render_match_by_email' ),
242
			'jetpack-sso',
243
			'jetpack_sso_settings'
244
		);
245
	}
246
247
	/**
248
	 * Builds the display for the checkbox allowing user to require two step
249
	 * auth be enabled on WordPress.com accounts before login. Displays in Settings > General
250
	 *
251
	 * @since 2.7
252
	 **/
253
	public function render_require_two_step() {
254
		?>
255
		<label>
256
			<input
257
				type="checkbox"
258
				name="jetpack_sso_require_two_step"
259
				<?php checked( Jetpack_SSO_Helpers::is_two_step_required() ); ?>
260
				<?php disabled( Jetpack_SSO_Helpers::is_require_two_step_checkbox_disabled() ); ?>
261
			>
262
			<?php esc_html_e( 'Require Two-Step Authentication' , 'jetpack' ); ?>
263
		</label>
264
		<?php
265
	}
266
267
	/**
268
	 * Validate the require  two step checkbox in Settings > General
269
	 *
270
	 * @since 2.7
271
	 * @return boolean
272
	 **/
273
	public function validate_jetpack_sso_require_two_step( $input ) {
274
		return ( ! empty( $input ) ) ? 1 : 0;
275
	}
276
277
	/**
278
	 * Builds the display for the checkbox allowing the user to allow matching logins by email
279
	 * Displays in Settings > General
280
	 *
281
	 * @since 2.9
282
	 **/
283
	public function render_match_by_email() {
284
		?>
285
			<label>
286
				<input
287
					type="checkbox"
288
					name="jetpack_sso_match_by_email"
289
					<?php checked( Jetpack_SSO_Helpers::match_by_email() ); ?>
290
					<?php disabled( Jetpack_SSO_Helpers::is_match_by_email_checkbox_disabled() ); ?>
291
				>
292
				<?php esc_html_e( 'Match by Email', 'jetpack' ); ?>
293
			</label>
294
		<?php
295
	}
296
297
	/**
298
	 * Validate the match by email check in Settings > General
299
	 *
300
	 * @since 2.9
301
	 * @return boolean
302
	 **/
303
	public function validate_jetpack_sso_match_by_email( $input ) {
304
		return ( ! empty( $input ) ) ? 1 : 0;
305
	}
306
307
	/**
308
	 * Checks to determine if the user wants to login on wp-login
309
	 *
310
	 * This function mostly exists to cover the exceptions to login
311
	 * that may exist as other parameters to $_GET[action] as $_GET[action]
312
	 * does not have to exist. By default WordPress assumes login if an action
313
	 * is not set, however this may not be true, as in the case of logout
314
	 * where $_GET[loggedout] is instead set
315
	 *
316
	 * @return boolean
317
	 **/
318
	private function wants_to_login() {
319
		$wants_to_login = false;
320
321
		// Cover default WordPress behavior
322
		$action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login';
323
324
		// And now the exceptions
325
		$action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;
326
327
		if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
328
			$wants_to_login = true;
329
		}
330
331
		return $wants_to_login;
332
	}
333
334
	function login_init() {
335
		global $action;
336
337
		if ( Jetpack_SSO_Helpers::should_hide_login_form() ) {
338
			/**
339
			 * Since the default authenticate filters fire at priority 20 for checking username and password,
340
			 * let's fire at priority 30. wp_authenticate_spam_check is fired at priority 99, but since we return a
341
			 * WP_Error in disable_default_login_form, then we won't trigger spam processing logic.
342
			 */
343
			add_filter( 'authenticate', array( 'Jetpack_SSO_Notices', 'disable_default_login_form' ), 30 );
344
345
			/**
346
			 * Filter the display of the disclaimer message appearing when default WordPress login form is disabled.
347
			 *
348
			 * @module sso
349
			 *
350
			 * @since 2.8.0
351
			 *
352
			 * @param bool true Should the disclaimer be displayed. Default to true.
353
			 */
354
			$display_sso_disclaimer = apply_filters( 'jetpack_sso_display_disclaimer', true );
355
			if ( $display_sso_disclaimer ) {
356
				add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'msg_login_by_jetpack' ) );
357
			}
358
		}
359
360
		 if ( 'jetpack-sso' === $action ) {
361
			if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
362
				$this->handle_login();
363
				$this->display_sso_login_form();
364
			} else {
365
				if ( Jetpack::is_staging_site() ) {
366
					add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
367 View Code Duplication
				} else {
368
					// Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
369
					add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
370
					$reauth = ! empty( $_GET['force_reauth'] );
371
					$sso_url = $this->get_sso_url_or_die( $reauth );
372
					JetpackTracking::record_user_event( 'sso_login_redirect_success' );
373
					wp_safe_redirect( $sso_url );
374
					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...
375
				}
376
			}
377
		} else if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
378
379
			// Save cookies so we can handle redirects after SSO
380
			$this->save_cookies();
381
382
			/**
383
			 * Check to see if the site admin wants to automagically forward the user
384
			 * to the WordPress.com login page AND  that the request to wp-login.php
385
			 * is not something other than login (Like logout!)
386
			 */
387 View Code Duplication
			if ( Jetpack_SSO_Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) {
388
				add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
389
				$reauth = ! empty( $_GET['force_reauth'] );
390
				$sso_url = $this->get_sso_url_or_die( $reauth );
391
				JetpackTracking::record_user_event( 'sso_login_redirect_bypass_success' );
392
				wp_safe_redirect( $sso_url );
393
				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...
394
			}
395
396
			$this->display_sso_login_form();
397
		}
398
	}
399
400
	/**
401
	 * Ensures that we can get a nonce from WordPress.com via XML-RPC before setting
402
	 * up the hooks required to display the SSO form.
403
	 */
404
	public function display_sso_login_form() {
405
		add_filter( 'login_body_class', array( $this, 'login_body_class' ) );
406
		add_action( 'login_head',       array( $this, 'print_inline_admin_css' ) );
407
408
		if ( Jetpack::is_staging_site() ) {
409
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
410
			return;
411
		}
412
413
		$sso_nonce = self::request_initial_nonce();
414
		if ( is_wp_error( $sso_nonce ) ) {
415
			return;
416
		}
417
418
		add_action( 'login_form',            array( $this, 'login_form' ) );
419
		add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) );
420
	}
421
422
	/**
423
	 * Conditionally save the redirect_to url as a cookie.
424
	 *
425
	 * @since 4.6.0 Renamed to save_cookies from maybe_save_redirect_cookies
426
	 */
427
	public static function save_cookies() {
428
		if ( headers_sent() ) {
429
			return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
430
		}
431
432
		setcookie(
433
			'jetpack_sso_original_request',
434
			esc_url_raw( set_url_scheme( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ),
435
			time() + HOUR_IN_SECONDS,
436
			COOKIEPATH,
437
			COOKIE_DOMAIN,
438
			false,
439
			true
440
		);
441
442
		if ( ! empty( $_GET['redirect_to'] ) ) {
443
			// If we have something to redirect to
444
			$url = esc_url_raw( $_GET['redirect_to'] );
445
			setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
446 View Code Duplication
		} elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
447
			// Otherwise, if it's already set, purge it.
448
			setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
449
		}
450
	}
451
452
	/**
453
	 * Outputs the Jetpack SSO button and description as well as the toggle link
454
	 * for switching between Jetpack SSO and default login.
455
	 */
456
	function login_form() {
457
		$site_name = get_bloginfo( 'name' );
458
		if ( ! $site_name ) {
459
			$site_name = get_bloginfo( 'url' );
460
		}
461
462
		$display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] )
463
			? $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ]
464
			: false;
465
		$gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] )
466
			? $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ]
467
			: false;
468
469
		?>
470
		<div id="jetpack-sso-wrap">
471
			<?php if ( $display_name && $gravatar ) : ?>
472
				<div id="jetpack-sso-wrap__user">
473
					<img width="72" height="72" src="<?php echo esc_html( $gravatar ); ?>" />
474
475
					<h2>
476
						<?php
477
							echo wp_kses(
478
								sprintf( __( 'Log in as <span>%s</span>', 'jetpack' ), esc_html( $display_name ) ),
479
								array( 'span' => true )
480
							);
481
						?>
482
					</h2>
483
				</div>
484
485
			<?php endif; ?>
486
487
488
			<div id="jetpack-sso-wrap__action">
489
				<?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...
490
491
				<?php if ( $display_name && $gravatar ) : ?>
492
					<a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>">
493
						<?php esc_html_e( 'Log in as a different WordPress.com user', 'jetpack' ); ?>
494
					</a>
495
				<?php else : ?>
496
					<p>
497
						<?php
498
							echo esc_html(
499
								sprintf(
500
									__( 'You can now save time spent logging in by connecting your WordPress.com account to %s.', 'jetpack' ),
501
									esc_html( $site_name )
502
								)
503
							);
504
						?>
505
					</p>
506
				<?php endif; ?>
507
			</div>
508
509
			<?php if ( ! Jetpack_SSO_Helpers::should_hide_login_form() ) : ?>
510
				<div class="jetpack-sso-or">
511
					<span><?php esc_html_e( 'Or', 'jetpack' ); ?></span>
512
				</div>
513
514
				<a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '1' ) ); ?>" class="jetpack-sso-toggle wpcom">
515
					<?php
516
						esc_html_e( 'Log in with username and password', 'jetpack' )
517
					?>
518
				</a>
519
520
				<a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '0' ) ); ?>" class="jetpack-sso-toggle default">
521
					<?php
522
						esc_html_e( 'Log in with WordPress.com', 'jetpack' )
523
					?>
524
				</a>
525
			<?php endif; ?>
526
		</div>
527
		<?php
528
	}
529
530
	/**
531
	 * Clear the cookies that store the profile information for the last
532
	 * WPCOM user to connect.
533
	 */
534
	static function clear_wpcom_profile_cookies() {
535 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
536
			setcookie(
537
				'jetpack_sso_wpcom_name_' . COOKIEHASH,
538
				' ',
539
				time() - YEAR_IN_SECONDS,
540
				COOKIEPATH,
541
				COOKIE_DOMAIN
542
			);
543
		}
544
545 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
546
			setcookie(
547
				'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
548
				' ',
549
				time() - YEAR_IN_SECONDS,
550
				COOKIEPATH,
551
				COOKIE_DOMAIN
552
			);
553
		}
554
	}
555
556
	/**
557
	 * Clear cookies that are no longer needed once the user has logged in.
558
	 *
559
	 * @since 4.8.0
560
	 */
561
	static function clear_cookies_after_login() {
562
		self::clear_wpcom_profile_cookies();
563 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_nonce' ] ) ) {
564
			setcookie(
565
				'jetpack_sso_nonce',
566
				' ',
567
				time() - YEAR_IN_SECONDS,
568
				COOKIEPATH,
569
				COOKIE_DOMAIN
570
			);
571
		}
572
573 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_original_request' ] ) ) {
574
			setcookie(
575
				'jetpack_sso_original_request',
576
				' ',
577
				time() - YEAR_IN_SECONDS,
578
				COOKIEPATH,
579
				COOKIE_DOMAIN
580
			);
581
		}
582
583 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_redirect_to' ] ) ) {
584
			setcookie(
585
				'jetpack_sso_redirect_to',
586
				' ',
587
				time() - YEAR_IN_SECONDS,
588
				COOKIEPATH,
589
				COOKIE_DOMAIN
590
			);
591
		}
592
	}
593
594
	static function delete_connection_for_user( $user_id ) {
595
		if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
596
			return;
597
		}
598
		Jetpack::load_xml_rpc_client();
599
		$xml = new Jetpack_IXR_Client( array(
600
			'wpcom_user_id' => $user_id,
601
		) );
602
		$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
603
604
		if ( $xml->isError() ) {
605
			return false;
606
		}
607
608
		// Clean up local data stored for SSO
609
		delete_user_meta( $user_id, 'wpcom_user_id' );
610
		delete_user_meta( $user_id, 'wpcom_user_data'  );
611
		self::clear_wpcom_profile_cookies();
612
613
		return $xml->getResponse();
614
	}
615
616
	static function request_initial_nonce() {
617
		$nonce = ! empty( $_COOKIE[ 'jetpack_sso_nonce' ] )
618
			? $_COOKIE[ 'jetpack_sso_nonce' ]
619
			: false;
620
621
		if ( ! $nonce ) {
622
			Jetpack::load_xml_rpc_client();
623
			$xml = new Jetpack_IXR_Client( array(
624
				'user_id' => get_current_user_id(),
625
			) );
626
			$xml->query( 'jetpack.sso.requestNonce' );
627
628
			if ( $xml->isError() ) {
629
				return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() );
630
			}
631
632
			$nonce = $xml->getResponse();
633
634
			setcookie(
635
				'jetpack_sso_nonce',
636
				$nonce,
637
				time() + ( 10 * MINUTE_IN_SECONDS ),
638
				COOKIEPATH,
639
				COOKIE_DOMAIN
640
			);
641
		}
642
643
		return sanitize_key( $nonce );
644
	}
645
646
	/**
647
	 * The function that actually handles the login!
648
	 */
649
	function handle_login() {
650
		$wpcom_nonce   = sanitize_key( $_GET['sso_nonce'] );
651
		$wpcom_user_id = (int) $_GET['user_id'];
652
653
		Jetpack::load_xml_rpc_client();
654
		$xml = new Jetpack_IXR_Client( array(
655
			'user_id' => get_current_user_id(),
656
		) );
657
		$xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id );
658
659
		$user_data = $xml->isError() ? false : $xml->getResponse();
660
		if ( empty( $user_data ) ) {
661
			add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
662
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_invalid_response_data' ) );
663
			return;
664
		}
665
666
		$user_data = (object) $user_data;
667
		$user = null;
668
669
		/**
670
		 * Fires before Jetpack's SSO modifies the log in form.
671
		 *
672
		 * @module sso
673
		 *
674
		 * @since 2.6.0
675
		 *
676
		 * @param object $user_data WordPress.com User information.
677
		 */
678
		do_action( 'jetpack_sso_pre_handle_login', $user_data );
679
680
		if ( Jetpack_SSO_Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) {
681
			$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...
682
683
			JetpackTracking::record_user_event( 'sso_login_failed', array(
684
				'error_message' => 'error_msg_enable_two_step'
685
			) );
686
687
			/** This filter is documented in core/src/wp-includes/pluggable.php */
688
			do_action( 'wp_login_failed', $user_data->login );
689
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_enable_two_step' ) );
690
			return;
691
		}
692
693
		$user_found_with = '';
694
		if ( empty( $user ) && isset( $user_data->external_user_id ) ) {
695
			$user_found_with = 'external_user_id';
696
			$user = get_user_by( 'id', intval( $user_data->external_user_id ) );
697
			if ( $user ) {
698
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
699
			}
700
		}
701
702
		// If we don't have one by wpcom_user_id, try by the email?
703
		if ( empty( $user ) && Jetpack_SSO_Helpers::match_by_email() ) {
704
			$user_found_with = 'match_by_email';
705
			$user = get_user_by( 'email', $user_data->email );
706
			if ( $user ) {
707
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
708
			}
709
		}
710
711
		// If we've still got nothing, create the user.
712
		$new_user_override_role = false;
713
		if ( empty( $user ) && ( get_option( 'users_can_register' ) || ( $new_user_override_role = Jetpack_SSO_Helpers::new_user_override( $user_data ) ) ) ) {
714
			/**
715
			 * If not matching by email we still need to verify the email does not exist
716
			 * or this blows up
717
			 *
718
			 * If match_by_email is true, we know the email doesn't exist, as it would have
719
			 * been found in the first pass.  If get_user_by( 'email' ) doesn't find the
720
			 * user, then we know that email is unused, so it's safe to add.
721
			 */
722
			if ( Jetpack_SSO_Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
723
				
724
				if ( $new_user_override_role ) {
725
					$user_data->role = $new_user_override_role;
726
				}
727
728
				$user = Jetpack_SSO_Helpers::generate_user( $user_data );
729
				if ( ! $user ) {
730
					JetpackTracking::record_user_event( 'sso_login_failed', array(
731
						'error_message' => 'could_not_create_username'
732
					) );
733
					add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_unable_to_create_user' ) );
734
					return;
735
				}
736
737
				$user_found_with = $new_user_override_role
738
					? 'user_created_new_user_override'
739
					: 'user_created_users_can_register';
740
			} else {
741
				JetpackTracking::record_user_event( 'sso_login_failed', array(
742
					'error_message' => 'error_msg_email_already_exists'
743
				) );
744
745
				$this->user_data = $user_data;
746
				add_action( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_email_already_exists' ) );
747
				return;
748
			}
749
		}
750
751
		/**
752
		 * Fires after we got login information from WordPress.com.
753
		 *
754
		 * @module sso
755
		 *
756
		 * @since 2.6.0
757
		 *
758
		 * @param array  $user      Local User information.
759
		 * @param object $user_data WordPress.com User Login information.
760
		 */
761
		do_action( 'jetpack_sso_handle_login', $user, $user_data );
762
763
		if ( $user ) {
764
			// Cache the user's details, so we can present it back to them on their user screen
765
			update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
766
767
			add_filter( 'auth_cookie_expiration',    array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
768
			wp_set_auth_cookie( $user->ID, true );
769
			remove_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
770
771
			/** This filter is documented in core/src/wp-includes/user.php */
772
			do_action( 'wp_login', $user->user_login, $user );
773
774
			wp_set_current_user( $user->ID );
775
776
			$_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( $_REQUEST['redirect_to'] ) : '';
777
			$redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
778
779
			// If we have a saved redirect to request in a cookie
780
			if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
781
				// Set that as the requested redirect to
782
				$redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
783
			}
784
785
			self::clear_cookies_after_login();
786
787
			$json_api_auth_environment = Jetpack_SSO_Helpers::get_json_api_auth_environment();
788
789
			$is_json_api_auth = ! empty( $json_api_auth_environment );
790
			$is_user_connected = Jetpack::is_user_connected( $user->ID );
791
			JetpackTracking::record_user_event( 'sso_user_logged_in', array(
792
				'user_found_with'  => $user_found_with,
793
				'user_connected'   => (bool) $is_user_connected,
794
				'user_role'        => Jetpack::translate_current_user_to_role(),
795
				'is_json_api_auth' => (bool) $is_json_api_auth,
796
			) );
797
798
			if ( $is_json_api_auth ) {
799
				Jetpack::init()->verify_json_api_authorization_request( $json_api_auth_environment );
800
				Jetpack::init()->store_json_api_authorization_token( $user->user_login, $user );
801
802
			} else if ( ! $is_user_connected ) {
803
				$calypso_env = ! empty( $_GET['calypso_env'] )
804
					? sanitize_key( $_GET['calypso_env'] )
805
					: '';
806
807
				wp_safe_redirect(
808
					add_query_arg(
809
						array(
810
							'redirect_to'               => $redirect_to,
811
							'request_redirect_to'       => $_request_redirect_to,
812
							'calypso_env'               => $calypso_env,
813
							'jetpack-sso-auth-redirect' => '1',
814
						),
815
						admin_url()
816
					)
817
				);
818
				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...
819
			}
820
821
			add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
822
			wp_safe_redirect(
823
				/** This filter is documented in core/src/wp-login.php */
824
				apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
825
			);
826
			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...
827
		}
828
829
		add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
830
831
		JetpackTracking::record_user_event( 'sso_login_failed', array(
832
			'error_message' => 'cant_find_user'
833
		) );
834
835
		$this->user_data = $user_data;
836
		/** This filter is documented in core/src/wp-includes/pluggable.php */
837
		do_action( 'wp_login_failed', $user_data->login );
838
		add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'cant_find_user' ) );
839
	}
840
841
	static function profile_page_url() {
842
		return admin_url( 'profile.php' );
843
	}
844
845
	/**
846
	 * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page.
847
	 *
848
	 * @param  array   $args       An array of arguments to add to the SSO URL.
849
	 * @param  boolean $is_primary Should the button have the `button-primary` class?
850
	 * @return string              Returns the HTML markup for the button.
851
	 */
852
	function build_sso_button( $args = array(), $is_primary = false ) {
853
		$url = $this->build_sso_button_url( $args );
854
		$classes = $is_primary
855
			? 'jetpack-sso button button-primary'
856
			: 'jetpack-sso button';
857
858
		return sprintf(
859
			'<a rel="nofollow" href="%1$s" class="%2$s"><span>%3$s %4$s</span></a>',
860
			esc_url( $url ),
861
			$classes,
862
			'<span class="genericon genericon-wordpress"></span>',
863
			esc_html__( 'Log in with WordPress.com', 'jetpack' )
864
		);
865
	}
866
867
	/**
868
	 * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO.
869
	 *
870
	 * @param  array  $args An array of arguments to add to the SSO URL.
871
	 * @return string       The URL used for SSO.
872
	 */
873
	function build_sso_button_url( $args = array() ) {
874
		$defaults = array(
875
			'action'  => 'jetpack-sso',
876
		);
877
878
		$args = wp_parse_args( $args, $defaults );
879
880
		if ( ! empty( $_GET['redirect_to'] ) ) {
881
			$args['redirect_to'] = urlencode( esc_url_raw( $_GET['redirect_to'] ) );
882
		}
883
884
		return add_query_arg( $args, wp_login_url() );
885
	}
886
887
	/**
888
	 * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies.
889
	 *
890
	 * @param  boolean  $reauth  Should the user be forced to reauthenticate on WordPress.com?
891
	 * @param  array    $args    Optional query parameters.
892
	 * @return string            The WordPress.com SSO URL.
893
	 */
894
	function get_sso_url_or_die( $reauth = false, $args = array() ) {
895
		if ( empty( $reauth ) ) {
896
			$sso_redirect = $this->build_sso_url( $args );
897
		} else {
898
			self::clear_wpcom_profile_cookies();
899
			$sso_redirect = $this->build_reauth_and_sso_url( $args );
900
		}
901
902
		// If there was an error retrieving the SSO URL, then error.
903
		if ( is_wp_error( $sso_redirect ) ) {
904
			$error_message = sanitize_text_field(
905
				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...
906
			);
907
			JetpackTracking::record_user_event( 'sso_login_redirect_failed', array(
908
				'error_message' => $error_message
909
			) );
910
			wp_die( $error_message );
911
		}
912
913
		return $sso_redirect;
914
	}
915
916
	/**
917
	 * Build WordPress.com SSO URL with appropriate query parameters.
918
	 *
919
	 * @param  array  $args Optional query parameters.
920
	 * @return string       WordPress.com SSO URL
921
	 */
922
	function build_sso_url( $args = array() ) {
923
		$sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
924
		$defaults = array(
925
			'action'       => 'jetpack-sso',
926
			'site_id'      => Jetpack_Options::get_option( 'id' ),
927
			'sso_nonce'    => $sso_nonce,
928
			'calypso_auth' => '1',
929
		);
930
931
		$args = wp_parse_args( $args, $defaults );
932
933
		if ( is_wp_error( $args['sso_nonce'] ) ) {
934
			return $args['sso_nonce'];
935
		}
936
937
		return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
938
	}
939
940
	/**
941
	 * Build WordPress.com SSO URL with appropriate query parameters,
942
	 * including the parameters necessary to force the user to reauthenticate
943
	 * on WordPress.com.
944
	 *
945
	 * @param  array  $args Optional query parameters.
946
	 * @return string       WordPress.com SSO URL
947
	 */
948
	function build_reauth_and_sso_url( $args = array() ) {
949
		$sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
950
		$redirect = $this->build_sso_url( array( 'force_auth' => '1', 'sso_nonce' => $sso_nonce ) );
951
952
		if ( is_wp_error( $redirect ) ) {
953
			return $redirect;
954
		}
955
956
		$defaults = array(
957
			'action'       => 'jetpack-sso',
958
			'site_id'      => Jetpack_Options::get_option( 'id' ),
959
			'sso_nonce'    => $sso_nonce,
960
			'reauth'       => '1',
961
			'redirect_to'  => urlencode( $redirect ),
962
			'calypso_auth' => '1',
963
		);
964
965
		$args = wp_parse_args( $args, $defaults );
966
967
		if ( is_wp_error( $args['sso_nonce'] ) ) {
968
			return $args['sso_nonce'];
969
		}
970
971
		return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
972
	}
973
974
	/**
975
	 * Determines local user associated with a given WordPress.com user ID.
976
	 *
977
	 * @since 2.6.0
978
	 *
979
	 * @param int $wpcom_user_id User ID from WordPress.com
980
	 * @return object Local user object if found, null if not.
981
	 */
982
	static function get_user_by_wpcom_id( $wpcom_user_id ) {
983
		$user_query = new WP_User_Query( array(
984
			'meta_key'   => 'wpcom_user_id',
985
			'meta_value' => intval( $wpcom_user_id ),
986
			'number'     => 1,
987
		) );
988
989
		$users = $user_query->get_results();
990
		return $users ? array_shift( $users ) : null;
991
	}
992
993
	/**
994
	 * When jetpack-sso-auth-redirect query parameter is set, will redirect user to
995
	 * WordPress.com authorization flow.
996
	 *
997
	 * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url
998
	 * calls menu_page_url() which doesn't work properly until admin menus are registered.
999
	 */
1000
	function maybe_authorize_user_after_sso() {
1001
		if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) {
1002
			return;
1003
		}
1004
1005
		$redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( $_GET['redirect_to'] ) : admin_url();
1006
		$request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( $_GET['request_redirect_to'] ) : $redirect_to;
1007
1008
		/** This filter is documented in core/src/wp-login.php */
1009
		$redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() );
1010
1011
		/**
1012
		 * Since we are passing this redirect to WordPress.com and therefore can not use wp_safe_redirect(),
1013
		 * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url().
1014
		 */
1015
		$redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth );
1016
		$redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() );
1017
1018
		/**
1019
		 * Return the raw connect URL with our redirect and attribute connection to SSO.
1020
		 */
1021
		$connect_url = Jetpack::init()->build_connect_url( true, $redirect_after_auth, 'sso' );
1022
1023
		add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
1024
		wp_safe_redirect( $connect_url );
1025
		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...
1026
	}
1027
1028
	/**
1029
	 * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are
1030
	 * stored when the user logs out, and then deleted when the user logs in.
1031
	 */
1032
	function store_wpcom_profile_cookies_on_logout() {
1033
		if ( ! Jetpack::is_user_connected( get_current_user_id() ) ) {
1034
			return;
1035
		}
1036
1037
		$user_data = $this->get_user_data( get_current_user_id() );
1038
		if ( ! $user_data ) {
1039
			return;
1040
		}
1041
1042
		setcookie(
1043
			'jetpack_sso_wpcom_name_' . COOKIEHASH,
1044
			$user_data->display_name,
1045
			time() + WEEK_IN_SECONDS,
1046
			COOKIEPATH,
1047
			COOKIE_DOMAIN
1048
		);
1049
1050
		setcookie(
1051
			'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
1052
			get_avatar_url(
1053
				$user_data->email,
1054
				array( 'size' => 144, 'default' => 'mystery' )
1055
			),
1056
			time() + WEEK_IN_SECONDS,
1057
			COOKIEPATH,
1058
			COOKIE_DOMAIN
1059
		);
1060
	}
1061
1062
	/**
1063
	 * Determines if a local user is connected to WordPress.com
1064
	 *
1065
	 * @since 2.8
1066
	 * @param integer $user_id - Local user id
1067
	 * @return boolean
1068
	 **/
1069
	public function is_user_connected( $user_id ) {
1070
		return $this->get_user_data( $user_id );
1071
	}
1072
1073
	/**
1074
	 * Retrieves a user's WordPress.com data
1075
	 *
1076
	 * @since 2.8
1077
	 * @param integer $user_id - Local user id
1078
	 * @return mixed null or stdClass
1079
	 **/
1080
	public function get_user_data( $user_id ) {
1081
		return get_user_meta( $user_id, 'wpcom_user_data', true );
1082
	}
1083
}
1084
1085
Jetpack_SSO::get_instance();
1086