From a84c2a20d6e3d271a23aa3959160a86607b0eb1b Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Wed, 25 Feb 2026 07:21:06 +0530 Subject: [PATCH 1/4] Move inline JavaScript to external script files Extract 6 inline script blocks from 4 PHP files into 5 new external JS files under providers/js/, improving CSP compatibility and enabling proper WordPress script dependency management. New files: - totp-admin-qrcode.js: QR code generation for TOTP setup - totp-admin.js: TOTP setup form, checkbox focus, reset key - backup-codes-admin.js: Backup code generation, copy, download - two-factor-login.js: Shared login page authcode clear/focus - two-factor-login-authcode.js: Numeric-only enforcement, auto-submit The customizer messenger one-liner uses wp_add_inline_script() instead of a raw + - + 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 +217,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, + ) + ); + wp_enqueue_script( 'two-factor-totp-admin' ); ?>
@@ -391,80 +413,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 +432,6 @@ public function user_two_factor_options( $user ) { -

@@ -833,16 +774,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..ca370578 --- /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.role = 'image'; + svg.ariaLabel = twoFactorTotpQrcode.qrCodeLabel; + title.innerText = svg.ariaLabel; + 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..e8d3b386 --- /dev/null +++ b/providers/js/totp-admin.js @@ -0,0 +1,61 @@ +/* global twoFactorTotpAdmin, wp, document, jQuery */ +( function( $ ) { + 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.responseJSON.message || 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 ) { + $( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ); + $( '#two-factor-totp-options' ).html( response.html ); + } ); + } ); +}( 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..7ea2e23c --- /dev/null +++ b/providers/js/two-factor-login-authcode.js @@ -0,0 +1,34 @@ +/* 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, '' ).trimStart(); + + 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 ( undefined !== form.requestSubmit ) { + form.requestSubmit(); + form.submit.disabled = 'disabled'; + } + } + } + ); + } +}() ); 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 ); +}() ); From 070d21a5d5d224ec4452a3f98d7d45d6b329433d Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Thu, 26 Feb 2026 06:40:38 +0530 Subject: [PATCH 2/4] Fix QR code not rendering after resetting authenticator app The "Reset authenticator app" button's AJAX response injects HTML with a QR code placeholder, but the QR code generator script only runs on page load and never re-triggers. Additionally, the qrcode library wasn't loaded when TOTP was already configured. Add qr-code-generator as a dependency of totp-admin script and generate the QR code in JavaScript after the reset response HTML is inserted. --- providers/class-two-factor-totp.php | 2 +- providers/js/totp-admin.js | 31 ++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/providers/class-two-factor-totp.php b/providers/class-two-factor-totp.php index 05ce32fb..8677a765 100644 --- a/providers/class-two-factor-totp.php +++ b/providers/class-two-factor-totp.php @@ -189,7 +189,7 @@ public function enqueue_assets( $hook_suffix ) { wp_register_script( 'two-factor-totp-admin', plugins_url( 'js/totp-admin.js', __FILE__ ), - array( 'jquery', 'wp-api-request' ), + array( 'jquery', 'wp-api-request', 'two-factor-qr-code-generator' ), TWO_FACTOR_VERSION, true ); diff --git a/providers/js/totp-admin.js b/providers/js/totp-admin.js index e8d3b386..7ffe2d7f 100644 --- a/providers/js/totp-admin.js +++ b/providers/js/totp-admin.js @@ -1,5 +1,29 @@ -/* global twoFactorTotpAdmin, wp, document, jQuery */ +/* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */ ( function( $ ) { + var generateQrCode = function( totpUrl ) { + var $qrLink = $( '#two-factor-qr-code a' ); + if ( ! $qrLink.length || typeof qrcode === 'undefined' ) { + return; + } + + var qr = qrcode( 0, 'L' ), + svg, + title; + + qr.addData( totpUrl ); + qr.make(); + $qrLink.html( qr.createSvgTag( 5 ) ); + + svg = $qrLink.find( 'svg' )[ 0 ]; + if ( svg ) { + title = document.createElement( 'title' ); + svg.role = 'image'; + svg.ariaLabel = 'Authenticator App QR Code'; + title.innerText = svg.ariaLabel; + svg.appendChild( title ); + } + }; + var checkbox = document.getElementById( 'enabled-Two_Factor_Totp' ); // Focus the auth code input when the checkbox is clicked. @@ -56,6 +80,11 @@ } ).then( function( response ) { $( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ); $( '#two-factor-totp-options' ).html( response.html ); + + var totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' ); + if ( totpUrl ) { + generateQrCode( totpUrl ); + } } ); } ); }( jQuery ) ); From 8bca6e7d1a4a231c84189a64a451553439b884ba Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Thu, 26 Feb 2026 06:43:36 +0530 Subject: [PATCH 3/4] Fix vars-on-top lint errors in totp-admin.js Move all var declarations to the top of their respective function scopes. --- providers/js/totp-admin.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/providers/js/totp-admin.js b/providers/js/totp-admin.js index 7ffe2d7f..e56ec816 100644 --- a/providers/js/totp-admin.js +++ b/providers/js/totp-admin.js @@ -1,14 +1,16 @@ /* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */ ( function( $ ) { var generateQrCode = function( totpUrl ) { - var $qrLink = $( '#two-factor-qr-code a' ); + var $qrLink = $( '#two-factor-qr-code a' ), + qr, + svg, + title; + if ( ! $qrLink.length || typeof qrcode === 'undefined' ) { return; } - var qr = qrcode( 0, 'L' ), - svg, - title; + qr = qrcode( 0, 'L' ); qr.addData( totpUrl ); qr.make(); @@ -78,10 +80,12 @@ 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 ); - var totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' ); + totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' ); if ( totpUrl ) { generateQrCode( totpUrl ); } From 283df92f8be66c0f6d7ee314e5cc4da0f1d841be Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Tue, 3 Mar 2026 20:25:00 +0530 Subject: [PATCH 4/4] Fix issues from Copilot review on external JS migration - Add $hook_suffix parameter to backup codes enqueue_assets() for PHP 8+ - Replace trimStart() with ES5-compatible regex in login authcode - Fix submit button disable using querySelector instead of form.submit - Guard responseJSON access in TOTP admin fail handler - Fix SVG ARIA: use setAttribute with role="img" instead of DOM properties - Localize QR code aria-label string for translation - Move wp_add_inline_script before login_footer action --- class-two-factor-core.php | 12 ++++-------- providers/class-two-factor-backup-codes.php | 4 +++- providers/class-two-factor-totp.php | 5 +++-- providers/js/totp-admin-qrcode.js | 6 +++--- providers/js/totp-admin.js | 9 +++++---- providers/js/two-factor-login-authcode.js | 10 +++++++--- 6 files changed, 25 insertions(+), 21 deletions(-) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index f36fa904..ec814c1c 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -1575,6 +1575,10 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = $customize_login = isset( $_REQUEST['customize-login'] ); if ( $customize_login ) { wp_enqueue_script( 'customize-base' ); + wp_add_inline_script( + 'customize-base', + 'setTimeout( function(){ new wp.customize.Messenger({ url: ' . wp_json_encode( esc_url( wp_customize_url() ) ) . ', channel: \'login\' }).send(\'login\') }, 1000 );' + ); } $message = '

    ' . __( 'You have logged in successfully.', 'two-factor' ) . '

    '; $interim_login = 'success'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited @@ -1585,14 +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 . '/totp', - 'userId' => $user->ID, + 'restPath' => Two_Factor_Core::REST_NAMESPACE . '/totp', + 'userId' => $user->ID, + 'qrCodeAriaLabel' => __( 'Authenticator App QR Code', 'two-factor' ), ) ); wp_enqueue_script( 'two-factor-totp-admin' ); diff --git a/providers/js/totp-admin-qrcode.js b/providers/js/totp-admin-qrcode.js index ca370578..bb2cf4ef 100644 --- a/providers/js/totp-admin-qrcode.js +++ b/providers/js/totp-admin-qrcode.js @@ -20,9 +20,9 @@ svg = document.querySelector( '#two-factor-qr-code a svg' ); title = document.createElement( 'title' ); - svg.role = 'image'; - svg.ariaLabel = twoFactorTotpQrcode.qrCodeLabel; - title.innerText = svg.ariaLabel; + svg.setAttribute( 'role', 'img' ); + svg.setAttribute( 'aria-label', twoFactorTotpQrcode.qrCodeLabel ); + title.innerText = twoFactorTotpQrcode.qrCodeLabel; svg.appendChild( title ); }; diff --git a/providers/js/totp-admin.js b/providers/js/totp-admin.js index e56ec816..4d59e800 100644 --- a/providers/js/totp-admin.js +++ b/providers/js/totp-admin.js @@ -18,10 +18,11 @@ 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.role = 'image'; - svg.ariaLabel = 'Authenticator App QR Code'; - title.innerText = svg.ariaLabel; + svg.setAttribute( 'role', 'img' ); + svg.setAttribute( 'aria-label', ariaLabel ); + title.innerText = ariaLabel; svg.appendChild( title ); } }; @@ -53,7 +54,7 @@ enable_provider: true } } ).fail( function( response, status ) { - var errorMessage = response.responseJSON.message || status, + var errorMessage = ( response && response.responseJSON && response.responseJSON.message ) || ( response && response.statusText ) || status || '', $error = $( '#totp-setup-error' ); if ( ! $error.length ) { diff --git a/providers/js/two-factor-login-authcode.js b/providers/js/two-factor-login-authcode.js index 7ea2e23c..f7e6e7aa 100644 --- a/providers/js/two-factor-login-authcode.js +++ b/providers/js/two-factor-login-authcode.js @@ -10,7 +10,8 @@ inputEl.addEventListener( 'input', function() { - var value = this.value.replace( /[^0-9 ]/g, '' ).trimStart(); + var value = this.value.replace( /[^0-9 ]/g, '' ).replace( /^\s+/, '' ), + submitControl; if ( ! spaceInserted && expectedLength && value.length === Math.floor( expectedLength / 2 ) ) { value += ' '; @@ -23,9 +24,12 @@ // Auto-submit if it's the expected length. if ( expectedLength && value.replace( / /g, '' ).length === parseInt( expectedLength, 10 ) ) { - if ( undefined !== form.requestSubmit ) { + if ( form && typeof form.requestSubmit === 'function' ) { form.requestSubmit(); - form.submit.disabled = 'disabled'; + submitControl = form.querySelector( '[type="submit"]' ); + if ( submitControl ) { + submitControl.disabled = true; + } } } }