Completed
Push — fix/remove-legacy-config-pages ( 567494...37f1a4 )
by
unknown
117:14 queued 110:14
created

Jetpack_SSO::save_cookies()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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