diff --git a/class-two-factor-core.php b/class-two-factor-core.php index a1c47558..ec814c1c 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -126,6 +126,7 @@ public static function add_hooks( $compat ) { add_filter( 'attach_session_information', array( __CLASS__, 'filter_session_information' ), 10, 2 ); + add_action( 'login_enqueue_scripts', array( __CLASS__, 'login_enqueue_scripts' ), 5 ); add_action( 'admin_init', array( __CLASS__, 'trigger_user_settings_action' ) ); add_filter( 'two_factor_providers', array( __CLASS__, 'enable_dummy_method_for_debug' ) ); @@ -135,6 +136,33 @@ public static function add_hooks( $compat ) { $compat->init(); } + /** + * Register login page scripts. + * + * @since 0.10.0 + * + * @codeCoverageIgnore + */ + public static function login_enqueue_scripts() { + $environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : ''; + + wp_register_script( + 'two-factor-login', + plugins_url( $environment_prefix . '/providers/js/two-factor-login.js', __FILE__ ), + array(), + TWO_FACTOR_VERSION, + true + ); + + wp_register_script( + 'two-factor-login-authcode', + plugins_url( $environment_prefix . '/providers/js/two-factor-login-authcode.js', __FILE__ ), + array(), + TWO_FACTOR_VERSION, + true + ); + } + /** * Delete all plugin data on uninstall. * @@ -1127,41 +1155,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg opacity: 0.5; } - + ' . __( 'You have logged in successfully.', 'two-factor' ) . '

'; $interim_login = 'success'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited @@ -1591,9 +1589,6 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = /** This action is documented in wp-login.php */ do_action( 'login_footer' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core WordPress action. ?> - - - Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes', + 'userId' => $user->ID, + ) + ); + wp_enqueue_script( 'two-factor-backup-codes-admin' ); $count = self::codes_remaining_for_user( $user ); ?> @@ -191,54 +219,6 @@ public function user_options( $user ) {

-

- + get_user_totp_key( $user->ID ); - wp_enqueue_script( 'two-factor-qr-code-generator' ); - wp_enqueue_script( 'wp-api-request' ); - wp_enqueue_script( 'jquery' ); + wp_localize_script( + 'two-factor-totp-admin', + 'twoFactorTotpAdmin', + array( + 'restPath' => Two_Factor_Core::REST_NAMESPACE . '/totp', + 'userId' => $user->ID, + 'qrCodeAriaLabel' => __( 'Authenticator App QR Code', 'two-factor' ), + ) + ); + wp_enqueue_script( 'two-factor-totp-admin' ); ?>
@@ -391,80 +414,17 @@ public function user_two_factor_options( $user ) {

- + $totp_url, + 'qrCodeLabel' => __( 'Authenticator App QR Code', 'two-factor' ), + ) + ); + wp_enqueue_script( 'two-factor-totp-qrcode' ); + ?>

@@ -473,24 +433,6 @@ public function user_two_factor_options( $user ) { -

@@ -833,16 +775,9 @@ public function authentication_page( $user ) {

- ' ).val( csvCodes ).css( { position: 'absolute', left: '-9999px' } ); + $( 'body' ).append( $temp ); + $temp[0].select(); + document.execCommand( 'copy' ); + $temp.remove(); + } ); + + $( '.button-two-factor-backup-codes-generate' ).click( function() { + wp.apiRequest( { + method: 'POST', + path: twoFactorBackupCodes.restPath, + data: { + user_id: parseInt( twoFactorBackupCodes.userId, 10 ) + } + } ).then( function( response ) { + var $codesList = $( '.two-factor-backup-codes-unused-codes' ), + i; + + $( '.two-factor-backup-codes-wrapper' ).show(); + $codesList.html( '' ); + $codesList.css( { 'column-count': 2, 'column-gap': '80px', 'max-width': '420px' } ); + $( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv', response.codes.join( ',' ) ); + + // Append the codes. + for ( i = 0; i < response.codes.length; i++ ) { + $codesList.append( '
  • ' + response.codes[ i ] + '
  • ' ); + } + + // Update counter. + $( '.two-factor-backup-codes-count' ).html( response.i18n.count ); + $( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link ); + } ); + } ); +}( jQuery ) ); diff --git a/providers/js/totp-admin-qrcode.js b/providers/js/totp-admin-qrcode.js new file mode 100644 index 00000000..bb2cf4ef --- /dev/null +++ b/providers/js/totp-admin-qrcode.js @@ -0,0 +1,35 @@ +/* global twoFactorTotpQrcode, qrcode, document, window */ +( function() { + var qrGenerator = function() { + /* + * 0 = Automatically select the version, to avoid going over the limit of URL + * length. + * L = Least amount of error correction, because it's not needed when scanning + * on a monitor, and it lowers the image size. + */ + var qr = qrcode( 0, 'L' ), + svg, + title; + + qr.addData( twoFactorTotpQrcode.totpUrl ); + qr.make(); + + document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 ); + + // For accessibility, markup the SVG with a title and role. + svg = document.querySelector( '#two-factor-qr-code a svg' ); + title = document.createElement( 'title' ); + + svg.setAttribute( 'role', 'img' ); + svg.setAttribute( 'aria-label', twoFactorTotpQrcode.qrCodeLabel ); + title.innerText = twoFactorTotpQrcode.qrCodeLabel; + svg.appendChild( title ); + }; + + // Run now if the document is loaded, otherwise on DOMContentLoaded. + if ( document.readyState === 'complete' ) { + qrGenerator(); + } else { + window.addEventListener( 'DOMContentLoaded', qrGenerator ); + } +}() ); diff --git a/providers/js/totp-admin.js b/providers/js/totp-admin.js new file mode 100644 index 00000000..4d59e800 --- /dev/null +++ b/providers/js/totp-admin.js @@ -0,0 +1,95 @@ +/* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */ +( function( $ ) { + var generateQrCode = function( totpUrl ) { + var $qrLink = $( '#two-factor-qr-code a' ), + qr, + svg, + title; + + if ( ! $qrLink.length || typeof qrcode === 'undefined' ) { + return; + } + + qr = qrcode( 0, 'L' ); + + qr.addData( totpUrl ); + qr.make(); + $qrLink.html( qr.createSvgTag( 5 ) ); + + svg = $qrLink.find( 'svg' )[ 0 ]; + if ( svg ) { + var ariaLabel = ( typeof twoFactorTotpAdmin !== 'undefined' && twoFactorTotpAdmin && twoFactorTotpAdmin.qrCodeAriaLabel ) ? twoFactorTotpAdmin.qrCodeAriaLabel : 'Authenticator App QR Code'; + title = document.createElement( 'title' ); + svg.setAttribute( 'role', 'img' ); + svg.setAttribute( 'aria-label', ariaLabel ); + title.innerText = ariaLabel; + svg.appendChild( title ); + } + }; + + var checkbox = document.getElementById( 'enabled-Two_Factor_Totp' ); + + // Focus the auth code input when the checkbox is clicked. + if ( checkbox ) { + checkbox.addEventListener( 'click', function( e ) { + if ( e.target.checked ) { + document.getElementById( 'two-factor-totp-authcode' ).focus(); + } + } ); + } + + $( '.totp-submit' ).click( function( e ) { + var key = $( '#two-factor-totp-key' ).val(), + code = $( '#two-factor-totp-authcode' ).val(); + + e.preventDefault(); + + wp.apiRequest( { + method: 'POST', + path: twoFactorTotpAdmin.restPath, + data: { + user_id: parseInt( twoFactorTotpAdmin.userId, 10 ), + key: key, + code: code, + enable_provider: true + } + } ).fail( function( response, status ) { + var errorMessage = ( response && response.responseJSON && response.responseJSON.message ) || ( response && response.statusText ) || status || '', + $error = $( '#totp-setup-error' ); + + if ( ! $error.length ) { + $error = $( '

    ' ).insertAfter( $( '.totp-submit' ) ); + } + + $error.find( 'p' ).text( errorMessage ); + + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ).trigger( 'change' ); + $( '#two-factor-totp-authcode' ).val( '' ); + } ).then( function( response ) { + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', true ).trigger( 'change' ); + $( '#two-factor-totp-options' ).html( response.html ); + } ); + } ); + + $( '.button.reset-totp-key' ).click( function( e ) { + e.preventDefault(); + + wp.apiRequest( { + method: 'DELETE', + path: twoFactorTotpAdmin.restPath, + data: { + user_id: parseInt( twoFactorTotpAdmin.userId, 10 ) + } + } ).then( function( response ) { + var totpUrl; + + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ); + $( '#two-factor-totp-options' ).html( response.html ); + + totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' ); + if ( totpUrl ) { + generateQrCode( totpUrl ); + } + } ); + } ); +}( jQuery ) ); diff --git a/providers/js/two-factor-login-authcode.js b/providers/js/two-factor-login-authcode.js new file mode 100644 index 00000000..f7e6e7aa --- /dev/null +++ b/providers/js/two-factor-login-authcode.js @@ -0,0 +1,38 @@ +/* global document */ +( function() { + // Enforce numeric-only input for numeric inputmode elements. + var form = document.querySelector( '#loginform' ), + inputEl = document.querySelector( 'input.authcode[inputmode="numeric"]' ), + expectedLength = ( inputEl && inputEl.dataset ) ? inputEl.dataset.digits : 0, + spaceInserted = false; + + if ( inputEl ) { + inputEl.addEventListener( + 'input', + function() { + var value = this.value.replace( /[^0-9 ]/g, '' ).replace( /^\s+/, '' ), + submitControl; + + if ( ! spaceInserted && expectedLength && value.length === Math.floor( expectedLength / 2 ) ) { + value += ' '; + spaceInserted = true; + } else if ( spaceInserted && ! this.value ) { + spaceInserted = false; + } + + this.value = value; + + // Auto-submit if it's the expected length. + if ( expectedLength && value.replace( / /g, '' ).length === parseInt( expectedLength, 10 ) ) { + if ( form && typeof form.requestSubmit === 'function' ) { + form.requestSubmit(); + submitControl = form.querySelector( '[type="submit"]' ); + if ( submitControl ) { + submitControl.disabled = true; + } + } + } + } + ); + } +}() ); diff --git a/providers/js/two-factor-login.js b/providers/js/two-factor-login.js new file mode 100644 index 00000000..6526581f --- /dev/null +++ b/providers/js/two-factor-login.js @@ -0,0 +1,11 @@ +/* global document, setTimeout */ +( function() { + setTimeout( function() { + var d; + try { + d = document.getElementById( 'authcode' ); + d.value = ''; + d.focus(); + } catch ( e ) {} + }, 200 ); +}() );