1<?php
2
3namespace WPaaS;
4
5if ( ! defined( 'ABSPATH' ) ) {
6
7 exit;
8
9}
10
11final class SSO {
12
13 /**
14 * Query arg to identify sso problems
15 */
16 const INVALID_SSO_QARG = 'wpaas_invalid_sso';
17
18 /**
19 * Instance of the API.
20 *
21 * @var API_Interface
22 */
23 private $api;
24
25 /**
26 * Excluded classes hooked into 'login_form' or 'login_enqueue_scripts'.
27 *
28 * @var array
29 */
30 private $excluded_classes;
31
32 /**
33 * Class constructor.
34 *
35 * @param API_Interface $api
36 */
37 public function __construct( API_Interface $api ) {
38
39 $this->api = $api;
40
41 /**
42 * Excluded plugin classes.
43 *
44 * @var array
45 */
46 $this->excluded_classes = (array) apply_filters(
47 'wpaas_gd_login_enqueue_scripts_plugin_classes',
48 array(
49 'Limit_Login_Attempts',
50 'Wpsec\twofa\Core\TwoFactorAuthCore',
51 'Wpsec\captcha\handlers\LoginFormEventHandler',
52 )
53 );
54
55 /**
56 * We must + 1 the minimum integer when hooking into SSO to ensure
57 * that the Log class captures these events properly.
58 *
59 * Note: In WordPress 4.7 the SORT_NUMERIC flag was added to ksort()
60 * for sorting filter priorities. There is also a bug in PHP 5 that
61 * treats ~PHP_INT_MAX as greater than -PHP_INT_MAX. For this reason,
62 * ~PHP_INT_MAX should never be used as a filter priority in WP.
63 *
64 * Note: Must hook into setup_theme for customize.php to work.
65 *
66 * @link https://gist.github.com/fjarrett/d2d1d60930d2ca4e67d35cf672ac9b13
67 */
68 add_action( 'setup_theme', [ $this, 'init' ], -PHP_INT_MAX + 1 );
69 add_action( 'login_init', [ $this, 'login_init' ], PHP_INT_MAX );
70 add_action( 'shake_error_codes', [ $this, 'shake_error_codes' ] );
71
72 add_filter( 'wp_login_errors', [ $this, 'wp_login_errors' ] );
73
74 }
75
76 /**
77 * Initialize script.
78 *
79 * @action setup_theme
80 */
81 public function init() {
82
83 // @codingStandardsIgnoreStart
84 $action = ! empty( $_REQUEST['GD_COMMAND'] ) ? strtolower( $_REQUEST['GD_COMMAND'] ) : filter_input( INPUT_GET, 'wpaas_action' ); // Backward compat.
85 $hash = ! empty( $_REQUEST['SSO_HASH'] ) ? $_REQUEST['SSO_HASH'] : filter_input( INPUT_GET, 'wpaas_sso_hash' ); // Backward compat.
86 // @codingStandardsIgnoreEnd
87
88 if ( 'sso_login' !== $action || ! $hash ) {
89
90 return;
91
92 }
93
94 $uri = sanitize_url( filter_input( INPUT_SERVER, 'REQUEST_URI' ) );
95 $redirect = remove_query_arg( [ 'GD_COMMAND', 'SSO_HASH', 'wpaas_action', 'wpaas_sso_hash', 'nocache' ], home_url( $uri ) );
96 $redirect = preg_match( '~^/wp-login\.php~', $uri ) ? admin_url() : $redirect;
97
98 // Go theme users should go straight to the Colors panel.
99 if ( urldecode( $redirect ) === admin_url( 'customize.php' ) && 'go' === get_option( 'stylesheet' ) ) {
100
101 $redirect = admin_url( 'customize.php?autofocus[section]=colors' );
102
103 }
104
105 if ( is_user_logged_in() ) {
106
107 wp_safe_redirect( esc_url_raw( $redirect ) );
108
109 exit;
110
111 }
112
113 $user_id = $this->user_id();
114
115 if ( is_int( $user_id ) ) {
116
117 if ( $hash && $this->api->is_valid_sso_hash( $hash ) ) {
118
119 @wp_set_auth_cookie( $user_id ); // @codingStandardsIgnoreLine
120
121 /** Logg user action */
122 $GLOBALS['wpaas_activity_logger']->log_sp_action($user_id, 'SSO login success' );
123
124 wp_safe_redirect( esc_url_raw( $redirect ) );
125
126 exit;
127
128 }
129 $GLOBALS['wpaas_activity_logger']->log_sp_action($user_id, sprintf('SSO invalid hash: %s user_id: %d', $hash, $user_id) );
130
131 }
132
133 /** Logg user action */
134 $GLOBALS['wpaas_activity_logger']->log_sp_action($user_id, 'SSO login fail' );
135
136 wp_safe_redirect( add_query_arg( static::INVALID_SSO_QARG, '', wp_login_url( admin_url() ) ) );
137
138 exit;
139
140 }
141
142 /**
143 * Initialize the GD SSO login button.
144 *
145 * @action login_init
146 */
147 public function login_init() {
148
149 /**
150 * Filter to forcefully disable the SSO login functionality.
151 *
152 * @var bool
153 */
154 $enabled = (bool) apply_filters( 'wpaas_gd_sso_button_enabled', true );
155
156 // Only show if all conditions are met. Bail if any other plugin is customizing the login form.
157 if ( ! $enabled || ! Plugin::is_gd() || ! defined( 'GD_ACCOUNT_UID' ) || ! GD_ACCOUNT_UID || ( isset( $_GET['wpaas-standard-login'] ) && (bool) $_GET['wpaas-standard-login'] ) || $this->is_plugin_hooked( 'login_form' ) || $this->is_plugin_hooked( 'login_enqueue_scripts' ) ) {
158
159 return;
160
161 }
162
163 // Add a body class for our SSO login form styles.
164 add_filter( 'login_body_class', function ( $classes ) {
165
166 $classes[] = 'wpaas-show-sso-login';
167
168 return $classes;
169
170 } );
171
172 add_action( 'login_head', [ $this, 'login_head' ] );
173 add_action( 'login_form', [ $this, 'login_form' ] );
174 add_action( 'login_footer', [ $this, 'login_footer' ] );
175 add_action( 'login_enqueue_scripts', [ $this, 'login_enqueue_scripts' ] );
176
177 }
178
179 /**
180 * Determine if a plugin is hooked into 'login_enqueue_scripts' and customizing the login screen.
181 *
182 * @param string $action The action to check against.
183 *
184 * @return boolean True when a plugin is hooked into $action, else false.
185 */
186 private function is_plugin_hooked( $action ) {
187
188 global $wp_filter;
189
190 if ( empty( $action ) || ! array_key_exists( $action, $wp_filter ) ) {
191
192 return false;
193
194 }
195
196 $hooked_actions = array();
197 $data = $wp_filter[ $action ]->callbacks[ key( $wp_filter[ $action ]->callbacks ) ];
198
199 foreach ( $data as $info ) {
200
201 // Standard function.
202 if ( is_string( $info['function'] ) ) {
203
204 $hooked_actions[] = $info['function'];
205
206 continue;
207
208 }
209
210 // Closure.
211 if ( is_object( $info['function'] ) ) {
212
213 $hooked_actions[] = get_class( $info['function'] );
214
215 continue;
216
217 }
218
219 // Class method.
220 if ( is_array( $info['function'] ) ) {
221
222 foreach ( $info['function'] as $hook ) {
223
224 if ( is_object( $hook ) ) {
225
226 $hooked_actions[] = get_class( $hook );
227
228 }
229
230 }
231
232 }
233
234 }
235
236 if ( ! empty( $hooked_actions ) ) {
237
238 $hooked_actions = array_diff( $hooked_actions, $this->excluded_classes );
239
240 }
241
242 return ! empty( array_filter( $hooked_actions ) );
243
244 }
245
246 /**
247 * Load fonts for SSO login button.
248 *
249 * @action login_head
250 */
251 public function login_head() {
252
253 ?>
254 <link rel="preload" href="//img1.wsimg.com/ux/fonts/sherpa/1.1/gdsherpa-bold.woff2" as="font" type="font/woff2" crossorigin=""/>
255 <style>
256 @font-face {
257 font-family: gdsherpa;
258 src: url(//img1.wsimg.com/ux/fonts/sherpa/1.1/gdsherpa-bold.woff2) format("woff2"),
259 url(//img1.wsimg.com/ux/fonts/sherpa/1.1/gdsherpa-bold.woff) format("woff");
260 font-weight: 500;
261 font-display: swap;
262 }
263 </style>
264 <?php
265
266 }
267
268 /**
269 * Display the SSO login button.
270 *
271 * @action login_form
272 */
273 public function login_form() {
274
275 $env = Plugin::get_env();
276
277 $sso_url = sprintf(
278 'https://%s/mwp/site/%s/sso?path=/wp-admin&type=wp&origin=wp-login',
279 ( 'prod' === $env ) ? 'host.godaddy.com' : "host.{$env}-godaddy.com",
280 GD_ACCOUNT_UID
281 );
282
283 ?>
284 <div class="wpaas-sso-login-wrapper">
285
286 <div class="wpaas-sso-login-button">
287 <a href="<?php echo esc_url( $sso_url ); ?>" rel="nofollow" class="button button-primary">
288 <svg width="42" height="37" viewBox="0 0 42 37" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M37.633 18.13c-.562 2.115-1.472 4.177-2.705 6.13a22.93 22.93 0 0 1-2.649 3.435c1.112-4.482.36-9.973-2.367-15.122a.69.69 0 0 0-.985-.265l-8.49 5.25a.683.683 0 0 0-.221.946l1.245 1.97c.203.322.631.42.956.22l5.503-3.403c.184.523.35 1.05.49 1.58.53 1.991.727 3.936.587 5.778-.262 3.429-1.673 6.101-3.974 7.524-1.149.71-2.484 1.086-3.934 1.127h-.177c-1.451-.04-2.786-.417-3.936-1.128-2.3-1.422-3.711-4.094-3.973-7.523-.14-1.842.057-3.787.586-5.779.562-2.114 1.472-4.177 2.706-6.13a22.321 22.321 0 0 1 4.382-5.093c1.578-1.344 3.258-2.372 4.993-3.054 3.23-1.271 6.275-1.187 8.576.235 2.3 1.422 3.712 4.094 3.973 7.524.141 1.842-.056 3.786-.586 5.778m-30.56 6.13c-1.234-1.953-2.144-4.015-2.706-6.13-.53-1.992-.727-3.936-.586-5.778.261-3.43 1.672-6.102 3.973-7.524 2.3-1.422 5.347-1.506 8.576-.235.487.191.968.413 1.444.66a26.242 26.242 0 0 0-4.649 5.528C9.562 16.422 8.48 22.689 9.721 27.696a22.939 22.939 0 0 1-2.649-3.436M36.227 1.692C31.86-1.007 26.115-.365 20.999 2.822 15.883-.363 10.138-1.005 5.773 1.693-1.122 5.955-1.96 16.937 3.903 26.22 8.226 33.064 14.983 37.074 21 36.999c6.017.074 12.774-3.935 17.097-10.78 5.863-9.282 5.025-20.264-1.87-24.527" id="a"/></defs><use fill="#FFF" xlink:href="#a" fill-rule="evenodd"/></svg>
289 <?php esc_html_e( 'Log in with GoDaddy', 'gd-system-plugin' ); ?>
290 </a>
291 </div>
292
293 <div class="wpaas-sso-login-divider">
294 <span><?php esc_html_e( 'Or', 'gd-system-plugin' ); ?></span>
295 </div>
296
297 <a href="<?php echo esc_url( add_query_arg( 'wpaas-standard-login', 1 ) ); ?>" rel="nofollow" class="wpaas-sso-login-toggle">
298 <?php esc_html_e( 'Log in with username and password', 'gd-system-plugin' ); ?>
299 </a>
300 </div>
301 <?php
302
303 }
304
305 /**
306 * Remove target="_blank" from the login links
307 *
308 * @action login_footer
309 */
310 public function login_footer() {
311
312 if ( ! isset( $_REQUEST['interim-login'] ) ) {
313
314 return;
315
316 }
317
318 ?>
319 <script type="text/javascript">
320 ( function() {
321 try {
322 var i, links = document.getElementsByTagName( 'a' );
323 for ( i in links ) {
324 if ( links[i].href && 'wpaas-sso-login-toggle' === links[i].className ) {
325 links[i].target = '';
326 links[i].rel = '';
327 }
328 }
329 } catch( er ) {}
330 }());
331 </script>
332 <?php
333
334 }
335
336 /**
337 * Enqueue scripts and styles for SSO login button.
338 *
339 * @action login_enqueue_scripts
340 */
341 public function login_enqueue_scripts() {
342
343 $rtl = is_rtl() ? '-rtl' : '';
344 $suffix = SCRIPT_DEBUG ? '' : '.min';
345
346 wp_enqueue_style( 'wpaas-sso-login', Plugin::assets_url( "css/sso-login{$rtl}{$suffix}.css" ), [], Plugin::version() );
347
348 }
349
350 /**
351 * Return the SSO user ID.
352 *
353 * @return int|false
354 */
355 private function user_id() {
356
357 $user_id = ! empty( $_REQUEST['SSO_USER_ID'] ) ? $_REQUEST['SSO_USER_ID'] : filter_input( INPUT_GET, 'wpaas_sso_user_id', FILTER_VALIDATE_INT ); // Backwards compat - @codingStandardsIgnoreLine
358
359 if ( $user_id ) {
360
361 return absint( $user_id );
362
363 }
364
365 $user = Plugin::get_first_admin_user();
366
367 return ( ! $user || ! isset( $user->ID ) ) ? false : $user->ID;
368
369 }
370
371 /**
372 * Add our custom error message to the shaking messages
373 *
374 * @action shake_error_codes
375 * @param $shake_error_codes
376 *
377 * @return array
378 */
379 public function shake_error_codes( $shake_error_codes ) {
380
381 $shake_error_codes[] = static::INVALID_SSO_QARG;
382
383 return $shake_error_codes;
384
385 }
386
387 /**
388 * Check if there were any SSO problems.
389 *
390 * @filter wp_login_errors
391 * @param $errors
392 *
393 * @return mixed
394 */
395 public function wp_login_errors( $errors ) {
396
397 if ( ! isset( $_GET[ static::INVALID_SSO_QARG ] ) ) { // @codingStandardsIgnoreLine
398
399 return $errors;
400
401 }
402
403 $errors->add( static::INVALID_SSO_QARG, __( 'We were unable to log you in automatically. Please enter your WordPress username and password.', 'gd-system-plugin' ), 'error' );
404
405 return $errors;
406
407 }
408
409}
410