PluginProbe
DoLogin Security / trunk
DoLogin Security vtrunk
5.0.10 4.8.3 trunk 1.0 1.1 1.1.1 1.2 1.2.1 1.2.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 1.6 All 64 releases
dologin / src / gui.cls.php

gui.cls.php in DoLogin Security trunk, at src/gui.cls.php

501 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GUI class
4 *
5 * @since 1.0
6 */
7 namespace dologin;
8
9 defined( 'WPINC' ) || exit;
10
11 class GUI extends Instance {
12 const DB_MSG = 'dologin.msg';
13 const NOTICE_BLUE = 'notice notice-info';
14 const NOTICE_GREEN = 'notice notice-success';
15 const NOTICE_RED = 'notice notice-error';
16 const NOTICE_YELLOW = 'notice notice-warning';
17
18 /**
19 * Init
20 *
21 * @since 1.3
22 * @access public
23 */
24 public function init() {
25 add_action( 'login_message', array( $this, 'login_message' ) );
26 add_filter( 'login_body_class', array( $this, 'login_body_class' ), 10, 2 );
27 add_filter( 'body_class', array( $this, 'frontend_body_class' ) );
28 add_action( 'wp_enqueue_scripts', array( $this, 'frontend_enqueue_styles' ) );
29 add_filter( 'lost_password_html_link', array( $this, 'lost_password_html_link' ), PHP_INT_MAX );
30 add_filter( 'login_link_separator', array( $this, 'login_link_separator' ), PHP_INT_MAX );
31
32 add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) );
33
34 // Inject Cloudflare Turnstile into the registration form.
35 add_action( 'register_form', array( $this, 'register_form' ) );
36
37 add_action( 'lostpassword_form', array( $this, 'lostpassword_form' ) );
38
39 // Append js and set ajax url
40 add_action( 'login_form', array( $this, 'login_form' ) );
41
42 add_action( 'woocommerce_login_form', array( $this, 'login_enqueue_scripts' ) );
43 add_action( 'woocommerce_login_form', array( $this, 'login_form' ) );
44 }
45
46 /**
47 * Mark forced-SSO login screens for immediate password-reset link hiding.
48 *
49 * @since 4.8.1
50 */
51 public function login_body_class( $classes, $action ) {
52 if ( 'login' === $action && KLSso::force_enabled() ) {
53 $classes[] = 'dologin-kl-force-login';
54 }
55
56 return $classes;
57 }
58
59 /**
60 * Mark frontend forced-SSO screens for non-JavaScript password-form hiding.
61 *
62 * @since 4.9.5
63 */
64 public function frontend_body_class( $classes ) {
65 if ( KLSso::force_enabled() ) {
66 $classes[] = 'dologin-kl-force-login';
67 }
68
69 return $classes;
70 }
71
72 /**
73 * Load forced-login styles in the frontend head before WooCommerce renders.
74 *
75 * @since 5.0.1
76 */
77 public function frontend_enqueue_styles() {
78 if ( KLSso::force_enabled() ) {
79 wp_enqueue_style( 'dologin-kl-force', DOLOGIN_PLUGIN_URL . 'assets/force-login.css', array(), Core::VER, 'all' );
80 }
81 }
82
83 /**
84 * Drop the core lost-password link server-side while forced SSO is active (WP 6.1+).
85 *
86 * @since 4.8.1
87 */
88 public function lost_password_html_link( $link ) {
89 return KLSso::force_enabled() ? '' : $link;
90 }
91
92 /**
93 * Drop the nav separator that would otherwise dangle after the removed lost-password link.
94 *
95 * @since 4.8.1
96 */
97 public function login_link_separator( $separator ) {
98 return KLSso::force_enabled() ? '' : $separator;
99 }
100
101 /**
102 * Enqueue js
103 *
104 * @since 1.3
105 * @access public
106 */
107 public function login_enqueue_scripts() {
108 $this->enqueue_style();
109
110 if ( Conf::val( '2fa' ) && ! KLSso::force_enabled() ) {
111 wp_register_script( 'dologin', DOLOGIN_PLUGIN_URL . 'assets/login.js', array( 'jquery' ), Core::VER, false );
112
113 $localize_data = array();
114 $localize_data['login_url'] = get_rest_url( null, 'dologin/v1/2fa' );
115 wp_localize_script( 'dologin', 'dologin', $localize_data );
116
117 wp_enqueue_script( 'dologin' );
118 }
119
120 if ( KLSso::enabled() || KLSso::force_enabled() ) {
121 $this->enqueue_klsso_script( 'login' );
122 }
123 }
124
125 /**
126 * Load style
127 *
128 * @since 1.3
129 */
130 public function enqueue_style() {
131 wp_enqueue_style( 'dologin', DOLOGIN_PLUGIN_URL . 'assets/login.css', array(), Core::VER, 'all' );
132 wp_enqueue_style( 'dologin-kl-sso', DOLOGIN_PLUGIN_URL . 'assets/kl-sso.css', array( 'dologin' ), Core::VER, 'all' );
133 }
134
135 /**
136 * Load css/js for admin
137 *
138 * @since 2.0
139 */
140 public function enqueue_admin( $hook ) {
141 $page = '';
142 if ( ! empty( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading current admin page slug only, no state change.
143 $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading current admin page slug only, no state change.
144 }
145
146 $is_dologin_page = $page && 0 === strpos( $page, 'dologin' );
147 $is_users_page = 'users.php' === $hook;
148 $is_profile_page = 'profile.php' === $hook;
149 if ( ! $is_dologin_page && ! $is_users_page && ! $is_profile_page ) {
150 return;
151 }
152 $this->enqueue_style();
153 wp_enqueue_style( 'dologin-components', DOLOGIN_PLUGIN_URL . 'assets/login-components.css', array( 'dologin' ), Core::VER, 'all' );
154
155 if ( $is_dologin_page || $is_users_page ) {
156 wp_register_script( 'dologin_admin', DOLOGIN_PLUGIN_URL . 'assets/admin.js', array( 'jquery' ), Core::VER, false );
157
158 $localize_data = array();
159 $localize_data['url_myip'] = get_rest_url( null, 'dologin/v1/myip' );
160 $localize_data['url_kl_reset_keys'] = get_rest_url( null, 'dologin/v1/kl_sso/reset_keys' );
161 $localize_data['nonce'] = wp_create_nonce( 'wp_rest' );
162 $localize_data['ip_lookup_progress'] = __( 'Looking up this IP address...', 'dologin' );
163 $localize_data['ip_lookup_failed'] = __( 'Failed to look up this IP address.', 'dologin' );
164 $localize_data['clear_log_confirm'] = __( 'Clear login-attempt records older than one month? This action cannot be undone.', 'dologin' );
165 $localize_data['reset_keys_confirm'] = __( 'Reset the KeyLockr site keys? As the service owner, you must update Service key in the SSO Keys block in MyDeveloper before scanning again. Future scans will create a new KeyLockr connection, and linked accounts must then pass Verify Connection or a successful SSO login. Existing KeyLockr connection records are not removed.', 'dologin' );
166 $localize_data['resetting_keys'] = __( 'Resetting KeyLockr site keys...', 'dologin' );
167 $localize_data['reset_keys_failed'] = __( 'Failed to reset KeyLockr site keys.', 'dologin' );
168 $localize_data['copy_public_key'] = __( 'Copy Public Key', 'dologin' );
169 $localize_data['copied'] = __( 'Copied!', 'dologin' );
170 $localize_data['copy_failed'] = __( 'Copy failed. Select and copy manually.', 'dologin' );
171 wp_localize_script( 'dologin_admin', 'dologin_admin', $localize_data );
172
173 wp_enqueue_script( 'dologin_admin' );
174 }
175
176 if ( KLSso::configured() && ( $is_dologin_page || $is_profile_page ) ) {
177 $this->enqueue_klsso_script( 'bind' );
178 }
179 }
180
181 /**
182 * Load KeyLockr SSO QR client.
183 */
184 public function enqueue_klsso_script( $mode ) {
185 wp_register_script( 'dologin_qrcode', DOLOGIN_PLUGIN_URL . 'qilu/npm/qrcode-generator/qrcode.js', array(), Core::VER, true );
186 wp_register_script( 'dologin_kl_sso', DOLOGIN_PLUGIN_URL . 'assets/kl-sso.js', array( 'jquery', 'dologin_qrcode' ), Core::VER, true );
187
188 wp_localize_script(
189 'dologin_kl_sso',
190 'dologin_kl_sso',
191 array(
192 'mode' => $mode,
193 'force' => KLSso::force_enabled(),
194 'url_start' => get_rest_url( null, 'dologin/v1/kl_sso/start' ),
195 'url_frame' => get_rest_url( null, 'dologin/v1/kl_sso/frame' ),
196 'url_unbind' => get_rest_url( null, 'dologin/v1/kl_sso/unbind' ),
197 'lostpassword_url' => wp_lostpassword_url(),
198 'nonce' => wp_create_nonce( 'wp_rest' ),
199 'i18n' => array(
200 'connecting' => __( 'Connecting to KeyLockr...', 'dologin' ),
201 'new_qr' => __( 'Get New QR', 'dologin' ),
202 'failed' => __( 'KeyLockr SSO failed.', 'dologin' ),
203 'done' => __( 'KeyLockr SSO verified.', 'dologin' ),
204 'unlink' => __( 'Unlink KeyLockr SSO from this WordPress account?', 'dologin' ),
205 ),
206 )
207 );
208
209 wp_enqueue_script( 'dologin_qrcode' );
210 wp_enqueue_script( 'dologin_kl_sso' );
211 }
212
213 /**
214 * Display login form
215 *
216 * @since 1.3
217 * @access public
218 */
219 public function login_form() {
220 if ( Conf::val( '2fa' ) && ! KLSso::force_enabled() ) {
221 echo ' <p id="dologin-process">
222 Dologin Security:
223 <span id="dologin-process-msg"></span>
224 </p>
225 <p id="dologin-dynamic_code">
226 <label for="dologin-two_factor_code">' . esc_html__( 'Dynamic Code', 'dologin' ) . '</label>
227 <br /><input type="text" name="dologin-two_factor_code" id="dologin-two_factor_code" autocomplete="off" />
228 </p>
229 ';
230 }
231
232 $this->cls( 'KLSso' )->login_form();
233
234 if ( Conf::val( 'cf' ) && ! KLSso::force_enabled() ) {
235 $this->cls( 'Captcha' )->show();
236 }
237 }
238
239 /**
240 * Inject register form
241 *
242 * @since 1.9
243 * @access public
244 */
245 public function register_form() {
246 if ( Conf::val( 'cf' ) && Conf::val( 'recapt_register' ) ) {
247 $this->cls( 'Captcha' )->show();
248 }
249 }
250
251 /**
252 * Inject lost password form
253 *
254 * @since 1.9
255 * @access public
256 */
257 public function lostpassword_form() {
258 if ( Conf::val( 'cf' ) && Conf::val( 'recapt_forget' ) ) {
259 $this->cls( 'Captcha' )->show();
260 }
261 }
262
263 /**
264 * Login default display messages
265 *
266 * @since 1.1
267 * @access public
268 */
269 public function login_message( $msg ) {
270 if ( defined( 'DOLOGIN_ERR' ) ) {
271 return;
272 }
273
274 $msg .= '<div class="success">' . Lang::msg( 'under_protected' ) . '<img src="' . DOLOGIN_PLUGIN_URL . 'assets/shield.svg" class="dologin-shield"></div>';
275
276 return $msg;
277 }
278
279 /**
280 * Register this setting to save
281 *
282 * @since 2.0
283 * @access public
284 */
285 public function enroll( $id ) {
286 echo '<input type="hidden" name="_settings-enroll[]" value="' . esc_attr( $id ) . '" />';
287 }
288
289 /**
290 * Build a textarea
291 *
292 * @since 2.0
293 * @access public
294 */
295 public function build_textarea( $id, $cols = false, $val = null ) {
296 if ( $val === null ) {
297 $val = Conf::val( $id );
298
299 if ( is_array( $val ) ) {
300 $val = implode( "\n", $val );
301 }
302 }
303
304 if ( ! $cols ) {
305 $cols = 80;
306 }
307
308 $this->enroll( $id );
309
310 echo "<textarea name='" . esc_attr( $id ) . "' rows='9' cols='" . esc_attr( $cols ) . "'>" . esc_textarea( $val ) . '</textarea>';
311 }
312
313 /**
314 * Build a text input field
315 *
316 * @since 2.0
317 * @access public
318 */
319 public function build_input( $id, $cls = null, $val = null, $type = 'text' ) {
320 if ( $val === null ) {
321 $val = Conf::val( $id );
322 }
323
324 $label_id = preg_replace( '|\W|', '', $id );
325
326 if ( $type == 'text' ) {
327 $cls = "regular-text $cls";
328 }
329
330 $this->enroll( $id );
331
332 echo "<input type='" . esc_attr( $type ) . "' class='" . esc_attr( $cls ) . "' name='" . esc_attr( $id ) . "' value='" . esc_textarea( $val ) . "' id='input_" . esc_attr( $label_id ) . "' /> ";
333 }
334
335 /**
336 * Build a switch div html snippet
337 *
338 * @since 1.2
339 * @access public
340 */
341 public function build_switch( $id, $title_list = false ) {
342 $this->enroll( $id );
343
344 echo '<div class="dologin-switch">';
345
346 if ( ! $title_list ) {
347 $title_list = array(
348 __( 'OFF', 'dologin' ),
349 __( 'ON', 'dologin' ),
350 );
351 }
352
353 foreach ( $title_list as $k => $v ) {
354 $this->_build_radio( $id, $k, $v );
355 }
356
357 echo '</div>';
358 }
359
360 /**
361 * Build a radio input html codes and output
362 *
363 * @since 1.2
364 * @access private
365 */
366 private function _build_radio( $id, $val, $txt ) {
367 $id_attr = 'input_radio_' . preg_replace( '|\W|', '', $id ) . '_' . $val;
368
369 if ( ! is_string( Conf::$_default_options[ $id ] ) ) {
370 $checked = (int) Conf::val( $id, true ) === (int) $val ? ' checked ' : '';
371 } else {
372 $checked = Conf::val( $id, true ) === $val ? ' checked ' : '';
373 }
374
375 echo "<input type='radio' autocomplete='off' name='" . esc_attr( $id ) . "' id='" . esc_attr( $id_attr ) . "' value='" . esc_attr( $val ) . "' " . esc_attr( $checked ) . " /> <label for='" . esc_attr( $id_attr ) . "'>" . esc_html( $txt ) . '</label>';
376 }
377
378 /**
379 * Builds a single msg.
380 *
381 * @access private
382 */
383 private static function _build_msg( $color, $str ) {
384 return '<div class="' . $color . ' is-dismissible"><p>' . $str . '</p></div>';
385 }
386
387 /**
388 * Display info notice
389 *
390 * @access public
391 */
392 public static function info( $msg, $echo = false ) {
393 self::_add_notice( self::NOTICE_BLUE, $msg, $echo );
394 }
395
396 /**
397 * Display note notice
398 *
399 * @access public
400 */
401 public static function note( $msg, $echo = false ) {
402 self::_add_notice( self::NOTICE_YELLOW, $msg, $echo );
403 }
404
405 /**
406 * Display success notice
407 *
408 * @access public
409 */
410 public static function succeed( $msg, $echo = false ) {
411 self::_add_notice( self::NOTICE_GREEN, $msg, $echo );
412 }
413
414 /**
415 * Display error notice
416 *
417 * @access public
418 */
419 public static function error( $msg, $echo = false ) {
420 self::_add_notice( self::NOTICE_RED, $msg, $echo );
421 }
422
423 /**
424 * Render a standalone localized error page for a public token endpoint.
425 *
426 * The caller remains responsible for terminating the request after rendering.
427 */
428 public static function error_page( $tag, $status_code = 400 ) {
429 status_header( (int) $status_code );
430 nocache_headers();
431
432 $message = Lang::text( $tag );
433 require DOLOGIN_DIR . 'tpl/error.tpl.php';
434 }
435
436 /**
437 * Adds a notice to display on the admin page
438 *
439 * @access private
440 */
441 private static function _add_notice( $color, $msg, $echo = false ) {
442 // Bypass adding for CLI or cron
443 if ( defined( 'DOING_CRON' ) ) {
444 // WP CLI will show the info directly
445 if ( defined( 'WP_CLI' ) && WP_CLI ) {
446 $msg = wp_strip_all_tags( $msg );
447 if ( $color == self::NOTICE_RED ) {
448 \WP_CLI::error( $msg );
449 } else {
450 \WP_CLI::success( $msg );
451 }
452 }
453 return;
454 }
455
456 if ( $echo ) {
457 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted plugin-generated admin notice markup.
458 echo self::_build_msg( $color, $msg );
459 return;
460 }
461
462 $messages = get_option( self::DB_MSG );
463
464 if ( is_array( $msg ) ) {
465 foreach ( $msg as $str ) {
466 $messages[] = self::_build_msg( $color, $str );
467 }
468 } else {
469 $messages[] = self::_build_msg( $color, $msg );
470 }
471 update_option( self::DB_MSG, $messages );
472 }
473
474 /**
475 * Display admin msg
476 *
477 * @access public
478 */
479 public function display_msg() {
480 $this->cls( 'TwoFA' )->gui_notice();
481
482 // One time msg
483 $messages = get_option( self::DB_MSG );
484 if ( is_array( $messages ) ) {
485 $messages = array_unique( $messages );
486
487 $added_thickbox = false;
488 foreach ( $messages as $msg ) {
489 // Added for popup links
490 if ( strpos( $msg, 'TB_iframe' ) && ! $added_thickbox ) {
491 add_thickbox();
492 $added_thickbox = true;
493 }
494 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted plugin-generated admin notice markup (may contain the 2FA setup form).
495 echo $msg;
496 }
497 }
498 delete_option( self::DB_MSG );
499 }
500 }
501