Completed
Push — add/e2e-mailchimp-block-test ( e217db...6066d0 )
by Yaroslav
98:30 queued 85:55
created

Jetpack_SSO::login_body_class()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
425
		}
426
427
		setcookie(
428
			'jetpack_sso_original_request',
429
			esc_url_raw( set_url_scheme( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ),
430
			time() + HOUR_IN_SECONDS,
431
			COOKIEPATH,
432
			COOKIE_DOMAIN,
433
			is_ssl(),
434
			true
435
		);
436
437
		if ( ! empty( $_GET['redirect_to'] ) ) {
438
			// If we have something to redirect to
439
			$url = esc_url_raw( $_GET['redirect_to'] );
440
			setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
441
		} elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
442
			// Otherwise, if it's already set, purge it.
443
			setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
444
		}
445
	}
446
447
	/**
448
	 * Outputs the Jetpack SSO button and description as well as the toggle link
449
	 * for switching between Jetpack SSO and default login.
450
	 */
451
	function login_form() {
452
		$site_name = get_bloginfo( 'name' );
453
		if ( ! $site_name ) {
454
			$site_name = get_bloginfo( 'url' );
455
		}
456
457
		$display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] )
458
			? $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ]
459
			: false;
460
		$gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] )
461
			? $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ]
462
			: false;
463
464
		?>
465
		<div id="jetpack-sso-wrap">
466
			<?php if ( $display_name && $gravatar ) : ?>
467
				<div id="jetpack-sso-wrap__user">
468
					<img width="72" height="72" src="<?php echo esc_html( $gravatar ); ?>" />
469
470
					<h2>
471
						<?php
472
							echo wp_kses(
473
								sprintf( __( 'Log in as <span>%s</span>', 'jetpack' ), esc_html( $display_name ) ),
474
								array( 'span' => true )
475
							);
476
						?>
477
					</h2>
478
				</div>
479
480
			<?php endif; ?>
481
482
483
			<div id="jetpack-sso-wrap__action">
484
				<?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...
485
486
				<?php if ( $display_name && $gravatar ) : ?>
487
					<a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>">
488
						<?php esc_html_e( 'Log in as a different WordPress.com user', 'jetpack' ); ?>
489
					</a>
490
				<?php else : ?>
491
					<p>
492
						<?php
493
							echo esc_html(
494
								sprintf(
495
									__( 'You can now save time spent logging in by connecting your WordPress.com account to %s.', 'jetpack' ),
496
									esc_html( $site_name )
497
								)
498
							);
499
						?>
500
					</p>
501
				<?php endif; ?>
502
			</div>
503
504
			<?php if ( ! Jetpack_SSO_Helpers::should_hide_login_form() ) : ?>
505
				<div class="jetpack-sso-or">
506
					<span><?php esc_html_e( 'Or', 'jetpack' ); ?></span>
507
				</div>
508
509
				<a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '1' ) ); ?>" class="jetpack-sso-toggle wpcom">
510
					<?php
511
						esc_html_e( 'Log in with username and password', 'jetpack' )
512
					?>
513
				</a>
514
515
				<a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '0' ) ); ?>" class="jetpack-sso-toggle default">
516
					<?php
517
						esc_html_e( 'Log in with WordPress.com', 'jetpack' )
518
					?>
519
				</a>
520
			<?php endif; ?>
521
		</div>
522
		<?php
523
	}
524
525
	/**
526
	 * Clear the cookies that store the profile information for the last
527
	 * WPCOM user to connect.
528
	 */
529
	static function clear_wpcom_profile_cookies() {
530 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
531
			setcookie(
532
				'jetpack_sso_wpcom_name_' . COOKIEHASH,
533
				' ',
534
				time() - YEAR_IN_SECONDS,
535
				COOKIEPATH,
536
				COOKIE_DOMAIN,
537
				is_ssl()
538
			);
539
		}
540
541 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
542
			setcookie(
543
				'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
544
				' ',
545
				time() - YEAR_IN_SECONDS,
546
				COOKIEPATH,
547
				COOKIE_DOMAIN,
548
				is_ssl()
549
			);
550
		}
551
	}
552
553
	/**
554
	 * Clear cookies that are no longer needed once the user has logged in.
555
	 *
556
	 * @since 4.8.0
557
	 */
558
	static function clear_cookies_after_login() {
559
		self::clear_wpcom_profile_cookies();
560 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_nonce' ] ) ) {
561
			setcookie(
562
				'jetpack_sso_nonce',
563
				' ',
564
				time() - YEAR_IN_SECONDS,
565
				COOKIEPATH,
566
				COOKIE_DOMAIN,
567
				is_ssl()
568
			);
569
		}
570
571 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_original_request' ] ) ) {
572
			setcookie(
573
				'jetpack_sso_original_request',
574
				' ',
575
				time() - YEAR_IN_SECONDS,
576
				COOKIEPATH,
577
				COOKIE_DOMAIN,
578
				is_ssl()
579
			);
580
		}
581
582 View Code Duplication
		if ( isset( $_COOKIE[ 'jetpack_sso_redirect_to' ] ) ) {
583
			setcookie(
584
				'jetpack_sso_redirect_to',
585
				' ',
586
				time() - YEAR_IN_SECONDS,
587
				COOKIEPATH,
588
				COOKIE_DOMAIN,
589
				is_ssl()
590
			);
591
		}
592
	}
593
594
	static function delete_connection_for_user( $user_id ) {
595
		if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
596
			return;
597
		}
598
		Jetpack::load_xml_rpc_client();
599
		$xml = new Jetpack_IXR_Client( array(
600
			'wpcom_user_id' => $user_id,
601
		) );
602
		$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
603
604
		if ( $xml->isError() ) {
605
			return false;
606
		}
607
608
		// Clean up local data stored for SSO
609
		delete_user_meta( $user_id, 'wpcom_user_id' );
610
		delete_user_meta( $user_id, 'wpcom_user_data'  );
611
		self::clear_wpcom_profile_cookies();
612
613
		return $xml->getResponse();
614
	}
615
616
	static function request_initial_nonce() {
617
		$nonce = ! empty( $_COOKIE[ 'jetpack_sso_nonce' ] )
618
			? $_COOKIE[ 'jetpack_sso_nonce' ]
619
			: false;
620
621
		if ( ! $nonce ) {
622
			Jetpack::load_xml_rpc_client();
623
			$xml = new Jetpack_IXR_Client( array(
624
				'user_id' => get_current_user_id(),
625
			) );
626
			$xml->query( 'jetpack.sso.requestNonce' );
627
628
			if ( $xml->isError() ) {
629
				return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $xml->getErrorCode().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
630
			}
631
632
			$nonce = $xml->getResponse();
633
634
			setcookie(
635
				'jetpack_sso_nonce',
636
				$nonce,
637
				time() + ( 10 * MINUTE_IN_SECONDS ),
638
				COOKIEPATH,
639
				COOKIE_DOMAIN,
640
				is_ssl()
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
		$tracking = new Tracking();
682
683
		if ( Jetpack_SSO_Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) {
684
			$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...
685
686
			$tracking->record_user_event( 'sso_login_failed', array(
687
				'error_message' => 'error_msg_enable_two_step'
688
			) );
689
690
			/** This filter is documented in core/src/wp-includes/pluggable.php */
691
			do_action( 'wp_login_failed', $user_data->login );
692
			add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_enable_two_step' ) );
693
			return;
694
		}
695
696
		$user_found_with = '';
697
		if ( empty( $user ) && isset( $user_data->external_user_id ) ) {
698
			$user_found_with = 'external_user_id';
699
			$user = get_user_by( 'id', intval( $user_data->external_user_id ) );
700
			if ( $user ) {
701
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
702
			}
703
		}
704
705
		// If we don't have one by wpcom_user_id, try by the email?
706
		if ( empty( $user ) && Jetpack_SSO_Helpers::match_by_email() ) {
707
			$user_found_with = 'match_by_email';
708
			$user = get_user_by( 'email', $user_data->email );
709
			if ( $user ) {
710
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
711
			}
712
		}
713
714
		// If we've still got nothing, create the user.
715
		$new_user_override_role = false;
716
		if ( empty( $user ) && ( get_option( 'users_can_register' ) || ( $new_user_override_role = Jetpack_SSO_Helpers::new_user_override( $user_data ) ) ) ) {
717
			/**
718
			 * If not matching by email we still need to verify the email does not exist
719
			 * or this blows up
720
			 *
721
			 * If match_by_email is true, we know the email doesn't exist, as it would have
722
			 * been found in the first pass.  If get_user_by( 'email' ) doesn't find the
723
			 * user, then we know that email is unused, so it's safe to add.
724
			 */
725
			if ( Jetpack_SSO_Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
726
727
				if ( $new_user_override_role ) {
728
					$user_data->role = $new_user_override_role;
729
				}
730
731
				$user = Jetpack_SSO_Helpers::generate_user( $user_data );
732
				if ( ! $user ) {
733
					$tracking->record_user_event( 'sso_login_failed', array(
734
						'error_message' => 'could_not_create_username'
735
					) );
736
					add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_unable_to_create_user' ) );
737
					return;
738
				}
739
740
				$user_found_with = $new_user_override_role
741
					? 'user_created_new_user_override'
742
					: 'user_created_users_can_register';
743
			} else {
744
				$tracking->record_user_event( 'sso_login_failed', array(
745
					'error_message' => 'error_msg_email_already_exists'
746
				) );
747
748
				$this->user_data = $user_data;
749
				add_action( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_email_already_exists' ) );
750
				return;
751
			}
752
		}
753
754
		/**
755
		 * Fires after we got login information from WordPress.com.
756
		 *
757
		 * @module sso
758
		 *
759
		 * @since 2.6.0
760
		 *
761
		 * @param array  $user      Local User information.
762
		 * @param object $user_data WordPress.com User Login information.
763
		 */
764
		do_action( 'jetpack_sso_handle_login', $user, $user_data );
765
766
		if ( $user ) {
767
			// Cache the user's details, so we can present it back to them on their user screen
768
			update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
769
770
			add_filter( 'auth_cookie_expiration',    array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
771
			wp_set_auth_cookie( $user->ID, true );
772
			remove_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
773
774
			/** This filter is documented in core/src/wp-includes/user.php */
775
			do_action( 'wp_login', $user->user_login, $user );
776
777
			wp_set_current_user( $user->ID );
778
779
			$_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( $_REQUEST['redirect_to'] ) : '';
780
			$redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
781
782
			// If we have a saved redirect to request in a cookie
783
			if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
784
				// Set that as the requested redirect to
785
				$redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
786
			}
787
788
			$json_api_auth_environment = Jetpack_SSO_Helpers::get_json_api_auth_environment();
789
790
			$is_json_api_auth = ! empty( $json_api_auth_environment );
791
			$is_user_connected = Jetpack::is_user_connected( $user->ID );
792
			$tracking->record_user_event( 'sso_user_logged_in', array(
793
				'user_found_with'  => $user_found_with,
794
				'user_connected'   => (bool) $is_user_connected,
795
				'user_role'        => Jetpack::translate_current_user_to_role(),
796
				'is_json_api_auth' => (bool) $is_json_api_auth,
797
			) );
798
799
			if ( $is_json_api_auth ) {
800
				Jetpack::init()->verify_json_api_authorization_request( $json_api_auth_environment );
801
				Jetpack::init()->store_json_api_authorization_token( $user->user_login, $user );
802
803
			} else if ( ! $is_user_connected ) {
804
				wp_safe_redirect(
805
					add_query_arg(
806
						array(
807
							'redirect_to'               => $redirect_to,
808
							'request_redirect_to'       => $_request_redirect_to,
809
							'calypso_env'               => Jetpack::get_calypso_env(),
810
							'jetpack-sso-auth-redirect' => '1',
811
						),
812
						admin_url()
813
					)
814
				);
815
				exit;
816
			}
817
818
			add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
819
			wp_safe_redirect(
820
				/** This filter is documented in core/src/wp-login.php */
821
				apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
822
			);
823
			exit;
824
		}
825
826
		add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
827
828
		$tracking->record_user_event( 'sso_login_failed', array(
829
			'error_message' => 'cant_find_user'
830
		) );
831
832
		$this->user_data = $user_data;
833
		/** This filter is documented in core/src/wp-includes/pluggable.php */
834
		do_action( 'wp_login_failed', $user_data->login );
835
		add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'cant_find_user' ) );
836
	}
837
838
	static function profile_page_url() {
839
		return admin_url( 'profile.php' );
840
	}
841
842
	/**
843
	 * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page.
844
	 *
845
	 * @param  array   $args       An array of arguments to add to the SSO URL.
846
	 * @param  boolean $is_primary Should the button have the `button-primary` class?
847
	 * @return string              Returns the HTML markup for the button.
848
	 */
849
	function build_sso_button( $args = array(), $is_primary = false ) {
850
		$url = $this->build_sso_button_url( $args );
851
		$classes = $is_primary
852
			? 'jetpack-sso button button-primary'
853
			: 'jetpack-sso button';
854
855
		return sprintf(
856
			'<a rel="nofollow" href="%1$s" class="%2$s"><span>%3$s %4$s</span></a>',
857
			esc_url( $url ),
858
			$classes,
859
			'<span class="genericon genericon-wordpress"></span>',
860
			esc_html__( 'Log in with WordPress.com', 'jetpack' )
861
		);
862
	}
863
864
	/**
865
	 * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO.
866
	 *
867
	 * @param  array  $args An array of arguments to add to the SSO URL.
868
	 * @return string       The URL used for SSO.
869
	 */
870
	function build_sso_button_url( $args = array() ) {
871
		$defaults = array(
872
			'action'  => 'jetpack-sso',
873
		);
874
875
		$args = wp_parse_args( $args, $defaults );
876
877
		if ( ! empty( $_GET['redirect_to'] ) ) {
878
			$args['redirect_to'] = urlencode( esc_url_raw( $_GET['redirect_to'] ) );
879
		}
880
881
		return add_query_arg( $args, wp_login_url() );
882
	}
883
884
	/**
885
	 * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies.
886
	 *
887
	 * @param  boolean  $reauth  Should the user be forced to reauthenticate on WordPress.com?
888
	 * @param  array    $args    Optional query parameters.
889
	 * @return string            The WordPress.com SSO URL.
890
	 */
891
	function get_sso_url_or_die( $reauth = false, $args = array() ) {
892
		if ( empty( $reauth ) ) {
893
			$sso_redirect = $this->build_sso_url( $args );
894
		} else {
895
			self::clear_wpcom_profile_cookies();
896
			$sso_redirect = $this->build_reauth_and_sso_url( $args );
897
		}
898
899
		// If there was an error retrieving the SSO URL, then error.
900
		if ( is_wp_error( $sso_redirect ) ) {
901
			$error_message = sanitize_text_field(
902
				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...
903
			);
904
			$tracking = new Tracking();
905
			$tracking->record_user_event( 'sso_login_redirect_failed', array(
906
				'error_message' => $error_message
907
			) );
908
			wp_die( $error_message );
909
		}
910
911
		return $sso_redirect;
912
	}
913
914
	/**
915
	 * Build WordPress.com SSO URL with appropriate query parameters.
916
	 *
917
	 * @param  array  $args Optional query parameters.
918
	 * @return string       WordPress.com SSO URL
919
	 */
920
	function build_sso_url( $args = array() ) {
921
		$sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
922
		$defaults = array(
923
			'action'       => 'jetpack-sso',
924
			'site_id'      => Jetpack_Options::get_option( 'id' ),
925
			'sso_nonce'    => $sso_nonce,
926
			'calypso_auth' => '1',
927
		);
928
929
		$args = wp_parse_args( $args, $defaults );
930
931
		if ( is_wp_error( $args['sso_nonce'] ) ) {
932
			return $args['sso_nonce'];
933
		}
934
935
		return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
936
	}
937
938
	/**
939
	 * Build WordPress.com SSO URL with appropriate query parameters,
940
	 * including the parameters necessary to force the user to reauthenticate
941
	 * on WordPress.com.
942
	 *
943
	 * @param  array  $args Optional query parameters.
944
	 * @return string       WordPress.com SSO URL
945
	 */
946
	function build_reauth_and_sso_url( $args = array() ) {
947
		$sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
948
		$redirect = $this->build_sso_url( array( 'force_auth' => '1', 'sso_nonce' => $sso_nonce ) );
949
950
		if ( is_wp_error( $redirect ) ) {
951
			return $redirect;
952
		}
953
954
		$defaults = array(
955
			'action'       => 'jetpack-sso',
956
			'site_id'      => Jetpack_Options::get_option( 'id' ),
957
			'sso_nonce'    => $sso_nonce,
958
			'reauth'       => '1',
959
			'redirect_to'  => urlencode( $redirect ),
960
			'calypso_auth' => '1',
961
		);
962
963
		$args = wp_parse_args( $args, $defaults );
964
965
		if ( is_wp_error( $args['sso_nonce'] ) ) {
966
			return $args['sso_nonce'];
967
		}
968
969
		return add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
970
	}
971
972
	/**
973
	 * Determines local user associated with a given WordPress.com user ID.
974
	 *
975
	 * @since 2.6.0
976
	 *
977
	 * @param int $wpcom_user_id User ID from WordPress.com
978
	 * @return object Local user object if found, null if not.
979
	 */
980
	static function get_user_by_wpcom_id( $wpcom_user_id ) {
981
		$user_query = new WP_User_Query( array(
982
			'meta_key'   => 'wpcom_user_id',
983
			'meta_value' => intval( $wpcom_user_id ),
984
			'number'     => 1,
985
		) );
986
987
		$users = $user_query->get_results();
988
		return $users ? array_shift( $users ) : null;
989
	}
990
991
	/**
992
	 * When jetpack-sso-auth-redirect query parameter is set, will redirect user to
993
	 * WordPress.com authorization flow.
994
	 *
995
	 * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url
996
	 * calls menu_page_url() which doesn't work properly until admin menus are registered.
997
	 */
998
	function maybe_authorize_user_after_sso() {
999
		if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) {
1000
			return;
1001
		}
1002
1003
		$redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( $_GET['redirect_to'] ) : admin_url();
1004
		$request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( $_GET['request_redirect_to'] ) : $redirect_to;
1005
1006
		/** This filter is documented in core/src/wp-login.php */
1007
		$redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() );
1008
1009
		/**
1010
		 * Since we are passing this redirect to WordPress.com and therefore can not use wp_safe_redirect(),
1011
		 * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url().
1012
		 */
1013
		$redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth );
1014
		$redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() );
1015
1016
		/**
1017
		 * Return the raw connect URL with our redirect and attribute connection to SSO.
1018
		 */
1019
		$connect_url = Jetpack::init()->build_connect_url( true, $redirect_after_auth, 'sso' );
1020
1021
		add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
1022
		wp_safe_redirect( $connect_url );
1023
		exit;
1024
	}
1025
1026
	/**
1027
	 * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are
1028
	 * stored when the user logs out, and then deleted when the user logs in.
1029
	 */
1030
	function store_wpcom_profile_cookies_on_logout() {
1031
		if ( ! Jetpack::is_user_connected( get_current_user_id() ) ) {
1032
			return;
1033
		}
1034
1035
		$user_data = $this->get_user_data( get_current_user_id() );
1036
		if ( ! $user_data ) {
1037
			return;
1038
		}
1039
1040
		setcookie(
1041
			'jetpack_sso_wpcom_name_' . COOKIEHASH,
1042
			$user_data->display_name,
1043
			time() + WEEK_IN_SECONDS,
1044
			COOKIEPATH,
1045
			COOKIE_DOMAIN,
1046
			is_ssl()
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
			is_ssl()
1059
		);
1060
	}
1061
1062
	/**
1063
	 * Determines if a local user is connected to WordPress.com
1064
	 *
1065
	 * @since 2.8
1066
	 * @param integer $user_id - Local user id
1067
	 * @return boolean
1068
	 **/
1069
	public function is_user_connected( $user_id ) {
1070
		return $this->get_user_data( $user_id );
1071
	}
1072
1073
	/**
1074
	 * Retrieves a user's WordPress.com data
1075
	 *
1076
	 * @since 2.8
1077
	 * @param integer $user_id - Local user id
1078
	 * @return mixed null or stdClass
1079
	 **/
1080
	public function get_user_data( $user_id ) {
1081
		return get_user_meta( $user_id, 'wpcom_user_data', true );
1082
	}
1083
1084
	/**
1085
	 * Mark SSO as discovered when an SSO JITM is viewed.
1086
	 *
1087
	 * @since 6.9.0
1088
	 *
1089
	 * @param array $envelopes Array of JITM messages received after API call.
1090
	 *
1091
	 * @return array $envelopes New array of JITM messages. May now contain only one message, about SSO.
1092
	 */
1093
	public function inject_sso_jitm( $envelopes ) {
1094
		// Bail early if that's not the first time the user uses SSO.
1095
		if ( true != Jetpack_Options::get_option( 'sso_first_login' ) ) {
1096
			return $envelopes;
1097
		}
1098
1099
		// Update our option to mark that SSO was discovered.
1100
		Jetpack_Options::update_option( 'sso_first_login', false );
1101
1102
		return $this->prepare_sso_first_login_jitm();
1103
	}
1104
1105
	/**
1106
	 * Prepare JITM array for new SSO users
1107
	 *
1108
	 * @since 6.9.0
1109
	 *
1110
	 * @return array $sso_first_login_jitm array containting one object of information about our message.
1111
	 */
1112
	private function prepare_sso_first_login_jitm() {
1113
		// Build our custom SSO JITM.
1114
		$discover_sso_message = array(
1115
			'content'         => array(
1116
				'message'     => esc_html__( "You've successfully signed in with WordPress.com Secure Sign On!", 'jetpack' ),
1117
				'icon'        => 'jetpack',
1118
				'list'        => array(),
1119
				'description' => esc_html__( 'Interested in learning more about how Secure Sign On keeps your site safer?', 'jetpack' ),
1120
				'classes'     => '',
1121
			),
1122
			'CTA'             => array(
1123
				'message'   => esc_html__( 'Learn More', 'jetpack' ),
1124
				'hook'      => '',
1125
				'newWindow' => true,
1126
				'primary'   => true,
1127
			),
1128
			'template'        => 'default',
1129
			'ttl'             => 300,
1130
			'id'              => 'sso_discover',
1131
			'feature_class'   => 'sso',
1132
			'expires'         => 3628800,
1133
			'max_dismissal'   => 1,
1134
			'activate_module' => null,
1135
		);
1136
1137
		return array( json_decode( json_encode( $discover_sso_message ) ) );
1138
	}
1139
}
1140
1141
Jetpack_SSO::get_instance();
1142