PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / js / profile / wp2fa-profile.js
wp-2fa / js / profile Last commit date
wp2fa-profile-backup-codes.js 2 days ago wp2fa-profile.js 2 days ago
wp2fa-profile.js
204 lines
1 /**
2 * WP 2FA - Profile Section JavaScript
3 *
4 * Vanilla JS for the user profile 2FA section.
5 * Handles: TOTP QR toggle, copy-to-clipboard, confirmation dialogs,
6 * remove 2FA AJAX, generate backup codes AJAX, auto-open wizard.
7 *
8 * @package wp2fa
9 * @since 4.0.0
10 */
11 (function () {
12 'use strict';
13
14 /**
15 * Wait for DOM ready.
16 *
17 * @param {Function} fn Callback.
18 */
19 function ready(fn) {
20 if (document.readyState !== 'loading') {
21 fn();
22 } else {
23 document.addEventListener('DOMContentLoaded', fn);
24 }
25 }
26
27 ready(function () {
28 var container = document.getElementById('wp2fa-profile-section');
29 if (!container) {
30 return;
31 }
32
33 // -----------------------------------------------------------------
34 // TOTP QR Code toggle
35 // -----------------------------------------------------------------
36 var totpToggle = container.querySelector('[data-wp2fa-toggle-totp]');
37 if (totpToggle) {
38 totpToggle.addEventListener('click', function () {
39 var content = document.getElementById('wp2fa-totp-qr-content');
40 if (!content) {
41 return;
42 }
43 var isOpen = content.classList.toggle('wp2fa-profile__totp-content--open');
44 totpToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
45 totpToggle.innerHTML = (isOpen ? '▼ ' : '▶ ') + totpToggle.textContent.trim().replace(/^[▶▼]\s*/, '');
46 // Fix: re-set the text properly.
47 var label = isOpen
48 ? (wp2faProfileData && wp2faProfileData.i18n && wp2faProfileData.i18n.hideQrCode ? wp2faProfileData.i18n.hideQrCode : 'Hide QR code')
49 : (wp2faProfileData && wp2faProfileData.i18n && wp2faProfileData.i18n.showQrCode ? wp2faProfileData.i18n.showQrCode : 'Show QR code');
50 totpToggle.innerHTML = (isOpen ? '▼ ' : '▶ ') + label;
51 });
52 }
53
54 // -----------------------------------------------------------------
55 // Copy to clipboard
56 // -----------------------------------------------------------------
57 container.addEventListener('click', function (e) {
58 var btn = e.target.closest('[data-wp2fa-copy]');
59 if (!btn) {
60 return;
61 }
62 e.preventDefault();
63 var selector = btn.getAttribute('data-wp2fa-copy');
64 var input = container.querySelector(selector);
65 if (!input) {
66 return;
67 }
68 var text = input.value || input.textContent;
69 if (navigator.clipboard && navigator.clipboard.writeText) {
70 navigator.clipboard.writeText(text).then(function () {
71 showCopyFeedback(btn);
72 });
73 } else {
74 // Fallback.
75 input.select();
76 document.execCommand('copy');
77 showCopyFeedback(btn);
78 }
79 });
80
81 function showCopyFeedback(btn) {
82 var original = btn.textContent;
83 var copiedText = (wp2faProfileData && wp2faProfileData.i18n && wp2faProfileData.i18n.copied) ? wp2faProfileData.i18n.copied : 'Copied!';
84 btn.textContent = copiedText;
85 setTimeout(function () {
86 btn.textContent = original;
87 }, 1500);
88 }
89
90 // -----------------------------------------------------------------
91 // Confirmation dialogs
92 // -----------------------------------------------------------------
93 container.addEventListener('click', function (e) {
94 var trigger = e.target.closest('[data-wp2fa-confirm]');
95 if (!trigger) {
96 return;
97 }
98 e.preventDefault();
99 var dialogId = 'wp2fa-confirm-' + trigger.getAttribute('data-wp2fa-confirm');
100 var overlay = document.getElementById(dialogId);
101 if (overlay) {
102 overlay.classList.add('wp2fa-profile__confirm--open');
103 // Focus the first button inside.
104 var firstBtn = overlay.querySelector('.wp2fa-profile__btn');
105 if (firstBtn) {
106 firstBtn.focus();
107 }
108 }
109 });
110
111 // Close confirmation dialogs.
112 document.addEventListener('click', function (e) {
113 // Close button inside dialog.
114 if (e.target.closest('[data-wp2fa-confirm-close]')) {
115 e.preventDefault();
116 var overlay = e.target.closest('.wp2fa-profile__confirm-overlay');
117 if (overlay) {
118 overlay.classList.remove('wp2fa-profile__confirm--open');
119 }
120 return;
121 }
122 // Click on overlay background (not dialog itself).
123 if (e.target.classList && e.target.classList.contains('wp2fa-profile__confirm-overlay')) {
124 e.target.classList.remove('wp2fa-profile__confirm--open');
125 }
126 });
127
128 // ESC to close dialogs.
129 document.addEventListener('keydown', function (e) {
130 if (e.key === 'Escape') {
131 var openOverlays = document.querySelectorAll('.wp2fa-profile__confirm--open');
132 openOverlays.forEach(function (el) {
133 el.classList.remove('wp2fa-profile__confirm--open');
134 });
135 }
136 });
137
138 // -----------------------------------------------------------------
139 // Remove 2FA via AJAX
140 // -----------------------------------------------------------------
141 container.addEventListener('click', function (e) {
142 var btn = e.target.closest('[data-wp2fa-remove-2fa]');
143 if (!btn) {
144 return;
145 }
146 e.preventDefault();
147 var userId = btn.getAttribute('data-user-id');
148 var nonce = btn.getAttribute('data-nonce');
149 var ajaxUrl = wp2faProfileData ? wp2faProfileData.ajaxUrl : (typeof ajaxurl !== 'undefined' ? ajaxurl : '/wp-admin/admin-ajax.php');
150
151 var formData = new FormData();
152 formData.append('action', 'remove_user_2fa');
153 formData.append('user_id', userId);
154 formData.append('wp_2fa_nonce', nonce);
155
156 fetch(ajaxUrl, {
157 method: 'POST',
158 credentials: 'same-origin',
159 body: formData
160 }).then(function (response) {
161 return response.json();
162 }).then(function (result) {
163 if (result.success) {
164 window.location.reload();
165 } else {
166 var errorMsg = (result.data && result.data.error) ? result.data.error : 'An error occurred.';
167 alert(errorMsg); // eslint-disable-line no-alert
168 }
169 }).catch(function () {
170 alert('Network error. Please try again.'); // eslint-disable-line no-alert
171 });
172
173 // Close the dialog.
174 var overlay = btn.closest('.wp2fa-profile__confirm-overlay');
175 if (overlay) {
176 overlay.classList.remove('wp2fa-profile__confirm--open');
177 }
178 });
179
180 // -----------------------------------------------------------------
181 // -----------------------------------------------------------------
182 // Generate backup codes — handled by wp2fa-profile-backup-codes.js
183 // -----------------------------------------------------------------
184
185 // -----------------------------------------------------------------
186 // Auto-open wizard on page load if required
187 // -----------------------------------------------------------------
188 if (wp2faProfileData && wp2faProfileData.autoOpenWizard) {
189 // Retry with increasing delay to ensure wizard scripts have initialized.
190 var attempts = 0;
191 var maxAttempts = 10;
192 function tryOpenWizard() {
193 var wizardBtn = container.querySelector('[data-wp2fa-open-wizard]');
194 if (wizardBtn && typeof wp !== 'undefined' && wp.hooks && typeof wp2faWizardData !== 'undefined') {
195 wizardBtn.click();
196 } else if (++attempts < maxAttempts) {
197 setTimeout(tryOpenWizard, 300);
198 }
199 }
200 setTimeout(tryOpenWizard, 300);
201 }
202 });
203 })();
204