Completed
Push — update/jitm-test ( 03ced2...c48940 )
by
unknown
212:35 queued 203:56
created

Jetpack_SSO::inject_sso_jitm()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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