Completed
Push — feature/videopress-uploader ( a974f7...c50b88 )
by
unknown
57:36 queued 48:36
created

Jetpack_SSO::module_configuration_load()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
nc 1
cc 1
eloc 1
nop 0
1
<?php
2
3
/**
4
 * Module Name: Single Sign On
5
 * Module Description: Secure user authentication.
6
 * Jumpstart Description: Lets you log in to all your Jetpack-enabled sites with one click using your WordPress.com account.
7
 * Sort Order: 30
8
 * Recommendation Order: 5
9
 * First Introduced: 2.6
10
 * Requires Connection: Yes
11
 * Auto Activate: No
12
 * Module Tags: Developers
13
 * Feature: Jumpstart, Performance-Security
14
 * Additional Search Queries: sso, single sign on, login, log in
15
 */
16
17
class Jetpack_SSO {
18
	static $instance = null;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $instance.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
19
20
	private function __construct() {
21
22
		self::$instance = $this;
23
24
		add_action( 'admin_init',  array( $this, 'admin_init' ) );
25
		add_action( 'admin_init',  array( $this, 'register_settings' ) );
26
		add_action( 'login_init',  array( $this, 'login_init' ) );
27
		add_action( 'delete_user', array( $this, 'delete_connection_for_user' ) );
28
		add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
29
		add_action( 'init', array( $this, 'maybe_logout_user' ), 5 );
30
		add_action( 'jetpack_modules_loaded', array( $this, 'module_configure_button' ) );
31
		add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) );
32
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
33
34
		// Adding this action so that on login_init, the action won't be sanitized out of the $action global.
35
		add_action( 'login_form_jetpack-sso', '__return_true' );
36
37
		if (
38
			$this->should_hide_login_form() &&
39
			/**
40
			 * Filter the display of the disclaimer message appearing when default WordPress login form is disabled.
41
			 *
42
			 * @module sso
43
			 *
44
			 * @since 2.8.0
45
			 *
46
			 * @param bool true Should the disclaimer be displayed. Default to true.
47
			 */
48
			apply_filters( 'jetpack_sso_display_disclaimer', true )
49
		) {
50
			add_action( 'login_message', array( $this, 'msg_login_by_jetpack' ) );
51
		}
52
	}
53
54
	/**
55
	 * Returns the single instance of the Jetpack_SSO object
56
	 *
57
	 * @since 2.8
58
	 * @return Jetpack_SSO
59
	 **/
60
	public static function get_instance() {
61
		if ( ! is_null( self::$instance ) ) {
62
			return self::$instance;
63
		}
64
65
		return self::$instance = new Jetpack_SSO;
66
	}
67
68
	/**
69
	 * Add configure button and functionality to the module card on the Jetpack screen
70
	 **/
71
	public static function module_configure_button() {
72
		Jetpack::enable_module_configurable( __FILE__ );
73
		Jetpack::module_configuration_load( __FILE__, array( __CLASS__, 'module_configuration_load' ) );
74
		Jetpack::module_configuration_head( __FILE__, array( __CLASS__, 'module_configuration_head' ) );
75
		Jetpack::module_configuration_screen( __FILE__, array( __CLASS__, 'module_configuration_screen' ) );
76
	}
77
78
	public static function module_configuration_load() {
79
		// wp_safe_redirect( admin_url( 'options-general.php#configure-sso' ) );
80
		// exit;
81
	}
82
83
	public static function module_configuration_head() {}
84
85
	public static function module_configuration_screen() {
86
		?>
87
		<form method="post" action="options.php">
88
			<?php settings_fields( 'jetpack-sso' ); ?>
89
			<?php do_settings_sections( 'jetpack-sso' ); ?>
90
			<?php submit_button(); ?>
91
		</form>
92
		<?php
93
	}
94
95
	/**
96
	 * If jetpack_force_logout == 1 in current user meta the user will be forced
97
	 * to logout and reauthenticate with the site.
98
	 **/
99
	public function maybe_logout_user() {
100
		global $current_user;
101
102
		if ( 1 == $current_user->jetpack_force_logout ) {
103
			delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
104
			self::delete_connection_for_user( $current_user->ID );
105
			wp_logout();
106
			wp_safe_redirect( wp_login_url() );
107
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method maybe_logout_user() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
108
		}
109
	}
110
111
112
	/**
113
	 * Adds additional methods the WordPress xmlrpc API for handling SSO specific features
114
	 *
115
	 * @param array $methods
116
	 * @return array
117
	 **/
118
	public function xmlrpc_methods( $methods ) {
119
		$methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' );
120
		return $methods;
121
	}
122
123
	/**
124
	 * Marks a user's profile for disconnect from WordPress.com and forces a logout
125
	 * the next time the user visits the site.
126
	 **/
127
	public function xmlrpc_user_disconnect( $user_id ) {
128
		$user_query = new WP_User_Query(
129
			array(
130
				'meta_key' => 'wpcom_user_id',
131
				'meta_value' => $user_id,
132
			)
133
		);
134
		$user = $user_query->get_results();
135
		$user = $user[0];
136
137
		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...
138
			$user = wp_set_current_user( $user->ID );
139
			update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
140
			self::delete_connection_for_user( $user->ID );
141
			return true;
142
		}
143
		return false;
144
	}
145
146
	/**
147
	 * Enqueues scripts and styles necessary for SSO login.
148
	 */
149
	public function login_enqueue_scripts() {
150
		global $action;
151
152
		if ( ! in_array( $action, array( 'jetpack-sso', 'login' ) ) ) {
153
			return;
154
		}
155
156
		wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
157
		wp_enqueue_script( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION );
158
	}
159
160
	/**
161
	 * Enqueue styles neceessary for Jetpack SSO on users' profiles
162
	 */
163
	public function admin_enqueue_scripts() {
164
		$screen = get_current_screen();
165
166
		if ( empty( $screen ) || ! in_array( $screen->base, array( 'edit-user', 'profile' ) ) ) {
167
			return;
168
		}
169
170
		wp_enqueue_style( 'jetpack-sso-profile', plugins_url( 'modules/sso/jetpack-sso-profile.css', JETPACK__PLUGIN_FILE ), array( 'genericons' ), JETPACK__VERSION );
171
	}
172
173
	/**
174
	 * Adds settings fields to Settings > General > Single Sign On that allows users to
175
	 * turn off the login form on wp-login.php
176
	 *
177
	 * @since 2.7
178
	 **/
179
	public function register_settings() {
180
181
		add_settings_section(
182
			'jetpack_sso_settings',
183
			__( 'Single Sign On' , 'jetpack' ),
184
			'__return_false',
185
			'jetpack-sso'
186
		);
187
188
		/*
189
		 * Settings > General > Single Sign On
190
		 * Checkbox for Remove default login form
191
		 */
192
		 /* Hide in 2.9
193
		register_setting(
194
			'general',
195
			'jetpack_sso_remove_login_form',
196
			array( $this, 'validate_settings_remove_login_form_checkbox' )
197
		);
198
199
		add_settings_field(
200
			'jetpack_sso_remove_login_form',
201
			__( 'Remove default login form?' , 'jetpack' ),
202
			array( $this, 'render_remove_login_form_checkbox' ),
203
			'general',
204
			'jetpack_sso_settings'
205
		);
206
		*/
207
208
		/*
209
		 * Settings > General > Single Sign On
210
		 * Require two step authentication
211
		 */
212
		register_setting(
213
			'jetpack-sso',
214
			'jetpack_sso_require_two_step',
215
			array( $this, 'validate_jetpack_sso_require_two_step' )
216
		);
217
218
		add_settings_field(
219
			'jetpack_sso_require_two_step',
220
			'', // __( 'Require Two-Step Authentication' , 'jetpack' ),
221
			array( $this, 'render_require_two_step' ),
222
			'jetpack-sso',
223
			'jetpack_sso_settings'
224
		);
225
226
		/*
227
		 * Settings > General > Single Sign On
228
		 */
229
		register_setting(
230
			'jetpack-sso',
231
			'jetpack_sso_match_by_email',
232
			array( $this, 'validate_jetpack_sso_match_by_email' )
233
		);
234
235
		add_settings_field(
236
			'jetpack_sso_match_by_email',
237
			'', // __( 'Match by Email' , 'jetpack' ),
238
			array( $this, 'render_match_by_email' ),
239
			'jetpack-sso',
240
			'jetpack_sso_settings'
241
		);
242
	}
243
244
	/**
245
	 * Builds the display for the checkbox allowing user to require two step
246
	 * auth be enabled on WordPress.com accounts before login. Displays in Settings > General
247
	 *
248
	 * @since 2.7
249
	 **/
250
	public function render_require_two_step() {
251
		/** This filter is documented in modules/sso.php */
252
		$require_two_step = ( 1 == apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step' ) ) );
253
		$disabled = $require_two_step ? ' disabled="disabled"' : '';
254
		echo '<label>';
255
		echo '<input type="checkbox" name="jetpack_sso_require_two_step" ' . checked( $require_two_step, true, false ) . "$disabled>";
256
		esc_html_e( 'Require Two-Step Authentication' , 'jetpack' );
257
		echo '</label>';
258
	}
259
260
	/**
261
	 * Validate the require  two step checkbox in Settings > General
262
	 *
263
	 * @since 2.7
264
	 * @return boolean
265
	 **/
266
	public function validate_jetpack_sso_require_two_step( $input ) {
267
		return ( ! empty( $input ) ) ? 1 : 0;
268
	}
269
270
	/**
271
	 * Builds the display for the checkbox allowing the user to allow matching logins by email
272
	 * Displays in Settings > General
273
	 *
274
	 * @since 2.9
275
	 **/
276
	public function render_match_by_email() {
277
		$match_by_email = 1 == $this->match_by_email();
278
		$disabled = $match_by_email ? ' disabled="disabled"' : '';
279
		echo '<label>';
280
		echo '<input type="checkbox" name="jetpack_sso_match_by_email"' . checked( $match_by_email, true, false ) . "$disabled>";
281
		esc_html_e( 'Match by Email', 'jetpack' );
282
		echo '</label>';
283
	}
284
285
	/**
286
	 * Validate the match by email check in Settings > General
287
	 *
288
	 * @since 2.9
289
	 * @return boolean
290
	 **/
291
	public function validate_jetpack_sso_match_by_email( $input ) {
292
		return ( ! empty( $input ) ) ? 1 : 0;
293
	}
294
295
	/**
296
	 * Builds the display for the checkbox allowing users to remove the default
297
	 * WordPress login form from wp-login.php. Displays in Settings > General
298
	 *
299
	 * @since 2.7
300
	 **/
301
	public function render_remove_login_form_checkbox() {
302
		if ( $this->is_user_connected( get_current_user_id() ) ) {
303
			echo '<a name="configure-sso"></a>';
304
			echo '<input type="checkbox" name="jetpack_sso_remove_login_form[remove_login_form]" ' . checked( 1 == get_option( 'jetpack_sso_remove_login_form' ), true, false ) . '>';
305
			echo '<p class="description">Removes default login form and disallows login via POST</p>';
306
		} else {
307
			echo 'Your account must be connected to WordPress.com before disabling the login form.';
308
			echo '<br/>' . $this->button();
309
		}
310
	}
311
312
	/**
313
	 * Validate settings input from Settings > General
314
	 *
315
	 * @since 2.7
316
	 * @return boolean
317
	 **/
318
	public function validate_settings_remove_login_form_checkbox( $input ) {
319
		return ( isset( $input['remove_login_form'] ) )? 1: 0;
320
	}
321
322
	/**
323
	 * Removes 'Lost your password?' text from the login form if user
324
	 * does not want to show the login form
325
	 *
326
	 * @since 2.7
327
	 * @return string
328
	 **/
329
	public function remove_lost_password_text( $text ) {
330
		if ( 'Lost your password?' == $text ) {
331
			$text = '';
332
		}
333
		return $text;
334
	}
335
336
	/**
337
	 * Checks to determine if the user wants to login on wp-login
338
	 *
339
	 * This function mostly exists to cover the exceptions to login
340
	 * that may exist as other parameters to $_GET[action] as $_GET[action]
341
	 * does not have to exist. By default WordPress assumes login if an action
342
	 * is not set, however this may not be true, as in the case of logout
343
	 * where $_GET[loggedout] is instead set
344
	 *
345
	 * @return boolean
346
	 **/
347
	private function wants_to_login() {
348
		$wants_to_login = false;
349
350
		// Cover default WordPress behavior
351
		$action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login';
352
353
		// And now the exceptions
354
		$action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;
355
356
		if ( 'login' == $action ) {
357
			$wants_to_login = true;
358
		}
359
360
		return $wants_to_login;
361
	}
362
363
	private function bypass_login_forward_wpcom() {
364
		/**
365
		 * Redirect the site's log in form to WordPress.com's log in form.
366
		 *
367
		 * @module sso
368
		 *
369
		 * @since 3.1.0
370
		 *
371
		 * @param bool false Should the site's log in form be automatically forwarded to WordPress.com's log in form.
372
		 */
373
		return apply_filters( 'jetpack_sso_bypass_login_forward_wpcom', false );
374
	}
375
376
	function login_init() {
377
		global $action;
378
379
		/**
380
		 * If the user is attempting to logout AND the auto-forward to WordPress.com
381
		 * login is set then we need to ensure we do not auto-forward the user and get
382
		 * them stuck in an infinite logout loop.
383
		 */
384
		if ( isset( $_GET['loggedout'] ) && $this->bypass_login_forward_wpcom() ) {
385
			add_filter( 'jetpack_remove_login_form', '__return_true' );
386
			add_filter( 'gettext', array( $this, 'remove_lost_password_text' ) );
387
		}
388
389
		/**
390
		 * Check to see if the site admin wants to automagically forward the user
391
		 * to the WordPress.com login page AND  that the request to wp-login.php
392
		 * is not something other than login (Like logout!)
393
		 */
394
		if (
395
			$this->wants_to_login()
396
			&& $this->bypass_login_forward_wpcom()
397
		) {
398
			add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) );
399
			$this->maybe_save_cookie_redirect();
400
			wp_safe_redirect( $this->build_sso_url() );
401
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method login_init() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
402
		}
403
404
		if ( 'login' === $action ) {
405
			add_action( 'login_footer', array( $this, 'login_form' ) );
406
407
			/*
408
			if ( get_option( 'jetpack_sso_remove_login_form' ) ) {
409
				// Check to see if the user is attempting to login via the default login form.
410
				// If so we need to deny it and forward elsewhere.
411
				if( isset( $_REQUEST['wp-submit'] ) && 'Log In' == $_REQUEST['wp-submit'] ) {
412
					wp_die( 'Login not permitted by this method. ');
413
				}
414
				add_filter( 'gettext', array( $this, 'remove_lost_password_text' ) );
415
			}
416
			*/
417
		} elseif ( 'jetpack-sso' === $action ) {
418
			if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
419
				$this->handle_login();
420
				add_action( 'login_footer', array( $this, 'login_form' ) );
421
			} else {
422
				if ( Jetpack::check_identity_crisis() ) {
423
					wp_die( __( "Error: This site's Jetpack connection is currently experiencing problems.", 'jetpack' ) );
424
				} else {
425
					$this->maybe_save_cookie_redirect();
426
					// Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
427
					add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) );
428
					wp_safe_redirect( $this->build_sso_url() );
429
					exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method login_init() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
430
				}
431
			}
432
		}
433
	}
434
435
	/**
436
	 * Conditionally save the redirect_to url as a cookie.
437
	 */
438
	public static function maybe_save_cookie_redirect() {
439
		if ( headers_sent() ) {
440
			return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
441
		}
442
443
		if ( ! empty( $_GET['redirect_to'] ) ) {
444
			// If we have something to redirect to
445
			$url = esc_url_raw( $_GET['redirect_to'] );
446
			setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
447
448
		} elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
449
			// Otherwise, if it's already set, purge it.
450
			setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
451
		}
452
453
		if ( ! empty( $_GET['rememberme'] ) ) {
454
			setcookie( 'jetpack_sso_remember_me', '1', time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
455
		} elseif ( ! empty( $_COOKIE['jetpack_sso_remember_me'] ) ) {
456
			setcookie( 'jetpack_sso_remember_me', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
457
		}
458
	}
459
460
	/**
461
	 * Determine if the login form should be hidden or not
462
	 *
463
	 * Method is private only because it is only used in this class so far.
464
	 * Feel free to change it later
465
	 *
466
	 * @return bool
467
	 **/
468
	private function should_hide_login_form() {
469
		/**
470
		 * Remove the default log in form, only leave the WordPress.com log in button.
471
		 *
472
		 * @module sso
473
		 *
474
		 * @since 3.1.0
475
		 *
476
		 * @param bool get_option( 'jetpack_sso_remove_login_form', false ) Should the default log in form be removed. Default to false.
477
		 */
478
		return apply_filters( 'jetpack_remove_login_form', get_option( 'jetpack_sso_remove_login_form', false ) );
479
	}
480
481
	function login_form() {
482
		$classes = '';
483
484
		if ( $this->should_hide_login_form() ) {
485
			$classes .= ' forced-sso';
486
		}
487
		echo '<div id="jetpack-sso-wrap" class="jetpack-sso-wrap' . $classes . '">' . $this->button() . '</div>';
488
	}
489
490 View Code Duplication
	static function delete_connection_for_user( $user_id ) {
491
		if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
492
			return;
493
		}
494
		Jetpack::load_xml_rpc_client();
495
		$xml = new Jetpack_IXR_Client( array(
496
			'user_id' => $user_id,
497
		) );
498
		$xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
499
500
		if ( $xml->isError() ) {
501
			return false;
502
		}
503
504
		return $xml->getResponse();
505
	}
506
507 View Code Duplication
	static function request_initial_nonce() {
508
		Jetpack::load_xml_rpc_client();
509
		$xml = new Jetpack_IXR_Client( array(
510
			'user_id' => get_current_user_id(),
511
		) );
512
		$xml->query( 'jetpack.sso.requestNonce' );
513
514
		if ( $xml->isError() ) {
515
			wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
516
		}
517
518
		return $xml->getResponse();
519
	}
520
521
	/**
522
	 * The function that actually handles the login!
523
	 */
524
	function handle_login() {
525
		$wpcom_nonce   = sanitize_key( $_GET['sso_nonce'] );
526
		$wpcom_user_id = (int) $_GET['user_id'];
527
		$result        = sanitize_key( $_GET['result'] );
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
528
529
		Jetpack::load_xml_rpc_client();
530
		$xml = new Jetpack_IXR_Client( array(
531
			'user_id' => get_current_user_id(),
532
		) );
533
		$xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id );
534
535
		if ( $xml->isError() ) {
536
			wp_die( sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
537
		}
538
539
		$user_data = $xml->getResponse();
540
541
		if ( empty( $user_data ) ) {
542
			wp_die( __( 'Error, invalid response data.', 'jetpack' ) );
543
		}
544
545
		$user_data = (object) $user_data;
546
		$user = null;
547
548
		/**
549
		 * Fires before Jetpack's SSO modifies the log in form.
550
		 *
551
		 * @module sso
552
		 *
553
		 * @since 2.6.0
554
		 *
555
		 * @param object $user_data User login information.
556
		 */
557
		do_action( 'jetpack_sso_pre_handle_login', $user_data );
558
559
		/**
560
		 * Is it required to have 2-step authentication enabled on WordPress.com to use SSO?
561
		 *
562
		 * @module sso
563
		 *
564
		 * @since 2.8.0
565
		 *
566
		 * @param bool get_option( 'jetpack_sso_require_two_step' ) Does SSO require 2-step authentication?
567
		 */
568
		$require_two_step = apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step' ) );
569
		if ( $require_two_step && 0 == (int) $user_data->two_step_enabled ) {
570
			$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...
571
			/** This filter is documented in core/src/wp-includes/pluggable.php */
572
			do_action( 'wp_login_failed', $user_data->login );
573
			add_action( 'login_message', array( $this, 'error_msg_enable_two_step' ) );
574
			return;
575
		}
576
577
		if ( isset( $_GET['state'] ) && ( 0 < strpos( $_GET['state'], '|' ) ) ) {
578
			list( $state, $nonce ) = explode( '|', $_GET['state'] );
579
580
			if ( wp_verify_nonce( $nonce, $state ) ) {
581
				if ( 'sso-link-user' == $state ) {
582
					$user = wp_get_current_user();
583
					update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
584
					add_filter( 'login_redirect', array( __CLASS__, 'profile_page_url' ) );
585
				}
586
			} else {
587
				wp_nonce_ays();
588
			}
589
		}
590
591
		if ( empty( $user ) ) {
592
			$user = $this->get_user_by_wpcom_id( $user_data->ID );
593
		}
594
595
		// If we don't have one by wpcom_user_id, try by the email?
596
		if ( empty( $user ) && self::match_by_email() ) {
597
			$user = get_user_by( 'email', $user_data->email );
598
			if ( $user ) {
599
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
600
			}
601
		}
602
603
		// If we've still got nothing, create the user.
604
		if ( empty( $user ) && ( get_option( 'users_can_register' ) || self::new_user_override() ) ) {
605
			// If not matching by email we still need to verify the email does not exist
606
			// or this blows up
607
			/**
608
			 * If match_by_email is true, we know the email doesn't exist, as it would have
609
			 * been found in the first pass.  If get_user_by( 'email' ) doesn't find the
610
			 * user, then we know that email is unused, so it's safe to add.
611
			 */
612
			if ( self::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
613
				$username = $user_data->login;
614
615
				if ( username_exists( $username ) ) {
616
					$username = $user_data->login . '_' . $user_data->ID;
617
				}
618
619
				$tries = 0;
620
				while ( username_exists( $username ) ) {
621
					$username = $user_data->login . '_' . $user_data->ID . '_' . mt_rand();
622
					if ( $tries++ >= 5 ) {
623
						wp_die( __( "Error: Couldn't create suitable username.", 'jetpack' ) );
624
					}
625
				}
626
627
				$password = wp_generate_password( 20 );
628
				$user_id  = wp_create_user( $username, $password, $user_data->email );
629
				$user     = get_userdata( $user_id );
630
631
				$user->display_name = $user_data->display_name;
632
				$user->first_name   = $user_data->first_name;
633
				$user->last_name    = $user_data->last_name;
634
				$user->url          = $user_data->url;
635
				$user->description  = $user_data->description;
636
				wp_update_user( $user );
637
638
				update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
639
			} else {
640
				$this->user_data = $user_data;
641
				// do_action( 'wp_login_failed', $user_data->login );
642
				add_action( 'login_message', array( $this, 'error_msg_email_already_exists' ) );
643
				return;
644
			}
645
		}
646
647
		/**
648
		 * Fires after we got login information from WordPress.com.
649
		 *
650
		 * @module sso
651
		 *
652
		 * @since 2.6.0
653
		 *
654
		 * @param array $user WordPress.com User information.
655
		 * @param object $user_data User Login information.
656
		 */
657
		do_action( 'jetpack_sso_handle_login', $user, $user_data );
658
659
		if ( $user ) {
660
			// Cache the user's details, so we can present it back to them on their user screen.
661
			update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
662
663
			$remember = false;
664 View Code Duplication
			if ( ! empty( $_COOKIE['jetpack_sso_remember_me'] ) ) {
665
				$remember = true;
666
				// And then purge it
667
				setcookie( 'jetpack_sso_remember_me', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
668
			}
669
			/**
670
			 * Filter the remember me value.
671
			 *
672
			 * @module sso
673
			 *
674
			 * @since 2.8.0
675
			 *
676
			 * @param bool $remember Is the remember me option checked?
677
			 */
678
			$remember = apply_filters( 'jetpack_remember_login', $remember );
679
			wp_set_auth_cookie( $user->ID, $remember );
680
681
			/** This filter is documented in core/src/wp-includes/user.php */
682
			do_action( 'wp_login', $user->user_login, $user );
683
684
			$_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
685
			$redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
686
687
			// If we have a saved redirect to request in a cookie
688 View Code Duplication
			if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
689
				// Set that as the requested redirect to
690
				$redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
691
				// And then purge it
692
				setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
693
			}
694
695
			wp_safe_redirect(
696
				/** This filter is documented in core/src/wp-login.php */
697
				apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
698
			);
699
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method handle_login() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
700
		}
701
702
		$this->user_data = $user_data;
703
		/** This filter is documented in core/src/wp-includes/pluggable.php */
704
		do_action( 'wp_login_failed', $user_data->login );
705
		add_action( 'login_message', array( $this, 'cant_find_user' ) );
706
	}
707
708
	static function profile_page_url() {
709
		return admin_url( 'profile.php' );
710
	}
711
712
	static function match_by_email() {
713
		$match_by_email = ( 1 == get_option( 'jetpack_sso_match_by_email', true ) ) ? true: false;
714
		$match_by_email = defined( 'WPCC_MATCH_BY_EMAIL' ) ? WPCC_MATCH_BY_EMAIL : $match_by_email;
715
716
		/**
717
		 * Link the local account to an account on WordPress.com using the same email address.
718
		 *
719
		 * @module sso
720
		 *
721
		 * @since 2.6.0
722
		 *
723
		 * @param bool $match_by_email Should we link the local account to an account on WordPress.com using the same email address. Default to false.
724
		 */
725
		return apply_filters( 'jetpack_sso_match_by_email', $match_by_email );
726
	}
727
728
	static function new_user_override() {
729
		$new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? WPCC_NEW_USER_OVERRIDE : false;
730
731
		/**
732
		 * Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations.
733
		 *
734
		 * @module sso
735
		 *
736
		 * @since 2.6.0
737
		 *
738
		 * @param bool $new_user_override Allow users to register on your site with a WordPress.com account. Default to false.
739
		 */
740
		return apply_filters( 'jetpack_sso_new_user_override', $new_user_override );
741
	}
742
743
	function allowed_redirect_hosts( $hosts ) {
744
		if ( empty( $hosts ) ) {
745
			$hosts = array();
746
		}
747
		$hosts[] = 'wordpress.com';
748
749
		return array_unique( $hosts );
750
	}
751
752
	function button( $args = array() ) {
753
		$defaults = array(
754
			'action'  => 'jetpack-sso',
755
		);
756
757
		$args = wp_parse_args( $args, $defaults );
758
759
		if ( ! empty( $_GET['redirect_to'] ) ) {
760
			$args['redirect_to'] = esc_url_raw( $_GET['redirect_to'] );
761
		}
762
763
		$url  = add_query_arg( $args, wp_login_url() );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
764
765
		$button = sprintf( '<a href="%1$s" class="jetpack-sso button">%2$s</a>', esc_url( $url ), esc_html__( 'Log in with WordPress.com', 'jetpack' ) );
766
		return $button;
767
	}
768
769
	function build_sso_url( $args = array() ) {
770
		$defaults = array(
771
			'action'    => 'jetpack-sso',
772
			'site_id'   => Jetpack_Options::get_option( 'id' ),
773
			'sso_nonce' => self::request_initial_nonce(),
774
		);
775
776
		if ( isset( $_GET['state'] ) && check_admin_referer( $_GET['state'] ) ) {
777
			$defaults['state'] = rawurlencode( $_GET['state'] . '|' . $_GET['_wpnonce'] );
778
		}
779
780
		$args = wp_parse_args( $args, $defaults );
781
		$url  = add_query_arg( $args, 'https://wordpress.com/wp-login.php' );
782
783
		return $url;
784
	}
785
786
	/**
787
	 * Determines local user associated with a given WordPress.com user ID.
788
	 *
789
	 * @since 2.6.0
790
	 *
791
	 * @param int $wpcom_user_id User ID from WordPress.com
792
	 * @return object Local user object if found, null if not.
793
	 */
794
	static function get_user_by_wpcom_id( $wpcom_user_id ) {
795
		$user_query = new WP_User_Query( array(
796
			'meta_key'   => 'wpcom_user_id',
797
			'meta_value' => intval( $wpcom_user_id ),
798
			'number'     => 1,
799
		) );
800
801
		$users = $user_query->get_results();
802
		return $users ? array_shift( $users ) : null;
803
	}
804
805
	/**
806
	 * Error message displayed on the login form when two step is required and
807
	 * the user's account on WordPress.com does not have two step enabled.
808
	 *
809
	 * @since 2.7
810
	 * @param string $message
811
	 * @return string
812
	 **/
813
	public function error_msg_enable_two_step( $message ) {
814
		$err = __( sprintf( 'This site requires two step authentication be enabled for your user account on WordPress.com. Please visit the <a href="%1$s" target="_blank"> Security Settings</a> page to enable two step', 'https://wordpress.com/me/security/two-step' ) , 'jetpack' );
815
816
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
817
818
		return $message;
819
	}
820
821
	/**
822
	 * Error message displayed when the user tries to SSO, but match by email
823
	 * is off and they already have an account with their email address on
824
	 * this site.
825
	 *
826
	 * @param string $message
827
	 * @return string
828
	 */
829
	public function error_msg_email_already_exists( $message ) {
830
		$err = __( sprintf( 'You already have an account on this site. Please visit your <a href="%1$s">profile page</a> page to link your account to WordPress.com!', admin_url( 'profile.php' ) ) , 'jetpack' );
831
832
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
833
834
		return $message;
835
	}
836
837
	/**
838
	 * Message displayed when the site admin has disabled the default WordPress
839
	 * login form in Settings > General > Single Sign On
840
	 *
841
	 * @since 2.7
842
	 * @param string $message
843
	 * @return string
844
	 **/
845
	public function msg_login_by_jetpack( $message ) {
846
847
		$msg = __( sprintf( 'Jetpack authenticates through WordPress.com — to log in, enter your WordPress.com username and password, or <a href="%1$s" target="_blank">visit WordPress.com</a> to create a free account now.', 'http://wordpress.com/signup' ) , 'jetpack' );
848
849
		/**
850
		 * Filter the message displayed when the default WordPress login form is disabled.
851
		 *
852
		 * @module sso
853
		 *
854
		 * @since 2.8.0
855
		 *
856
		 * @param string $msg Disclaimer when default WordPress login form is disabled.
857
		 */
858
		$msg = apply_filters( 'jetpack_sso_disclaimer_message', $msg );
859
860
		$message .= sprintf( '<p class="message">%s</p>', $msg );
861
		return $message;
862
	}
863
864
	/**
865
	 * Error message displayed on the login form when the user attempts
866
	 * to post to the login form and it is disabled.
867
	 *
868
	 * @since 2.8
869
	 * @param string $message
870
	 * @param string
871
	 **/
872
	public function error_msg_login_method_not_allowed( $message ) {
873
		$err = __( 'Login method not allowed' , 'jetpack' );
874
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
875
876
		return $message;
877
	}
878
	function cant_find_user( $message ) {
879
		if ( self::match_by_email() ) {
880
			$err_format = __( 'We couldn\'t find an account with the email <strong><code>%1$s</code></strong> to log you in with.  If you already have an account on <strong>%2$s</strong>, please make sure that <strong><code>%1$s</code></strong> is configured as the email address, or that you have connected to WordPress.com on your profile page.', 'jetpack' );
881
		} else {
882
			$err_format = __( 'We couldn\'t find any account on <strong>%2$s</strong> that is linked to your WordPress.com account to log you in with.  If you already have an account on <strong>%2$s</strong>, please make sure that you have connected to WordPress.com on your profile page.', 'jetpack' );
883
		}
884
		$err = sprintf( $err_format, $this->user_data->email, get_bloginfo( 'name' ) );
885
		$message .= sprintf( '<p class="message" id="login_error">%s</p>', $err );
886
		return $message;
887
	}
888
889
	/**
890
	 * Deal with user connections...
891
	 */
892
	function admin_init() {
893
		add_action( 'show_user_profile', array( $this, 'edit_profile_fields' ) ); // For their own profile
894
		add_action( 'edit_user_profile', array( $this, 'edit_profile_fields' ) ); // For folks editing others profiles
895
896
		if ( isset( $_GET['jetpack_sso'] ) && 'purge' == $_GET['jetpack_sso'] && check_admin_referer( 'jetpack_sso_purge' ) ) {
897
			$user = wp_get_current_user();
898
			// Remove the connection on the wpcom end.
899
			self::delete_connection_for_user( $user->ID );
900
			// Clear it locally.
901
			delete_user_meta( $user->ID, 'wpcom_user_id' );
902
			delete_user_meta( $user->ID, 'wpcom_user_data' );
903
			// Forward back to the profile page.
904
			wp_safe_redirect( remove_query_arg( array( 'jetpack_sso', '_wpnonce' ) ) );
905
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method admin_init() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
906
		}
907
	}
908
909
	/**
910
	 * Determines if a local user is connected to WordPress.com
911
	 *
912
	 * @since 2.8
913
	 * @param integer $user_id - Local user id
914
	 * @return boolean
915
	 **/
916
	public function is_user_connected( $user_id ) {
917
		return $this->get_user_data( $user_id );
918
	}
919
920
	/**
921
	 * Retrieves a user's WordPress.com data
922
	 *
923
	 * @since 2.8
924
	 * @param integer $user_id - Local user id
925
	 * @return mixed null or stdClass
926
	 **/
927
	public function get_user_data( $user_id ) {
928
		return get_user_meta( $user_id, 'wpcom_user_data', true );
929
	}
930
931
	function edit_profile_fields( $user ) {
932
		?>
933
934
		<h3 id="single-sign-on"><?php _e( 'Single Sign On', 'jetpack' ); ?></h3>
935
		<p><?php _e( 'Connecting with Single Sign On enables you to log in via your WordPress.com account.', 'jetpack' ); ?></p>
936
937
		<?php if ( $this->is_user_connected( $user->ID ) ) : /* If the user is currently connected... */ ?>
938
			<?php $user_data = $this->get_user_data( $user->ID ); ?>
939
			<table class="form-table jetpack-sso-form-table">
940
				<tbody>
941
					<tr>
942
						<td>
943
							<div class="profile-card">
944
								<?php echo get_avatar( $user_data->email ); ?>
945
								<p class="connected"><strong><?php _e( 'Connected', 'jetpack' ); ?></strong></p>
946
								<p><?php echo esc_html( $user_data->login ); ?></p>
947
								<span class="two_step">
948
									<?php
949
										if ( $user_data->two_step_enabled ) {
950
											?> <p class="enabled"><a href="https://wordpress.com/me/security/two-step" target="_blank"><?php _e( 'Two-Step Authentication Enabled', 'jetpack' ); ?></a></p> <?php
951
										} else {
952
											?> <p class="disabled"><a href="https://wordpress.com/me/security/two-step" target="_blank"><?php _e( 'Two-Step Authentication Disabled', 'jetpack' ); ?></a></p> <?php
953
										}
954
									?>
955
								</span>
956
957
							</div>
958
							<p><a class="button button-secondary" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'jetpack_sso', 'purge' ), 'jetpack_sso_purge' ) ); ?>"><?php _e( 'Unlink This Account', 'jetpack' ); ?></a></p>
959
						</td>
960
					</tr>
961
				</tbody>
962
			</table>
963
		<?php elseif ( get_current_user_id() == $user->ID && Jetpack::is_user_connected( $user->ID ) ) : ?>
964
965
			<?php echo $this->button( 'state=sso-link-user&_wpnonce=' . wp_create_nonce( 'sso-link-user' ) ); // update ?>
0 ignored issues
show
Documentation introduced by
'state=sso-link-user&_wp..._nonce('sso-link-user') is of type string, but the function expects a array.

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...
966
967
		<?php else : ?>
968
969
			<p><?php esc_html_e( wptexturize( __( "If you don't have a WordPress.com account yet, you can sign up for free in just a few seconds.", 'jetpack' ) ) ); ?></p>
970
			<a href="<?php echo Jetpack::init()->build_connect_url( false, get_edit_profile_url( get_current_user_id() ) . '#single-sign-on' ); ?>" class="button button-connector" id="wpcom-connect"><?php esc_html_e( 'Link account with WordPress.com', 'jetpack' ); ?></a>
971
972
		<?php endif;
973
	}
974
}
975
976
Jetpack_SSO::get_instance();
977