Completed
Push — fix/disconnected-settings ( d97055...213165 )
by
unknown
82:42 queued 70:39
created

Jetpack_SSO::request_initial_nonce()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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