updated plugin WP-WebAuthn version 1.4.1

This commit is contained in:
2026-06-03 21:27:37 +00:00
committed by Gitium
parent e4b9b8235b
commit 5cd2237fad
39 changed files with 2660 additions and 1265 deletions

View File

@ -0,0 +1,45 @@
= 1.2.8 =
Fix: privilege check for admin panel
= 1.2.7 =
Add: Now a security warning will be shown if user verification is disabled
Fix: Style broken with some locales
Fix: privilege check for admin panel (thanks to @vanpop)
Update: Third party libraries
= 1.2.6 =
Update: Third party libraries
= 1.2.5 =
Update: German translation (thanks to niiconn)
Fix: HTTPS check
= 1.2.4 =
Add: French translation (thanks to Spomky) and Turkish translate (thanks to Sn0bzy)
Fix: HTTPS check
Update: Existing translations
Update: Third party libraries
= 1.2.3 =
Feat: Avoid locking users out if WebAuthn is not available
Update: translations
Update: Third party libraries
= 1.2.2 =
Fix: Cannot access to js files in apache 2.4+
= 1.2.1 =
Feat: Allow to disable password login completely
Feat: Now we use WordPress transients instead of PHP sessions
Feat: Move register related settings to user's profile
Feat: Gutenberg block support
Feat: Traditional Chinese (Hong Kong) & Traditional Chinese (Taiwan) translation
Update: Chinese translation
Update: Third-party libraries
= 1.1.0 =
Add: Allow to remember login option
Add: Only allow a specific type of authenticator option
Fix: Toggle button may not working in login form
Update: Chinese translation
Update: Third-party libraries

View File

@ -51,4 +51,10 @@
} }
#wp-webauthn-uv-warning { #wp-webauthn-uv-warning {
margin: 15px 0 0; margin: 15px 0 0;
}
.wwa-table-svg {
display: inline-block;
margin-right: 5px;
vertical-align: middle;
width: 20px;
} }

View File

@ -1,5 +1,6 @@
#wp-webauthn { #wp-webauthn {
margin-right: 5px; margin-right: 5px;
width: 56px;
} }
#wp-webauthn span { #wp-webauthn span {
line-height: 30px; line-height: 30px;
@ -47,8 +48,32 @@
#loginform.wwa-webauthn-only { #loginform.wwa-webauthn-only {
padding-bottom: 30px; padding-bottom: 30px;
} }
#loginform .wwa-passkey-notice {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
#loginform .password-icon {
margin-right: 4px;
}
#loginform .password-icon::after {
display: inline-block;
content: "";
width: 1px;
height: 80%;
background-color: currentColor;
position: relative;
left: 3px;
bottom: 10%;
}
@media(max-width: 782px){ @media(max-width: 782px){
#wp-webauthn span { #wp-webauthn span {
line-height: 38px; line-height: 38px;
} }
}
@media(max-width: 782px){
.interim-login #wp-webauthn span {
line-height: 30px;
}
} }

View File

@ -33,7 +33,8 @@ function updateLog() {
url: php_vars.ajax_url, url: php_vars.ajax_url,
type: 'GET', type: 'GET',
data: { data: {
action: 'wwa_get_log' action: 'wwa_get_log',
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function (data) { success: function (data) {
if (typeof data === 'string') { if (typeof data === 'string') {
@ -71,7 +72,8 @@ jQuery('#clear_log').click((e) => {
url: php_vars.ajax_url, url: php_vars.ajax_url,
type: 'GET', type: 'GET',
data: { data: {
action: 'wwa_clear_log' action: 'wwa_clear_log',
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function () { success: function () {
updateLog(); updateLog();

View File

@ -8,44 +8,44 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
window.onload = () => { window.onload = () => {
if (php_vars.webauthn_only === 'true') { if (wwa_login_php_vars.webauthn_only === 'true') {
if ((window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function')) { if ((window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function')) {
// Not support, show a message // Not support, show a message
if (document.querySelectorAll('#login > h1').length > 0) { if (document.querySelectorAll('#login > h1').length > 0) {
let dom = document.createElement('p'); let dom = document.createElement('p');
dom.className = 'message'; dom.className = 'message';
dom.innerHTML = php_vars.i18n_8; dom.innerHTML = wwa_login_php_vars.i18n_8;
document.querySelectorAll('#login > h1')[0].parentNode.insertBefore(dom, document.querySelectorAll('#login > h1')[0].nextElementSibling) document.querySelectorAll('#login > h1')[0].parentNode.insertBefore(dom, document.querySelectorAll('#login > h1')[0].nextElementSibling)
} }
} }
wwa_dom('#loginform', (dom) => { dom.classList.add('wwa-webauthn-only') }); wwa_dom('#loginform', (dom) => { dom.classList.add('wwa-webauthn-only') });
if (document.getElementsByClassName('user-pass-wrap').length > 0) { if (document.getElementsByClassName('user-pass-wrap').length > 0) {
wwa_dom('.user-pass-wrap, #wp-submit', (dom) => { dom.parentNode.removeChild(dom) }); wwa_dom('.user-pass-wrap, #wp-submit', (dom) => { dom.parentNode.removeChild(dom) });
if (php_vars.remember_me === 'false' ) { if (wwa_login_php_vars.remember_me === 'false' ) {
wwa_dom('.forgetmenot', (dom) => { dom.parentNode.removeChild(dom) }); wwa_dom('.forgetmenot', (dom) => { dom.parentNode.removeChild(dom) });
} }
} else { } else {
// WordPress 5.2- // WordPress 5.2-
wwa_dom('#wp-submit', (dom) => { dom.parentNode.removeChild(dom) }); wwa_dom('#wp-submit', (dom) => { dom.parentNode.removeChild(dom) });
if (php_vars.remember_me === 'false' ) { if (wwa_login_php_vars.remember_me === 'false' ) {
wwa_dom('.forgetmenot', (dom) => { dom.parentNode.removeChild(dom) }); wwa_dom('.forgetmenot', (dom) => { dom.parentNode.removeChild(dom) });
} }
const targetDOM = document.getElementById('loginform').getElementsByTagName('p')[1]; const targetDOM = document.getElementById('loginform').getElementsByTagName('p')[1];
targetDOM.parentNode.removeChild(targetDOM); targetDOM.parentNode.removeChild(targetDOM);
} }
} }
if (!(window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function') || php_vars.webauthn_only === 'true') { if (!(window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function') || wwa_login_php_vars.webauthn_only === 'true') {
// If supported, toggle // If supported, toggle
if (php_vars.webauthn_only !== 'true') { if (wwa_login_php_vars.webauthn_only !== 'true') {
if (document.getElementsByClassName('user-pass-wrap').length > 0) { if (document.getElementsByClassName('user-pass-wrap').length > 0) {
wwa_dom('.user-pass-wrap, #wp-submit', (dom) => { dom.style.display = 'none' }); wwa_dom('.user-pass-wrap, #wp-submit', (dom) => { dom.style.display = 'none' });
if (php_vars.remember_me === 'false' ) { if (wwa_login_php_vars.remember_me === 'false' ) {
wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' }); wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' });
} }
} else { } else {
// WordPress 5.2- // WordPress 5.2-
wwa_dom('#wp-submit', (dom) => { dom.style.display = 'none' }); wwa_dom('#wp-submit', (dom) => { dom.style.display = 'none' });
if (php_vars.remember_me === 'false' ) { if (wwa_login_php_vars.remember_me === 'false' ) {
wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' }); wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' });
} }
document.getElementById('loginform').getElementsByTagName('p')[1].style.display = 'none'; document.getElementById('loginform').getElementsByTagName('p')[1].style.display = 'none';
@ -53,8 +53,17 @@ document.addEventListener('DOMContentLoaded', () => {
} }
wwa_dom('wp-webauthn-notice', (dom) => { dom.style.display = 'flex' }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.style.display = 'flex' }, 'class');
wwa_dom('wp-webauthn-check', (dom) => { dom.style.cssText = `${dom.style.cssText}display: block !important` }, 'id'); wwa_dom('wp-webauthn-check', (dom) => { dom.style.cssText = `${dom.style.cssText}display: block !important` }, 'id');
wwa_dom('user_login', (dom) => { dom.focus() }, 'id'); wwa_dom('user_login', (dom) => {
dom.setAttribute('autocomplete', 'username webauthn');
setTimeout(() => {
dom.focus();
}, 0);
}, 'id');
wwa_dom('wp-submit', (dom) => { dom.disabled = true }, 'id'); wwa_dom('wp-submit', (dom) => { dom.disabled = true }, 'id');
// Start Conditional UI (passkey autofill) once WebAuthn mode is active
if (typeof wwa_start_conditional_ui === 'function') {
wwa_start_conditional_ui();
}
} }
if (document.querySelectorAll('#lostpasswordform, #registerform, .admin-email-confirm-form, #resetpassform').length > 0) { if (document.querySelectorAll('#lostpasswordform, #registerform, .admin-email-confirm-form, #resetpassform').length > 0) {
return; return;
@ -64,9 +73,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (dom.length > 0) { if (dom.length > 0) {
if (dom[0].getElementsByTagName('input').length > 0) { if (dom[0].getElementsByTagName('input').length > 0) {
// WordPress 5.2- // WordPress 5.2-
dom[0].innerHTML = `<span id="wwa-username-label">${php_vars.email_login === 'true' ? php_vars.i18n_10 : php_vars.i18n_9}</span>${dom[0].innerHTML.split('<br>')[1]}`; dom[0].innerHTML = `<span id="wwa-username-label">${wwa_login_php_vars.email_login === 'true' ? wwa_login_php_vars.i18n_10 : wwa_login_php_vars.i18n_9}</span>${dom[0].innerHTML.split('<br>')[1]}`;
} else { } else {
dom[0].innerText = php_vars.email_login === 'true' ? php_vars.i18n_10 : php_vars.i18n_9; dom[0].innerText = wwa_login_php_vars.email_login === 'true' ? wwa_login_php_vars.i18n_10 : wwa_login_php_vars.i18n_9;
} }
} }
} }

View File

@ -305,7 +305,8 @@ function wwa_bind() {
alert(wwa_php_vars.i18n_12); alert(wwa_php_vars.i18n_12);
return; return;
} }
let wwa_type = this.parentNode.parentNode.getElementsByClassName('wwa-authenticator-type')[0].value; const wwa_type_el = this.parentNode.parentNode.getElementsByClassName('wwa-authenticator-type')[0];
let wwa_type = wwa_type_el ? wwa_type_el.value : (wwa_php_vars.allow_authenticator_type !== 'none' ? wwa_php_vars.allow_authenticator_type : 'none');
let wwa_usernameless = this.parentNode.parentNode.querySelectorAll('.wwa-authenticator-usernameless:checked')[0] ? this.parentNode.parentNode.querySelectorAll('.wwa-authenticator-usernameless:checked')[0].value : 'false'; let wwa_usernameless = this.parentNode.parentNode.querySelectorAll('.wwa-authenticator-usernameless:checked')[0] ? this.parentNode.parentNode.querySelectorAll('.wwa-authenticator-usernameless:checked')[0].value : 'false';
button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_3; button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_3;
wwa_disable_buttons(); wwa_disable_buttons();
@ -314,7 +315,7 @@ function wwa_bind() {
wwa_dom('wwa-authenticator-type', (dom) => { dom.disabled = true }, 'class'); wwa_dom('wwa-authenticator-type', (dom) => { dom.disabled = true }, 'class');
wwa_dom('wwa-authenticator-usernameless', (dom) => { dom.disabled = true }, 'class'); wwa_dom('wwa-authenticator-usernameless', (dom) => { dom.disabled = true }, 'class');
let request = wwa_ajax(); let request = wwa_ajax();
request.get(wwa_php_vars.ajax_url, `?action=wwa_create&name=${encodeURIComponent(wwa_name)}&type=${encodeURIComponent(wwa_type)}&usernameless=${wwa_usernameless}`, (rawData, status) => { request.get(wwa_php_vars.ajax_url, `?action=wwa_create&name=${encodeURIComponent(wwa_name)}&type=${encodeURIComponent(wwa_type)}&usernameless=${wwa_usernameless}&_ajax_nonce=${wwa_php_vars._ajax_nonce}`, (rawData, status) => {
if (status) { if (status) {
button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_28; button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_28;
let data = rawData; let data = rawData;
@ -381,7 +382,7 @@ function wwa_bind() {
return publicKeyCredential; return publicKeyCredential;
}).then(JSON.stringify).then((AuthenticatorAttestationResponse) => { }).then(JSON.stringify).then((AuthenticatorAttestationResponse) => {
let response = wwa_ajax(); let response = wwa_ajax();
response.post(`${wwa_php_vars.ajax_url}?action=wwa_create_response`, `data=${encodeURIComponent(window.btoa(AuthenticatorAttestationResponse))}&name=${encodeURIComponent(wwa_name)}&type=${encodeURIComponent(wwa_type)}&usernameless=${wwa_usernameless}&clientid=${clientID}`, (rawData, status) => { response.post(`${wwa_php_vars.ajax_url}?action=wwa_create_response`, `data=${encodeURIComponent(window.btoa(AuthenticatorAttestationResponse))}&name=${encodeURIComponent(wwa_name)}&type=${encodeURIComponent(wwa_type)}&usernameless=${wwa_usernameless}&clientid=${clientID}&_ajax_nonce=${wwa_php_vars._ajax_nonce}`, (rawData, status) => {
if (status) { if (status) {
if (rawData === 'true') { if (rawData === 'true') {
button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_29; button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_29;
@ -521,19 +522,33 @@ function wwa_verify() {
} }
// Update authenticator list // Update authenticator list
// Compute current number of visible columns for colspan
function getFrontendColspan() {
let cols = 4; // Identifier, Registered, Last used, Action
const typeTh = document.getElementsByClassName('wwa-type-th')[0];
if (typeTh && typeTh.style.display !== 'none') {
cols++;
}
const ulTh = document.getElementsByClassName('wwa-usernameless-th')[0];
if (ulTh && ulTh.style.display !== 'none') {
cols++;
}
return cols;
}
function updateList() { function updateList() {
if (document.getElementsByClassName('wwa-authenticator-list').length === 0) { if (document.getElementsByClassName('wwa-authenticator-list').length === 0) {
return; return;
} }
let request = wwa_ajax(); let request = wwa_ajax();
request.get(wwa_php_vars.ajax_url, '?action=wwa_authenticator_list', (rawData, status) => { request.get(wwa_php_vars.ajax_url, `?action=wwa_authenticator_list&_ajax_nonce=${wwa_php_vars._ajax_nonce}`, (rawData, status) => {
if (status) { if (status) {
let data = rawData; let data = rawData;
try { try {
data = JSON.parse(rawData); data = JSON.parse(rawData);
} catch (e) { } catch (e) {
console.warn(rawData); console.warn(rawData);
wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = `<tr><td colspan="${document.getElementsByClassName('wwa-usernameless-th')[0].style.display === 'none' ? '5' : '6'}">${wwa_php_vars.i18n_17}</td></tr>` }, 'class'); wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = `<tr><td colspan="${getFrontendColspan()}">${wwa_php_vars.i18n_17}</td></tr>` }, 'class');
return; return;
} }
if (data.length === 0) { if (data.length === 0) {
@ -542,7 +557,7 @@ function updateList() {
} else { } else {
wwa_dom('.wwa-usernameless-th, .wwa-usernameless-td', (dom) => { dom.style.display = 'none' }); wwa_dom('.wwa-usernameless-th, .wwa-usernameless-td', (dom) => { dom.style.display = 'none' });
} }
wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = `<tr><td colspan="${document.getElementsByClassName('wwa-usernameless-th')[0].style.display === 'none' ? '5' : '6'}">${wwa_php_vars.i18n_23}</td></tr>` }, 'class'); wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = `<tr><td colspan="${getFrontendColspan()}">${wwa_php_vars.i18n_23}</td></tr>` }, 'class');
wwa_dom('wwa-authenticator-list-usernameless-tip', (dom) => { dom.innerText = '' }, 'class'); wwa_dom('wwa-authenticator-list-usernameless-tip', (dom) => { dom.innerText = '' }, 'class');
wwa_dom('wwa-authenticator-list-type-tip', (dom) => { dom.innerText = '' }, 'class'); wwa_dom('wwa-authenticator-list-type-tip', (dom) => { dom.innerText = '' }, 'class');
return; return;
@ -561,7 +576,7 @@ function updateList() {
item_type_disabled = true; item_type_disabled = true;
} }
} }
htmlStr += `<tr><td>${item.name}</td><td>${item.type === 'none' ? wwa_php_vars.i18n_24 : (item.type === 'platform' ? wwa_php_vars.i18n_25 : wwa_php_vars.i18n_26)}${item_type_disabled ? wwa_php_vars.i18n_35 : ''}</td><td>${item.added}</td><td>${item.last_used}</td><td class="wwa-usernameless-td">${item.usernameless ? wwa_php_vars.i18n_1 + (wwa_php_vars.usernameless === 'true' ? '' : wwa_php_vars.i18n_9) : wwa_php_vars.i18n_8}</td><td class="wwa-key-${item.key}"><a href="javascript:renameAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${wwa_php_vars.i18n_20}</a> | <a href="javascript:removeAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${wwa_php_vars.i18n_27}</a></td></tr>`; htmlStr += `<tr><td>${item.name}</td>${wwa_php_vars.show_authenticator_type === 'true' ? `<td class="wwa-type-td">${item.type === 'none' ? wwa_php_vars.i18n_24 : (item.type === 'platform' ? wwa_php_vars.i18n_25 : wwa_php_vars.i18n_26)}${item_type_disabled ? wwa_php_vars.i18n_35 : ''}</td>` : ''}<td>${item.added}</td><td>${item.last_used}</td><td class="wwa-usernameless-td">${item.usernameless ? wwa_php_vars.i18n_1 + (wwa_php_vars.usernameless === 'true' ? '' : wwa_php_vars.i18n_9) : wwa_php_vars.i18n_8}</td><td class="wwa-key-${item.key}"><a href="javascript:renameAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${wwa_php_vars.i18n_20}</a> | <a href="javascript:removeAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${wwa_php_vars.i18n_27}</a></td></tr>`;
} }
wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = htmlStr }, 'class'); wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = htmlStr }, 'class');
if (has_usernameless || wwa_php_vars.usernameless === 'true') { if (has_usernameless || wwa_php_vars.usernameless === 'true') {
@ -586,7 +601,7 @@ function updateList() {
wwa_dom('wwa-authenticator-list-type-tip', (dom) => { dom.innerText = ''; dom.style.display = 'none' }, 'class'); wwa_dom('wwa-authenticator-list-type-tip', (dom) => { dom.innerText = ''; dom.style.display = 'none' }, 'class');
} }
} else { } else {
wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = `<tr><td colspan="${document.getElementsByClassName('wwa-usernameless-th')[0].style.display === 'none' ? '5' : '6'}">${wwa_php_vars.i18n_17}</td></tr>` }, 'class'); wwa_dom('wwa-authenticator-list', (dom) => { dom.innerHTML = `<tr><td colspan="${getFrontendColspan()}">${wwa_php_vars.i18n_17}</td></tr>` }, 'class');
} }
}) })
} }
@ -603,7 +618,7 @@ function renameAuthenticator(id, name) {
} else if (new_name !== null && new_name !== name) { } else if (new_name !== null && new_name !== name) {
let request = wwa_ajax(); let request = wwa_ajax();
wwa_dom(`wwa-key-${id}`, (dom) => { dom.innerText = wwa_php_vars.i18n_22 }, 'class'); wwa_dom(`wwa-key-${id}`, (dom) => { dom.innerText = wwa_php_vars.i18n_22 }, 'class');
request.get(wwa_php_vars.ajax_url, `?action=wwa_modify_authenticator&id=${encodeURIComponent(id)}&name=${encodeURIComponent(new_name)}&target=rename`, (data, status) => { request.get(wwa_php_vars.ajax_url, `?action=wwa_modify_authenticator&id=${encodeURIComponent(id)}&name=${encodeURIComponent(new_name)}&target=rename&_ajax_nonce=${wwa_php_vars._ajax_nonce}`, (data, status) => {
if (status) { if (status) {
updateList(); updateList();
} else { } else {
@ -623,7 +638,7 @@ function removeAuthenticator(id, name) {
if (confirm(wwa_php_vars.i18n_18 + name + (document.getElementsByClassName('wwa-authenticator-list')[0].children.length === 1 ? '\n' + wwa_php_vars.i18n_34 : ''))) { if (confirm(wwa_php_vars.i18n_18 + name + (document.getElementsByClassName('wwa-authenticator-list')[0].children.length === 1 ? '\n' + wwa_php_vars.i18n_34 : ''))) {
wwa_dom(`wwa-key-${id}`, (dom) => { dom.innerText = wwa_php_vars.i18n_19 }, 'class'); wwa_dom(`wwa-key-${id}`, (dom) => { dom.innerText = wwa_php_vars.i18n_19 }, 'class');
let request = wwa_ajax(); let request = wwa_ajax();
request.get(wwa_php_vars.ajax_url, `?action=wwa_modify_authenticator&id=${encodeURIComponent(id)}&target=remove`, (data, status) => { request.get(wwa_php_vars.ajax_url, `?action=wwa_modify_authenticator&id=${encodeURIComponent(id)}&target=remove&_ajax_nonce=${wwa_php_vars._ajax_nonce}`, (data, status) => {
if (status) { if (status) {
updateList(); updateList();
} else { } else {

View File

@ -1,5 +1,8 @@
'use strict'; 'use strict';
const wwa_passkey_notice_svg = '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 216 216;width:28px" viewBox="0 0 216 216"><style>.st2{fill-rule:evenodd;clip-rule:evenodd}</style><g id="Isolation_Mode"><path d="M0 0h216v216H0z" style="fill:none"/><path d="M172.32 96.79c0 13.78-8.48 25.5-20.29 29.78l7.14 11.83-10.57 13 10.57 12.71-17.04 22.87-12.01-12.82V125.7c-10.68-4.85-18.15-15.97-18.15-28.91 0-17.4 13.51-31.51 30.18-31.51 16.66 0 30.17 14.11 30.17 31.51zm-30.18 4.82c4.02 0 7.28-3.4 7.28-7.6 0-4.2-3.26-7.61-7.28-7.61s-7.28 3.4-7.28 7.61c-.01 4.2 3.26 7.6 7.28 7.6z" style="fill-rule:evenodd;clip-rule:evenodd;fill:#353535"/><path d="M172.41 96.88c0 13.62-8.25 25.23-19.83 29.67l6.58 11.84-9.73 13 9.73 12.71-17.03 23.05v-85.54c4.02 0 7.28-3.41 7.28-7.6 0-4.2-3.26-7.61-7.28-7.61V65.28c16.73 0 30.28 14.15 30.28 31.6zM120.24 131.43c-9.75-8-16.3-20.3-17.2-34.27H50.8c-10.96 0-19.84 9.01-19.84 20.13v25.17c0 5.56 4.44 10.07 9.92 10.07h69.44c5.48 0 9.92-4.51 9.92-10.07v-11.03z" class="st2"/><path d="M73.16 91.13c-2.42-.46-4.82-.89-7.11-1.86-8.65-3.63-13.69-10.32-15.32-19.77-1.12-6.47-.59-12.87 2.03-18.92 3.72-8.6 10.39-13.26 19.15-14.84 5.24-.94 10.46-.73 15.5 1.15 7.59 2.82 12.68 8.26 15.03 16.24 2.38 8.05 2.03 16.1-1.56 23.72-3.72 7.96-10.21 12.23-18.42 13.9-.68.14-1.37.27-2.05.41-2.41-.03-4.83-.03-7.25-.03z" style="fill:#141313"/></g></svg>';
const wwa_passkey_btn_svg = '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 216 216;width:28px" viewBox="0 0 216 216"><style>.st2{fill-rule:evenodd;clip-rule:evenodd}</style><g id="Isolation_Mode"><path d="M0 0h216v216H0z" style="fill:none"/><path d="M172.32 96.79c0 13.78-8.48 25.5-20.29 29.78l7.14 11.83-10.57 13 10.57 12.71-17.04 22.87-12.01-12.82V125.7c-10.68-4.85-18.15-15.97-18.15-28.91 0-17.4 13.51-31.51 30.18-31.51 16.66 0 30.17 14.11 30.17 31.51zm-30.18 4.82c4.02 0 7.28-3.4 7.28-7.6 0-4.2-3.26-7.61-7.28-7.61s-7.28 3.4-7.28 7.61c-.01 4.2 3.26 7.6 7.28 7.6z" style="fill-rule:evenodd;clip-rule:evenodd;fill:currentcolor"/><path d="M172.41 96.88c0 13.62-8.25 25.23-19.83 29.67l6.58 11.84-9.73 13 9.73 12.71-17.03 23.05v-85.54c4.02 0 7.28-3.41 7.28-7.6 0-4.2-3.26-7.61-7.28-7.61V65.28c16.73 0 30.28 14.15 30.28 31.6zM120.24 131.43c-9.75-8-16.3-20.3-17.2-34.27H50.8c-10.96 0-19.84 9.01-19.84 20.13v25.17c0 5.56 4.44 10.07 9.92 10.07h69.44c5.48 0 9.92-4.51 9.92-10.07v-11.03z" class="st2" style="fill:currentcolor"/><path d="M73.16 91.13c-2.42-.46-4.82-.89-7.11-1.86-8.65-3.63-13.69-10.32-15.32-19.77-1.12-6.47-.59-12.87 2.03-18.92 3.72-8.6 10.39-13.26 19.15-14.84 5.24-.94 10.46-.73 15.5 1.15 7.59 2.82 12.68 8.26 15.03 16.24 2.38 8.05 2.03 16.1-1.56 23.72-3.72 7.96-10.21 12.23-18.42 13.9-.68.14-1.37.27-2.05.41-2.41-.03-4.83-.03-7.25-.03z" style="fill:currentcolor"/></g></svg>';
// Send an AJAX request and get the response // Send an AJAX request and get the response
const wwa_ajax = function () { const wwa_ajax = function () {
let xmlHttpReq = new XMLHttpRequest(); let xmlHttpReq = new XMLHttpRequest();
@ -70,19 +73,154 @@ const wwa_dom = (selector, callback = () => { }, method = 'query') => {
} }
let wwaSupported = true; let wwaSupported = true;
let wwa_conditional_ui_abort = null;
let wwa_conditional_ui_active = false;
/**
* Start a Conditional UI (passkey autofill) request.
* The browser will show a passkey picker in the username autocomplete dropdown.
* Aborted automatically when the user manually triggers a normal WebAuthn check.
*/
function wwa_start_conditional_ui() {
if (wwa_conditional_ui_active) return;
if (!window.PublicKeyCredential) return;
// Prefer getClientCapabilities() (Chrome 128+) for a richer capability check;
// fall back to isConditionalMediationAvailable() for older browsers.
const conditionalAvailablePromise =
typeof PublicKeyCredential.getClientCapabilities === 'function'
? PublicKeyCredential.getClientCapabilities().then((caps) => !!caps.conditionalGet)
: typeof PublicKeyCredential.isConditionalMediationAvailable === 'function'
? PublicKeyCredential.isConditionalMediationAvailable()
: Promise.resolve(false);
conditionalAvailablePromise.then((available) => {
if (!available) return;
wwa_conditional_ui_active = true;
wwa_conditional_ui_abort = new AbortController();
const signal = wwa_conditional_ui_abort.signal;
// Request an empty-user challenge (usernameless style: no allowCredentials)
let startReq = wwa_ajax();
startReq.get(wwa_login_php_vars.ajax_url,
'?action=wwa_auth_start&type=auth&usernameless=true&conditional=true',
(rawData, status) => {
if (!status || signal.aborted) {
wwa_conditional_ui_active = false;
return;
}
let data;
try {
data = JSON.parse(rawData);
} catch (e) {
wwa_conditional_ui_active = false;
return;
}
data.challenge = Uint8Array.from(
window.atob(base64url2base64(data.challenge)),
(c) => c.charCodeAt(0)
);
if (data.allowCredentials) {
data.allowCredentials = data.allowCredentials.map((item) => {
item.id = Uint8Array.from(
window.atob(base64url2base64(item.id)),
(c) => c.charCodeAt(0)
);
return item;
});
}
const clientID = data.clientID;
delete data.clientID;
navigator.credentials.get({
publicKey: data,
mediation: 'conditional',
signal: signal
}).then((credentialInfo) => {
if (!credentialInfo) {
wwa_conditional_ui_active = false;
return;
}
// Show authenticating state
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_5; }, 'class');
wwa_dom('user_login', (dom) => { dom.readOnly = true; }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = true; });
const publicKeyCredential = {
id: credentialInfo.id,
type: credentialInfo.type,
rawId: arrayToBase64String(new Uint8Array(credentialInfo.rawId)),
response: {
authenticatorData: arrayToBase64String(new Uint8Array(credentialInfo.response.authenticatorData)),
clientDataJSON: arrayToBase64String(new Uint8Array(credentialInfo.response.clientDataJSON)),
signature: arrayToBase64String(new Uint8Array(credentialInfo.response.signature)),
userHandle: credentialInfo.response.userHandle
? arrayToBase64String(new Uint8Array(credentialInfo.response.userHandle))
: null
}
};
const AuthenticatorResponse = JSON.stringify(publicKeyCredential);
// The server needs the usernameless path, so username is empty
const userField = document.getElementById('user_login');
const username = userField ? userField.value : '';
let response = wwa_ajax();
response.post(
`${wwa_login_php_vars.ajax_url}?action=wwa_auth`,
`data=${encodeURIComponent(window.btoa(AuthenticatorResponse))}&type=auth&clientid=${clientID}&user=${encodeURIComponent(username)}&conditional=true&remember=${wwa_login_php_vars.remember_me === 'false' ? 'false' : (document.getElementById('rememberme') ? (document.getElementById('rememberme').checked ? 'true' : 'false') : 'false')}` ,
(resData, resStatus) => {
wwa_conditional_ui_active = false;
if (resStatus && resData === 'true') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_6; }, 'class');
const redirectInput = document.querySelector('p.submit input[name="redirect_to"]');
const redirectTo = redirectInput
? redirectInput.value
: (getQueryString('redirect_to') || wwa_login_php_vars.admin_url);
setTimeout(() => { window.location.href = redirectTo; }, 200);
} else {
wwa_dom('wp-webauthn-notice', (dom) => {
dom.innerHTML = wwa_login_php_vars.terminology === 'passkey'
? `<span class="wwa-passkey-notice">${wwa_passkey_notice_svg} ${wwa_login_php_vars.i18n_2}</span>`
: `<span><span class="dashicons dashicons-shield-alt"></span> ${wwa_login_php_vars.i18n_2}</span>`;
}, 'class');
wwa_dom('user_login', (dom) => { dom.readOnly = false; }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false; });
// Restart Conditional UI after a failed attempt
wwa_start_conditional_ui();
}
}
);
}).catch((err) => {
wwa_conditional_ui_active = false;
// AbortError is expected when check() aborts — do nothing
if (err && err.name !== 'AbortError') {
console.warn('WP-WebAuthn Conditional UI error:', err);
}
});
}
);
}).catch(() => {
// isConditionalMediationAvailable not supported or threw — silently ignore
});
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('p#nav') && php_vars.password_reset !== 'false') { if (document.querySelector('p#nav') && wwa_login_php_vars.password_reset !== 'false') {
const placeholder = document.getElementById('wwa-lost-password-link-placeholder'); const placeholder = document.getElementById('wwa-lost-password-link-placeholder');
if (placeholder) { if (placeholder) {
const previous = placeholder.previousSibling; const previous = placeholder.previousSibling;
const next = placeholder.nextElementSibling; const next = placeholder.nextElementSibling;
if (previous && previous.nodeType === Node.TEXT_NODE && previous.data.trim() === php_vars.separator.trim()) { if (previous && previous.nodeType === Node.TEXT_NODE && previous.data.trim() === wwa_login_php_vars.separator.trim()) {
previous.remove(); previous.remove();
} else if (next && next.nodeType === Node.TEXT_NODE && next.data.trim() === php_vars.separator.trim()) { } else if (next && next.nodeType === Node.TEXT_NODE && next.data.trim() === wwa_login_php_vars.separator.trim() && !next.nextElementSibling) {
next.remove(); next.remove();
} }
placeholder.remove();
} }
placeholder.remove();
} }
if (document.querySelectorAll('#lostpasswordform, #registerform, .admin-email-confirm-form, #resetpassform').length > 0) { if (document.querySelectorAll('#lostpasswordform, #registerform, .admin-email-confirm-form, #resetpassform').length > 0) {
return; return;
@ -95,24 +233,25 @@ document.addEventListener('DOMContentLoaded', () => {
button_check.id = 'wp-webauthn-check'; button_check.id = 'wp-webauthn-check';
button_check.type = 'button'; button_check.type = 'button';
button_check.className = 'button button-large button-primary'; button_check.className = 'button button-large button-primary';
button_check.innerHTML = php_vars.i18n_1; button_check.innerHTML = wwa_login_php_vars.i18n_1;
let button_toggle = document.createElement('button'); let button_toggle = document.createElement('button');
if (php_vars.webauthn_only !== 'true') { if (wwa_login_php_vars.webauthn_only !== 'true') {
button_toggle.id = 'wp-webauthn'; button_toggle.id = 'wp-webauthn';
button_toggle.type = 'button'; button_toggle.type = 'button';
button_toggle.className = 'button button-large'; button_toggle.className = 'button button-large';
button_toggle.innerHTML = '<span class="dashicons dashicons-update-alt"></span>'; button_toggle.innerHTML = '<span class="dashicons dashicons-ellipsis password-icon"></span>';
button_toggle.title = wwa_login_php_vars.i18n_13;
} }
let submit = document.getElementById('wp-submit'); let submit = document.getElementById('wp-submit');
if (submit) { if (submit) {
if (php_vars.webauthn_only !== 'true') { if (wwa_login_php_vars.webauthn_only !== 'true') {
submit.parentNode.insertBefore(button_toggle, submit.nextElementSibling); submit.parentNode.insertBefore(button_toggle, submit.nextElementSibling);
} }
submit.parentNode.insertBefore(button_check, submit.nextElementSibling); submit.parentNode.insertBefore(button_check, submit.nextElementSibling);
} }
let notice = document.createElement('div'); let notice = document.createElement('div');
notice.className = 'wp-webauthn-notice'; notice.className = 'wp-webauthn-notice';
notice.innerHTML = `<span><span class="dashicons dashicons-shield-alt"></span> ${php_vars.i18n_2}<span>`; notice.innerHTML = wwa_login_php_vars.terminology ==='passkey' ? `<span class="wwa-passkey-notice">${wwa_passkey_notice_svg} ${wwa_login_php_vars.i18n_2}</span>` : `<span><span class="dashicons dashicons-shield-alt"></span> ${wwa_login_php_vars.i18n_2}</span>`;
let forgetmenot = document.getElementsByClassName('forgetmenot'); let forgetmenot = document.getElementsByClassName('forgetmenot');
if (forgetmenot.length > 0) { if (forgetmenot.length > 0) {
forgetmenot[0].parentNode.insertBefore(notice, forgetmenot[0]); forgetmenot[0].parentNode.insertBefore(notice, forgetmenot[0]);
@ -211,46 +350,71 @@ function toggle() {
wwa_dom('wp-webauthn-notice', (dom) => { dom.style.display = 'none' }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.style.display = 'none' }, 'class');
wwa_dom('wp-webauthn-check', (dom) => { dom.style.cssText = `${dom.style.cssText.split('display: block !important')[0]}display: none !important` }, 'id'); wwa_dom('wp-webauthn-check', (dom) => { dom.style.cssText = `${dom.style.cssText.split('display: block !important')[0]}display: none !important` }, 'id');
wwa_dom('user_pass', (dom) => { dom.disabled = false }, 'id'); wwa_dom('user_pass', (dom) => { dom.disabled = false }, 'id');
wwa_dom('user_login', (dom) => { dom.focus() }, 'id'); wwa_dom('user_login', (dom) => {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = `<span><span class="dashicons dashicons-shield-alt"></span> ${php_vars.i18n_2}</span>` }, 'class'); dom.setAttribute('autocomplete', 'username');
setTimeout(() => {
dom.focus();
}, 0);
}, 'id');
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.terminology ==='passkey' ? `<span class="wwa-passkey-notice">${wwa_passkey_notice_svg} ${wwa_login_php_vars.i18n_2}</span>` : `<span><span class="dashicons dashicons-shield-alt"></span> ${wwa_login_php_vars.i18n_2}</span>` }, 'class');
wwa_dom('wp-submit', (dom) => { dom.disabled = false }, 'id'); wwa_dom('wp-submit', (dom) => { dom.disabled = false }, 'id');
wwa_dom('wp-webauthn', wwa_login_php_vars.terminology ==='passkey' ? (dom) => {
dom.innerHTML = wwa_passkey_btn_svg;
dom.title = wwa_login_php_vars.i18n_14;
dom.style.lineHeight = '0';
} : (dom) => {
dom.innerHTML = '<span class="dashicons dashicons-shield-alt"></span>';
dom.title = wwa_login_php_vars.i18n_14;
}, 'id');
let inputDom = document.querySelectorAll('#loginform label') let inputDom = document.querySelectorAll('#loginform label')
if (inputDom.length > 0) { if (inputDom.length > 0) {
if (document.getElementById('wwa-username-label')) { if (document.getElementById('wwa-username-label')) {
// WordPress 5.2- // WordPress 5.2-
document.getElementById('wwa-username-label').innerText = php_vars.i18n_10; document.getElementById('wwa-username-label').innerText = wwa_login_php_vars.i18n_10;
} else { } else {
inputDom[0].innerText = php_vars.i18n_10; inputDom[0].innerText = wwa_login_php_vars.i18n_10;
} }
} }
} else { } else {
if (document.getElementsByClassName('user-pass-wrap').length > 0) { if (document.getElementsByClassName('user-pass-wrap').length > 0) {
wwa_dom('.user-pass-wrap, #wp-submit', (dom) => { dom.style.display = 'none' }); wwa_dom('.user-pass-wrap, #wp-submit', (dom) => { dom.style.display = 'none' });
if (php_vars.remember_me === 'false' ) { if (wwa_login_php_vars.remember_me === 'false' ) {
wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' }); wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' });
} }
} else { } else {
// WordPress 5.2- // WordPress 5.2-
wwa_dom('#wp-submit', (dom) => { dom.style.display = 'none' }); wwa_dom('#wp-submit', (dom) => { dom.style.display = 'none' });
if (php_vars.remember_me === 'false' ) { if (wwa_login_php_vars.remember_me === 'false' ) {
wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' }); wwa_dom('.forgetmenot', (dom) => { dom.style.display = 'none' });
} }
document.getElementById('loginform').getElementsByTagName('p')[1].style.display = 'none'; document.getElementById('loginform').getElementsByTagName('p')[1].style.display = 'none';
} }
wwa_dom('wp-webauthn-notice', (dom) => { dom.style.display = 'flex' }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.style.display = 'flex' }, 'class');
wwa_dom('wp-webauthn-check', (dom) => { dom.style.cssText = `${dom.style.cssText.split('display: none !important')[0]}display: block !important` }, 'id'); wwa_dom('wp-webauthn-check', (dom) => { dom.style.cssText = `${dom.style.cssText.split('display: none !important')[0]}display: block !important` }, 'id');
wwa_dom('user_login', (dom) => { dom.focus() }, 'id'); wwa_dom('user_login', (dom) => {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = `<span><span class="dashicons dashicons-shield-alt"></span> ${php_vars.i18n_2}</span>` }, 'class'); dom.setAttribute('autocomplete', 'username webauthn');
setTimeout(() => {
dom.focus();
}, 0);
}, 'id');
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.terminology ==='passkey' ? `<span class="wwa-passkey-notice">${wwa_passkey_notice_svg} ${wwa_login_php_vars.i18n_2}</span>` : `<span><span class="dashicons dashicons-shield-alt"></span> ${wwa_login_php_vars.i18n_2}</span>` }, 'class');
wwa_dom('wp-submit', (dom) => { dom.disabled = true }, 'id'); wwa_dom('wp-submit', (dom) => { dom.disabled = true }, 'id');
wwa_dom('wp-webauthn', (dom) => {
dom.innerHTML = '<span class="dashicons dashicons-ellipsis password-icon"></span>';
dom.title = wwa_login_php_vars.i18n_13;
dom.style.lineHeight = '';
}, 'id');
let inputDom = document.querySelectorAll('#loginform label') let inputDom = document.querySelectorAll('#loginform label')
if (inputDom.length > 0) { if (inputDom.length > 0) {
if (document.getElementById('wwa-username-label')) { if (document.getElementById('wwa-username-label')) {
// WordPress 5.2- // WordPress 5.2-
document.getElementById('wwa-username-label').innerText = php_vars.email_login === 'true' ? php_vars.i18n_10 : php_vars.i18n_9; document.getElementById('wwa-username-label').innerText = wwa_login_php_vars.email_login === 'true' ? wwa_login_php_vars.i18n_10 : wwa_login_php_vars.i18n_9;
} else { } else {
inputDom[0].innerText = php_vars.email_login === 'true' ? php_vars.i18n_10 : php_vars.i18n_9; inputDom[0].innerText = wwa_login_php_vars.email_login === 'true' ? wwa_login_php_vars.i18n_10 : wwa_login_php_vars.i18n_9;
} }
} }
// Start Conditional UI when switching into WebAuthn mode
wwa_start_conditional_ui();
} }
} }
} }
@ -276,13 +440,13 @@ function check() {
return; return;
} }
if (wwaSupported) { if (wwaSupported) {
if (document.getElementById('user_login').value === '' && php_vars.usernameless !== 'true') { if (document.getElementById('user_login').value === '' && wwa_login_php_vars.usernameless !== 'true') {
wwa_dom('login_error', (dom) => { dom.remove() }, 'id'); wwa_dom('login_error', (dom) => { dom.remove() }, 'id');
wwa_dom('p.message', (dom) => { dom.remove() }); wwa_dom('p.message', (dom) => { dom.remove() });
if (document.querySelectorAll('#login > h1').length > 0) { if (document.querySelectorAll('#login > h1').length > 0) {
let dom = document.createElement('div'); let dom = document.createElement('div');
dom.id = 'login_error'; dom.id = 'login_error';
dom.innerHTML = php_vars.i18n_11; dom.innerHTML = wwa_login_php_vars.i18n_11;
document.querySelectorAll('#login > h1')[0].parentNode.insertBefore(dom, document.querySelectorAll('#login > h1')[0].nextElementSibling) document.querySelectorAll('#login > h1')[0].parentNode.insertBefore(dom, document.querySelectorAll('#login > h1')[0].nextElementSibling)
} }
// Shake the login form, code from WordPress // Shake the login form, code from WordPress
@ -293,22 +457,28 @@ function check() {
wwa_shake(form, shake, 20); wwa_shake(form, shake, 20);
return; return;
} }
// Abort any pending Conditional UI before starting a new modal request
if (wwa_conditional_ui_abort) {
wwa_conditional_ui_abort.abort();
wwa_conditional_ui_abort = null;
wwa_conditional_ui_active = false;
}
wwa_dom('user_login', (dom) => { dom.readOnly = true }, 'id'); wwa_dom('user_login', (dom) => { dom.readOnly = true }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = true }); wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = true });
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_3 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_3 }, 'class');
let request = wwa_ajax(); let request = wwa_ajax();
request.get(php_vars.ajax_url, `?action=wwa_auth_start&user=${encodeURIComponent(document.getElementById('user_login').value)}&type=auth`, (rawData, status) => { request.get(wwa_login_php_vars.ajax_url, `?action=wwa_auth_start&user=${encodeURIComponent(document.getElementById('user_login').value)}&type=auth`, (rawData, status) => {
if (status) { if (status) {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_4 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_4 }, 'class');
let data = rawData; let data = rawData;
try { try {
data = JSON.parse(rawData); data = JSON.parse(rawData);
} catch (e) { } catch (e) {
console.warn(rawData); console.warn(rawData);
if (php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') { if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 + php_vars.i18n_12 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} else { } else {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 }, 'class');
} }
wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id'); wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false }); wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
@ -323,11 +493,11 @@ function check() {
}); });
} }
if (data.allowCredentials && php_vars.allow_authenticator_type && php_vars.allow_authenticator_type !== 'none') { if (data.allowCredentials && wwa_login_php_vars.allow_authenticator_type && wwa_login_php_vars.allow_authenticator_type !== 'none') {
for (let credential of data.allowCredentials) { for (let credential of data.allowCredentials) {
if (php_vars.allow_authenticator_type === 'cross-platform') { if (wwa_login_php_vars.allow_authenticator_type === 'cross-platform') {
credential.transports = ['usb', 'nfc', 'ble']; credential.transports = ['usb', 'nfc', 'ble'];
} else if (php_vars.allow_authenticator_type === 'platform') { } else if (wwa_login_php_vars.allow_authenticator_type === 'platform') {
credential.transports = ['internal']; credential.transports = ['internal'];
} }
} }
@ -338,7 +508,7 @@ function check() {
delete data.clientID; delete data.clientID;
navigator.credentials.get({ 'publicKey': data }).then((credentialInfo) => { navigator.credentials.get({ 'publicKey': data }).then((credentialInfo) => {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_5 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_5 }, 'class');
return credentialInfo; return credentialInfo;
}).then((data) => { }).then((data) => {
const publicKeyCredential = { const publicKeyCredential = {
@ -355,10 +525,10 @@ function check() {
return publicKeyCredential; return publicKeyCredential;
}).then(JSON.stringify).then((AuthenticatorResponse) => { }).then(JSON.stringify).then((AuthenticatorResponse) => {
let response = wwa_ajax(); let response = wwa_ajax();
response.post(`${php_vars.ajax_url}?action=wwa_auth`, `data=${encodeURIComponent(window.btoa(AuthenticatorResponse))}&type=auth&clientid=${clientID}&user=${encodeURIComponent(document.getElementById('user_login').value)}&remember=${php_vars.remember_me === 'false' ? 'false' : (document.getElementById('rememberme') ? (document.getElementById('rememberme').checked ? 'true' : 'false') : 'false')}`, (data, status) => { response.post(`${wwa_login_php_vars.ajax_url}?action=wwa_auth`, `data=${encodeURIComponent(window.btoa(AuthenticatorResponse))}&type=auth&clientid=${clientID}&user=${encodeURIComponent(document.getElementById('user_login').value)}&remember=${wwa_login_php_vars.remember_me === 'false' ? 'false' : (document.getElementById('rememberme') ? (document.getElementById('rememberme').checked ? 'true' : 'false') : 'false')}`, (data, status) => {
if (status) { if (status) {
if (data === 'true') { if (data === 'true') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_6 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_6 }, 'class');
if (document.querySelectorAll('p.submit input[name="redirect_to"]').length > 0) { if (document.querySelectorAll('p.submit input[name="redirect_to"]').length > 0) {
setTimeout(() => { setTimeout(() => {
window.location.href = document.querySelectorAll('p.submit input[name="redirect_to"]')[0].value; window.location.href = document.querySelectorAll('p.submit input[name="redirect_to"]')[0].value;
@ -370,24 +540,24 @@ function check() {
}, 200); }, 200);
} else { } else {
setTimeout(() => { setTimeout(() => {
window.location.href = php_vars.admin_url window.location.href = wwa_login_php_vars.admin_url
}, 200); }, 200);
} }
} }
} else { } else {
if (php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') { if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 + php_vars.i18n_12 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} else { } else {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 }, 'class');
} }
wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id'); wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false }); wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
} }
} else { } else {
if (php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') { if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 + php_vars.i18n_12 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} else { } else {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 }, 'class');
} }
wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id'); wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false }); wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
@ -395,23 +565,23 @@ function check() {
}) })
}).catch((error) => { }).catch((error) => {
console.warn(error); console.warn(error);
if (php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') { if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 + php_vars.i18n_12 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} else { } else {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 }, 'class');
} }
wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id'); wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false }); wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
}) })
} else { } else {
if (php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') { if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 + php_vars.i18n_12 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} else { } else {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = php_vars.i18n_7 }, 'class'); wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 }, 'class');
} }
wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id'); wwa_dom('user_login', (dom) => { dom.readOnly = false }, 'id');
wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false }); wwa_dom('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
} }
}) })
} }
} }

View File

@ -1,3 +1,5 @@
const svg = `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 216 216" viewBox="0 0 216 216" class="wwa-table-svg"><style>.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#818286}</style><g id="Isolation_Mode"><path d="M0 0h216v216H0z" style="fill:none"/><path d="M172.32 96.79c0 13.78-8.48 25.5-20.29 29.78l7.14 11.83-10.57 13 10.57 12.71-17.04 22.87-12.01-12.82V125.7c-10.68-4.85-18.15-15.97-18.15-28.91 0-17.4 13.51-31.51 30.18-31.51 16.66 0 30.17 14.11 30.17 31.51zm-30.18 4.82c4.02 0 7.28-3.4 7.28-7.6 0-4.2-3.26-7.61-7.28-7.61s-7.28 3.4-7.28 7.61c-.01 4.2 3.26 7.6 7.28 7.6z" style="fill-rule:evenodd;clip-rule:evenodd;fill:#a2a1a3"/><path d="M172.41 96.88c0 13.62-8.25 25.23-19.83 29.67l6.58 11.84-9.73 13 9.73 12.71-17.03 23.05v-85.54c4.02 0 7.28-3.41 7.28-7.6 0-4.2-3.26-7.61-7.28-7.61V65.28c16.73 0 30.28 14.15 30.28 31.6zM120.24 131.43c-9.75-8-16.3-20.3-17.2-34.27H50.8c-10.96 0-19.84 9.01-19.84 20.13v25.17c0 5.56 4.44 10.07 9.92 10.07h69.44c5.48 0 9.92-4.51 9.92-10.07v-11.03z" class="st2"/><path d="M73.16 91.13c-2.42-.46-4.82-.89-7.11-1.86-8.65-3.63-13.69-10.32-15.32-19.77-1.12-6.47-.59-12.87 2.03-18.92 3.72-8.6 10.39-13.26 19.15-14.84 5.24-.94 10.46-.73 15.5 1.15 7.59 2.82 12.68 8.26 15.03 16.24 2.38 8.05 2.03 16.1-1.56 23.72-3.72 7.96-10.21 12.23-18.42 13.9-.68.14-1.37.27-2.05.41-2.41-.03-4.83-.03-7.25-.03z" style="fill:#818286"/></g></svg>`;
// Whether the broswer supports WebAuthn // Whether the broswer supports WebAuthn
if (window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function') { if (window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function') {
jQuery('#wwa-bind, #wwa-test').attr('disabled', 'disabled'); jQuery('#wwa-bind, #wwa-test').attr('disabled', 'disabled');
@ -24,12 +26,13 @@ function updateList() {
type: 'GET', type: 'GET',
data: { data: {
action: 'wwa_authenticator_list', action: 'wwa_authenticator_list',
user_id: php_vars.user_id user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function (data) { success: function (data) {
if (typeof data === 'string') { if (typeof data === 'string') {
console.warn(data); console.warn(data);
jQuery('#wwa-authenticator-list').html(`<tr><td colspan="${jQuery('.wwa-usernameless-th').css('display') === 'none' ? '5' : '6'}">${php_vars.i18n_8}</td></tr>`); jQuery('#wwa-authenticator-list').html(`<tr><td colspan="${getColspan()}">${php_vars.i18n_8}</td></tr>`);
return; return;
} }
if (data.length === 0) { if (data.length === 0) {
@ -38,7 +41,12 @@ function updateList() {
} else { } else {
jQuery('.wwa-usernameless-th, .wwa-usernameless-td').hide(); jQuery('.wwa-usernameless-th, .wwa-usernameless-td').hide();
} }
jQuery('#wwa-authenticator-list').html(`<tr><td colspan="${jQuery('.wwa-usernameless-th').css('display') === 'none' ? '5' : '6'}">${php_vars.i18n_17}</td></tr>`); if (configs.show_authenticator_type === 'true') {
jQuery('.wwa-type-th, .wwa-type-td').show();
} else {
jQuery('.wwa-type-th, .wwa-type-td').hide();
}
jQuery('#wwa-authenticator-list').html(`<tr><td colspan="${getColspan()}">${php_vars.i18n_17}</td></tr>`);
jQuery('#wwa_usernameless_tip').text(''); jQuery('#wwa_usernameless_tip').text('');
jQuery('#wwa_usernameless_tip').hide(); jQuery('#wwa_usernameless_tip').hide();
jQuery('#wwa_type_tip').text(''); jQuery('#wwa_type_tip').text('');
@ -59,7 +67,7 @@ function updateList() {
item_type_disabled = true; item_type_disabled = true;
} }
} }
htmlStr += `<tr><td>${item.name}</td><td>${item.type === 'none' ? php_vars.i18n_9 : (item.type === 'platform' ? php_vars.i18n_10 : php_vars.i18n_11)}${item_type_disabled ? php_vars.i18n_29 : ''}</td><td>${item.added}</td><td>${item.last_used}</td><td class="wwa-usernameless-td">${item.usernameless ? php_vars.i18n_24 + (configs.usernameless === 'true' ? '' : php_vars.i18n_26) : php_vars.i18n_25}</td><td id="${item.key}"><a href="javascript:renameAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${php_vars.i18n_20}</a> | <a href="javascript:removeAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${php_vars.i18n_12}</a></td></tr>`; htmlStr += `<tr><td>${svg}${item.name}</td>${configs.show_authenticator_type === 'true' ? `<td class="wwa-type-td">${item.type === 'none' ? php_vars.i18n_9 : (item.type === 'platform' ? php_vars.i18n_10 : php_vars.i18n_11)}${item_type_disabled ? php_vars.i18n_29 : ''}</td>` : ''}<td>${item.added}</td><td>${item.last_used}</td><td class="wwa-usernameless-td">${item.usernameless ? php_vars.i18n_24 + (configs.usernameless === 'true' ? '' : php_vars.i18n_26) : php_vars.i18n_25}</td><td id="${item.key}"><a href="javascript:renameAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${php_vars.i18n_20}</a> | <a href="javascript:removeAuthenticator('${item.key}', '${item.name.replaceAll('\'', '\\\'').replaceAll('&#039;', '\\&#039;').replaceAll('"', '\\"')}')">${php_vars.i18n_12}</a></td></tr>`;
} }
jQuery('#wwa-authenticator-list').html(htmlStr); jQuery('#wwa-authenticator-list').html(htmlStr);
if (has_usernameless || configs.usernameless === 'true') { if (has_usernameless || configs.usernameless === 'true') {
@ -67,6 +75,11 @@ function updateList() {
} else { } else {
jQuery('.wwa-usernameless-th, .wwa-usernameless-td').hide(); jQuery('.wwa-usernameless-th, .wwa-usernameless-td').hide();
} }
if (configs.show_authenticator_type === 'true') {
jQuery('.wwa-type-th, .wwa-type-td').show();
} else {
jQuery('.wwa-type-th, .wwa-type-td').hide();
}
if (has_usernameless && configs.usernameless !== 'true') { if (has_usernameless && configs.usernameless !== 'true') {
jQuery('#wwa_usernameless_tip').text(php_vars.i18n_27); jQuery('#wwa_usernameless_tip').text(php_vars.i18n_27);
jQuery('#wwa_usernameless_tip').show(); jQuery('#wwa_usernameless_tip').show();
@ -87,11 +100,23 @@ function updateList() {
} }
}, },
error: function () { error: function () {
jQuery('#wwa-authenticator-list').html(`<tr><td colspan="${jQuery('.wwa-usernameless-th').css('display') === 'none' ? '5' : '6'}">${php_vars.i18n_8}</td></tr>`); jQuery('#wwa-authenticator-list').html(`<tr><td colspan="${getColspan()}">${php_vars.i18n_8}</td></tr>`);
} }
}) })
} }
// Compute current number of visible columns for colspan
function getColspan() {
let cols = 4; // Identifier, Registered, Last used, Action
if (jQuery('.wwa-type-th').length > 0 && jQuery('.wwa-type-th').css('display') !== 'none') {
cols++;
}
if (jQuery('.wwa-usernameless-th').length > 0 && jQuery('.wwa-usernameless-th').css('display') !== 'none') {
cols++;
}
return cols;
}
/** Code Base64URL into Base64 /** Code Base64URL into Base64
* *
* @param {string} input Base64URL coded string * @param {string} input Base64URL coded string
@ -140,6 +165,10 @@ jQuery('.wwa-cancel').click((e) => {
jQuery('#wwa-verify-block').hide(); jQuery('#wwa-verify-block').hide();
}) })
// Prevent WebAuthn registration fields from triggering WordPress's unsaved changes dialog.
// The form="wwa-registration" attribute on these inputs disassociates them from #your-profile,
// so they are excluded from jQuery serialize() comparisons in user-profile.js.
jQuery('#wwa_authenticator_name').keydown((e) => { jQuery('#wwa_authenticator_name').keydown((e) => {
if (e.keyCode === 13) { if (e.keyCode === 13) {
jQuery('#wwa-bind').trigger('click'); jQuery('#wwa-bind').trigger('click');
@ -160,16 +189,19 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').attr('disabled', 'disabled'); jQuery('#wwa-bind').attr('disabled', 'disabled');
jQuery('#wwa_authenticator_name').attr('disabled', 'disabled'); jQuery('#wwa_authenticator_name').attr('disabled', 'disabled');
jQuery('.wwa_authenticator_usernameless').attr('disabled', 'disabled'); jQuery('.wwa_authenticator_usernameless').attr('disabled', 'disabled');
jQuery('#wwa_authenticator_type').attr('disabled', 'disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').attr('disabled', 'disabled');
}
jQuery.ajax({ jQuery.ajax({
url: php_vars.ajax_url, url: php_vars.ajax_url,
type: 'GET', type: 'GET',
data: { data: {
action: 'wwa_create', action: 'wwa_create',
name: jQuery('#wwa_authenticator_name').val(), name: jQuery('#wwa_authenticator_name').val(),
type: jQuery('#wwa_authenticator_type').val(), type: configs.show_authenticator_type === 'true' ? jQuery('#wwa_authenticator_type').val() : (configs.allow_authenticator_type !== 'none' ? configs.allow_authenticator_type : 'none'),
usernameless: jQuery('.wwa_authenticator_usernameless:checked').val() ? jQuery('.wwa_authenticator_usernameless:checked').val() : 'false', usernameless: jQuery('.wwa_authenticator_usernameless:checked').val() ? jQuery('.wwa_authenticator_usernameless:checked').val() : 'false',
user_id: php_vars.user_id user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function (data) { success: function (data) {
if (typeof data === 'string') { if (typeof data === 'string') {
@ -178,7 +210,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled'); jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').removeAttr('disabled'); jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('.wwa_authenticator_usernameless').removeAttr('disabled'); jQuery('.wwa_authenticator_usernameless').removeAttr('disabled');
jQuery('#wwa_authenticator_type').removeAttr('disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').removeAttr('disabled');
}
updateList(); updateList();
return; return;
} }
@ -241,10 +275,11 @@ jQuery('#wwa-bind').click((e) => {
data: { data: {
data: window.btoa(AuthenticatorAttestationResponse), data: window.btoa(AuthenticatorAttestationResponse),
name: jQuery('#wwa_authenticator_name').val(), name: jQuery('#wwa_authenticator_name').val(),
type: jQuery('#wwa_authenticator_type').val(), type: configs.show_authenticator_type === 'true' ? jQuery('#wwa_authenticator_type').val() : (configs.allow_authenticator_type !== 'none' ? configs.allow_authenticator_type : 'none'),
usernameless: jQuery('.wwa_authenticator_usernameless:checked').val() ? jQuery('.wwa_authenticator_usernameless:checked').val() : 'false', usernameless: jQuery('.wwa_authenticator_usernameless:checked').val() ? jQuery('.wwa_authenticator_usernameless:checked').val() : 'false',
clientid: clientID, clientid: clientID,
user_id: php_vars.user_id user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function (data) { success: function (data) {
if (data.trim() === 'true') { if (data.trim() === 'true') {
@ -254,7 +289,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa_authenticator_name').removeAttr('disabled'); jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('#wwa_authenticator_name').val(''); jQuery('#wwa_authenticator_name').val('');
jQuery('.wwa_authenticator_usernameless').removeAttr('disabled'); jQuery('.wwa_authenticator_usernameless').removeAttr('disabled');
jQuery('#wwa_authenticator_type').removeAttr('disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').removeAttr('disabled');
}
updateList(); updateList();
} else { } else {
// Register failed // Register failed
@ -262,7 +299,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled'); jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').removeAttr('disabled'); jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('.wwa_authenticator_usernameless').removeAttr('disabled'); jQuery('.wwa_authenticator_usernameless').removeAttr('disabled');
jQuery('#wwa_authenticator_type').removeAttr('disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').removeAttr('disabled');
}
updateList(); updateList();
} }
}, },
@ -271,7 +310,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled'); jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').removeAttr('disabled'); jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('.wwa_authenticator_usernameless').removeAttr('disabled'); jQuery('.wwa_authenticator_usernameless').removeAttr('disabled');
jQuery('#wwa_authenticator_type').removeAttr('disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').removeAttr('disabled');
}
updateList(); updateList();
} }
}) })
@ -282,7 +323,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled'); jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').removeAttr('disabled'); jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('.wwa_authenticator_usernameless').removeAttr('disabled'); jQuery('.wwa_authenticator_usernameless').removeAttr('disabled');
jQuery('#wwa_authenticator_type').removeAttr('disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').removeAttr('disabled');
}
updateList(); updateList();
}) })
}, },
@ -291,7 +334,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled'); jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').removeAttr('disabled'); jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('.wwa_authenticator_usernameless').removeAttr('disabled'); jQuery('.wwa_authenticator_usernameless').removeAttr('disabled');
jQuery('#wwa_authenticator_type').removeAttr('disabled'); if (configs.show_authenticator_type === 'true') {
jQuery('#wwa_authenticator_type').removeAttr('disabled');
}
updateList(); updateList();
} }
}) })
@ -428,7 +473,8 @@ function renameAuthenticator(id, name) {
id: id, id: id,
name: new_name, name: new_name,
target: 'rename', target: 'rename',
user_id: php_vars.user_id user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function () { success: function () {
updateList(); updateList();
@ -456,7 +502,8 @@ function removeAuthenticator(id, name) {
action: 'wwa_modify_authenticator', action: 'wwa_modify_authenticator',
id: id, id: id,
target: 'remove', target: 'remove',
user_id: php_vars.user_id user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
}, },
success: function () { success: function () {
updateList(); updateList();

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
=== WP-WebAuthn === === WP-WebAuthn ===
Contributors: axton Contributors: axton
Donate link: https://flyhigher.top/about Donate link: https://flyhigher.top/about
Tags: u2f, webauthn, passkey, login, security Tags: webauthn, passkey, login, security, fido, password, faceid
Requires at least: 5.0 Requires at least: 5.0
Tested up to: 6.6 Tested up to: 6.9
Stable tag: 1.3.4 Stable tag: 1.4.1
Requires PHP: 7.2 Requires PHP: 7.4
License: GPLv3 License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -25,13 +25,15 @@ This plugin has 4 built-in shortcodes and 4 built-in Gutenberg blocks, so you ca
Please refer to the [documentation](http://doc.flyhigher.top/wp-webauthn) before using the plugin. Please refer to the [documentation](http://doc.flyhigher.top/wp-webauthn) before using the plugin.
This plugin currently has *BETA* multisite support, if you find any issue in multisite, feel free to [open an issue](https://github.com/yrccondor/wp-webauthn/issues/new) on GitHub.
**PHP extensions gmp and mbstring are required.** **PHP extensions gmp and mbstring are required.**
**WebAuthn requires HTTPS connection or `localhost` to function normally.** **WebAuthn requires HTTPS connection or `localhost` to function normally.**
You can contribute to this plugin on [GitHub](https://github.com/yrccondor/wp-webauthn). You can contribute to this plugin on [GitHub](https://github.com/yrccondor/wp-webauthn).
Please note that this plugin does NOT support Internet Explorer (including IE 11). To use FaceID or TouchID, you need to use iOS/iPadOS 14+. > Please note that this plugin does NOT support Internet Explorer (including IE 11). To use FaceID or TouchID, you need to use iOS/iPadOS 14+.
= Security and Privacy = = Security and Privacy =
@ -80,6 +82,18 @@ To use FaceID or TouchID, you need to use iOS/iPadOS 14+.
== Changelog == == Changelog ==
= 1.4.1 =
Fix: Error when saving settings
= 1.4.0 =
Add: "Passkey" terminology option
Add: Multisite support (beta)
Update: Improved Passkey experience on login page
Update: Minimum PHP version raised to 7.4
Update: Translations
Update: Third party libraries
Chore: Updated role checking
= 1.3.4 = = 1.3.4 =
Fix: Make sure AJAX works with extra spaces/new lines Fix: Make sure AJAX works with extra spaces/new lines
Note: We'll soon drop support for PHP 7.4 and below. Please upgrade your PHP version to 8.0+. Note: We'll soon drop support for PHP 7.4 and below. Please upgrade your PHP version to 8.0+.
@ -106,68 +120,7 @@ Fix: 2FA compatibility
Update: Translations Update: Translations
Update: Third party libraries Update: Third party libraries
= 1.2.8 =
Fix: privilege check for admin panel
= 1.2.7 =
Add: Now a security warning will be shown if user verification is disabled
Fix: Style broken with some locales
Fix: privilege check for admin panel (thanks to @vanpop)
Update: Third party libraries
= 1.2.6 =
Update: Third party libraries
= 1.2.5 =
Update: German translation (thanks to niiconn)
Fix: HTTPS check
= 1.2.4 =
Add: French translation (thanks to Spomky) and Turkish translate (thanks to Sn0bzy)
Fix: HTTPS check
Update: Existing translations
Update: Third party libraries
= 1.2.3 =
Feat: Avoid locking users out if WebAuthn is not available
Update: translations
Update: Third party libraries
= 1.2.2 =
Fix: Cannot access to js files in apache 2.4+
= 1.2.1 =
Feat: Allow to disable password login completely
Feat: Now we use WordPress transients instead of PHP sessions
Feat: Move register related settings to user's profile
Feat: Gutenberg block support
Feat: Traditional Chinese (Hong Kong) & Traditional Chinese (Taiwan) translation
Update: Chinese translation
Update: Third-party libraries
= 1.1.0 =
Add: Allow to remember login option
Add: Only allow a specific type of authenticator option
Fix: Toggle button may not working in login form
Update: Chinese translation
Update: Third-party libraries
== Upgrade Notice == == Upgrade Notice ==
= 1.2.5 = = 1.4.1 =
Improvred HTTPS checking and updated German translation (by niiconn) New "Passkey" terminology option, multisite support (beta), improved Passkey experience and more
= 1.2.4 =
Improvred HTTPS checking and added new translations
= 1.2.3 =
Avoid locking users out if WebAuthn is not available and update translations
= 1.2.2 =
Fixed a problem that js files were broken in apache 2.4+
= 1.2.1 =
New features, bug fixing and new translations
= 1.1.0 =
2 new features & bug fixing

View File

@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err; echo $err;
} }
} }
trigger_error( throw new RuntimeException($err);
$err,
E_USER_ERROR
);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';

View File

@ -1,64 +0,0 @@
on: [push, pull_request]
name: CI
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: "cs2pr"
- name: "Cache dependencies installed with composer"
uses: "actions/cache@v1"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
- name: "Composer"
run: "composer update --prefer-stable"
- name: "PHPUnit"
run: "php vendor/bin/phpunit"
# lint:
# name: Lint
# runs-on: ubuntu-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v1
# - name: Setup PHP
# uses: shivammathur/setup-php@v2
# with:
# php-version: 7.4
# - name: "Cache dependencies installed with composer"
# uses: "actions/cache@v1"
# with:
# path: "~/.composer/cache"
# key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
# restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
# - name: "Composer"
# run: "composer update --prefer-stable"
# - name: "assert:cs-lint"
# run: "composer assert:cs-lint"
# - name: "assert:sa-code"
# run: "composer assert:sa-code"
# - name: "assert:sa-tests"
# run: "composer assert:sa-tests"

View File

@ -23,7 +23,7 @@
"sort-packages": true "sort-packages": true
}, },
"require": { "require": {
"php": "^7.0 || ^8.0", "php": "^7.1 || ^8.0",
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-ctype": "*", "ext-ctype": "*",

View File

@ -42,7 +42,7 @@ abstract class Assert
* The assertion chain can be stateful, that means be careful when you reuse * The assertion chain can be stateful, that means be careful when you reuse
* it. You should never pass around the chain. * it. You should never pass around the chain.
*/ */
public static function that($value, $defaultMessage = null, string $defaultPropertyPath = null): AssertionChain public static function that($value, $defaultMessage = null, ?string $defaultPropertyPath = null): AssertionChain
{ {
$assertionChain = new AssertionChain($value, $defaultMessage, $defaultPropertyPath); $assertionChain = new AssertionChain($value, $defaultMessage, $defaultPropertyPath);
@ -55,7 +55,7 @@ abstract class Assert
* @param mixed $values * @param mixed $values
* @param string|callable|null $defaultMessage * @param string|callable|null $defaultMessage
*/ */
public static function thatAll($values, $defaultMessage = null, string $defaultPropertyPath = null): AssertionChain public static function thatAll($values, $defaultMessage = null, ?string $defaultPropertyPath = null): AssertionChain
{ {
return static::that($values, $defaultMessage, $defaultPropertyPath)->all(); return static::that($values, $defaultMessage, $defaultPropertyPath)->all();
} }
@ -66,7 +66,7 @@ abstract class Assert
* @param mixed $value * @param mixed $value
* @param string|callable|null $defaultMessage * @param string|callable|null $defaultMessage
*/ */
public static function thatNullOr($value, $defaultMessage = null, string $defaultPropertyPath = null): AssertionChain public static function thatNullOr($value, $defaultMessage = null, ?string $defaultPropertyPath = null): AssertionChain
{ {
return static::that($value, $defaultMessage, $defaultPropertyPath)->nullOr(); return static::that($value, $defaultMessage, $defaultPropertyPath)->nullOr();
} }

View File

@ -307,7 +307,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function eq($value, $value2, $message = null, string $propertyPath = null): bool public static function eq($value, $value2, $message = null, ?string $propertyPath = null): bool
{ {
if ($value != $value2) { if ($value != $value2) {
$message = \sprintf( $message = \sprintf(
@ -331,7 +331,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function eqArraySubset($value, $value2, $message = null, string $propertyPath = null): bool public static function eqArraySubset($value, $value2, $message = null, ?string $propertyPath = null): bool
{ {
static::isArray($value, $message, $propertyPath); static::isArray($value, $message, $propertyPath);
static::isArray($value2, $message, $propertyPath); static::isArray($value2, $message, $propertyPath);
@ -358,7 +358,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function same($value, $value2, $message = null, string $propertyPath = null): bool public static function same($value, $value2, $message = null, ?string $propertyPath = null): bool
{ {
if ($value !== $value2) { if ($value !== $value2) {
$message = \sprintf( $message = \sprintf(
@ -382,7 +382,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notEq($value1, $value2, $message = null, string $propertyPath = null): bool public static function notEq($value1, $value2, $message = null, ?string $propertyPath = null): bool
{ {
if ($value1 == $value2) { if ($value1 == $value2) {
$message = \sprintf( $message = \sprintf(
@ -412,7 +412,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notSame($value1, $value2, $message = null, string $propertyPath = null): bool public static function notSame($value1, $value2, $message = null, ?string $propertyPath = null): bool
{ {
if ($value1 === $value2) { if ($value1 === $value2) {
$message = \sprintf( $message = \sprintf(
@ -434,7 +434,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notInArray($value, array $choices, $message = null, string $propertyPath = null): bool public static function notInArray($value, array $choices, $message = null, ?string $propertyPath = null): bool
{ {
if (true === \in_array($value, $choices)) { if (true === \in_array($value, $choices)) {
$message = \sprintf( $message = \sprintf(
@ -461,7 +461,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function integer($value, $message = null, string $propertyPath = null): bool public static function integer($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_int($value)) { if (!\is_int($value)) {
$message = \sprintf( $message = \sprintf(
@ -488,7 +488,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function float($value, $message = null, string $propertyPath = null): bool public static function float($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_float($value)) { if (!\is_float($value)) {
$message = \sprintf( $message = \sprintf(
@ -515,7 +515,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function digit($value, $message = null, string $propertyPath = null): bool public static function digit($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\ctype_digit((string)$value)) { if (!\ctype_digit((string)$value)) {
$message = \sprintf( $message = \sprintf(
@ -537,7 +537,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function integerish($value, $message = null, string $propertyPath = null): bool public static function integerish($value, $message = null, ?string $propertyPath = null): bool
{ {
if ( if (
\is_resource($value) || \is_resource($value) ||
@ -577,7 +577,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function boolean($value, $message = null, string $propertyPath = null): bool public static function boolean($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_bool($value)) { if (!\is_bool($value)) {
$message = \sprintf( $message = \sprintf(
@ -604,7 +604,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function scalar($value, $message = null, string $propertyPath = null): bool public static function scalar($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_scalar($value)) { if (!\is_scalar($value)) {
$message = \sprintf( $message = \sprintf(
@ -631,7 +631,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notEmpty($value, $message = null, string $propertyPath = null): bool public static function notEmpty($value, $message = null, ?string $propertyPath = null): bool
{ {
if (empty($value)) { if (empty($value)) {
$message = \sprintf( $message = \sprintf(
@ -658,7 +658,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function noContent($value, $message = null, string $propertyPath = null): bool public static function noContent($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!empty($value)) { if (!empty($value)) {
$message = \sprintf( $message = \sprintf(
@ -683,7 +683,7 @@ class Assertion
* *
* @return bool * @return bool
*/ */
public static function null($value, $message = null, string $propertyPath = null): bool public static function null($value, $message = null, ?string $propertyPath = null): bool
{ {
if (null !== $value) { if (null !== $value) {
$message = \sprintf( $message = \sprintf(
@ -710,7 +710,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notNull($value, $message = null, string $propertyPath = null): bool public static function notNull($value, $message = null, ?string $propertyPath = null): bool
{ {
if (null === $value) { if (null === $value) {
$message = \sprintf( $message = \sprintf(
@ -737,7 +737,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function string($value, $message = null, string $propertyPath = null) public static function string($value, $message = null, ?string $propertyPath = null)
{ {
if (!\is_string($value)) { if (!\is_string($value)) {
$message = \sprintf( $message = \sprintf(
@ -766,7 +766,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function regex($value, $pattern, $message = null, string $propertyPath = null): bool public static function regex($value, $pattern, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -794,7 +794,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notRegex($value, $pattern, $message = null, string $propertyPath = null): bool public static function notRegex($value, $pattern, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -825,7 +825,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function length($value, $length, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function length($value, $length, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -858,7 +858,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function minLength($value, $minLength, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function minLength($value, $minLength, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -891,7 +891,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function maxLength($value, $maxLength, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function maxLength($value, $maxLength, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -925,7 +925,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function betweenLength($value, $minLength, $maxLength, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function betweenLength($value, $minLength, $maxLength, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
static::minLength($value, $minLength, $message, $propertyPath, $encoding); static::minLength($value, $minLength, $message, $propertyPath, $encoding);
@ -949,7 +949,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function startsWith($string, $needle, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function startsWith($string, $needle, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($string, $message, $propertyPath); static::string($string, $message, $propertyPath);
@ -981,7 +981,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function endsWith($string, $needle, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function endsWith($string, $needle, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($string, $message, $propertyPath); static::string($string, $message, $propertyPath);
@ -1015,7 +1015,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function contains($string, $needle, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function contains($string, $needle, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($string, $message, $propertyPath); static::string($string, $message, $propertyPath);
@ -1047,7 +1047,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notContains($string, $needle, $message = null, string $propertyPath = null, $encoding = 'utf8'): bool public static function notContains($string, $needle, $message = null, ?string $propertyPath = null, $encoding = 'utf8'): bool
{ {
static::string($string, $message, $propertyPath); static::string($string, $message, $propertyPath);
@ -1072,7 +1072,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function choice($value, array $choices, $message = null, string $propertyPath = null): bool public static function choice($value, array $choices, $message = null, ?string $propertyPath = null): bool
{ {
if (!\in_array($value, $choices, true)) { if (!\in_array($value, $choices, true)) {
$message = \sprintf( $message = \sprintf(
@ -1097,7 +1097,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function inArray($value, array $choices, $message = null, string $propertyPath = null): bool public static function inArray($value, array $choices, $message = null, ?string $propertyPath = null): bool
{ {
return static::choice($value, $choices, $message, $propertyPath); return static::choice($value, $choices, $message, $propertyPath);
} }
@ -1115,7 +1115,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function numeric($value, $message = null, string $propertyPath = null): bool public static function numeric($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_numeric($value)) { if (!\is_numeric($value)) {
$message = \sprintf( $message = \sprintf(
@ -1140,7 +1140,7 @@ class Assertion
* *
* @return bool * @return bool
*/ */
public static function isResource($value, $message = null, string $propertyPath = null): bool public static function isResource($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_resource($value)) { if (!\is_resource($value)) {
$message = \sprintf( $message = \sprintf(
@ -1167,7 +1167,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isArray($value, $message = null, string $propertyPath = null): bool public static function isArray($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_array($value)) { if (!\is_array($value)) {
$message = \sprintf( $message = \sprintf(
@ -1194,7 +1194,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isTraversable($value, $message = null, string $propertyPath = null): bool public static function isTraversable($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_array($value) && !$value instanceof Traversable) { if (!\is_array($value) && !$value instanceof Traversable) {
$message = \sprintf( $message = \sprintf(
@ -1216,7 +1216,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isArrayAccessible($value, $message = null, string $propertyPath = null): bool public static function isArrayAccessible($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_array($value) && !$value instanceof ArrayAccess) { if (!\is_array($value) && !$value instanceof ArrayAccess) {
$message = \sprintf( $message = \sprintf(
@ -1233,7 +1233,7 @@ class Assertion
/** /**
* Assert that value is countable. * Assert that value is countable.
* *
* @param array|Countable|ResourceBundle|SimpleXMLElement $value * @param mixed $value
* @param string|callable|null $message * @param string|callable|null $message
* @param string|null $propertyPath * @param string|null $propertyPath
* *
@ -1243,7 +1243,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isCountable($value, $message = null, string $propertyPath = null): bool public static function isCountable($value, $message = null, ?string $propertyPath = null): bool
{ {
if (\function_exists('is_countable')) { if (\function_exists('is_countable')) {
$assert = \is_countable($value); $assert = \is_countable($value);
@ -1272,7 +1272,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function keyExists($value, $key, $message = null, string $propertyPath = null): bool public static function keyExists($value, $key, $message = null, ?string $propertyPath = null): bool
{ {
static::isArray($value, $message, $propertyPath); static::isArray($value, $message, $propertyPath);
@ -1297,7 +1297,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function keyNotExists($value, $key, $message = null, string $propertyPath = null): bool public static function keyNotExists($value, $key, $message = null, ?string $propertyPath = null): bool
{ {
static::isArray($value, $message, $propertyPath); static::isArray($value, $message, $propertyPath);
@ -1321,7 +1321,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function uniqueValues(array $values, $message = null, string $propertyPath = null): bool public static function uniqueValues(array $values, $message = null, ?string $propertyPath = null): bool
{ {
foreach ($values as $key => $value) { foreach ($values as $key => $value) {
if (\array_search($value, $values, true) !== $key) { if (\array_search($value, $values, true) !== $key) {
@ -1346,7 +1346,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function keyIsset($value, $key, $message = null, string $propertyPath = null): bool public static function keyIsset($value, $key, $message = null, ?string $propertyPath = null): bool
{ {
static::isArrayAccessible($value, $message, $propertyPath); static::isArrayAccessible($value, $message, $propertyPath);
@ -1371,7 +1371,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notEmptyKey($value, $key, $message = null, string $propertyPath = null): bool public static function notEmptyKey($value, $key, $message = null, ?string $propertyPath = null): bool
{ {
static::keyIsset($value, $key, $message, $propertyPath); static::keyIsset($value, $key, $message, $propertyPath);
static::notEmpty($value[$key], $message, $propertyPath); static::notEmpty($value[$key], $message, $propertyPath);
@ -1387,7 +1387,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notBlank($value, $message = null, string $propertyPath = null): bool public static function notBlank($value, $message = null, ?string $propertyPath = null): bool
{ {
if (false === $value || (empty($value) && '0' != $value) || (\is_string($value) && '' === \trim($value))) { if (false === $value || (empty($value) && '0' != $value) || (\is_string($value) && '' === \trim($value))) {
$message = \sprintf( $message = \sprintf(
@ -1417,7 +1417,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isInstanceOf($value, $className, $message = null, string $propertyPath = null): bool public static function isInstanceOf($value, $className, $message = null, ?string $propertyPath = null): bool
{ {
if (!($value instanceof $className)) { if (!($value instanceof $className)) {
$message = \sprintf( $message = \sprintf(
@ -1448,7 +1448,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function notIsInstanceOf($value, $className, $message = null, string $propertyPath = null): bool public static function notIsInstanceOf($value, $className, $message = null, ?string $propertyPath = null): bool
{ {
if ($value instanceof $className) { if ($value instanceof $className) {
$message = \sprintf( $message = \sprintf(
@ -1472,7 +1472,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function subclassOf($value, $className, $message = null, string $propertyPath = null): bool public static function subclassOf($value, $className, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_subclass_of($value, $className)) { if (!\is_subclass_of($value, $className)) {
$message = \sprintf( $message = \sprintf(
@ -1502,7 +1502,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function range($value, $minValue, $maxValue, $message = null, string $propertyPath = null): bool public static function range($value, $minValue, $maxValue, $message = null, ?string $propertyPath = null): bool
{ {
static::numeric($value, $message, $propertyPath); static::numeric($value, $message, $propertyPath);
@ -1534,7 +1534,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function min($value, $minValue, $message = null, string $propertyPath = null): bool public static function min($value, $minValue, $message = null, ?string $propertyPath = null): bool
{ {
static::numeric($value, $message, $propertyPath); static::numeric($value, $message, $propertyPath);
@ -1565,7 +1565,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function max($value, $maxValue, $message = null, string $propertyPath = null): bool public static function max($value, $maxValue, $message = null, ?string $propertyPath = null): bool
{ {
static::numeric($value, $message, $propertyPath); static::numeric($value, $message, $propertyPath);
@ -1590,7 +1590,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function file($value, $message = null, string $propertyPath = null): bool public static function file($value, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
static::notEmpty($value, $message, $propertyPath); static::notEmpty($value, $message, $propertyPath);
@ -1615,7 +1615,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function directory($value, $message = null, string $propertyPath = null): bool public static function directory($value, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -1639,7 +1639,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function readable($value, $message = null, string $propertyPath = null): bool public static function readable($value, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -1663,7 +1663,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function writeable($value, $message = null, string $propertyPath = null): bool public static function writeable($value, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -1692,7 +1692,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function email($value, $message = null, string $propertyPath = null): bool public static function email($value, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -1726,7 +1726,7 @@ class Assertion
* @see https://github.com/symfony/Validator/blob/master/Constraints/UrlValidator.php * @see https://github.com/symfony/Validator/blob/master/Constraints/UrlValidator.php
* @see https://github.com/symfony/Validator/blob/master/Constraints/Url.php * @see https://github.com/symfony/Validator/blob/master/Constraints/Url.php
*/ */
public static function url($value, $message = null, string $propertyPath = null): bool public static function url($value, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
@ -1772,7 +1772,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function alnum($value, $message = null, string $propertyPath = null): bool public static function alnum($value, $message = null, ?string $propertyPath = null): bool
{ {
try { try {
static::regex($value, '(^([a-zA-Z]{1}[a-zA-Z0-9]*)$)', $message, $propertyPath); static::regex($value, '(^([a-zA-Z]{1}[a-zA-Z0-9]*)$)', $message, $propertyPath);
@ -1801,7 +1801,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function true($value, $message = null, string $propertyPath = null): bool public static function true($value, $message = null, ?string $propertyPath = null): bool
{ {
if (true !== $value) { if (true !== $value) {
$message = \sprintf( $message = \sprintf(
@ -1828,7 +1828,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function false($value, $message = null, string $propertyPath = null): bool public static function false($value, $message = null, ?string $propertyPath = null): bool
{ {
if (false !== $value) { if (false !== $value) {
$message = \sprintf( $message = \sprintf(
@ -1855,7 +1855,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function classExists($value, $message = null, string $propertyPath = null): bool public static function classExists($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\class_exists($value)) { if (!\class_exists($value)) {
$message = \sprintf( $message = \sprintf(
@ -1882,7 +1882,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function interfaceExists($value, $message = null, string $propertyPath = null): bool public static function interfaceExists($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\interface_exists($value)) { if (!\interface_exists($value)) {
$message = \sprintf( $message = \sprintf(
@ -1905,7 +1905,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function implementsInterface($class, $interfaceName, $message = null, string $propertyPath = null): bool public static function implementsInterface($class, $interfaceName, $message = null, ?string $propertyPath = null): bool
{ {
try { try {
$reflection = new ReflectionClass($class); $reflection = new ReflectionClass($class);
@ -1948,7 +1948,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isJsonString($value, $message = null, string $propertyPath = null): bool public static function isJsonString($value, $message = null, ?string $propertyPath = null): bool
{ {
if (null === \json_decode($value) && JSON_ERROR_NONE !== \json_last_error()) { if (null === \json_decode($value) && JSON_ERROR_NONE !== \json_last_error()) {
$message = \sprintf( $message = \sprintf(
@ -1972,7 +1972,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function uuid($value, $message = null, string $propertyPath = null): bool public static function uuid($value, $message = null, ?string $propertyPath = null): bool
{ {
$value = \str_replace(['urn:', 'uuid:', '{', '}'], '', $value); $value = \str_replace(['urn:', 'uuid:', '{', '}'], '', $value);
@ -2002,7 +2002,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function e164($value, $message = null, string $propertyPath = null): bool public static function e164($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\preg_match('/^\+?[1-9]\d{1,14}$/', $value)) { if (!\preg_match('/^\+?[1-9]\d{1,14}$/', $value)) {
$message = \sprintf( $message = \sprintf(
@ -2028,7 +2028,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function count($countable, $count, $message = null, string $propertyPath = null): bool public static function count($countable, $count, $message = null, ?string $propertyPath = null): bool
{ {
if ($count !== \count($countable)) { if ($count !== \count($countable)) {
$message = \sprintf( $message = \sprintf(
@ -2052,7 +2052,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function minCount($countable, $count, $message = null, string $propertyPath = null): bool public static function minCount($countable, $count, $message = null, ?string $propertyPath = null): bool
{ {
if ($count > \count($countable)) { if ($count > \count($countable)) {
$message = \sprintf( $message = \sprintf(
@ -2076,7 +2076,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function maxCount($countable, $count, $message = null, string $propertyPath = null): bool public static function maxCount($countable, $count, $message = null, ?string $propertyPath = null): bool
{ {
if ($count < \count($countable)) { if ($count < \count($countable)) {
$message = \sprintf( $message = \sprintf(
@ -2147,7 +2147,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function choicesNotEmpty(array $values, array $choices, $message = null, string $propertyPath = null): bool public static function choicesNotEmpty(array $values, array $choices, $message = null, ?string $propertyPath = null): bool
{ {
static::notEmpty($values, $message, $propertyPath); static::notEmpty($values, $message, $propertyPath);
@ -2167,7 +2167,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function methodExists($value, $object, $message = null, string $propertyPath = null): bool public static function methodExists($value, $object, $message = null, ?string $propertyPath = null): bool
{ {
static::isObject($object, $message, $propertyPath); static::isObject($object, $message, $propertyPath);
@ -2196,7 +2196,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isObject($value, $message = null, string $propertyPath = null): bool public static function isObject($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_object($value)) { if (!\is_object($value)) {
$message = \sprintf( $message = \sprintf(
@ -2219,7 +2219,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function lessThan($value, $limit, $message = null, string $propertyPath = null): bool public static function lessThan($value, $limit, $message = null, ?string $propertyPath = null): bool
{ {
if ($value >= $limit) { if ($value >= $limit) {
$message = \sprintf( $message = \sprintf(
@ -2243,7 +2243,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function lessOrEqualThan($value, $limit, $message = null, string $propertyPath = null): bool public static function lessOrEqualThan($value, $limit, $message = null, ?string $propertyPath = null): bool
{ {
if ($value > $limit) { if ($value > $limit) {
$message = \sprintf( $message = \sprintf(
@ -2267,7 +2267,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function greaterThan($value, $limit, $message = null, string $propertyPath = null): bool public static function greaterThan($value, $limit, $message = null, ?string $propertyPath = null): bool
{ {
if ($value <= $limit) { if ($value <= $limit) {
$message = \sprintf( $message = \sprintf(
@ -2291,7 +2291,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function greaterOrEqualThan($value, $limit, $message = null, string $propertyPath = null): bool public static function greaterOrEqualThan($value, $limit, $message = null, ?string $propertyPath = null): bool
{ {
if ($value < $limit) { if ($value < $limit) {
$message = \sprintf( $message = \sprintf(
@ -2317,7 +2317,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function between($value, $lowerLimit, $upperLimit, $message = null, string $propertyPath = null): bool public static function between($value, $lowerLimit, $upperLimit, $message = null, ?string $propertyPath = null): bool
{ {
if ($lowerLimit > $value || $value > $upperLimit) { if ($lowerLimit > $value || $value > $upperLimit) {
$message = \sprintf( $message = \sprintf(
@ -2344,7 +2344,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function betweenExclusive($value, $lowerLimit, $upperLimit, $message = null, string $propertyPath = null): bool public static function betweenExclusive($value, $lowerLimit, $upperLimit, $message = null, ?string $propertyPath = null): bool
{ {
if ($lowerLimit >= $value || $value >= $upperLimit) { if ($lowerLimit >= $value || $value >= $upperLimit) {
$message = \sprintf( $message = \sprintf(
@ -2368,7 +2368,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function extensionLoaded($value, $message = null, string $propertyPath = null): bool public static function extensionLoaded($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\extension_loaded($value)) { if (!\extension_loaded($value)) {
$message = \sprintf( $message = \sprintf(
@ -2394,7 +2394,7 @@ class Assertion
* *
* @see http://php.net/manual/function.date.php#refsect1-function.date-parameters * @see http://php.net/manual/function.date.php#refsect1-function.date-parameters
*/ */
public static function date($value, $format, $message = null, string $propertyPath = null): bool public static function date($value, $format, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
static::string($format, $message, $propertyPath); static::string($format, $message, $propertyPath);
@ -2422,7 +2422,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function objectOrClass($value, $message = null, string $propertyPath = null): bool public static function objectOrClass($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_object($value)) { if (!\is_object($value)) {
static::classExists($value, $message, $propertyPath); static::classExists($value, $message, $propertyPath);
@ -2440,7 +2440,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function propertyExists($value, $property, $message = null, string $propertyPath = null): bool public static function propertyExists($value, $property, $message = null, ?string $propertyPath = null): bool
{ {
static::objectOrClass($value); static::objectOrClass($value);
@ -2465,7 +2465,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function propertiesExist($value, array $properties, $message = null, string $propertyPath = null): bool public static function propertiesExist($value, array $properties, $message = null, ?string $propertyPath = null): bool
{ {
static::objectOrClass($value); static::objectOrClass($value);
static::allString($properties, $message, $propertyPath); static::allString($properties, $message, $propertyPath);
@ -2500,7 +2500,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function version($version1, $operator, $version2, $message = null, string $propertyPath = null): bool public static function version($version1, $operator, $version2, $message = null, ?string $propertyPath = null): bool
{ {
static::notEmpty($operator, 'versionCompare operator is required and cannot be empty.'); static::notEmpty($operator, 'versionCompare operator is required and cannot be empty.');
@ -2527,7 +2527,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function phpVersion($operator, $version, $message = null, string $propertyPath = null): bool public static function phpVersion($operator, $version, $message = null, ?string $propertyPath = null): bool
{ {
static::defined('PHP_VERSION'); static::defined('PHP_VERSION');
@ -2544,7 +2544,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function extensionVersion($extension, $operator, $version, $message = null, string $propertyPath = null): bool public static function extensionVersion($extension, $operator, $version, $message = null, ?string $propertyPath = null): bool
{ {
static::extensionLoaded($extension, $message, $propertyPath); static::extensionLoaded($extension, $message, $propertyPath);
@ -2564,7 +2564,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function isCallable($value, $message = null, string $propertyPath = null): bool public static function isCallable($value, $message = null, ?string $propertyPath = null): bool
{ {
if (!\is_callable($value)) { if (!\is_callable($value)) {
$message = \sprintf( $message = \sprintf(
@ -2589,7 +2589,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function satisfy($value, $callback, $message = null, string $propertyPath = null): bool public static function satisfy($value, $callback, $message = null, ?string $propertyPath = null): bool
{ {
static::isCallable($callback); static::isCallable($callback);
@ -2617,7 +2617,7 @@ class Assertion
* *
* @see http://php.net/manual/filter.filters.flags.php * @see http://php.net/manual/filter.filters.flags.php
*/ */
public static function ip($value, $flag = null, $message = null, string $propertyPath = null): bool public static function ip($value, $flag = null, $message = null, ?string $propertyPath = null): bool
{ {
static::string($value, $message, $propertyPath); static::string($value, $message, $propertyPath);
if ($flag === null) { if ($flag === null) {
@ -2648,7 +2648,7 @@ class Assertion
* *
* @see http://php.net/manual/filter.filters.flags.php * @see http://php.net/manual/filter.filters.flags.php
*/ */
public static function ipv4($value, $flag = null, $message = null, string $propertyPath = null): bool public static function ipv4($value, $flag = null, $message = null, ?string $propertyPath = null): bool
{ {
static::ip($value, $flag | FILTER_FLAG_IPV4, static::generateMessage($message ?: 'Value "%s" was expected to be a valid IPv4 address.'), $propertyPath); static::ip($value, $flag | FILTER_FLAG_IPV4, static::generateMessage($message ?: 'Value "%s" was expected to be a valid IPv4 address.'), $propertyPath);
@ -2667,7 +2667,7 @@ class Assertion
* *
* @see http://php.net/manual/filter.filters.flags.php * @see http://php.net/manual/filter.filters.flags.php
*/ */
public static function ipv6($value, $flag = null, $message = null, string $propertyPath = null): bool public static function ipv6($value, $flag = null, $message = null, ?string $propertyPath = null): bool
{ {
static::ip($value, $flag | FILTER_FLAG_IPV6, static::generateMessage($message ?: 'Value "%s" was expected to be a valid IPv6 address.'), $propertyPath); static::ip($value, $flag | FILTER_FLAG_IPV6, static::generateMessage($message ?: 'Value "%s" was expected to be a valid IPv6 address.'), $propertyPath);
@ -2680,7 +2680,7 @@ class Assertion
* @param mixed $constant * @param mixed $constant
* @param string|callable|null $message * @param string|callable|null $message
*/ */
public static function defined($constant, $message = null, string $propertyPath = null): bool public static function defined($constant, $message = null, ?string $propertyPath = null): bool
{ {
if (!\defined($constant)) { if (!\defined($constant)) {
$message = \sprintf(static::generateMessage($message ?: 'Value "%s" expected to be a defined constant.'), $constant); $message = \sprintf(static::generateMessage($message ?: 'Value "%s" expected to be a defined constant.'), $constant);
@ -2699,7 +2699,7 @@ class Assertion
* *
* @throws AssertionFailedException * @throws AssertionFailedException
*/ */
public static function base64($value, $message = null, string $propertyPath = null): bool public static function base64($value, $message = null, ?string $propertyPath = null): bool
{ {
if (false === \base64_decode($value, true)) { if (false === \base64_decode($value, true)) {
$message = \sprintf(static::generateMessage($message ?: 'Value "%s" is not a valid base64 string.'), $value); $message = \sprintf(static::generateMessage($message ?: 'Value "%s" is not a valid base64 string.'), $value);

View File

@ -151,7 +151,7 @@ class AssertionChain
* @param mixed $value * @param mixed $value
* @param string|callable|null $defaultMessage * @param string|callable|null $defaultMessage
*/ */
public function __construct($value, $defaultMessage = null, string $defaultPropertyPath = null) public function __construct($value, $defaultMessage = null, ?string $defaultPropertyPath = null)
{ {
$this->value = $value; $this->value = $value;
$this->defaultMessage = $defaultMessage; $this->defaultMessage = $defaultMessage;

View File

@ -31,7 +31,7 @@ class InvalidArgumentException extends \InvalidArgumentException implements Asse
*/ */
private $constraints; private $constraints;
public function __construct($message, $code, string $propertyPath = null, $value = null, array $constraints = []) public function __construct($message, $code, ?string $propertyPath = null, $value = null, array $constraints = [])
{ {
parent::__construct($message, $code); parent::__construct($message, $code);

View File

@ -133,7 +133,7 @@ class LazyAssertion
* *
* @return static * @return static
*/ */
public function that($value, string $propertyPath = null, $defaultMessage = null) public function that($value, ?string $propertyPath = null, $defaultMessage = null)
{ {
$this->currentChainFailed = false; $this->currentChainFailed = false;
$this->thisChainTryAll = false; $this->thisChainTryAll = false;

View File

@ -32,7 +32,7 @@ namespace Assert;
* The assertion chain can be stateful, that means be careful when you reuse * The assertion chain can be stateful, that means be careful when you reuse
* it. You should never pass around the chain. * it. You should never pass around the chain.
*/ */
function that($value, $defaultMessage = null, string $defaultPropertyPath = null): AssertionChain function that($value, $defaultMessage = null, ?string $defaultPropertyPath = null): AssertionChain
{ {
return Assert::that($value, $defaultMessage, $defaultPropertyPath); return Assert::that($value, $defaultMessage, $defaultPropertyPath);
} }
@ -44,7 +44,7 @@ function that($value, $defaultMessage = null, string $defaultPropertyPath = null
* @param string|callable|null $defaultMessage * @param string|callable|null $defaultMessage
* @param string $defaultPropertyPath * @param string $defaultPropertyPath
*/ */
function thatAll($values, $defaultMessage = null, string $defaultPropertyPath = null): AssertionChain function thatAll($values, $defaultMessage = null, ?string $defaultPropertyPath = null): AssertionChain
{ {
return Assert::thatAll($values, $defaultMessage, $defaultPropertyPath); return Assert::thatAll($values, $defaultMessage, $defaultPropertyPath);
} }
@ -58,7 +58,7 @@ function thatAll($values, $defaultMessage = null, string $defaultPropertyPath =
* *
* @deprecated In favour of Assert::thatNullOr($value, $defaultMessage = null, $defaultPropertyPath = null) * @deprecated In favour of Assert::thatNullOr($value, $defaultMessage = null, $defaultPropertyPath = null)
*/ */
function thatNullOr($value, $defaultMessage = null, string $defaultPropertyPath = null): AssertionChain function thatNullOr($value, $defaultMessage = null, ?string $defaultPropertyPath = null): AssertionChain
{ {
return Assert::thatNullOr($value, $defaultMessage, $defaultPropertyPath); return Assert::thatNullOr($value, $defaultMessage, $defaultPropertyPath);
} }

View File

@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/ */
class InstalledVersions class InstalledVersions
{ {
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/** /**
* @var mixed[]|null * @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/ */
private static $installed; private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/** /**
* @var bool|null * @var bool|null
*/ */
@ -309,6 +320,24 @@ class InstalledVersions
{ {
self::$installed = $data; self::$installed = $data;
self::$installedByVendor = array(); self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
} }
/** /**
@ -322,19 +351,27 @@ class InstalledVersions
} }
$installed = array(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php'; $required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required; self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $installed[] = $required;
self::$installed = $installed[count($installed) - 1]; if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
} }
} }
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
} }
} }
@ -350,7 +387,7 @@ class InstalledVersions
} }
} }
if (self::$installed !== array()) { if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed; $installed[] = self::$installed;
} }

View File

@ -16,12 +16,12 @@ return array(
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'), 'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'), 'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'), 'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'),
'Nyholm\\Psr7Server\\' => array($vendorDir . '/nyholm/psr7-server/src'), 'Nyholm\\Psr7Server\\' => array($vendorDir . '/nyholm/psr7-server/src'),
'League\\Uri\\' => array($vendorDir . '/league/uri-interfaces/src', $vendorDir . '/league/uri/src'), 'League\\Uri\\' => array($vendorDir . '/league/uri/src', $vendorDir . '/league/uri-interfaces/src'),
'Jose\\Component\\Signature\\Algorithm\\' => array($vendorDir . '/web-token/jwt-signature-algorithm-ecdsa', $vendorDir . '/web-token/jwt-signature-algorithm-eddsa', $vendorDir . '/web-token/jwt-signature-algorithm-rsa'), 'Jose\\Component\\Signature\\Algorithm\\' => array($vendorDir . '/web-token/jwt-signature-algorithm-rsa', $vendorDir . '/web-token/jwt-signature-algorithm-eddsa', $vendorDir . '/web-token/jwt-signature-algorithm-ecdsa'),
'Jose\\Component\\Signature\\' => array($vendorDir . '/web-token/jwt-signature'), 'Jose\\Component\\Signature\\' => array($vendorDir . '/web-token/jwt-signature'),
'Jose\\Component\\KeyManagement\\' => array($vendorDir . '/web-token/jwt-key-mgmt'), 'Jose\\Component\\KeyManagement\\' => array($vendorDir . '/web-token/jwt-key-mgmt'),
'Jose\\Component\\Core\\' => array($vendorDir . '/web-token/jwt-core'), 'Jose\\Component\\Core\\' => array($vendorDir . '/web-token/jwt-core'),

View File

@ -104,12 +104,12 @@ class ComposerStaticInite99fdfd0dbb5e609b534e430fe6b54ef
); );
public static $prefixLengthsPsr4 = array ( public static $prefixLengthsPsr4 = array (
'W' => 'W' =>
array ( array (
'Webauthn\\MetadataService\\' => 25, 'Webauthn\\MetadataService\\' => 25,
'Webauthn\\' => 9, 'Webauthn\\' => 9,
), ),
'S' => 'S' =>
array ( array (
'Symfony\\Polyfill\\Php81\\' => 23, 'Symfony\\Polyfill\\Php81\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23,
@ -117,157 +117,157 @@ class ComposerStaticInite99fdfd0dbb5e609b534e430fe6b54ef
'Symfony\\Component\\Process\\' => 26, 'Symfony\\Component\\Process\\' => 26,
'Safe\\' => 5, 'Safe\\' => 5,
), ),
'R' => 'R' =>
array ( array (
'Ramsey\\Uuid\\' => 12, 'Ramsey\\Uuid\\' => 12,
'Ramsey\\Collection\\' => 18, 'Ramsey\\Collection\\' => 18,
), ),
'P' => 'P' =>
array ( array (
'Psr\\Log\\' => 8, 'Psr\\Log\\' => 8,
'Psr\\Http\\Message\\' => 17, 'Psr\\Http\\Message\\' => 17,
'Psr\\Http\\Client\\' => 16, 'Psr\\Http\\Client\\' => 16,
), ),
'N' => 'N' =>
array ( array (
'Nyholm\\Psr7\\' => 12, 'Nyholm\\Psr7\\' => 12,
'Nyholm\\Psr7Server\\' => 18, 'Nyholm\\Psr7Server\\' => 18,
), ),
'L' => 'L' =>
array ( array (
'League\\Uri\\' => 11, 'League\\Uri\\' => 11,
), ),
'J' => 'J' =>
array ( array (
'Jose\\Component\\Signature\\Algorithm\\' => 35, 'Jose\\Component\\Signature\\Algorithm\\' => 35,
'Jose\\Component\\Signature\\' => 25, 'Jose\\Component\\Signature\\' => 25,
'Jose\\Component\\KeyManagement\\' => 29, 'Jose\\Component\\KeyManagement\\' => 29,
'Jose\\Component\\Core\\' => 20, 'Jose\\Component\\Core\\' => 20,
), ),
'F' => 'F' =>
array ( array (
'FG\\' => 3, 'FG\\' => 3,
), ),
'C' => 'C' =>
array ( array (
'Cose\\' => 5, 'Cose\\' => 5,
'CBOR\\' => 5, 'CBOR\\' => 5,
), ),
'B' => 'B' =>
array ( array (
'Brick\\Math\\' => 11, 'Brick\\Math\\' => 11,
'Base64Url\\' => 10, 'Base64Url\\' => 10,
), ),
'A' => 'A' =>
array ( array (
'Assert\\' => 7, 'Assert\\' => 7,
), ),
); );
public static $prefixDirsPsr4 = array ( public static $prefixDirsPsr4 = array (
'Webauthn\\MetadataService\\' => 'Webauthn\\MetadataService\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-auth/metadata-service/src', 0 => __DIR__ . '/..' . '/web-auth/metadata-service/src',
), ),
'Webauthn\\' => 'Webauthn\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-auth/webauthn-lib/src', 0 => __DIR__ . '/..' . '/web-auth/webauthn-lib/src',
), ),
'Symfony\\Polyfill\\Php81\\' => 'Symfony\\Polyfill\\Php81\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php81', 0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
), ),
'Symfony\\Polyfill\\Php80\\' => 'Symfony\\Polyfill\\Php80\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80', 0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
), ),
'Symfony\\Polyfill\\Ctype\\' => 'Symfony\\Polyfill\\Ctype\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
), ),
'Symfony\\Component\\Process\\' => 'Symfony\\Component\\Process\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/process', 0 => __DIR__ . '/..' . '/symfony/process',
), ),
'Safe\\' => 'Safe\\' =>
array ( array (
0 => __DIR__ . '/..' . '/thecodingmachine/safe/lib', 0 => __DIR__ . '/..' . '/thecodingmachine/safe/lib',
1 => __DIR__ . '/..' . '/thecodingmachine/safe/deprecated', 1 => __DIR__ . '/..' . '/thecodingmachine/safe/deprecated',
2 => __DIR__ . '/..' . '/thecodingmachine/safe/generated', 2 => __DIR__ . '/..' . '/thecodingmachine/safe/generated',
), ),
'Ramsey\\Uuid\\' => 'Ramsey\\Uuid\\' =>
array ( array (
0 => __DIR__ . '/..' . '/ramsey/uuid/src', 0 => __DIR__ . '/..' . '/ramsey/uuid/src',
), ),
'Ramsey\\Collection\\' => 'Ramsey\\Collection\\' =>
array ( array (
0 => __DIR__ . '/..' . '/ramsey/collection/src', 0 => __DIR__ . '/..' . '/ramsey/collection/src',
), ),
'Psr\\Log\\' => 'Psr\\Log\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log', 0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
), ),
'Psr\\Http\\Message\\' => 'Psr\\Http\\Message\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/http-message/src', 0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-factory/src', 1 => __DIR__ . '/..' . '/psr/http-message/src',
), ),
'Psr\\Http\\Client\\' => 'Psr\\Http\\Client\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/http-client/src', 0 => __DIR__ . '/..' . '/psr/http-client/src',
), ),
'Nyholm\\Psr7\\' => 'Nyholm\\Psr7\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nyholm/psr7/src', 0 => __DIR__ . '/..' . '/nyholm/psr7/src',
), ),
'Nyholm\\Psr7Server\\' => 'Nyholm\\Psr7Server\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nyholm/psr7-server/src', 0 => __DIR__ . '/..' . '/nyholm/psr7-server/src',
), ),
'League\\Uri\\' => 'League\\Uri\\' =>
array ( array (
0 => __DIR__ . '/..' . '/league/uri-interfaces/src', 0 => __DIR__ . '/..' . '/league/uri/src',
1 => __DIR__ . '/..' . '/league/uri/src', 1 => __DIR__ . '/..' . '/league/uri-interfaces/src',
), ),
'Jose\\Component\\Signature\\Algorithm\\' => 'Jose\\Component\\Signature\\Algorithm\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-token/jwt-signature-algorithm-ecdsa', 0 => __DIR__ . '/..' . '/web-token/jwt-signature-algorithm-rsa',
1 => __DIR__ . '/..' . '/web-token/jwt-signature-algorithm-eddsa', 1 => __DIR__ . '/..' . '/web-token/jwt-signature-algorithm-eddsa',
2 => __DIR__ . '/..' . '/web-token/jwt-signature-algorithm-rsa', 2 => __DIR__ . '/..' . '/web-token/jwt-signature-algorithm-ecdsa',
), ),
'Jose\\Component\\Signature\\' => 'Jose\\Component\\Signature\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-token/jwt-signature', 0 => __DIR__ . '/..' . '/web-token/jwt-signature',
), ),
'Jose\\Component\\KeyManagement\\' => 'Jose\\Component\\KeyManagement\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-token/jwt-key-mgmt', 0 => __DIR__ . '/..' . '/web-token/jwt-key-mgmt',
), ),
'Jose\\Component\\Core\\' => 'Jose\\Component\\Core\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-token/jwt-core', 0 => __DIR__ . '/..' . '/web-token/jwt-core',
), ),
'FG\\' => 'FG\\' =>
array ( array (
0 => __DIR__ . '/..' . '/fgrosse/phpasn1/lib', 0 => __DIR__ . '/..' . '/fgrosse/phpasn1/lib',
), ),
'Cose\\' => 'Cose\\' =>
array ( array (
0 => __DIR__ . '/..' . '/web-auth/cose-lib/src', 0 => __DIR__ . '/..' . '/web-auth/cose-lib/src',
), ),
'CBOR\\' => 'CBOR\\' =>
array ( array (
0 => __DIR__ . '/..' . '/spomky-labs/cbor-php/src', 0 => __DIR__ . '/..' . '/spomky-labs/cbor-php/src',
), ),
'Brick\\Math\\' => 'Brick\\Math\\' =>
array ( array (
0 => __DIR__ . '/..' . '/brick/math/src', 0 => __DIR__ . '/..' . '/brick/math/src',
), ),
'Base64Url\\' => 'Base64Url\\' =>
array ( array (
0 => __DIR__ . '/..' . '/spomky-labs/base64url/src', 0 => __DIR__ . '/..' . '/spomky-labs/base64url/src',
), ),
'Assert\\' => 'Assert\\' =>
array ( array (
0 => __DIR__ . '/..' . '/beberlei/assert/lib/Assert', 0 => __DIR__ . '/..' . '/beberlei/assert/lib/Assert',
), ),

View File

@ -2,17 +2,17 @@
"packages": [ "packages": [
{ {
"name": "beberlei/assert", "name": "beberlei/assert",
"version": "v3.3.2", "version": "v3.3.3",
"version_normalized": "3.3.2.0", "version_normalized": "3.3.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/beberlei/assert.git", "url": "https://github.com/beberlei/assert.git",
"reference": "cb70015c04be1baee6f5f5c953703347c0ac1655" "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655", "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"reference": "cb70015c04be1baee6f5f5c953703347c0ac1655", "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"shasum": "", "shasum": "",
"mirrors": [ "mirrors": [
{ {
@ -26,7 +26,7 @@
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"php": "^7.0 || ^8.0" "php": "^7.1 || ^8.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "*", "friendsofphp/php-cs-fixer": "*",
@ -37,7 +37,7 @@
"suggest": { "suggest": {
"ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
}, },
"time": "2021-12-16T21:41:27+00:00", "time": "2024-07-15T13:18:35+00:00",
"type": "library", "type": "library",
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
@ -72,7 +72,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/beberlei/assert/issues", "issues": "https://github.com/beberlei/assert/issues",
"source": "https://github.com/beberlei/assert/tree/v3.3.2" "source": "https://github.com/beberlei/assert/tree/v3.3.3"
}, },
"install-path": "../beberlei/assert" "install-path": "../beberlei/assert"
}, },
@ -991,11 +991,11 @@
"time": "2021-09-25T23:10:38+00:00", "time": "2021-09-25T23:10:38+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "4.x-dev"
},
"captainhook": { "captainhook": {
"force-install": true "force-install": true
},
"branch-alias": {
"dev-main": "4.x-dev"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
@ -1198,8 +1198,8 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.31.0", "version": "v1.32.0",
"version_normalized": "1.31.0.0", "version_normalized": "1.32.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
@ -1230,8 +1230,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
@ -1266,7 +1266,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
}, },
"funding": [ "funding": [
{ {
@ -1286,17 +1286,17 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.31.0", "version": "v1.33.0",
"version_normalized": "1.31.0.0", "version_normalized": "1.33.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": "", "shasum": "",
"mirrors": [ "mirrors": [
{ {
@ -1308,12 +1308,12 @@
"require": { "require": {
"php": ">=7.2" "php": ">=7.2"
}, },
"time": "2024-09-09T11:45:10+00:00", "time": "2025-01-02T08:10:11+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
@ -1355,7 +1355,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@ -1366,6 +1366,10 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
@ -1375,17 +1379,17 @@
}, },
{ {
"name": "symfony/polyfill-php81", "name": "symfony/polyfill-php81",
"version": "v1.30.0", "version": "v1.32.0",
"version_normalized": "1.30.0.0", "version_normalized": "1.32.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php81.git", "url": "https://github.com/symfony/polyfill-php81.git",
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": "", "shasum": "",
"mirrors": [ "mirrors": [
{ {
@ -1395,14 +1399,14 @@
] ]
}, },
"require": { "require": {
"php": ">=7.1" "php": ">=7.2"
}, },
"time": "2024-06-19T12:30:46+00:00", "time": "2024-09-09T11:45:10+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
@ -1440,7 +1444,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0"
}, },
"funding": [ "funding": [
{ {
@ -1460,17 +1464,17 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v5.4.40", "version": "v5.4.47",
"version_normalized": "5.4.40.0", "version_normalized": "5.4.47.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", "url": "https://api.github.com/repos/symfony/process/zipball/5d1662fb32ebc94f17ddb8d635454a776066733d",
"reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d",
"shasum": "", "shasum": "",
"mirrors": [ "mirrors": [
{ {
@ -1483,7 +1487,7 @@
"php": ">=7.2.5", "php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16" "symfony/polyfill-php80": "^1.16"
}, },
"time": "2024-05-31T14:33:22+00:00", "time": "2024-11-06T11:36:42+00:00",
"type": "library", "type": "library",
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
@ -1511,7 +1515,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v5.4.40" "source": "https://github.com/symfony/process/tree/v5.4.47"
}, },
"funding": [ "funding": [
{ {

View File

@ -1,9 +1,9 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => '__root__', 'name' => '__root__',
'pretty_version' => '1.3.4', 'pretty_version' => '1.4.1',
'version' => '1.3.4.0', 'version' => '1.4.1.0',
'reference' => '88207a7a7032f209d572a80f06699769886cdeb3', 'reference' => '01964b4933ae48870823ca1620868423cfd4bf2b',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -11,18 +11,18 @@
), ),
'versions' => array( 'versions' => array(
'__root__' => array( '__root__' => array(
'pretty_version' => '1.3.4', 'pretty_version' => '1.4.1',
'version' => '1.3.4.0', 'version' => '1.4.1.0',
'reference' => '88207a7a7032f209d572a80f06699769886cdeb3', 'reference' => '01964b4933ae48870823ca1620868423cfd4bf2b',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'beberlei/assert' => array( 'beberlei/assert' => array(
'pretty_version' => 'v3.3.2', 'pretty_version' => 'v3.3.3',
'version' => '3.3.2.0', 'version' => '3.3.3.0',
'reference' => 'cb70015c04be1baee6f5f5c953703347c0ac1655', 'reference' => 'b5fd8eacd8915a1b627b8bfc027803f1939734dd',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../beberlei/assert', 'install_path' => __DIR__ . '/../beberlei/assert',
'aliases' => array(), 'aliases' => array(),
@ -179,8 +179,8 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-ctype' => array( 'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.31.0', 'pretty_version' => 'v1.32.0',
'version' => '1.31.0.0', 'version' => '1.32.0.0',
'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638', 'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype', 'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
@ -188,27 +188,27 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-php80' => array( 'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.31.0', 'pretty_version' => 'v1.33.0',
'version' => '1.31.0.0', 'version' => '1.33.0.0',
'reference' => '60328e362d4c2c802a54fcbf04f9d3fb892b4cf8', 'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-php81' => array( 'symfony/polyfill-php81' => array(
'pretty_version' => 'v1.30.0', 'pretty_version' => 'v1.32.0',
'version' => '1.30.0.0', 'version' => '1.32.0.0',
'reference' => '3fb075789fb91f9ad9af537c4012d523085bd5af', 'reference' => '4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php81', 'install_path' => __DIR__ . '/../symfony/polyfill-php81',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/process' => array( 'symfony/process' => array(
'pretty_version' => 'v5.4.40', 'pretty_version' => 'v5.4.47',
'version' => '5.4.40.0', 'version' => '5.4.47.0',
'reference' => 'deedcb3bb4669cae2148bc920eafd2b16dc7c046', 'reference' => '5d1662fb32ebc94f17ddb8d635454a776066733d',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/process', 'install_path' => __DIR__ . '/../symfony/process',
'aliases' => array(), 'aliases' => array(),

View File

@ -29,7 +29,7 @@ class PhpToken implements \Stringable
public $text; public $text;
/** /**
* @var int * @var -1|positive-int
*/ */
public $line; public $line;
@ -38,6 +38,9 @@ class PhpToken implements \Stringable
*/ */
public $pos; public $pos;
/**
* @param -1|positive-int $line
*/
public function __construct(int $id, string $text, int $line = -1, int $position = -1) public function __construct(int $id, string $text, int $line = -1, int $position = -1)
{ {
$this->id = $id; $this->id = $id;
@ -80,7 +83,7 @@ class PhpToken implements \Stringable
} }
/** /**
* @return static[] * @return list<static>
*/ */
public static function tokenize(string $code, int $flags = 0): array public static function tokenize(string $code, int $flags = 0): array
{ {

View File

@ -16,7 +16,7 @@
} }
], ],
"require": { "require": {
"php": ">=7.1" "php": ">=7.2"
}, },
"autoload": { "autoload": {
"psr-4": { "Symfony\\Polyfill\\Php81\\": "" }, "psr-4": { "Symfony\\Polyfill\\Php81\\": "" },

View File

@ -19,7 +19,15 @@ namespace Symfony\Component\Process;
*/ */
class ExecutableFinder class ExecutableFinder
{ {
private $suffixes = ['.exe', '.bat', '.cmd', '.com']; private const CMD_BUILTINS = [
'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date',
'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto',
'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause',
'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set',
'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol',
];
private $suffixes = [];
/** /**
* Replaces default suffixes of executable. * Replaces default suffixes of executable.
@ -48,39 +56,48 @@ class ExecutableFinder
*/ */
public function find(string $name, ?string $default = null, array $extraDirs = []) public function find(string $name, ?string $default = null, array $extraDirs = [])
{ {
if (\ini_get('open_basedir')) { // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes
$searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) {
$dirs = []; return $name;
foreach ($searchPath as $path) {
// Silencing against https://bugs.php.net/69240
if (@is_dir($path)) {
$dirs[] = $path;
} else {
if (basename($path) == $name && @is_executable($path)) {
return $path;
}
}
}
} else {
$dirs = array_merge(
explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
$extraDirs
);
} }
$suffixes = ['']; $dirs = array_merge(
explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
$extraDirs
);
$suffixes = [];
if ('\\' === \DIRECTORY_SEPARATOR) { if ('\\' === \DIRECTORY_SEPARATOR) {
$pathExt = getenv('PATHEXT'); $pathExt = getenv('PATHEXT');
$suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); $suffixes = $this->suffixes;
$suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']);
} }
$suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']);
foreach ($suffixes as $suffix) { foreach ($suffixes as $suffix) {
foreach ($dirs as $dir) { foreach ($dirs as $dir) {
if ('' === $dir) {
$dir = '.';
}
if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) {
return $file; return $file;
} }
if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) {
return $dir;
}
} }
} }
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) {
return $default;
}
$execResult = exec('command -v -- '.escapeshellarg($name));
if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) {
return $executablePath;
}
return $default; return $default;
} }
} }

View File

@ -34,15 +34,8 @@ class PhpExecutableFinder
public function find(bool $includeArgs = true) public function find(bool $includeArgs = true)
{ {
if ($php = getenv('PHP_BINARY')) { if ($php = getenv('PHP_BINARY')) {
if (!is_executable($php)) { if (!is_executable($php) && !$php = $this->executableFinder->find($php)) {
$command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; return false;
if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) {
if (!is_executable($php)) {
return false;
}
} else {
return false;
}
} }
if (@is_dir($php)) { if (@is_dir($php)) {

View File

@ -352,7 +352,7 @@ class Process implements \IteratorAggregate
$this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
if (!\is_resource($this->process)) { if (!$this->process) {
throw new RuntimeException('Unable to launch a new process.'); throw new RuntimeException('Unable to launch a new process.');
} }
$this->status = self::STATUS_STARTED; $this->status = self::STATUS_STARTED;
@ -1456,8 +1456,9 @@ class Process implements \IteratorAggregate
private function close(): int private function close(): int
{ {
$this->processPipes->close(); $this->processPipes->close();
if (\is_resource($this->process)) { if ($this->process) {
proc_close($this->process); proc_close($this->process);
$this->process = null;
} }
$this->exitcode = $this->processInformation['exitcode']; $this->exitcode = $this->processInformation['exitcode'];
$this->status = self::STATUS_TERMINATED; $this->status = self::STATUS_TERMINATED;
@ -1591,7 +1592,14 @@ class Process implements \IteratorAggregate
$cmd $cmd
); );
$cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; static $comSpec;
if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) {
// Escape according to CommandLineToArgvW rules
$comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"';
}
$cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
foreach ($this->processPipes->getFiles() as $offset => $filename) { foreach ($this->processPipes->getFiles() as $offset => $filename) {
$cmd .= ' '.$offset.'>"'.$filename.'"'; $cmd .= ' '.$offset.'>"'.$filename.'"';
} }
@ -1637,7 +1645,7 @@ class Process implements \IteratorAggregate
if (str_contains($argument, "\0")) { if (str_contains($argument, "\0")) {
$argument = str_replace("\0", '?', $argument); $argument = str_replace("\0", '?', $argument);
} }
if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { if (!preg_match('/[()%!^"<>&|\s]/', $argument)) {
return $argument; return $argument;
} }
$argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);

View File

@ -3,12 +3,13 @@
Plugin Name: WP-WebAuthn Plugin Name: WP-WebAuthn
Plugin URI: https://flyhigher.top Plugin URI: https://flyhigher.top
Description: WP-WebAuthn allows you to safely login to your WordPress site without password. Description: WP-WebAuthn allows you to safely login to your WordPress site without password.
Version: 1.3.4 Version: 1.4.1
Author: Axton Author: Axton
Author URI: https://axton.cc Author URI: https://axton.cc
License: GPLv3 License: GPLv3
Text Domain: wp-webauthn Text Domain: wp-webauthn
Domain Path: /languages Domain Path: /languages
Network: true
*/ */
/* Copyright 2020 Axton /* Copyright 2020 Axton
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version of the License, or (at your option) any later version. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version of the License, or (at your option) any later version.
@ -17,53 +18,284 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
if (!defined('ABSPATH')) {
exit;
}
function wwa_register_table() {
global $wpdb;
$wpdb->wwa_credentials = $wpdb->base_prefix . 'wwa_credentials';
}
wwa_register_table();
add_action('plugins_loaded', 'wwa_register_table', 0);
function wwa_create_table() {
global $wpdb;
$table = $wpdb->wwa_credentials;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table (
credential_id varchar(512) NOT NULL,
user_id bigint(20) unsigned NOT NULL,
registered_blog_id bigint(20) unsigned NOT NULL DEFAULT 1,
credential_source longtext NOT NULL,
user_handle varchar(255) NOT NULL,
human_name text NOT NULL,
authenticator_type varchar(50) NOT NULL,
usernameless tinyint(1) NOT NULL DEFAULT 0,
added datetime NOT NULL,
last_used varchar(50) NOT NULL DEFAULT '-',
PRIMARY KEY (credential_id),
KEY idx_user_id (user_id),
KEY idx_user_handle (user_handle),
KEY idx_blog_user (registered_blog_id, user_id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
function wwa_migrate_credentials_for_site(){
if(get_option('wwa_credentials_migrated')){
return;
}
global $wpdb;
$options = get_option('wwa_options');
if(!is_array($options)){
update_option('wwa_credentials_migrated', true);
return;
}
$user_id_map = isset($options['user_id']) && is_array($options['user_id']) ? $options['user_id'] : array();
$raw_creds = isset($options['user_credentials']) ? $options['user_credentials'] : '{}';
$raw_meta = isset($options['user_credentials_meta']) ? $options['user_credentials_meta'] : '{}';
$credentials = json_decode($raw_creds, true);
$credentials_meta = json_decode($raw_meta, true);
if(!is_array($credentials) || !is_array($credentials_meta)){
update_option('wwa_credentials_migrated', true);
return;
}
foreach($user_id_map as $user_login => $user_handle){
$wp_user = get_user_by('login', $user_login);
if($wp_user === false){
continue;
}
$existing = get_user_meta($wp_user->ID, 'wwa_user_handle', true);
if(!$existing){
update_user_meta($wp_user->ID, 'wwa_user_handle', $user_handle);
}
}
$handle_to_login = array();
foreach($user_id_map as $user_login => $user_handle){
$handle_to_login[$user_handle] = $user_login;
}
$blog_id = get_current_blog_id();
foreach($credentials_meta as $cred_id => $meta){
if(!isset($meta['user']) || !isset($credentials[$cred_id])){
continue;
}
$handle = $meta['user'];
if(!isset($handle_to_login[$handle])){
continue;
}
$wp_user = get_user_by('login', $handle_to_login[$handle]);
if($wp_user === false){
continue;
}
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$wpdb->wwa_credentials}
(credential_id, user_id, registered_blog_id, credential_source, user_handle, human_name, authenticator_type, usernameless, added, last_used)
VALUES (%s, %d, %d, %s, %s, %s, %s, %d, %s, %s)",
$cred_id,
$wp_user->ID,
$blog_id,
wp_json_encode($credentials[$cred_id]),
$handle,
isset($meta['human_name']) ? $meta['human_name'] : '',
isset($meta['authenticator_type']) ? $meta['authenticator_type'] : 'none',
!empty($meta['usernameless']) ? 1 : 0,
isset($meta['added']) ? $meta['added'] : current_time('mysql'),
isset($meta['last_used']) ? $meta['last_used'] : '-'
));
}
$site_users = get_users(array('blog_id' => $blog_id, 'fields' => 'ID'));
foreach($site_users as $uid){
if(get_user_option('webauthn_only', $uid) === 'true'){
update_user_meta($uid, 'wwa_webauthn_only', 'true');
}
}
update_option('wwa_credentials_migrated', true);
}
function wwa_migrate_network_options(){
if(!is_multisite() || get_site_option('wwa_network_options') !== false){
return;
}
$main_site_id = get_main_site_id();
switch_to_blog($main_site_id);
$options = get_option('wwa_options');
restore_current_blog();
$network_defaults = array(
'first_choice' => 'true',
'ror_origins' => '',
'user_verification' => 'false',
'usernameless_login' => 'false',
'allow_authenticator_type' => 'none',
'show_authenticator_type' => 'false'
);
$network_options = array();
foreach($network_defaults as $key => $default){
if(is_array($options) && isset($options[$key])){
$network_options[$key] = $options[$key];
}else{
$network_options[$key] = $default;
}
}
update_site_option('wwa_network_options', $network_options);
}
register_activation_hook(__FILE__, 'wwa_init'); register_activation_hook(__FILE__, 'wwa_init');
register_uninstall_hook(__FILE__, 'wwa_uninstall');
function wwa_init(){ function wwa_init(){
if(version_compare(get_bloginfo('version'), '5.0', '<')){ if(version_compare(get_bloginfo('version'), '5.0', '<')){
deactivate_plugins(basename(__FILE__)); //disable deactivate_plugins(basename(__FILE__));
return;
}
wwa_create_table();
if(is_multisite()){
$sites = get_sites(array('fields' => 'ids', 'number' => 0));
foreach($sites as $blog_id){
switch_to_blog($blog_id);
wwa_init_data();
wwa_apply_rewrite_rules();
restore_current_blog();
}
}else{ }else{
wwa_init_data(); wwa_init_data();
} }
} }
wwa_init_data(); function wwa_uninstall(){
global $wpdb;
if(is_multisite()){
$sites = get_sites(array('fields' => 'ids', 'number' => 0));
foreach($sites as $blog_id){
switch_to_blog($blog_id);
wwa_uninstall_site();
restore_current_blog();
}
delete_site_option('wwa_network_options');
delete_site_option('wwa_all_sites_migrated');
}else{
wwa_uninstall_site();
}
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->wwa_credentials}");
$wpdb->delete($wpdb->usermeta, array('meta_key' => 'wwa_user_handle'));
$wpdb->delete($wpdb->usermeta, array('meta_key' => 'wwa_webauthn_only'));
}
function wwa_uninstall_site(){
delete_option('wwa_options');
delete_option('wwa_version');
delete_option('wwa_log');
delete_option('wwa_init');
delete_option('wwa_credentials_migrated');
}
add_action('plugins_loaded', 'wwa_init_data');
function wwa_init_data(){ function wwa_init_data(){
if(!get_option('wwa_init')){ if(!get_option('wwa_init')){
// Init // Init
wwa_create_table();
$site_domain = wp_parse_url(site_url(), PHP_URL_HOST); $site_domain = wp_parse_url(site_url(), PHP_URL_HOST);
$wwa_init_options = array( $wwa_init_options = array(
'user_credentials' => '{}',
'user_credentials_meta' => '{}',
'user_id' => array(),
'first_choice' => 'true',
'website_name' => get_bloginfo('name'), 'website_name' => get_bloginfo('name'),
'website_domain' => $site_domain === NULL ? "" : $site_domain, 'website_domain' => $site_domain === NULL ? "" : $site_domain,
'remember_me' => 'false', 'remember_me' => 'false',
'email_login' => 'false', 'email_login' => 'false',
'user_verification' => 'false',
'usernameless_login' => 'false',
'allow_authenticator_type' => 'none',
'password_reset' => 'off', 'password_reset' => 'off',
'after_user_registration' => 'none', 'after_user_registration' => 'none',
'logging' => 'false' 'logging' => 'false',
'terminology' => 'passkey',
); );
if(!is_multisite()){
$wwa_init_options['first_choice'] = 'true';
$wwa_init_options['user_verification'] = 'false';
$wwa_init_options['usernameless_login'] = 'false';
$wwa_init_options['allow_authenticator_type'] = 'none';
$wwa_init_options['show_authenticator_type'] = 'false';
$wwa_init_options['ror_origins'] = '';
}
update_option('wwa_options', $wwa_init_options); update_option('wwa_options', $wwa_init_options);
include('wwa-version.php'); include('wwa-version.php');
update_option('wwa_version', $wwa_version); update_option('wwa_version', $wwa_version);
update_option('wwa_log', array()); update_option('wwa_log', array());
update_option('wwa_init', md5(date('Y-m-d H:i:s'))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date update_option('wwa_init', md5(date('Y-m-d H:i:s'))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
add_action('wp_loaded', 'wwa_apply_rewrite_rules');
}else{ }else{
include('wwa-version.php'); include('wwa-version.php');
if(!get_option('wwa_version') || get_option('wwa_version')['version'] != $wwa_version['version']){ if(!get_option('wwa_version') || get_option('wwa_version')['version'] != $wwa_version['version']){
update_option('wwa_version', $wwa_version); //update version wwa_create_table();
wwa_migrate_credentials_for_site();
if(is_multisite() && !get_site_option('wwa_all_sites_migrated')){
$all_sites = get_sites(array('fields' => 'ids', 'number' => 0));
foreach($all_sites as $site_id){
if(intval($site_id) === get_current_blog_id()){
continue;
}
switch_to_blog($site_id);
if(get_option('wwa_init')){
wwa_migrate_credentials_for_site();
update_option('wwa_version', $wwa_version);
}
restore_current_blog();
}
update_site_option('wwa_all_sites_migrated', true);
}
update_option('wwa_version', $wwa_version);
add_action('wp_loaded', 'wwa_apply_rewrite_rules');
} }
} }
if(is_multisite()){
wwa_migrate_network_options();
}
} }
// Wrap WP-WebAuthn settings // Wrap WP-WebAuthn settings
function wwa_get_option($option_name){ function wwa_get_option($option_name){
$network_options = array(
'first_choice', 'ror_origins', 'user_verification',
'usernameless_login', 'allow_authenticator_type', 'show_authenticator_type'
);
if(is_multisite() && in_array($option_name, $network_options, true)){
$val = get_site_option('wwa_network_options');
if(is_array($val) && isset($val[$option_name])){
return $val[$option_name];
}
return false;
}
$val = get_option('wwa_options'); $val = get_option('wwa_options');
if(isset($val[$option_name])){ if(isset($val[$option_name])){
return $val[$option_name]; return $val[$option_name];
@ -73,6 +305,18 @@ function wwa_get_option($option_name){
} }
function wwa_update_option($option_name, $option_value){ function wwa_update_option($option_name, $option_value){
$network_options = array(
'first_choice', 'ror_origins', 'user_verification',
'usernameless_login', 'allow_authenticator_type', 'show_authenticator_type'
);
if(is_multisite() && in_array($option_name, $network_options, true)){
$options = get_site_option('wwa_network_options', array());
$options[$option_name] = $option_value;
update_site_option('wwa_network_options', $options);
return true;
}
$options = get_option('wwa_options'); $options = get_option('wwa_options');
$options[$option_name] = $option_value; $options[$option_name] = $option_value;
update_option('wwa_options',$options); update_option('wwa_options',$options);
@ -83,3 +327,19 @@ include('wwa-menus.php');
include('wwa-functions.php'); include('wwa-functions.php');
include('wwa-ajax.php'); include('wwa-ajax.php');
include('wwa-shortcodes.php'); include('wwa-shortcodes.php');
register_activation_hook(__FILE__, 'wwa_apply_rewrite_rules');
register_deactivation_hook(__FILE__, 'wwa_deactivate');
function wwa_deactivate($network_wide){
if(is_multisite() && $network_wide){
$sites = get_sites(array('fields' => 'ids', 'number' => 0));
foreach($sites as $blog_id){
switch_to_blog($blog_id);
flush_rewrite_rules();
restore_current_blog();
}
}else{
flush_rewrite_rules();
}
}

View File

@ -1,8 +1,13 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
// Insert CSS and JS // Insert CSS and JS
wp_enqueue_script('wwa_admin', plugins_url('js/admin.js', __FILE__)); wp_enqueue_script('wwa_admin', plugins_url('js/admin.js', __FILE__), array(), get_option('wwa_version')['version']);
wp_localize_script('wwa_admin', 'php_vars', array( wp_localize_script('wwa_admin', 'php_vars', array(
'ajax_url' => admin_url('admin-ajax.php'), 'ajax_url' => admin_url('admin-ajax.php'),
'_ajax_nonce' => wp_create_nonce('wwa_admin_ajax'),
'i18n_1' => __('User verification is disabled by default because some mobile devices do not support it (especially on Android devices). But we <strong>recommend you to enable it</strong> if possible to further secure your login.', 'wp-webauthn'), 'i18n_1' => __('User verification is disabled by default because some mobile devices do not support it (especially on Android devices). But we <strong>recommend you to enable it</strong> if possible to further secure your login.', 'wp-webauthn'),
'i18n_2' => __('Log count: ', 'wp-webauthn'), 'i18n_2' => __('Log count: ', 'wp-webauthn'),
'i18n_3' => __('Loading failed, maybe try refreshing?', 'wp-webauthn') 'i18n_3' => __('Loading failed, maybe try refreshing?', 'wp-webauthn')
@ -25,7 +30,7 @@ if(!function_exists('sodium_crypto_sign_detached')){
$wwa_not_allowed = true; $wwa_not_allowed = true;
} }
if(!wwa_check_ssl() && (wp_parse_url(site_url(), PHP_URL_HOST) !== 'localhost' && wp_parse_url(site_url(), PHP_URL_HOST) !== '127.0.0.1')){ if(!wwa_check_ssl() && (wp_parse_url(site_url(), PHP_URL_HOST) !== 'localhost' && wp_parse_url(site_url(), PHP_URL_HOST) !== '127.0.0.1')){
add_settings_error('wwa_settings', 'https_error', __('WebAuthn features are restricted to websites in secure contexts. Please make sure your website is served over HTTPS or locally with <code>localhost</code>.', 'wp-webauthn')); add_settings_error('wwa_settings', 'https_error', wp_kses(__('WebAuthn features are restricted to websites in secure contexts. Please make sure your website is served over HTTPS or locally with <code>localhost</code>.', 'wp-webauthn'), array('code' => array())));
$wwa_not_allowed = true; $wwa_not_allowed = true;
} }
// Only admin can change settings // Only admin can change settings
@ -33,17 +38,20 @@ if(
(isset($_POST['wwa_ref']) && $_POST['wwa_ref'] === 'true') (isset($_POST['wwa_ref']) && $_POST['wwa_ref'] === 'true')
&& check_admin_referer('wwa_options_update') && check_admin_referer('wwa_options_update')
&& wwa_validate_privileges() && wwa_validate_privileges()
&& (isset($_POST['first_choice']) && ($_POST['first_choice'] === 'true' || $_POST['first_choice'] === 'false' || $_POST['first_choice'] === 'webauthn')) && (is_multisite() || (isset($_POST['first_choice']) && ($_POST['first_choice'] === 'true' || $_POST['first_choice'] === 'false' || $_POST['first_choice'] === 'webauthn')))
&& (isset($_POST['remember_me']) && ($_POST['remember_me'] === 'true' || $_POST['remember_me'] === 'false')) && (isset($_POST['remember_me']) && ($_POST['remember_me'] === 'true' || $_POST['remember_me'] === 'false'))
&& (isset($_POST['email_login']) && ($_POST['email_login'] === 'true' || $_POST['email_login'] === 'false')) && (isset($_POST['email_login']) && ($_POST['email_login'] === 'true' || $_POST['email_login'] === 'false'))
&& (isset($_POST['user_verification']) && ($_POST['user_verification'] === 'true' || $_POST['user_verification'] === 'false')) && (is_multisite() || (isset($_POST['user_verification']) && ($_POST['user_verification'] === 'true' || $_POST['user_verification'] === 'false')))
&& (isset($_POST['usernameless_login']) && ($_POST['usernameless_login'] === 'true' || $_POST['usernameless_login'] === 'false')) && (is_multisite() || (isset($_POST['usernameless_login']) && ($_POST['usernameless_login'] === 'true' || $_POST['usernameless_login'] === 'false')))
&& (isset($_POST['allow_authenticator_type']) && ($_POST['allow_authenticator_type'] === 'none' || $_POST['allow_authenticator_type'] === 'platform' || $_POST['allow_authenticator_type'] === 'cross-platform')) && (is_multisite() || (isset($_POST['allow_authenticator_type']) && ($_POST['allow_authenticator_type'] === 'none' || $_POST['allow_authenticator_type'] === 'platform' || $_POST['allow_authenticator_type'] === 'cross-platform')))
&& (is_multisite() || (isset($_POST['show_authenticator_type']) && ($_POST['show_authenticator_type'] === 'true' || $_POST['show_authenticator_type'] === 'false')))
&& (isset($_POST['password_reset']) && ($_POST['password_reset'] === 'off' || $_POST['password_reset'] === 'admin' || $_POST['password_reset'] === 'all')) && (isset($_POST['password_reset']) && ($_POST['password_reset'] === 'off' || $_POST['password_reset'] === 'admin' || $_POST['password_reset'] === 'all'))
&& (isset($_POST['after_user_registration']) && ($_POST['after_user_registration'] === 'none' || $_POST['after_user_registration'] === 'login')) && (isset($_POST['after_user_registration']) && ($_POST['after_user_registration'] === 'none' || $_POST['after_user_registration'] === 'login' || $_POST['after_user_registration'] === 'mail'))
&& (isset($_POST['terminology']) && ($_POST['terminology'] === 'webauthn' || $_POST['terminology'] === 'passkey'))
&& (isset($_POST['logging']) && ($_POST['logging'] === 'true' || $_POST['logging'] === 'false')) && (isset($_POST['logging']) && ($_POST['logging'] === 'true' || $_POST['logging'] === 'false'))
&& isset($_POST['website_name']) && isset($_POST['website_name'])
&& isset($_POST['website_domain']) && isset($_POST['website_domain'])
// && (is_multisite() || isset($_POST['ror_origins']))
){ ){
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
@ -63,16 +71,22 @@ if(
wwa_add_log($res_id, 'Warning: Not in security context', true); wwa_add_log($res_id, 'Warning: Not in security context', true);
} }
wwa_add_log($res_id, 'PHP Version => '.phpversion().', WordPress Version => '.get_bloginfo('version').', WP-WebAuthn Version => '.get_option('wwa_version')['version'], true); wwa_add_log($res_id, 'PHP Version => '.phpversion().', WordPress Version => '.get_bloginfo('version').', WP-WebAuthn Version => '.get_option('wwa_version')['version'], true);
wwa_add_log($res_id, 'Current config: first_choice => "'.wwa_get_option('first_choice').'", website_name => "'.wwa_get_option('website_name').'", website_domain => "'.wwa_get_option('website_domain').'", remember_me => "'.wwa_get_option('remember_me').'", email_login => "'.wwa_get_option('email_login').'", user_verification => "'.wwa_get_option('user_verification').'", allow_authenticator_type => "'.wwa_get_option('allow_authenticator_type').'", usernameless_login => "'.wwa_get_option('usernameless_login').'", password_reset => "'.wwa_get_option('password_reset').'", after_user_registration => "'.wwa_get_option('after_user_registration').'"', true); wwa_add_log($res_id, 'Current config: first_choice => "'.wwa_get_option('first_choice').'", website_name => "'.wwa_get_option('website_name').'", website_domain => "'.wwa_get_option('website_domain').'", remember_me => "'.wwa_get_option('remember_me').'", email_login => "'.wwa_get_option('email_login').'", user_verification => "'.wwa_get_option('user_verification').'", allow_authenticator_type => "'.wwa_get_option('allow_authenticator_type').'", show_authenticator_type => "'.wwa_get_option('show_authenticator_type').'", usernameless_login => "'.wwa_get_option('usernameless_login').'", password_reset => "'.wwa_get_option('password_reset').'", after_user_registration => "'.wwa_get_option('after_user_registration').'", terminology => "'.wwa_get_option('terminology').'", ror_origins => "'.str_replace("\n", ', ', wwa_get_option('ror_origins')).'"', true);
$extra_logger_info = apply_filters('wwa_logger_init', array());
foreach($extra_logger_info as $info){
wwa_add_log($res_id, $info, true);
}
wwa_add_log($res_id, 'Logger initialized', true); wwa_add_log($res_id, 'Logger initialized', true);
} }
wwa_update_option('logging', $post_logging); wwa_update_option('logging', $post_logging);
$post_first_choice = sanitize_text_field(wp_unslash($_POST['first_choice'])); if(!is_multisite()){
if($post_first_choice !== wwa_get_option('first_choice')){ $post_first_choice = sanitize_text_field(wp_unslash($_POST['first_choice']));
wwa_add_log($res_id, 'first_choice: "'.wwa_get_option('first_choice').'"->"'.$post_first_choice.'"'); if($post_first_choice !== wwa_get_option('first_choice')){
wwa_add_log($res_id, 'first_choice: "'.wwa_get_option('first_choice').'"->"'.$post_first_choice.'"');
}
wwa_update_option('first_choice', $post_first_choice);
} }
wwa_update_option('first_choice', $post_first_choice);
$post_website_name = sanitize_text_field(wp_unslash($_POST['website_name'])); $post_website_name = sanitize_text_field(wp_unslash($_POST['website_name']));
if($post_website_name !== wwa_get_option('website_name')){ if($post_website_name !== wwa_get_option('website_name')){
@ -86,6 +100,31 @@ if(
} }
wwa_update_option('website_domain', $post_website_domain); wwa_update_option('website_domain', $post_website_domain);
if(!is_multisite() && isset($_POST['ror_origins'])){
$raw_ror = wp_unslash($_POST['ror_origins']);
$ror_lines = explode("\n", $raw_ror);
$sanitized_ror = array();
foreach($ror_lines as $line){
$line = trim($line);
if($line === ''){
continue;
}
$parsed = wp_parse_url($line);
if(isset($parsed['scheme']) && isset($parsed['host'])){
$origin = $parsed['scheme'] . '://' . $parsed['host'];
if(isset($parsed['port'])){
$origin .= ':' . $parsed['port'];
}
$sanitized_ror[] = $origin;
}
}
$post_ror_origins = implode("\n", $sanitized_ror);
if($post_ror_origins !== wwa_get_option('ror_origins')){
wwa_add_log($res_id, 'ror_origins: "'.str_replace("\n", ', ', wwa_get_option('ror_origins')).'"->"'.str_replace("\n", ', ', $post_ror_origins).'"');
}
wwa_update_option('ror_origins', $post_ror_origins);
}
$post_remember_me = sanitize_text_field(wp_unslash($_POST['remember_me'])); $post_remember_me = sanitize_text_field(wp_unslash($_POST['remember_me']));
if($post_remember_me !== wwa_get_option('remember_me')){ if($post_remember_me !== wwa_get_option('remember_me')){
wwa_add_log($res_id, 'remember_me: "'.wwa_get_option('remember_me').'"->"'.$post_remember_me.'"'); wwa_add_log($res_id, 'remember_me: "'.wwa_get_option('remember_me').'"->"'.$post_remember_me.'"');
@ -98,23 +137,31 @@ if(
} }
wwa_update_option('email_login', $post_email_login); wwa_update_option('email_login', $post_email_login);
$post_user_verification = sanitize_text_field(wp_unslash($_POST['user_verification'])); if(!is_multisite()){
if($post_user_verification !== wwa_get_option('user_verification')){ $post_user_verification = sanitize_text_field(wp_unslash($_POST['user_verification']));
wwa_add_log($res_id, 'user_verification: "'.wwa_get_option('user_verification').'"->"'.$post_user_verification.'"'); if($post_user_verification !== wwa_get_option('user_verification')){
} wwa_add_log($res_id, 'user_verification: "'.wwa_get_option('user_verification').'"->"'.$post_user_verification.'"');
wwa_update_option('user_verification', $post_user_verification); }
wwa_update_option('user_verification', $post_user_verification);
$post_allow_authenticator_type = sanitize_text_field(wp_unslash($_POST['allow_authenticator_type'])); $post_allow_authenticator_type = sanitize_text_field(wp_unslash($_POST['allow_authenticator_type']));
if($post_allow_authenticator_type !== wwa_get_option('allow_authenticator_type')){ if($post_allow_authenticator_type !== wwa_get_option('allow_authenticator_type')){
wwa_add_log($res_id, 'allow_authenticator_type: "'.wwa_get_option('allow_authenticator_type').'"->"'.$post_allow_authenticator_type.'"'); wwa_add_log($res_id, 'allow_authenticator_type: "'.wwa_get_option('allow_authenticator_type').'"->"'.$post_allow_authenticator_type.'"');
} }
wwa_update_option('allow_authenticator_type', $post_allow_authenticator_type); wwa_update_option('allow_authenticator_type', $post_allow_authenticator_type);
$post_usernameless_login = sanitize_text_field(wp_unslash($_POST['usernameless_login'])); $post_show_authenticator_type = sanitize_text_field(wp_unslash($_POST['show_authenticator_type']));
if($post_usernameless_login !== wwa_get_option('usernameless_login')){ if($post_show_authenticator_type !== wwa_get_option('show_authenticator_type')){
wwa_add_log($res_id, 'usernameless_login: "'.wwa_get_option('usernameless_login').'"->"'.$post_usernameless_login.'"'); wwa_add_log($res_id, 'show_authenticator_type: "'.wwa_get_option('show_authenticator_type').'"->"'.$post_show_authenticator_type.'"');
}
wwa_update_option('show_authenticator_type', $post_show_authenticator_type);
$post_usernameless_login = sanitize_text_field(wp_unslash($_POST['usernameless_login']));
if($post_usernameless_login !== wwa_get_option('usernameless_login')){
wwa_add_log($res_id, 'usernameless_login: "'.wwa_get_option('usernameless_login').'"->"'.$post_usernameless_login.'"');
}
wwa_update_option('usernameless_login', $post_usernameless_login);
} }
wwa_update_option('usernameless_login', $post_usernameless_login);
$post_password_reset = sanitize_text_field(wp_unslash($_POST['password_reset'])); $post_password_reset = sanitize_text_field(wp_unslash($_POST['password_reset']));
if($post_password_reset !== wwa_get_option('password_reset')){ if($post_password_reset !== wwa_get_option('password_reset')){
@ -128,13 +175,35 @@ if(
} }
wwa_update_option('after_user_registration', $post_after_user_registration); wwa_update_option('after_user_registration', $post_after_user_registration);
$post_terminology = sanitize_text_field(wp_unslash($_POST['terminology']));
if($post_terminology !== wwa_get_option('terminology')){
wwa_add_log($res_id, 'terminology: "'.wwa_get_option('terminology').'"->"'.$post_terminology.'"');
}
wwa_update_option('terminology', $post_terminology);
do_action('wwa_save_settings', $res_id);
add_settings_error('wwa_settings', 'save_success', __('Settings saved.', 'wp-webauthn'), 'success'); add_settings_error('wwa_settings', 'save_success', __('Settings saved.', 'wp-webauthn'), 'success');
}elseif((isset($_POST['wwa_ref']) && $_POST['wwa_ref'] === 'true')){ }elseif((isset($_POST['wwa_ref']) && $_POST['wwa_ref'] === 'true')){
add_settings_error('wwa_settings', 'save_error', __('Settings NOT saved.', 'wp-webauthn')); add_settings_error('wwa_settings', 'save_error', __('Settings NOT saved.', 'wp-webauthn'));
} }
settings_errors('wwa_settings'); settings_errors('wwa_settings');
?>
wp_localize_script('wwa_admin', 'configs', array('usernameless' => (wwa_get_option('usernameless_login') === false ? 'false' : wwa_get_option('usernameless_login')), 'allow_authenticator_type' => (wwa_get_option('allow_authenticator_type') === false ? 'none' : wwa_get_option('allow_authenticator_type')))); <?php if(is_multisite() && wwa_validate_privileges()){ ?>
<div class="notice notice-info">
<p><?php
/* translators: %1$s: opening link tag, %2$s: closing link tag */
echo wp_kses(
sprintf(
__('Some settings are managed at the network level by the super administrator. %1$sConfigure network settings%2$s', 'wp-webauthn'),
'<a href="' . esc_url(network_admin_url('settings.php?page=wwa_network_admin')) . '">',
'</a>'
),
array('a' => array('href' => array()))
);
?></p>
</div>
<?php }
// Only admin can change settings // Only admin can change settings
if(wwa_validate_privileges()){ ?> if(wwa_validate_privileges()){ ?>
@ -144,37 +213,73 @@ wp_nonce_field('wwa_options_update');
?> ?>
<input type="hidden" name="wwa_ref" value="true"> <input type="hidden" name="wwa_ref" value="true">
<table class="form-table"> <table class="form-table">
<?php if(!is_multisite()){ ?>
<tr> <tr>
<th scope="row"><label for="first_choice"><?php _e('Preferred login method', 'wp-webauthn');?></label></th> <th scope="row"><label for="first_choice"><?php esc_html_e('Preferred login method', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_first_choice=wwa_get_option('first_choice');?> <?php $wwa_v_first_choice=wwa_get_option('first_choice');?>
<select name="first_choice" id="first_choice"> <select name="first_choice" id="first_choice">
<option value="true"<?php if($wwa_v_first_choice === 'true' || !$wwa_not_allowed){?> selected<?php }?>><?php _e('Prefer WebAuthn', 'wp-webauthn');?></option> <option value="true"<?php if($wwa_v_first_choice !== 'false' && !($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed)){?> selected<?php }?>><?php esc_html_e('Prefer WebAuthn', 'wp-webauthn');?></option>
<option value="false"<?php if($wwa_v_first_choice === 'false'){?> selected<?php }?>><?php _e('Prefer password', 'wp-webauthn');?></option> <option value="false"<?php if($wwa_v_first_choice === 'false'){?> selected<?php }?>><?php esc_html_e('Prefer password', 'wp-webauthn');?></option>
<option value="webauthn"<?php if($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed){?> selected<?php }if($wwa_not_allowed){?> disabled<?php }?>><?php _e('WebAuthn Only', 'wp-webauthn');?></option> <option value="webauthn"<?php if($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed){?> selected<?php }if($wwa_not_allowed){?> disabled<?php }?>><?php esc_html_e('WebAuthn Only', 'wp-webauthn');?></option>
</select> </select>
<p class="description"><?php _e('When using "WebAuthn Only", password login will be completely disabled. Please make sure your browser supports WebAuthn, otherwise you may unable to login.<br>User that doesn\'t have any registered authenticator (e.g. new user) will unable to login when using "WebAuthn Only".<br>When the browser does not support WebAuthn, the login method will default to password if password login is not disabled.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('When using "WebAuthn Only", password login will be completely disabled. Please make sure your browser supports WebAuthn, otherwise you may unable to login.<br>User that doesn\'t have any registered authenticator (e.g. new user) will unable to login when using "WebAuthn Only".<br>When the browser does not support WebAuthn, the login method will default to password if password login is not disabled.', 'wp-webauthn'), array('br' => array()));?></p>
</td> </td>
</tr> </tr>
<?php } ?>
<tr> <tr>
<th scope="row"><label for="website_name"><?php _e('Website identifier', 'wp-webauthn');?></label></th> <th scope="row"><label for="terminology"><?php esc_html_e('Terminology used for users', 'wp-webauthn');?></label></th>
<td> <td>
<input required name="website_name" type="text" id="website_name" value="<?php echo wwa_get_option('website_name');?>" class="regular-text"> <?php $wwa_v_t=wwa_get_option('terminology');
<p class="description"><?php _e('This identifier is for identification purpose only and <strong>DOES NOT</strong> affect the authentication process in anyway.', 'wp-webauthn');?></p> if($wwa_v_t === false){
</td> wwa_update_option('terminology', 'webauthn');
</tr> $wwa_v_t = 'webauthn';
<tr> }
<th scope="row"><label for="website_domain"><?php _e('Website domain', 'wp-webauthn');?></label></th> ?>
<td> <fieldset>
<input required name="website_domain" type="text" id="website_domain" value="<?php echo wwa_get_option('website_domain');?>" class="regular-text"> <label><input type="radio" name="terminology" value="webauthn" <?php if($wwa_v_t === 'webauthn'){?>checked="checked"<?php }?>> WebAuthn</label><br>
<p class="description"><?php _e('This field <strong>MUST</strong> be exactly the same with the current domain or parent domain.', 'wp-webauthn');?></p> <label><input type="radio" name="terminology" value="passkey" <?php if($wwa_v_t === 'passkey'){?>checked="checked"<?php }?>> <?php echo esc_html_x('Passkey', 'Please note Passkey is a trademark owned by FIDO Alliance, please follow their guidelines for translation', 'wp-webauthn');?></label><br>
<p class="description"><?php echo wp_kses(__('Choose how to name the authenticating technology to users.<br>Passkey is the brand name for this new way of digital authentication, while WebAuthn is the name of the technical standard under the hood.', 'wp-webauthn'), array('br' => array()));?></p>
</fieldset>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"></th> <th scope="row"></th>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="remember_me"><?php _e('Allow to remember login', 'wp-webauthn');?></label></th> <th scope="row"><label for="website_name"><?php esc_html_e('Website identifier', 'wp-webauthn');?></label></th>
<td>
<input required name="website_name" type="text" id="website_name" value="<?php echo esc_attr(wwa_get_option('website_name'));?>" class="regular-text">
<p class="description"><?php echo wp_kses(__('This identifier is for identification purpose only and <strong>DOES NOT</strong> affect the authentication process in anyway.', 'wp-webauthn'), array('strong' => array()));?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="website_domain"><?php esc_html_e('Website domain', 'wp-webauthn');?></label></th>
<td>
<input required name="website_domain" type="text" id="website_domain" value="<?php echo esc_attr(wwa_get_option('website_domain'));?>" class="regular-text">
<p class="description"><?php echo wp_kses(__('This field <strong>MUST</strong> be exactly the same with the current domain or parent domain.', 'wp-webauthn'), array('strong' => array()));?></p>
</td>
</tr>
<?php if(!is_multisite()){ ?>
<!-- Feature not fully ready <tr>
<th scope="row"><label for="ror_origins"><?php esc_html_e('Related origins', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_ror = wwa_get_option('ror_origins');
if($wwa_v_ror === false){
wwa_update_option('ror_origins', '');
$wwa_v_ror = '';
}
?>
<textarea name="ror_origins" id="ror_origins" rows="4" cols="50" class="large-text code"><?php echo esc_textarea($wwa_v_ror);?></textarea>
<p class="description"><?php echo wp_kses(__('Allow cross-site passkey usages (<a href="https://passkeys.dev/docs/advanced/related-origins/" target="_blank">Related Origin Requests</a>). May be useful for multi-site networks.<br> Enter one origin per line (e.g. <code>https://example.com</code>). Leave empty to disable.', 'wp-webauthn'), array('a' => array('href' => array(), 'target' => array()), 'br' => array(), 'code' => array()));?></p>
</td>
</tr> -->
<?php } ?>
<tr>
<th scope="row"></th>
</tr>
<tr>
<th scope="row"><label for="remember_me"><?php esc_html_e('Allow to remember login', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_rm=wwa_get_option('remember_me'); <?php $wwa_v_rm=wwa_get_option('remember_me');
if($wwa_v_rm === false){ if($wwa_v_rm === false){
@ -183,14 +288,14 @@ if($wwa_v_rm === false){
} }
?> ?>
<fieldset> <fieldset>
<label><input type="radio" name="remember_me" value="true" <?php if($wwa_v_rm === 'true'){?>checked="checked"<?php }?>> <?php _e("Enable", "wp-webauthn");?></label><br> <label><input type="radio" name="remember_me" value="true" <?php if($wwa_v_rm === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="remember_me" value="false" <?php if($wwa_v_rm === 'false'){?>checked="checked"<?php }?>> <?php _e("Disable", "wp-webauthn");?></label><br> <label><input type="radio" name="remember_me" value="false" <?php if($wwa_v_rm === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php _e('Show the \'Remember Me\' checkbox beside the login form when using WebAuthn.', 'wp-webauthn');?></p> <p class="description"><?php esc_html_e('Show the \'Remember Me\' checkbox beside the login form when using WebAuthn.', 'wp-webauthn');?></p>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="email_login"><?php _e('Allow to login with email addresses', 'wp-webauthn');?></label></th> <th scope="row"><label for="email_login"><?php esc_html_e('Allow to login with email addresses', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_el=wwa_get_option('email_login'); <?php $wwa_v_el=wwa_get_option('email_login');
if($wwa_v_el === false){ if($wwa_v_el === false){
@ -199,25 +304,26 @@ if($wwa_v_el === false){
} }
?> ?>
<fieldset> <fieldset>
<label><input type="radio" name="email_login" value="true" <?php if($wwa_v_el === 'true'){?>checked="checked"<?php }?>> <?php _e("Enable", "wp-webauthn");?></label><br> <label><input type="radio" name="email_login" value="true" <?php if($wwa_v_el === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="email_login" value="false" <?php if($wwa_v_el === 'false'){?>checked="checked"<?php }?>> <?php _e("Disable", "wp-webauthn");?></label><br> <label><input type="radio" name="email_login" value="false" <?php if($wwa_v_el === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php _e('Allow to find users via email addresses when logging in.<br><strong>Note that if enabled attackers may be able to brute force the correspondences between email addresses and users.</strong>', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('Allow to find users via email addresses when logging in through WebAuthn.<br><strong>Note that if enabled attackers may be able to brute force the correspondences between email addresses and users.</strong>', 'wp-webauthn'), array('br' => array(), 'strong' => array()));?></p>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
<?php if(!is_multisite()){ ?>
<tr> <tr>
<th scope="row"><label for="user_verification"><?php _e('Require user verification', 'wp-webauthn');?></label></th> <th scope="row"><label for="user_verification"><?php esc_html_e('Require user verification', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_uv=wwa_get_option('user_verification');?> <?php $wwa_v_uv=wwa_get_option('user_verification');?>
<fieldset id="wwa-uv-field"> <fieldset id="wwa-uv-field">
<label><input type="radio" name="user_verification" value="true" <?php if($wwa_v_uv === 'true'){?>checked="checked"<?php }?>> <?php _e("Enable", "wp-webauthn");?></label><br> <label><input type="radio" name="user_verification" value="true" <?php if($wwa_v_uv === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="user_verification" value="false" <?php if($wwa_v_uv === 'false'){?>checked="checked"<?php }?>> <?php _e("Disable", "wp-webauthn");?></label><br> <label><input type="radio" name="user_verification" value="false" <?php if($wwa_v_uv === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php _e('User verification can improve security, but is not fully supported by mobile devices. <br> If you cannot register or verify your authenticators, please consider disabling user verification.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('User verification can improve security, but is not fully supported by mobile devices. <br> If you cannot register or verify your authenticators, please consider disabling user verification.', 'wp-webauthn'), array('br' => array()));?></p>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="usernameless_login"><?php _e('Allow to login without username', 'wp-webauthn');?></label></th> <th scope="row"><label for="usernameless_login"><?php esc_html_e('Allow to login without username', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_ul=wwa_get_option('usernameless_login'); <?php $wwa_v_ul=wwa_get_option('usernameless_login');
if($wwa_v_ul === false){ if($wwa_v_ul === false){
@ -226,14 +332,14 @@ if($wwa_v_ul === false){
} }
?> ?>
<fieldset> <fieldset>
<label><input type="radio" name="usernameless_login" value="true" <?php if($wwa_v_ul === 'true'){?>checked="checked"<?php }?>> <?php _e("Enable", "wp-webauthn");?></label><br> <label><input type="radio" name="usernameless_login" value="true" <?php if($wwa_v_ul === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="usernameless_login" value="false" <?php if($wwa_v_ul === 'false'){?>checked="checked"<?php }?>> <?php _e("Disable", "wp-webauthn");?></label><br> <label><input type="radio" name="usernameless_login" value="false" <?php if($wwa_v_ul === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php _e('Allow users to register authenticator with usernameless authentication feature and login without username.<br><strong>User verification will be enabled automatically when authenticating with usernameless authentication feature.</strong><br>Some authenticators and some browsers <strong>DO NOT</strong> support this feature.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('Allow users to register authenticator with usernameless authentication feature and login without username.<br><strong>User verification will be enabled automatically when authenticating with usernameless authentication feature.</strong><br>Some authenticators and some browsers <strong>DO NOT</strong> support this feature.', 'wp-webauthn'), array('br' => array(), 'strong' => array()));?></p>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="allow_authenticator_type"><?php _e('Allow a specific type of authenticator', 'wp-webauthn');?></label></th> <th scope="row"><label for="allow_authenticator_type"><?php esc_html_e('Allow a specific type of authenticator', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_at=wwa_get_option('allow_authenticator_type'); <?php $wwa_v_at=wwa_get_option('allow_authenticator_type');
if($wwa_v_at === false){ if($wwa_v_at === false){
@ -242,18 +348,35 @@ if($wwa_v_at === false){
} }
?> ?>
<select name="allow_authenticator_type" id="allow_authenticator_type"> <select name="allow_authenticator_type" id="allow_authenticator_type">
<option value="none"<?php if($wwa_v_at === 'none'){?> selected<?php }?>><?php _e('Any', 'wp-webauthn');?></option> <option value="none"<?php if($wwa_v_at === 'none'){?> selected<?php }?>><?php esc_html_e('Any', 'wp-webauthn');?></option>
<option value="platform"<?php if($wwa_v_at === 'platform'){?> selected<?php }?>><?php _e('Platform (e.g. Passkey or built-in sensors)', 'wp-webauthn');?></option> <option value="platform"<?php if($wwa_v_at === 'platform'){?> selected<?php }?>><?php esc_html_e('Platform (e.g. Passkey or built-in sensors)', 'wp-webauthn');?></option>
<option value="cross-platform"<?php if($wwa_v_at === 'cross-platform'){?> selected<?php }?>><?php _e('Roaming (e.g. USB security keys)', 'wp-webauthn');?></option> <option value="cross-platform"<?php if($wwa_v_at === 'cross-platform'){?> selected<?php }?>><?php esc_html_e('Roaming (e.g. USB security keys)', 'wp-webauthn');?></option>
</select> </select>
<p class="description"><?php _e('If a type is selected, the browser will only prompt for authenticators of selected type when authenticating and user can only register authenticators of selected type.', 'wp-webauthn');?></p> <p class="description"><?php esc_html_e('If a type is selected, the browser will only prompt for authenticators of selected type when authenticating and user can only register authenticators of selected type.', 'wp-webauthn');?></p>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="show_authenticator_type"><?php esc_html_e('Allow users to choose authenticator type', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_sat=wwa_get_option('show_authenticator_type');
if($wwa_v_sat === false){
wwa_update_option('show_authenticator_type', 'true');
$wwa_v_sat = 'true';
}
?>
<fieldset>
<label><input type="radio" name="show_authenticator_type" value="true" <?php if($wwa_v_sat === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="show_authenticator_type" value="false" <?php if($wwa_v_sat === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php echo wp_kses(__('When enabled, users can select the authenticator type when registering.<br>The "Allow a specific type" restriction above still applies regardless of this setting.', 'wp-webauthn'), array('br' => array()));?></p>
</fieldset>
</td>
</tr>
<?php } ?>
<tr>
<th scope="row"></th> <th scope="row"></th>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="password_reset"><?php _e('Disable password reset for', 'wp-webauthn');?></label></th> <th scope="row"><label for="password_reset"><?php esc_html_e('Disable password reset for', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_pr=wwa_get_option('password_reset'); <?php $wwa_v_pr=wwa_get_option('password_reset');
if($wwa_v_pr === false){ if($wwa_v_pr === false){
@ -262,37 +385,53 @@ if($wwa_v_pr === false){
} }
?> ?>
<select name="password_reset" id="password_reset"> <select name="password_reset" id="password_reset">
<option value="off"<?php if($wwa_v_pr === 'off'){?> selected<?php }?>><?php _e('Off', 'wp-webauthn');?></option> <option value="off"<?php if($wwa_v_pr === 'off'){?> selected<?php }?>><?php esc_html_e('Off', 'wp-webauthn');?></option>
<option value="admin"<?php if($wwa_v_pr === 'admin'){?> selected<?php }?>><?php _e('Everyone except administrators', 'wp-webauthn');?></option> <option value="admin"<?php if($wwa_v_pr === 'admin'){?> selected<?php }?>><?php esc_html_e('Everyone except administrators', 'wp-webauthn');?></option>
<option value="all"<?php if($wwa_v_pr === 'all'){?> selected<?php }?>><?php _e('Everyone', 'wp-webauthn');?></option> <option value="all"<?php if($wwa_v_pr === 'all'){?> selected<?php }?>><?php esc_html_e('Everyone', 'wp-webauthn');?></option>
</select> </select>
<p class="description"><?php _e('Disable the "Set new password" and "Forgot password" features, and remove the "Forgot password" link on the login page. This may be useful when enabling "WebAuthn Only".<br>If "Everyone except administrators" is selected, only administrators with the "Edit user" permission will be able to update passwords (for all users).', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('Disable the "Set new password" and "Forgot password" features, and remove the "Forgot password" link on the login page. This may be useful when enabling "WebAuthn Only".<br>If "Everyone except administrators" is selected, only administrators with the "Edit user" permission will be able to update passwords (for all users).', 'wp-webauthn'), array('br' => array()));?></p>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"></th> <th scope="row"></th>
</tr> </tr>
<tr> <tr>
<th scope="row"><label for="after_user_registration"><?php _e('After User Registration', 'wp-webauthn');?></label></th> <th scope="row"><label for="after_user_registration"><?php esc_html_e('After User Registration', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_aur=wwa_get_option('after_user_registration'); <?php $wwa_v_aur=wwa_get_option('after_user_registration');
if($wwa_v_aur === false){ if($wwa_v_aur === false){
wwa_update_option('after_user_registration', 'none'); wwa_update_option('after_user_registration', 'none');
$wwa_v_aur = 'none'; $wwa_v_aur = 'none';
} }
/** @disregard P1009 Undefined type */
if($wwa_v_aur === 'mail' && (!function_exists('\WPWebAuthn\OTML\loaded') || !\WPWebAuthn\OTML\loaded())){
wwa_update_option('after_user_registration', 'none');
$wwa_v_aur = 'none';
}
?> ?>
<select name="after_user_registration" id="after_user_registration"> <select name="after_user_registration" id="after_user_registration">
<option value="none"<?php if($wwa_v_aur === 'none'){?> selected<?php }?>><?php _e('No action', 'wp-webauthn');?></option> <option value="none"<?php if($wwa_v_aur === 'none'){?> selected<?php }?>><?php esc_html_e('No action', 'wp-webauthn');?></option>
<option value="login"<?php if($wwa_v_aur === 'login'){?> selected<?php }?>><?php _e('Log user in and redirect to user\'s profile', 'wp-webauthn');?></option> <option value="login"<?php if($wwa_v_aur === 'login'){?> selected<?php }?>><?php esc_html_e('Log user in and redirect to user\'s profile', 'wp-webauthn');?></option>
<?php if(function_exists('\WPWebAuthn\OTML\loaded') && \WPWebAuthn\OTML\loaded()){ /** @disregard P1009 Undefined type */ ?>
<option value="mail"<?php if($wwa_v_aur === 'mail'){?> selected<?php }?>><?php esc_html_e('Send user an one-time login link via email', 'wp-webauthn');?></option>
<?php } ?>
</select> </select>
<p class="description"><?php _e('What to do when a new user registered.<br>By default, new users have to login manually after registration. If "WebAuthn Only" is enabled, they will not be able to login.<br>When using "Log user in", new users will be logged in automatically and redirected to their profile settings so that they can set up WebAuthn authenticators.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('What to do when a new user registered.<br>By default, new users have to login manually after registration. If "WebAuthn Only" is enabled, they will not be able to login.<br>When using "Log user in", new users will be logged in automatically and redirected to their profile settings so that they can set up WebAuthn authenticators.', 'wp-webauthn'), array('br' => array()));?>
<?php
/** @disregard P1009 Undefined type */
if(function_exists('\WPWebAuthn\OTML\loaded') && \WPWebAuthn\OTML\loaded()){
echo wp_kses(__('<br>When using "Send login link", an one-time login link will be automatically sent to the user\'s email adress. This will replace the default WordPress welcome email.<br><strong>"Send login link" will work even if "Allow user login by login link via email" is disabled.</strong>', 'wp-webauthn'), array('br' => array(), 'strong' => array()));
}
?>
</p>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"></th> <th scope="row"></th>
</tr> </tr>
<?php do_action('wwa_admin_page_extra'); ?>
<tr> <tr>
<th scope="row"><label for="logging"><?php _e('Logging', 'wp-webauthn');?></label></th> <th scope="row"><label for="logging"><?php esc_html_e('Logging', 'wp-webauthn');?></label></th>
<td> <td>
<?php $wwa_v_log=wwa_get_option('logging'); <?php $wwa_v_log=wwa_get_option('logging');
if($wwa_v_log === false){ if($wwa_v_log === false){
@ -301,12 +440,12 @@ if($wwa_v_log === false){
} }
?> ?>
<fieldset> <fieldset>
<label><input type="radio" name="logging" value="true" <?php if($wwa_v_log === 'true'){?>checked="checked"<?php }?>> <?php _e("Enable", "wp-webauthn");?></label><br> <label><input type="radio" name="logging" value="true" <?php if($wwa_v_log === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="logging" value="false" <?php if($wwa_v_log === 'false'){?>checked="checked"<?php }?>> <?php _e("Disable", "wp-webauthn");?></label><br> <label><input type="radio" name="logging" value="false" <?php if($wwa_v_log === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p> <p>
<button id="clear_log" class="button" <?php $log = get_option('wwa_log');if($log === false || ($log !== false && count($log) === 0)){?> disabled<?php }?>><?php _e('Clear log', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="log-count"><?php echo __("Log count: ", "wp-webauthn").($log === false ? "0" : strval(count($log)));?></span> <button id="clear_log" class="button" <?php $log = get_option('wwa_log');if($log === false || ($log !== false && count($log) === 0)){?> disabled<?php }?>><?php esc_html_e('Clear log', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="log-count"><?php echo esc_html(__('Log count: ', 'wp-webauthn') . ($log === false ? '0' : strval(count($log))));?></span>
</p> </p>
<p class="description"><?php _e('For debugging only. Enable only when needed.<br><strong>Note: Logs may contain sensitive information.</strong>', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('For debugging only. Enable only when needed.<br><strong>Note: Logs may contain sensitive information.</strong>', 'wp-webauthn'), array('br' => array(), 'strong' => array()));?></p>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
@ -315,12 +454,12 @@ if($wwa_v_log === false){
if(wwa_get_option('logging') === 'true' || ($log !== false && count($log) > 0)){ if(wwa_get_option('logging') === 'true' || ($log !== false && count($log) > 0)){
?> ?>
<div<?php if(wwa_get_option('logging') !== 'true'){?> id="wwa-remove-log"<?php }?>> <div<?php if(wwa_get_option('logging') !== 'true'){?> id="wwa-remove-log"<?php }?>>
<h2><?php _e('Log', 'wp-webauthn');?></h2> <h2><?php esc_html_e('Log', 'wp-webauthn');?></h2>
<textarea name="wwa_log" id="wwa_log" rows="20" cols="108" readonly><?php echo get_option("wwa_log") === false ? "" : implode("\n", get_option("wwa_log"));?></textarea> <textarea name="wwa_log" id="wwa_log" rows="20" cols="108" readonly><?php echo get_option("wwa_log") === false ? "" : esc_textarea(implode("\n", get_option("wwa_log")));?></textarea>
<p class="description"><?php _e('Automatic update every 5 seconds.', 'wp-webauthn');?></p> <p class="description"><?php esc_html_e('Automatic update every 5 seconds.', 'wp-webauthn');?></p>
<br> <br>
</div> </div>
<?php }} <?php }}
/* translators: %s: admin profile url */ ?> /* translators: %s: admin profile url */ ?>
<p class="description"><?php printf(__('To register a new authenticator or edit your authenticators, please go to <a href="%s#wwa-webauthn-start">your profile</a>.', 'wp-webauthn'), admin_url('profile.php'));?></p> <p class="description"><?php echo wp_kses(sprintf(__('To register a new authenticator or edit your authenticators, please go to <a href="%s#wwa-webauthn-start">your profile</a>.', 'wp-webauthn'), esc_url(admin_url('profile.php'))), array('a' => array('href' => array())));?></p>
</div> </div>

View File

@ -1,4 +1,8 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
require_once('wp-webauthn-vendor/autoload.php'); require_once('wp-webauthn-vendor/autoload.php');
use Webauthn\Server; use Webauthn\Server;
use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialRpEntity;
@ -7,169 +11,257 @@ use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface; use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface;
use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialSource;
use Webauthn\AuthenticatorSelectionCriteria; use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialDescriptor;
use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator; use Nyholm\Psr7Server\ServerRequestCreator;
/**
* Store all publickeys and pubilckey metas
*/
class PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepositoryInterface { class PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepositoryInterface {
// Get one credential by credential ID private $registration_context = null;
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource { public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource {
$data = $this->read(); global $wpdb;
if(isset($data[base64_encode($publicKeyCredentialId)])){ $key = base64_encode($publicKeyCredentialId);
return PublicKeyCredentialSource::createFromArray($data[base64_encode($publicKeyCredentialId)]);
$row = $wpdb->get_row($wpdb->prepare(
"SELECT credential_source FROM {$wpdb->wwa_credentials} WHERE credential_id = %s",
$key
));
if($row !== null){
$decoded = json_decode($row->credential_source, true);
if(is_array($decoded)){
try {
return PublicKeyCredentialSource::createFromArray($decoded);
} catch(\Throwable $e) {
return null;
}
}
return null;
} }
if(!get_option('wwa_credentials_migrated')){
$old = get_option('wwa_options');
if(isset($old['user_credentials'])){
$data = json_decode($old['user_credentials'], true);
if(is_array($data) && isset($data[$key]) && is_array($data[$key])){
try {
return PublicKeyCredentialSource::createFromArray($data[$key]);
} catch(\Throwable $e) {
return null;
}
}
}
}
return null; return null;
} }
// Get one credential's meta by credential ID
public function findOneMetaByCredentialId(string $publicKeyCredentialId): ?array { public function findOneMetaByCredentialId(string $publicKeyCredentialId): ?array {
$meta = json_decode(wwa_get_option("user_credentials_meta"), true); global $wpdb;
if(isset($meta[base64_encode($publicKeyCredentialId)])){ $key = base64_encode($publicKeyCredentialId);
return $meta[base64_encode($publicKeyCredentialId)];
$row = $wpdb->get_row($wpdb->prepare(
"SELECT user_handle, human_name, authenticator_type, usernameless, added, last_used
FROM {$wpdb->wwa_credentials} WHERE credential_id = %s",
$key
));
if($row !== null){
return array(
'human_name' => $row->human_name,
'added' => $row->added,
'authenticator_type' => $row->authenticator_type,
'user' => $row->user_handle,
'usernameless' => (bool) $row->usernameless,
'last_used' => $row->last_used,
);
} }
if(!get_option('wwa_credentials_migrated')){
$old = get_option('wwa_options');
if(isset($old['user_credentials_meta'])){
$meta = json_decode($old['user_credentials_meta'], true);
if(is_array($meta) && isset($meta[$key])){
return $meta[$key];
}
}
}
return null; return null;
} }
// Get all credentials of one user public function findAllForUserEntityByUserId(int $wp_user_id): array {
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array { global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT credential_source FROM {$wpdb->wwa_credentials} WHERE user_id = %d",
$wp_user_id
));
$sources = []; $sources = [];
foreach($this->read() as $data){ foreach($rows as $row){
$source = PublicKeyCredentialSource::createFromArray($data); $decoded = json_decode($row->credential_source, true);
if($source->getUserHandle() === $publicKeyCredentialUserEntity->getId()){ if(!is_array($decoded)){
$sources[] = $source; continue;
}
try {
$sources[] = PublicKeyCredentialSource::createFromArray($decoded);
} catch(\Throwable $e) {
continue;
} }
} }
return $sources; return $sources;
} }
public function findCredentialsForUserEntityByType(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity, string $credentialType): array { public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
$credentialsForUserEntity = $this->findAllForUserEntity($publicKeyCredentialUserEntity); global $wpdb;
$credentialsByType = []; $handle = $publicKeyCredentialUserEntity->getId();
foreach($credentialsForUserEntity as $credential){
if($this->findOneMetaByCredentialId($credential->getPublicKeyCredentialId())["authenticator_type"] === $credentialType){ $wp_user_id = $wpdb->get_var($wpdb->prepare(
$credentialsByType[] = $credential; "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = 'wwa_user_handle' AND meta_value = %s LIMIT 1",
$handle
));
if($wp_user_id !== null){
return $this->findAllForUserEntityByUserId(intval($wp_user_id));
}
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT credential_source FROM {$wpdb->wwa_credentials} WHERE user_handle = %s",
$handle
));
$sources = [];
foreach($rows as $row){
$decoded = json_decode($row->credential_source, true);
if(!is_array($decoded)){
continue;
}
try {
$sources[] = PublicKeyCredentialSource::createFromArray($decoded);
} catch(\Throwable $e) {
continue;
} }
} }
return $credentialsByType; return $sources;
} }
// Save credential into database public function findCredentialsForUserEntityByType(int $wp_user_id, string $credentialType): array {
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, bool $usernameless = false): void { global $wpdb;
$data = $this->read(); $rows = $wpdb->get_results($wpdb->prepare(
$data_key = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()); "SELECT credential_source FROM {$wpdb->wwa_credentials}
$data[$data_key] = $publicKeyCredentialSource; WHERE user_id = %d AND authenticator_type = %s",
$this->write($data, $data_key, $usernameless); $wp_user_id, $credentialType
} ));
$sources = [];
// Update credential's last used foreach($rows as $row){
public function updateCredentialLastUsed(string $publicKeyCredentialId): void { $decoded = json_decode($row->credential_source, true);
$credential = $this->findOneMetaByCredentialId($publicKeyCredentialId); if(!is_array($decoded)){
if($credential !== null){ continue;
$credential["last_used"] = date('Y-m-d H:i:s', current_time('timestamp')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date }
$meta = json_decode(wwa_get_option("user_credentials_meta"), true); try {
$meta[base64_encode($publicKeyCredentialId)] = $credential; $sources[] = PublicKeyCredentialSource::createFromArray($decoded);
wwa_update_option("user_credentials_meta", wp_json_encode($meta)); } catch(\Throwable $e) {
} continue;
}
// List all authenticators for the front-end
public function getShowList(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
$data = json_decode(wwa_get_option("user_credentials_meta"), true);
$arr = array();
$user_id = $publicKeyCredentialUserEntity->getId();
foreach($data as $key => $value){
if($user_id === $value["user"]){
array_push($arr, array(
"key" => rtrim(strtr(base64_encode($key), '+/', '-_'), '='),
"name" => base64_decode($value["human_name"]),
"type" => $value["authenticator_type"],
"added" => $value["added"],
"usernameless" => isset($value["usernameless"]) ? $value["usernameless"] : false,
"last_used" => isset($value["last_used"]) ? $value["last_used"] : "-"
));
} }
} }
return array_map(function($item){return array("key" => $item["key"], "name" => esc_html($item["name"]), "type" => $item["type"], "added" => $item["added"], "usernameless" => $item["usernameless"], "last_used" => $item["last_used"]);}, $arr); return $sources;
} }
// Modify an authenticator public function setRegistrationContext(int $user_id, string $name, string $type, bool $usernameless = false): void {
public function modifyAuthenticator(string $id, string $name, PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity, string $action, string $res_id): string { $this->registration_context = compact('user_id', 'name', 'type', 'usernameless');
$keys = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
$user_id = $publicKeyCredentialUserEntity->getId();
// Check if the user has the authenticator
foreach($keys as $item){
if($item->getUserHandle() === $user_id){
if(base64_encode($item->getPublicKeyCredentialId()) === base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT))){
if($action === "rename"){
$this->renameCredential(base64_encode($item->getPublicKeyCredentialId()), $name, $res_id);
}elseif($action === "remove"){
$this->removeCredential(base64_encode($item->getPublicKeyCredentialId()), $res_id);
}
wwa_add_log($res_id, "ajax_modify_authenticator: Done");
return "true";
}
}
}
wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Authenticator not found, exit");
return "Not Found.";
} }
// Rename a credential from database by credential ID public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void {
private function renameCredential(string $id, string $name, string $res_id): void { global $wpdb;
$meta = json_decode(wwa_get_option("user_credentials_meta"), true); $cred_id = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
wwa_add_log($res_id, "ajax_modify_authenticator: Rename \"".base64_decode($meta[$id]["human_name"])."\" -> \"".$name."\"");
$meta[$id]["human_name"] = base64_encode($name);
wwa_update_option("user_credentials_meta", wp_json_encode($meta));
}
// Remove a credential from database by credential ID $exists = $wpdb->get_var($wpdb->prepare(
private function removeCredential(string $id, string $res_id): void { "SELECT COUNT(*) FROM {$wpdb->wwa_credentials} WHERE credential_id = %s",
$data = $this->read(); $cred_id
unset($data[$id]); ));
$this->write($data, '');
$meta = json_decode(wwa_get_option("user_credentials_meta"), true);
wwa_add_log($res_id, "ajax_modify_authenticator: Remove \"".base64_decode($meta[$id]["human_name"])."\"");
unset($meta[$id]);
wwa_update_option("user_credentials_meta", wp_json_encode($meta));
}
// Read credential database if($exists > 0){
private function read(): array { $wpdb->update(
if(wwa_get_option("user_credentials") !== NULL){ $wpdb->wwa_credentials,
try{ array('credential_source' => wp_json_encode($publicKeyCredentialSource)),
return json_decode(wwa_get_option("user_credentials"), true); array('credential_id' => $cred_id)
}catch(\Throwable $exception) {
return [];
}
}
return [];
}
// Save credentials data
private function write(array $data, string $key, bool $usernameless = false): void {
if(isset($_POST["name"]) && isset($_POST["type"]) && ($_POST["type"] === "platform" || $_POST["type"] == "cross-platform" || $_POST["type"] === "none") && $key !== ''){
// Save credentials's meta separately
$source = $data[$key]->getUserHandle();
$meta = json_decode(wwa_get_option("user_credentials_meta"), true);
$meta[$key] = array(
"human_name" => base64_encode(sanitize_text_field(wp_unslash($_POST["name"]))),
"added" => date('Y-m-d H:i:s', current_time('timestamp')), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
"authenticator_type" => sanitize_text_field(wp_unslash($_POST["type"])),
"user" => $source,
"usernameless" => $usernameless,
"last_used" => "-"
); );
wwa_update_option("user_credentials_meta", wp_json_encode($meta)); return;
} }
wwa_update_option("user_credentials", wp_json_encode($data));
if($this->registration_context === null){
return;
}
$ctx = $this->registration_context;
$wpdb->insert($wpdb->wwa_credentials, array(
'credential_id' => $cred_id,
'user_id' => $ctx['user_id'],
'registered_blog_id' => get_current_blog_id(),
'credential_source' => wp_json_encode($publicKeyCredentialSource),
'user_handle' => $publicKeyCredentialSource->getUserHandle(),
'human_name' => base64_encode(sanitize_text_field($ctx['name'])),
'authenticator_type' => sanitize_text_field($ctx['type']),
'usernameless' => $ctx['usernameless'] ? 1 : 0,
'added' => current_time('mysql'),
'last_used' => '-',
));
$this->registration_context = null;
}
public function updateCredentialLastUsed(string $publicKeyCredentialId): void {
global $wpdb;
$wpdb->update(
$wpdb->wwa_credentials,
array('last_used' => current_time('mysql')),
array('credential_id' => base64_encode($publicKeyCredentialId))
);
}
public function getShowListByUserId(int $wp_user_id): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT credential_id, human_name, authenticator_type, added, usernameless, last_used
FROM {$wpdb->wwa_credentials}
WHERE user_id = %d AND registered_blog_id = %d
ORDER BY added ASC",
$wp_user_id, get_current_blog_id()
));
return array_map(function($row){
return array(
'key' => rtrim(strtr($row->credential_id, '+/', '-_'), '='),
'name' => esc_html(base64_decode($row->human_name)),
'type' => $row->authenticator_type,
'added' => $row->added,
'usernameless' => (bool) $row->usernameless,
'last_used' => $row->last_used,
);
}, $rows);
}
public function renameCredential(string $credential_id_urlsafe, int $wp_user_id, string $new_name, string $res_id): bool {
global $wpdb;
$credential_id = base64_encode(base64_decode(strtr($credential_id_urlsafe, '-_', '+/')));
wwa_add_log($res_id, "ajax_modify_authenticator: Rename credential");
$affected = $wpdb->update(
$wpdb->wwa_credentials,
array('human_name' => base64_encode(sanitize_text_field($new_name))),
array('credential_id' => $credential_id, 'user_id' => $wp_user_id, 'registered_blog_id' => get_current_blog_id())
);
return $affected !== false;
}
public function removeCredential(string $credential_id_urlsafe, int $wp_user_id, string $res_id): bool {
global $wpdb;
$credential_id = base64_encode(base64_decode(strtr($credential_id_urlsafe, '-_', '+/')));
wwa_add_log($res_id, "ajax_modify_authenticator: Remove credential");
$affected = $wpdb->delete(
$wpdb->wwa_credentials,
array('credential_id' => $credential_id, 'user_id' => $wp_user_id, 'registered_blog_id' => get_current_blog_id())
);
return $affected > 0;
} }
} }
// Bind an authenticator // Bind an authenticator
function wwa_ajax_create(){ function wwa_ajax_create(){
check_ajax_referer('wwa_ajax');
$client_id = false;
try{ try{
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
$client_id = strval(time()).wwa_generate_random_string(24); $client_id = strval(time()).wwa_generate_random_string(24);
@ -260,15 +352,17 @@ function wwa_ajax_create(){
wwa_add_log($res_id, "ajax_create: user => \"".$user_info->user_login."\""); wwa_add_log($res_id, "ajax_create: user => \"".$user_info->user_login."\"");
// Get user ID or create one // Get user ID or create one
$user_key = ""; $user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){ if(!$user_key){
wwa_add_log($res_id, "ajax_create: User not initialized, initialize"); $user_id_map = wwa_get_option("user_id");
$user_array = wwa_get_option("user_id"); if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = hash("sha256", $user_info->user_login."-".$user_info->display_name."-".wwa_generate_random_string(10)); $user_key = $user_id_map[$user_info->user_login];
$user_array[$user_info->user_login] = $user_key; update_user_meta($user_info->ID, 'wwa_user_handle', $user_key);
wwa_update_option("user_id", $user_array); }else{
}else{ wwa_add_log($res_id, "ajax_create: User not initialized, initialize");
$user_key = wwa_get_option("user_id")[$user_info->user_login]; $user_key = hash("sha256", $user_info->user_login."-".$user_info->display_name."-".wwa_generate_random_string(10));
update_user_meta($user_info->ID, 'wwa_user_handle', $user_key);
}
} }
$user = array( $user = array(
@ -287,7 +381,7 @@ function wwa_ajax_create(){
$credentialSourceRepository = new PublicKeyCredentialSourceRepository(); $credentialSourceRepository = new PublicKeyCredentialSourceRepository();
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity); $credentialSources = $credentialSourceRepository->findAllForUserEntityByUserId($user_info->ID);
// Convert the Credential Sources into Public Key Credential Descriptors for excluding // Convert the Credential Sources into Public Key Credential Descriptors for excluding
$excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) { $excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) {
@ -363,6 +457,7 @@ add_action("wp_ajax_wwa_create" , "wwa_ajax_create");
// Verify the attestation // Verify the attestation
function wwa_ajax_create_response(){ function wwa_ajax_create_response(){
check_ajax_referer('wwa_ajax');
$client_id = false; $client_id = false;
try{ try{
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
@ -400,7 +495,7 @@ function wwa_ajax_create_response(){
$wwa_post["type"] = sanitize_text_field(wp_unslash($_POST["type"])); $wwa_post["type"] = sanitize_text_field(wp_unslash($_POST["type"]));
$wwa_post["usernameless"] = sanitize_text_field(wp_unslash($_POST["usernameless"])); $wwa_post["usernameless"] = sanitize_text_field(wp_unslash($_POST["usernameless"]));
wwa_add_log($res_id, "ajax_create_response: name => \"".$wwa_post["name"]."\", type => \"".$wwa_post["type"]."\", usernameless => \"".$wwa_post["usernameless"]."\""); wwa_add_log($res_id, "ajax_create_response: name => \"".$wwa_post["name"]."\", type => \"".$wwa_post["type"]."\", usernameless => \"".$wwa_post["usernameless"]."\"");
wwa_add_log($res_id, "ajax_create_response: data => ".base64_decode(sanitize_text_field(wp_unslash($_POST["data"])))); wwa_add_log($res_id, "ajax_create_response: data => ".sanitize_text_field(base64_decode(sanitize_text_field(wp_unslash($_POST["data"])))));
} }
if(isset($_POST["user_id"])){ if(isset($_POST["user_id"])){
@ -487,13 +582,26 @@ function wwa_ajax_create_response(){
try { try {
$publicKeyCredentialSource = $server->loadAndCheckAttestationResponse( $publicKeyCredentialSource = $server->loadAndCheckAttestationResponse(
base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))), base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))),
unserialize(base64_decode($temp_val["pkcco"])), unserialize(base64_decode($temp_val["pkcco"]), ['allowed_classes' => [
Webauthn\PublicKeyCredentialCreationOptions::class,
Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class,
Webauthn\PublicKeyCredentialRpEntity::class,
Webauthn\PublicKeyCredentialUserEntity::class,
Webauthn\AuthenticatorSelectionCriteria::class,
]]),
$serverRequest $serverRequest
); );
wwa_add_log($res_id, "ajax_create_response: Challenge verified"); wwa_add_log($res_id, "ajax_create_response: Challenge verified");
$publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource, $temp_val["bind_config"]["usernameless"]); $user_info = isset($_POST["user_id"]) ? get_user_by('id', intval(sanitize_text_field(wp_unslash($_POST["user_id"])))) : wp_get_current_user();
$publicKeyCredentialSourceRepository->setRegistrationContext(
$user_info->ID,
$wwa_post["name"],
$wwa_post["type"],
$temp_val["bind_config"]["usernameless"]
);
$publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
if($temp_val["bind_config"]["usernameless"]){ if($temp_val["bind_config"]["usernameless"]){
wwa_add_log($res_id, "ajax_create_response: Authenticator added with usernameless authentication feature"); wwa_add_log($res_id, "ajax_create_response: Authenticator added with usernameless authentication feature");
@ -530,13 +638,15 @@ add_action("wp_ajax_wwa_create_response" , "wwa_ajax_create_response");
// Auth challenge // Auth challenge
function wwa_ajax_auth_start(){ function wwa_ajax_auth_start(){
$client_id = false;
try{ try{
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
$client_id = strval(time()).wwa_generate_random_string(24); $client_id = strval(time()).wwa_generate_random_string(24);
wwa_init_new_options(); wwa_init_new_options();
wwa_add_log($res_id, "ajax_auth: Start"); $is_conditional = isset($_GET['conditional']) && $_GET['conditional'] === 'true';
wwa_add_log($res_id, "ajax_auth: Start" . ($is_conditional ? " (conditional)" : ""));
// Check queries // Check queries
if(!isset($_GET["type"])){ if(!isset($_GET["type"])){
@ -598,13 +708,17 @@ function wwa_ajax_auth_start(){
wwa_add_log($res_id, "ajax_auth: type => \"test\", user => \"".$user_info->user_login."\", usernameless => \"false\""); wwa_add_log($res_id, "ajax_auth: type => \"test\", user => \"".$user_info->user_login."\", usernameless => \"false\"");
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){ $user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
wwa_add_log($res_id, "ajax_auth: (ERROR)User not initialized, exit"); if(!$user_key){
wwa_wp_die("User not inited.", $client_id); $user_id_map = wwa_get_option("user_id");
}else{ if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = wwa_get_option("user_id")[$user_info->user_login]; $user_key = $user_id_map[$user_info->user_login];
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https")); }else{
wwa_add_log($res_id, "ajax_auth: (ERROR)User not initialized, exit");
wwa_wp_die("User not inited.", $client_id);
}
} }
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
}else{ }else{
if(wwa_get_option("usernameless_login") === "true"){ if(wwa_get_option("usernameless_login") === "true"){
wwa_add_log($res_id, "ajax_auth: type => \"test\", usernameless => \"true\""); wwa_add_log($res_id, "ajax_auth: type => \"test\", usernameless => \"true\"");
@ -629,12 +743,16 @@ function wwa_ajax_auth_start(){
$user_info = $wp_user; $user_info = $wp_user;
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https")); $user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
wwa_add_log($res_id, "ajax_auth: type => \"auth\", user => \"".$user_info->user_login."\""); wwa_add_log($res_id, "ajax_auth: type => \"auth\", user => \"".$user_info->user_login."\"");
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){ $user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
wwa_add_log($res_id, "ajax_auth: User found but not initialized, create a fake id"); if(!$user_key){
$user_key = hash("sha256", $wwa_get["user"]."-".$wwa_get["user"]."-".wwa_generate_random_string(10)); $user_id_map = wwa_get_option("user_id");
$user_exist = false; if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
}else{ $user_key = $user_id_map[$user_info->user_login];
$user_key = wwa_get_option("user_id")[$user_info->user_login]; }else{
wwa_add_log($res_id, "ajax_auth: User found but not initialized, create a fake id");
$user_key = hash("sha256", $wwa_get["user"]."-".$wwa_get["user"]."-".wwa_generate_random_string(10));
$user_exist = false;
}
} }
}else{ }else{
$user_info = new stdClass(); $user_info = new stdClass();
@ -681,15 +799,40 @@ function wwa_ajax_auth_start(){
// Usernameless authentication, return empty allowed credentials list // Usernameless authentication, return empty allowed credentials list
wwa_add_log($res_id, "ajax_auth: Usernameless authentication, allowedCredentials => []"); wwa_add_log($res_id, "ajax_auth: Usernameless authentication, allowedCredentials => []");
$allowedCredentials = array(); $allowedCredentials = array();
}else if(!$user_exist){
// User doesn't exist or hasn't bound any authenticator,
// generate deterministic fake credentials
$fake_seed = hash_hmac('sha256', $user_info->user_login, wp_salt('auth'), true);
// Determine count: 0 => 25%, 1-5 => 15% each
$fake_count = ord($fake_seed[0]) % 20;
$fake_count = $fake_count < 5 ? 0 : intdiv($fake_count - 5, 3) + 1;
$allowedCredentials = array();
$id_length_ranges = [[16, 20], [32, 48], [48, 64], [64, 80], [20, 32]];
for($i = 0; $i < $fake_count; $i++){
$cred_seed = hash_hmac('sha512', $user_info->user_login . chr($i), wp_salt('auth'), true);
$range = $id_length_ranges[ord($cred_seed[0]) % count($id_length_ranges)];
$id_len = $range[0] + (ord($cred_seed[1]) % ($range[1] - $range[0] + 1));
// Use remaining bytes as credential ID, extend if needed for longer IDs
$id_bytes = substr($cred_seed, 2);
if(strlen($id_bytes) < $id_len){
$id_bytes .= hash_hmac('sha256', $cred_seed, wp_salt('auth'), true);
}
$allowedCredentials[] = new PublicKeyCredentialDescriptor(
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
substr($id_bytes, 0, $id_len)
);
}
wwa_add_log($res_id, "ajax_auth: User not exists, fake allowedCredentials count => ".$fake_count);
}else{ }else{
// Get the list of authenticators associated to the user // Get the list of authenticators associated to the user
// $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
$allow_authenticator_type = wwa_get_option("allow_authenticator_type"); $allow_authenticator_type = wwa_get_option("allow_authenticator_type");
if($allow_authenticator_type === false || $allow_authenticator_type === "none"){ if($allow_authenticator_type === false || $allow_authenticator_type === "none"){
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity); $credentialSources = $credentialSourceRepository->findAllForUserEntityByUserId($user_info->ID);
}elseif($allow_authenticator_type !== false && $allow_authenticator_type !== "none"){ }else{
wwa_add_log($res_id, "ajax_auth: allow_authenticator_type => \"".$allow_authenticator_type."\", filter authenticators"); wwa_add_log($res_id, "ajax_auth: allow_authenticator_type => \"".$allow_authenticator_type."\", filter authenticators");
$credentialSources = $credentialSourceRepository->findCredentialsForUserEntityByType($userEntity, $allow_authenticator_type); $credentialSources = $credentialSourceRepository->findCredentialsForUserEntityByType($user_info->ID, $allow_authenticator_type);
} }
// Logged in and testing, if the user haven't bind a authenticator yet, exit // Logged in and testing, if the user haven't bind a authenticator yet, exit
@ -768,7 +911,8 @@ function wwa_ajax_auth(){
wwa_init_new_options(); wwa_init_new_options();
wwa_add_log($res_id, "ajax_auth_response: Client response received"); $is_conditional = isset($_POST['conditional']) && $_POST['conditional'] === 'true';
wwa_add_log($res_id, "ajax_auth_response: Client response received" . ($is_conditional ? " (conditional)" : ""));
if(!isset($_POST["clientid"])){ if(!isset($_POST["clientid"])){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Missing parameters, exit"); wwa_add_log($res_id, "ajax_auth_response: (ERROR)Missing parameters, exit");
@ -823,7 +967,7 @@ function wwa_ajax_auth(){
wwa_wp_die("Bad request.", $client_id); wwa_wp_die("Bad request.", $client_id);
} }
$temp_val["usernameless_auth"] = unserialize($temp_val["usernameless_auth"]); $temp_val["usernameless_auth"] = unserialize($temp_val["usernameless_auth"], ['allowed_classes' => false]);
if($temp_val["usernameless_auth"] === false && $temp_val["user_name_auth"] === false){ if($temp_val["usernameless_auth"] === false && $temp_val["user_name_auth"] === false){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Username not found in transient, exit"); wwa_add_log($res_id, "ajax_auth_response: (ERROR)Username not found in transient, exit");
@ -878,13 +1022,17 @@ function wwa_ajax_auth(){
} }
} }
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){ $user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not initialized, exit"); if(!$user_key){
wwa_wp_die("User not inited.", $client_id); $user_id_map = wwa_get_option("user_id");
}else{ if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = wwa_get_option("user_id")[$user_info->user_login]; $user_key = $user_id_map[$user_info->user_login];
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https")); }else{
wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not initialized, exit");
wwa_wp_die("User not inited.", $client_id);
}
} }
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
$userEntity = new PublicKeyCredentialUserEntity( $userEntity = new PublicKeyCredentialUserEntity(
$user_info->user_login, $user_info->user_login,
@ -903,7 +1051,7 @@ function wwa_ajax_auth(){
} }
wwa_add_log($res_id, "ajax_auth_response: type => \"".$wwa_post["type"]."\""); wwa_add_log($res_id, "ajax_auth_response: type => \"".$wwa_post["type"]."\"");
wwa_add_log($res_id, "ajax_auth_response: Usernameless authentication, try to find user by credential_id => \"".$data_array["rawId"]."\", userHandle => \"".$data_array["response"]["userHandle"]."\""); wwa_add_log($res_id, "ajax_auth_response: Usernameless authentication, try to find user by credential_id => \"".sanitize_text_field($data_array["rawId"])."\"");
$credential_meta = $publicKeyCredentialSourceRepository->findOneMetaByCredentialId(base64_decode($data_array["rawId"])); $credential_meta = $publicKeyCredentialSourceRepository->findOneMetaByCredentialId(base64_decode($data_array["rawId"]));
@ -911,59 +1059,66 @@ function wwa_ajax_auth(){
$allow_authenticator_type = wwa_get_option("allow_authenticator_type"); $allow_authenticator_type = wwa_get_option("allow_authenticator_type");
if($allow_authenticator_type !== false && $allow_authenticator_type !== 'none'){ if($allow_authenticator_type !== false && $allow_authenticator_type !== 'none'){
if($credential_meta["authenticator_type"] !== $allow_authenticator_type){ if($credential_meta["authenticator_type"] !== $allow_authenticator_type){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential type error, authenticator_type => \"".$credential_meta["authenticator_type"]."\", allow_authenticator_type => \"".$allow_authenticator_type."\", exit"); wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential type error, exit");
wwa_wp_die("Bad request.", $client_id); wwa_wp_die("Bad request.", $client_id);
} }
} }
if($credential_meta["usernameless"] === true){ if($credential_meta["usernameless"] === true){
wwa_add_log($res_id, "ajax_auth_response: Credential found, usernameless => \"true\", user_key => \"".$credential_meta["user"]."\""); global $wpdb;
$cred_row = $wpdb->get_row($wpdb->prepare(
"SELECT user_id, user_handle FROM {$wpdb->wwa_credentials} WHERE credential_id = %s",
base64_encode(base64_decode($data_array["rawId"]))
));
// Try to find user $resolved_user_handle = null;
$all_user = wwa_get_option("user_id"); $resolved_user_info = null;
$user_login_name = false;
foreach($all_user as $user => $user_id){
if($user_id === $credential_meta["user"]){
$user_login_name = $user;
break;
}
}
// Match userHandle if($cred_row !== null){
if($credential_meta["user"] === base64_decode($data_array["response"]["userHandle"])){ $resolved_user_handle = $cred_row->user_handle;
// Found user $resolved_user_info = get_user_by('id', $cred_row->user_id);
if($user_login_name !== false){ }elseif(!get_option('wwa_credentials_migrated')){
wwa_add_log($res_id, "ajax_auth_response: Found user => \"".$user_login_name."\", user_key => \"".$credential_meta["user"]."\""); wwa_add_log($res_id, "ajax_auth_response: Credential not in global table, trying pre-migration fallback");
$old_handle = $credential_meta["user"];
// Testing, verify user $all_user = wwa_get_option("user_id");
if($wwa_post["type"] === "test" && current_user_can('read')){ if(is_array($all_user)){
$user_wp = wp_get_current_user(); foreach($all_user as $login => $handle){
if($user_login_name !== $user_wp->user_login){ if($handle === $old_handle){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but user not match, exit"); $resolved_user_info = get_user_by('login', $login);
wwa_wp_die("Bad request.", $client_id); $resolved_user_handle = $old_handle;
break;
} }
} }
$user_info = get_user_by('login', $user_login_name);
if($user_info === false){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong user ID, exit");
wwa_wp_die("Something went wrong.");
}
$userEntity = new PublicKeyCredentialUserEntity(
$user_info->user_login,
$credential_meta["user"],
$user_info->display_name,
get_avatar_url($user_info->user_email, array("scheme" => "https"))
);
}else{
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but user not found, exit");
wwa_wp_die("Bad request.", $client_id);
} }
}else{ }
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but userHandle not matched, exit");
if($resolved_user_info === false || $resolved_user_info === null){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not found, exit");
wwa_wp_die("Bad request.", $client_id); wwa_wp_die("Bad request.", $client_id);
} }
if($resolved_user_handle !== base64_decode($data_array["response"]["userHandle"])){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)userHandle not matched, exit");
wwa_wp_die("Bad request.", $client_id);
}
$user_info = $resolved_user_info;
$user_login_name = $user_info->user_login;
wwa_add_log($res_id, "ajax_auth_response: Found user => \"".$user_login_name."\"");
if($wwa_post["type"] === "test" && current_user_can('read')){
$user_wp = wp_get_current_user();
if($user_login_name !== $user_wp->user_login){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not match, exit");
wwa_wp_die("Bad request.", $client_id);
}
}
$userEntity = new PublicKeyCredentialUserEntity(
$user_info->user_login,
$resolved_user_handle,
$user_info->display_name,
get_avatar_url($user_info->user_email, array("scheme" => "https"))
);
}else{ }else{
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but usernameless => \"false\", exit"); wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but usernameless => \"false\", exit");
wwa_wp_die("Bad request.", $client_id); wwa_wp_die("Bad request.", $client_id);
@ -974,12 +1129,14 @@ function wwa_ajax_auth(){
} }
}else{ }else{
wwa_add_log($res_id, "ajax_auth_response: type => \"auth\", user => \"".$temp_val["user_name_auth"]."\""); wwa_add_log($res_id, "ajax_auth_response: type => \"auth\", user => \"".$temp_val["user_name_auth"]."\"");
$userEntity = unserialize($temp_val["user_auth"]); $userEntity = unserialize($temp_val["user_auth"], ['allowed_classes' => [
Webauthn\PublicKeyCredentialUserEntity::class,
]]);
} }
} }
$decoded_data = base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))); $decoded_data = base64_decode(sanitize_text_field(wp_unslash($_POST["data"])));
wwa_add_log($res_id, "ajax_auth_response: data => ".$decoded_data); wwa_add_log($res_id, "ajax_auth_response: data => ".sanitize_text_field($decoded_data));
if($temp_val["user_exist"]){ if($temp_val["user_exist"]){
$rpEntity = new PublicKeyCredentialRpEntity( $rpEntity = new PublicKeyCredentialRpEntity(
@ -1004,7 +1161,11 @@ function wwa_ajax_auth(){
try { try {
$server->loadAndCheckAssertionResponse( $server->loadAndCheckAssertionResponse(
$decoded_data, $decoded_data,
unserialize(base64_decode($temp_val["pkcco_auth"])), unserialize(base64_decode($temp_val["pkcco_auth"]), ['allowed_classes' => [
Webauthn\PublicKeyCredentialRequestOptions::class,
Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class,
Webauthn\PublicKeyCredentialDescriptor::class,
]]),
$userEntity, $userEntity,
$serverRequest $serverRequest
); );
@ -1084,6 +1245,7 @@ add_action("wp_ajax_nopriv_wwa_auth" , "wwa_ajax_auth");
// Get authenticator list // Get authenticator list
function wwa_ajax_authenticator_list(){ function wwa_ajax_authenticator_list(){
check_ajax_referer('wwa_ajax');
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
wwa_init_new_options(); wwa_init_new_options();
@ -1118,30 +1280,21 @@ function wwa_ajax_authenticator_list(){
header('Content-Type: application/json'); header('Content-Type: application/json');
$user_key = ""; $user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){ if(!$user_key){
// The user haven't bound any authenticator, return empty list
echo "[]"; echo "[]";
exit; exit;
}else{
$user_key = wwa_get_option("user_id")[$user_info->user_login];
} }
$userEntity = new PublicKeyCredentialUserEntity(
$user_info->user_login,
$user_key,
$user_info->display_name,
get_avatar_url($user_info->user_email, array("scheme" => "https"))
);
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository(); $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
echo wp_json_encode($publicKeyCredentialSourceRepository->getShowList($userEntity)); echo wp_json_encode($publicKeyCredentialSourceRepository->getShowListByUserId($user_info->ID));
exit; exit;
} }
add_action("wp_ajax_wwa_authenticator_list" , "wwa_ajax_authenticator_list"); add_action("wp_ajax_wwa_authenticator_list" , "wwa_ajax_authenticator_list");
// Modify an authenticator // Modify an authenticator
function wwa_ajax_modify_authenticator(){ function wwa_ajax_modify_authenticator(){
check_ajax_referer('wwa_ajax');
try{ try{
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
@ -1192,30 +1345,25 @@ function wwa_ajax_modify_authenticator(){
wwa_wp_die("Bad Request."); wwa_wp_die("Bad Request.");
} }
$user_key = "";
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
// The user haven't bound any authenticator, exit
wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)User not initialized, exit");
wwa_wp_die("User not inited.");
}else{
$user_key = wwa_get_option("user_id")[$user_info->user_login];
}
$userEntity = new PublicKeyCredentialUserEntity(
$user_info->user_login,
$user_key,
$user_info->display_name,
get_avatar_url($user_info->user_email, array("scheme" => "https"))
);
wwa_add_log($res_id, "ajax_modify_authenticator: user => \"".$user_info->user_login."\""); wwa_add_log($res_id, "ajax_modify_authenticator: user => \"".$user_info->user_login."\"");
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository(); $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
if($_GET["target"] === "rename"){ if($_GET["target"] === "rename"){
echo $publicKeyCredentialSourceRepository->modifyAuthenticator(sanitize_text_field(wp_unslash($_GET["id"])), sanitize_text_field(wp_unslash($_GET["name"])), $userEntity, "rename", $res_id); $result = $publicKeyCredentialSourceRepository->renameCredential(
sanitize_text_field(wp_unslash($_GET["id"])),
$user_info->ID,
sanitize_text_field(wp_unslash($_GET["name"])),
$res_id
);
echo $result ? "true" : "Not Found.";
}elseif($_GET["target"] === "remove"){ }elseif($_GET["target"] === "remove"){
echo $publicKeyCredentialSourceRepository->modifyAuthenticator(sanitize_text_field(wp_unslash($_GET["id"])), "", $userEntity, "remove", $res_id); $result = $publicKeyCredentialSourceRepository->removeCredential(
sanitize_text_field(wp_unslash($_GET["id"])),
$user_info->ID,
$res_id
);
echo $result ? "true" : "Not Found.";
} }
exit; exit;
}catch(\Exception $exception){ }catch(\Exception $exception){
@ -1234,6 +1382,7 @@ add_action("wp_ajax_wwa_modify_authenticator" , "wwa_ajax_modify_authenticator")
// Print log // Print log
function wwa_ajax_get_log(){ function wwa_ajax_get_log(){
check_ajax_referer('wwa_admin_ajax');
if(!wwa_validate_privileges()){ if(!wwa_validate_privileges()){
wwa_wp_die("Bad Request."); wwa_wp_die("Bad Request.");
} }
@ -1254,6 +1403,7 @@ add_action("wp_ajax_wwa_get_log" , "wwa_ajax_get_log");
// Clear log // Clear log
function wwa_ajax_clear_log(){ function wwa_ajax_clear_log(){
check_ajax_referer('wwa_admin_ajax');
if(!wwa_validate_privileges()){ if(!wwa_validate_privileges()){
wwa_wp_die("Bad Request."); wwa_wp_die("Bad Request.");
} }

View File

@ -1,5 +1,61 @@
<?php <?php
// Two Factor // Two Factor
if(has_action('wp_login', array('Two_Factor_Core', 'wp_login')) !== false){ if (!defined('ABSPATH')) {
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), 10, 2); exit;
}
if (!function_exists('wwa_is_webauthn_ajax_login_request')) {
function wwa_is_webauthn_ajax_login_request(): bool
{
if (!function_exists('wp_doing_ajax') || !wp_doing_ajax()) {
return false;
}
$action = isset($_REQUEST['action'])
? sanitize_text_field(wp_unslash($_REQUEST['action']))
: '';
return in_array($action, array('wwa_auth_start', 'wwa_auth'), true);
}
}
if (wwa_is_webauthn_ajax_login_request() && class_exists('Two_Factor_Core')) {
/**
* 1) Prevent Two-Factor from redirecting passwordless WebAuthn logins
* into its own wp_login challenge flow.
*/
$prio = has_action('wp_login', array('Two_Factor_Core', 'wp_login'));
if ($prio !== false) {
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), $prio);
}
// Defensive cleanup for common / unexpected priorities.
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), 1);
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), 10);
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), 100);
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), PHP_INT_MAX);
/**
* 2) Prevent Two-Factor from reporting enabled providers during
* the passwordless WebAuthn AJAX auth flow only.
*
* This keeps Two-Factor fully active for normal password logins.
*/
add_filter('two_factor_enabled_providers_for_user', function ($enabled, $user_id) {
return array();
}, 9, 2);
/**
* 3) If Two-Factor previously blocked auth cookies in this request,
* allow them again so WP-WebAuthn can complete login successfully.
*/
$cookie_prio = has_filter('send_auth_cookies', '__return_false');
if ($cookie_prio !== false) {
remove_filter('send_auth_cookies', '__return_false', $cookie_prio);
}
remove_filter('send_auth_cookies', '__return_false', 31);
remove_filter('send_auth_cookies', '__return_false', 100);
remove_filter('send_auth_cookies', '__return_false', PHP_INT_MAX);
} }

View File

@ -1,4 +1,8 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
// WordPress transient adapter // WordPress transient adapter
function wwa_set_temp_val($name, $value, $client_id){ function wwa_set_temp_val($name, $value, $client_id){
return set_transient('wwa_'.$name.$client_id, serialize($value), 90); return set_transient('wwa_'.$name.$client_id, serialize($value), 90);
@ -6,7 +10,7 @@ function wwa_set_temp_val($name, $value, $client_id){
function wwa_get_temp_val($name, $client_id){ function wwa_get_temp_val($name, $client_id){
$val = get_transient('wwa_'.$name.$client_id); $val = get_transient('wwa_'.$name.$client_id);
return $val === false ? false : unserialize($val); return $val === false ? false : maybe_unserialize($val);
} }
function wwa_delete_temp_val($name, $client_id){ function wwa_delete_temp_val($name, $client_id){
@ -37,6 +41,10 @@ function wwa_init_new_options(){
if(wwa_get_option('allow_authenticator_type') === false){ if(wwa_get_option('allow_authenticator_type') === false){
wwa_update_option('allow_authenticator_type', 'none'); wwa_update_option('allow_authenticator_type', 'none');
} }
// Existing installs default to 'true' to preserve previous behaviour
if(wwa_get_option('show_authenticator_type') === false){
wwa_update_option('show_authenticator_type', 'true');
}
if(wwa_get_option('remember_me') === false){ if(wwa_get_option('remember_me') === false){
wwa_update_option('remember_me', 'false'); wwa_update_option('remember_me', 'false');
} }
@ -52,6 +60,12 @@ function wwa_init_new_options(){
if(wwa_get_option('after_user_registration') === false){ if(wwa_get_option('after_user_registration') === false){
wwa_update_option('after_user_registration', 'none'); wwa_update_option('after_user_registration', 'none');
} }
if(wwa_get_option('terminology') === false){
wwa_update_option('terminology', 'webauthn');
}
if(wwa_get_option('ror_origins') === false){
wwa_update_option('ror_origins', '');
}
} }
// Create random strings for user ID // Create random strings for user ID
@ -80,7 +94,7 @@ function wwa_add_log($id, $content = '', $init = false){
if($log === false){ if($log === false){
$log = array(); $log = array();
} }
$log[] = '['.current_time('mysql').']['.$id.'] '.$content; $log[] = '['.current_time('mysql').']['.$id.'] '.wp_strip_all_tags($content);
update_option('wwa_log', $log); update_option('wwa_log', $log);
} }
@ -104,49 +118,72 @@ function wwa_generate_call_trace($exception = false){
return "Traceback:\n ".implode("\n ", $result); return "Traceback:\n ".implode("\n ", $result);
} }
// Delete all credentials when deleting user function wwa_cleanup_blog_credentials($user_id, $blog_id){
global $wpdb;
$wpdb->delete($wpdb->wwa_credentials, array(
'user_id' => $user_id,
'registered_blog_id' => $blog_id
));
}
function wwa_cleanup_all_user_credentials($user_id){
global $wpdb;
$wpdb->delete($wpdb->wwa_credentials, array('user_id' => $user_id));
delete_user_meta($user_id, 'wwa_user_handle');
delete_user_meta($user_id, 'wwa_webauthn_only');
}
function wwa_delete_user($user_id){ function wwa_delete_user($user_id){
$res_id = wwa_generate_random_string(5); $res_id = wwa_generate_random_string(5);
$user_data = get_userdata($user_id); $user_data = get_userdata($user_id);
$all_user_meta = wwa_get_option('user_id'); if($user_data !== false){
$user_key = ''; wwa_add_log($res_id, "Deleted user credentials for => \"".$user_data->user_login."\"");
// Delete user meta
foreach($all_user_meta as $user => $id){
if($user === $user_data->user_login){
$user_key = $id;
wwa_add_log($res_id, "Delete user_key => \"".$id."\"");
unset($all_user_meta[$user]);
}
} }
// Delete credentials if(is_multisite()){
$all_credentials_meta = json_decode(wwa_get_option('user_credentials_meta'), true); wwa_cleanup_blog_credentials($user_id, get_current_blog_id());
$all_credentials = json_decode(wwa_get_option('user_credentials'), true); }else{
foreach($all_credentials_meta as $credential => $meta){ wwa_cleanup_all_user_credentials($user_id);
if($user_key === $meta['user']){
wwa_add_log($res_id, "Delete credential => \"".$credential."\"");
unset($all_credentials_meta[$credential]);
unset($all_credentials[$credential]);
}
} }
wwa_update_option('user_id', $all_user_meta);
wwa_update_option('user_credentials_meta', wp_json_encode($all_credentials_meta));
wwa_update_option('user_credentials', wp_json_encode($all_credentials));
wwa_add_log($res_id, "Deleted user => \"".$user_data->user_login."\"");
} }
add_action('delete_user', 'wwa_delete_user'); add_action('delete_user', 'wwa_delete_user');
function wwa_delete_user_multisite($user_id){
$res_id = wwa_generate_random_string(5);
$user_data = get_userdata($user_id);
if($user_data !== false){
wwa_add_log($res_id, "Deleted all user credentials for => \"".$user_data->user_login."\" (network deletion)");
}
wwa_cleanup_all_user_credentials($user_id);
}
add_action('wpmu_delete_user', 'wwa_delete_user_multisite');
function wwa_remove_user_from_blog($user_id, $blog_id){
$res_id = wwa_generate_random_string(5);
$user_data = get_userdata($user_id);
if($user_data !== false){
wwa_add_log($res_id, "Deleted user credentials for => \"".$user_data->user_login."\" (removed from blog ".$blog_id.")");
}
wwa_cleanup_blog_credentials($user_id, $blog_id);
}
add_action('remove_user_from_blog', 'wwa_remove_user_from_blog', 10, 2);
// Add CSS and JS in login page // Add CSS and JS in login page
function wwa_login_js(){ function wwa_login_js(){
wwa_init_new_options();
$wwa_not_allowed = false; $wwa_not_allowed = false;
if(!function_exists('mb_substr') || !function_exists('gmp_intval') || !wwa_check_ssl() && (wp_parse_url(site_url(), PHP_URL_HOST) !== 'localhost' && wp_parse_url(site_url(), PHP_URL_HOST) !== '127.0.0.1')){ if(!function_exists('mb_substr') || !function_exists('gmp_intval') || !wwa_check_ssl() && (wp_parse_url(site_url(), PHP_URL_HOST) !== 'localhost' && wp_parse_url(site_url(), PHP_URL_HOST) !== '127.0.0.1')){
$wwa_not_allowed = true; $wwa_not_allowed = true;
} }
wp_enqueue_script('wwa_login', plugins_url('js/login.js', __FILE__), array(), get_option('wwa_version')['version'], true); wp_enqueue_script('wwa_login', plugins_url('js/login.js', __FILE__), array(), get_option('wwa_version')['version'], true);
$first_choice = wwa_get_option('first_choice'); $first_choice = wwa_get_option('first_choice');
wp_localize_script('wwa_login', 'php_vars', array( wp_localize_script('wwa_login', 'wwa_login_php_vars', array(
'ajax_url' => admin_url('admin-ajax.php'), 'ajax_url' => admin_url('admin-ajax.php'),
'admin_url' => admin_url(), 'admin_url' => admin_url(),
'usernameless' => (wwa_get_option('usernameless_login') === false ? 'false' : wwa_get_option('usernameless_login')), 'usernameless' => (wwa_get_option('usernameless_login') === false ? 'false' : wwa_get_option('usernameless_login')),
@ -156,8 +193,9 @@ function wwa_login_js(){
'webauthn_only' => ($first_choice === 'webauthn' && !$wwa_not_allowed) ? 'true' : 'false', 'webauthn_only' => ($first_choice === 'webauthn' && !$wwa_not_allowed) ? 'true' : 'false',
'password_reset' => ((wwa_get_option('password_reset') === false || wwa_get_option('password_reset') === 'off') ? 'false' : 'true'), 'password_reset' => ((wwa_get_option('password_reset') === false || wwa_get_option('password_reset') === 'off') ? 'false' : 'true'),
'separator' => apply_filters('login_link_separator', ' | '), 'separator' => apply_filters('login_link_separator', ' | '),
'terminology' => (wwa_get_option('terminology') === false ? 'passkey' : wwa_get_option('terminology')),
'i18n_1' => __('Auth', 'wp-webauthn'), 'i18n_1' => __('Auth', 'wp-webauthn'),
'i18n_2' => __('Authenticate with WebAuthn', 'wp-webauthn'), 'i18n_2' => wwa_get_option('terminology') === 'webauthn' ? __('Authenticate with WebAuthn', 'wp-webauthn') : __('Authenticate with a passkey', 'wp-webauthn'),
'i18n_3' => __('Hold on...', 'wp-webauthn'), 'i18n_3' => __('Hold on...', 'wp-webauthn'),
'i18n_4' => __('Please proceed...', 'wp-webauthn'), 'i18n_4' => __('Please proceed...', 'wp-webauthn'),
'i18n_5' => __('Authenticating...', 'wp-webauthn'), 'i18n_5' => __('Authenticating...', 'wp-webauthn'),
@ -167,7 +205,9 @@ function wwa_login_js(){
'i18n_9' => __('Username', 'wp-webauthn'), 'i18n_9' => __('Username', 'wp-webauthn'),
'i18n_10' => __('Username or Email Address'), 'i18n_10' => __('Username or Email Address'),
'i18n_11' => __('<strong>Error</strong>: The username field is empty.', 'wp-webauthn'), 'i18n_11' => __('<strong>Error</strong>: The username field is empty.', 'wp-webauthn'),
'i18n_12' => '<span class="wwa-try-username">'.__('Try to enter the username', 'wp-webauthn').'</span>' 'i18n_12' => '<span class="wwa-try-username">'.__('Try to enter the username', 'wp-webauthn').'</span>',
'i18n_13' => __('Password'),
'i18n_14' => wwa_get_option('terminology') === 'webauthn' ? 'WebAuthn' : __('Passkey', 'wp-webauthn')
)); ));
if($first_choice === 'true' || $first_choice === 'webauthn'){ if($first_choice === 'true' || $first_choice === 'webauthn'){
wp_enqueue_script('wwa_default', plugins_url('js/default_wa.js', __FILE__), array(), get_option('wwa_version')['version'], true); wp_enqueue_script('wwa_default', plugins_url('js/default_wa.js', __FILE__), array(), get_option('wwa_version')['version'], true);
@ -187,7 +227,7 @@ function wwa_disable_password($user){
if(is_wp_error($user)){ if(is_wp_error($user)){
return $user; return $user;
} }
if(get_the_author_meta('webauthn_only', $user->ID) === 'true'){ if(get_user_meta($user->ID, 'wwa_webauthn_only', true) === 'true'){
return new WP_Error('wwa_password_disabled_for_account', __('Logging in with password has been disabled for this account.', 'wp-webauthn')); return new WP_Error('wwa_password_disabled_for_account', __('Logging in with password has been disabled for this account.', 'wp-webauthn'));
} }
return $user; return $user;
@ -240,86 +280,73 @@ if(wwa_get_option('password_reset') === 'admin' || wwa_get_option('password_rese
add_filter('allow_password_reset', 'wwa_handle_password'); add_filter('allow_password_reset', 'wwa_handle_password');
} }
// Show a notice in admin pages
function wwa_no_authenticator_warning(){ function wwa_no_authenticator_warning(){
if(is_network_admin()){
return;
}
$user_info = wp_get_current_user(); $user_info = wp_get_current_user();
$first_choice = wwa_get_option('first_choice'); $first_choice = wwa_get_option('first_choice');
$check_self = true; $check_self = true;
if($first_choice !== 'webauthn' && get_the_author_meta('webauthn_only', $user_info->ID ) !== 'true'){ if($first_choice !== 'webauthn' && get_user_meta($user_info->ID, 'wwa_webauthn_only', true) !== 'true'){
$check_self = false; $check_self = false;
} }
if($check_self){ if($check_self){
// Check current user global $wpdb;
$user_id = ''; $count = $wpdb->get_var($wpdb->prepare(
$show_notice_flag = false; "SELECT COUNT(*) FROM {$wpdb->wwa_credentials}
if(!isset(wwa_get_option('user_id')[$user_info->user_login])){ WHERE user_id = %d AND registered_blog_id = %d",
$show_notice_flag = true; $user_info->ID, get_current_blog_id()
}else{ ));
$user_id = wwa_get_option('user_id')[$user_info->user_login]; if(intval($count) === 0){ ?>
}
if(!$show_notice_flag){
$show_notice_flag = true;
$data = json_decode(wwa_get_option('user_credentials_meta'), true);
foreach($data as $value){
if($user_id === $value['user']){
$show_notice_flag = false;
break;
}
}
}
if($show_notice_flag){?>
<div class="notice notice-warning"> <div class="notice notice-warning">
<?php /* translators: %s: 'the site' or 'your account', and admin profile url */ ?> <?php
<p><?php printf(__('Logging in with password has been disabled for %s but you haven\'t register any WebAuthn authenticator yet. You may unable to login again once you log out. <a href="%s#wwa-webauthn-start">Register</a>', 'wp-webauthn'), $first_choice === 'webauthn' ? __('the site', 'wp-webauthn') : __('your account', 'wp-webauthn'), admin_url('profile.php'));?></p> $wwa_scope_label = esc_html($first_choice === 'webauthn' ? __('the site', 'wp-webauthn') : __('your account', 'wp-webauthn'));
$wwa_cred_label = esc_html(wwa_get_option('terminology') === 'webauthn' ? __('WebAuthn authenticator', 'wp-webauthn') : __('passkey', 'wp-webauthn'));
/* translators: %1$s: 'the site' or 'your account', %2$s: 'WebAuthn authenticator' or 'passkey', %3$s: admin profile url */
?>
<p><?php echo wp_kses(sprintf(__('Logging in with password has been disabled for %1$s but you haven\'t register any %2$s on the current site yet. You may unable to login again once you log out. <a href="%3$s#wwa-webauthn-start">Register</a>', 'wp-webauthn'), $wwa_scope_label, $wwa_cred_label, esc_url(admin_url('profile.php'))), array('a' => array('href' => array())));?></p>
<?php if(is_multisite() && !is_subdomain_install()){
/* translators: %s: 'WebAuthn authenticators' or 'Passkeys' */ ?>
<p><?php echo esc_html(sprintf(__('%s registered on other sites within this network may also be used to log in.', 'wp-webauthn'), wwa_get_option('terminology') === 'webauthn' ? __('WebAuthn authenticators', 'wp-webauthn') : __('Passkeys', 'wp-webauthn'))); ?></p>
<?php } ?>
</div> </div>
<?php } <?php }
} }
// Check other user
global $pagenow; global $pagenow;
if($pagenow == 'user-edit.php' && isset($_GET['user_id']) && intval($_GET['user_id']) !== $user_info->ID){ if($pagenow == 'user-edit.php' && isset($_GET['user_id']) && intval($_GET['user_id']) !== $user_info->ID){
$user_id_wp = intval($_GET['user_id']); $user_id_wp = intval($_GET['user_id']);
if($user_id_wp <= 0){ if($user_id_wp <= 0 || !current_user_can('edit_user', $user_id_wp)){
return; return;
} }
if(!current_user_can('edit_user', $user_id_wp)){ $other_user = get_user_by('id', $user_id_wp);
if($other_user === false){
return; return;
} }
$user_info = get_user_by('id', $user_id_wp); if($first_choice !== 'webauthn' && get_user_meta($other_user->ID, 'wwa_webauthn_only', true) !== 'true'){
if($user_info === false){
return; return;
} }
if($first_choice !== 'webauthn' && get_the_author_meta('webauthn_only', $user_info->ID) !== 'true'){ global $wpdb;
return; $count = $wpdb->get_var($wpdb->prepare(
} "SELECT COUNT(*) FROM {$wpdb->wwa_credentials}
WHERE user_id = %d AND registered_blog_id = %d",
$user_id = ''; $other_user->ID, get_current_blog_id()
$show_notice_flag = false; ));
if(!isset(wwa_get_option('user_id')[$user_info->user_login])){ if(intval($count) === 0){ ?>
$show_notice_flag = true;
}else{
$user_id = wwa_get_option('user_id')[$user_info->user_login];
}
if(!$show_notice_flag){
$show_notice_flag = true;
$data = json_decode(wwa_get_option('user_credentials_meta'), true);
foreach($data as $value){
if($user_id === $value['user']){
$show_notice_flag = false;
break;
}
}
}
if($show_notice_flag){ ?>
<div class="notice notice-warning"> <div class="notice notice-warning">
<?php /* translators: %s: 'the site' or 'your account' */ ?> <?php
<p><?php printf(__('Logging in with password has been disabled for %s but <strong>this account</strong> haven\'t register any WebAuthn authenticator yet. This user may unable to login.', 'wp-webauthn'), $first_choice === 'webauthn' ? __('the site', 'wp-webauthn') : __('this account', 'wp-webauthn'));?></p> $wwa_scope_label = esc_html($first_choice === 'webauthn' ? __('the site', 'wp-webauthn') : __('this account', 'wp-webauthn'));
$wwa_cred_label = esc_html(wwa_get_option('terminology') === 'webauthn' ? __('WebAuthn authenticator', 'wp-webauthn') : __('passkey', 'wp-webauthn'));
/* translators: %1$s: 'the site' or 'this account', %2$s: 'WebAuthn authenticator' or 'passkey' */
?>
<p><?php echo wp_kses(sprintf(__('Logging in with password has been disabled for %1$s but <strong>this account</strong> haven\'t register any %2$s on the current site yet. This user may unable to login.', 'wp-webauthn'), $wwa_scope_label, $wwa_cred_label), array('strong' => array()));?></p>
<?php if(is_multisite() && !is_subdomain_install()){
/* translators: %s: 'WebAuthn authenticators' or 'Passkeys' */ ?>
<p><?php echo esc_html(sprintf(__('%s registered on other sites within this network may also be used to log in.', 'wp-webauthn'), wwa_get_option('terminology') === 'webauthn' ? __('WebAuthn authenticators', 'wp-webauthn') : __('Passkeys', 'wp-webauthn'))); ?></p>
<?php } ?>
</div> </div>
<?php } <?php }
} }
@ -353,17 +380,27 @@ function wwa_settings_link($links_array, $plugin_file_name){
} }
add_filter('plugin_action_links', 'wwa_settings_link', 10, 2); add_filter('plugin_action_links', 'wwa_settings_link', 10, 2);
function wwa_network_settings_link($links_array, $plugin_file_name){
if($plugin_file_name === 'wp-webauthn/wp-webauthn.php'){
$links_array[] = '<a href="'.esc_url(network_admin_url('settings.php?page=wwa_network_admin')).'">'.__('Network Settings', 'wp-webauthn').'</a>';
}
return $links_array;
}
if(is_multisite()){
add_filter('network_admin_plugin_action_links', 'wwa_network_settings_link', 10, 2);
}
function wwa_meta_link($links_array, $plugin_file_name){ function wwa_meta_link($links_array, $plugin_file_name){
if($plugin_file_name === 'wp-webauthn/wp-webauthn.php'){ if($plugin_file_name === 'wp-webauthn/wp-webauthn.php'){
$links_array[] = '<a href="https://github.com/yrccondor/wp-webauthn">'.__('GitHub', 'wp-webauthn').'</a>'; $links_array[] = '<a href="https://github.com/yrccondor/wp-webauthn">'.__('GitHub', 'wp-webauthn').'</a>';
$links_array[] = '<a href="http://doc.flyhigher.top/wp-webauthn">'.__('Documentation', 'wp-webauthn').'</a>'; $links_array[] = '<a href="https://doc.flyhigher.top/wp-webauthn">'.__('Documentation', 'wp-webauthn').'</a>';
} }
return $links_array; return $links_array;
} }
add_filter('plugin_row_meta', 'wwa_meta_link', 10, 2); add_filter('plugin_row_meta', 'wwa_meta_link', 10, 2);
// Check if we are under HTTPS // Check if we are under HTTPS
function wwa_check_ssl() { function wwa_check_ssl(){
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && $_SERVER['HTTPS'] !== '') { if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && $_SERVER['HTTPS'] !== '') {
return true; return true;
} }
@ -380,17 +417,37 @@ function wwa_check_ssl() {
} }
// Check user privileges // Check user privileges
function wwa_validate_privileges() { function wwa_validate_privileges(){
$user = wp_get_current_user(); return current_user_can('manage_options');
$allowed_roles = array('administrator'); }
if(array_intersect($allowed_roles, $user->roles)){
return true; // Get Related Origins Request list
function wwa_get_ror_list(){
$raw = wwa_get_option('ror_origins');
if($raw === false || $raw === ''){
return array();
} }
return false; $origins = array();
$lines = explode("\n", $raw);
foreach($lines as $line){
$line = trim($line);
if($line === ''){
continue;
}
$parsed = wp_parse_url($line);
if(isset($parsed['scheme']) && isset($parsed['host'])){
$origin = $parsed['scheme'] . '://' . $parsed['host'];
if(isset($parsed['port'])){
$origin .= ':' . $parsed['port'];
}
$origins[] = $origin;
}
}
return $origins;
} }
// Get user by username or email // Get user by username or email
function wwa_get_user($username) { function wwa_get_user($username){
if(wwa_get_option('email_login') !== 'true'){ if(wwa_get_option('email_login') !== 'true'){
return get_user_by('login', $username); return get_user_by('login', $username);
}else{ }else{
@ -400,3 +457,54 @@ function wwa_get_user($username) {
return get_user_by('login', $username); return get_user_by('login', $username);
} }
} }
// Provide plugin version for other plugins
function wwa_loaded_version(){
if(!get_option('wwa_version')){
return '0.0.1';
}
return get_option('wwa_version')['version'];
}
// Register query vars
function wwa_query_vars($vars) {
$vars[] = 'wwa-well-known-ror';
return $vars;
}
// Add rewrite rules for .well-known/webauthn
function wwa_add_rewrite_rules() {
add_rewrite_rule('^\.well-known/webauthn$', 'index.php?wwa-well-known-ror=true', 'top');
}
function wwa_apply_rewrite_rules() {
wwa_add_rewrite_rules();
flush_rewrite_rules();
}
// Handle .well-known/webauthn
function wwa_handle_ror($wp) {
if (array_key_exists('wwa-well-known-ror', $wp->query_vars)) {
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
echo wp_json_encode(array(
'origins'=> wwa_get_ror_list()
));
exit;
}
}
// Initialize plugin data for a new site created in a multisite network
function wwa_new_site_init($new_site){
$network_active = get_site_option('active_sitewide_plugins');
if(isset($network_active['wp-webauthn/wp-webauthn.php'])){
switch_to_blog($new_site->id);
wwa_init_data();
wwa_apply_rewrite_rules();
restore_current_blog();
}
}
add_action('wp_initialize_site', 'wwa_new_site_init');
add_filter('query_vars', 'wwa_query_vars');
add_action('parse_request', 'wwa_handle_ror', 99);
add_action('init', 'wwa_add_rewrite_rules', 1);

View File

@ -1,7 +1,11 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
// Add menu // Add menu
function wwa_admin_menu(){ function wwa_admin_menu(){
add_options_page('WP-WebAuthn' , 'WP-WebAuthn', 'read', 'wwa_admin','wwa_display_main_menu'); add_options_page('WP-WebAuthn' , 'WP-WebAuthn', 'manage_options', 'wwa_admin','wwa_display_main_menu');
} }
function wwa_display_main_menu(){ function wwa_display_main_menu(){
include('wwa-admin-content.php'); include('wwa-admin-content.php');
@ -28,11 +32,11 @@ function wwa_save_user_profile_fields($user_id){
} }
if(!isset($_POST['webauthn_only'])){ if(!isset($_POST['webauthn_only'])){
update_user_meta($user_id, 'webauthn_only', 'false'); update_user_meta($user_id, 'wwa_webauthn_only', 'false');
}elseif(sanitize_text_field(wp_unslash($_POST['webauthn_only'])) === 'true'){ }elseif(sanitize_text_field(wp_unslash($_POST['webauthn_only'])) === 'true'){
update_user_meta($user_id, 'webauthn_only', 'true'); update_user_meta($user_id, 'wwa_webauthn_only', 'true');
}else{ }else{
update_user_meta($user_id, 'webauthn_only', 'false'); update_user_meta($user_id, 'wwa_webauthn_only', 'false');
} }
} }
add_action('personal_options_update', 'wwa_save_user_profile_fields'); add_action('personal_options_update', 'wwa_save_user_profile_fields');
@ -48,3 +52,12 @@ function wwa_user_profile_fields_check(){
} }
} }
add_action('plugins_loaded', 'wwa_user_profile_fields_check'); add_action('plugins_loaded', 'wwa_user_profile_fields_check');
function wwa_network_admin_menu(){
add_submenu_page('settings.php', 'WP-WebAuthn', 'WP-WebAuthn', 'manage_network_options', 'wwa_network_admin', 'wwa_display_network_settings');
}
if(is_multisite()){
include('wwa-network-admin-content.php');
add_action('network_admin_menu', 'wwa_network_admin_menu');
add_action('network_admin_edit_wwa_network_options_update', 'wwa_handle_network_options_save');
}

View File

@ -0,0 +1,198 @@
<?php
if(!defined('ABSPATH')){
exit;
}
// Handle network options save
function wwa_handle_network_options_save(){
if(!current_user_can('manage_network_options')){
wp_die(esc_html__('You do not have sufficient permissions to access this page.'));
}
check_admin_referer('wwa_network_options_update', 'wwa_network_options_nonce');
$res_id = wwa_generate_random_string(5);
$post_first_choice = sanitize_text_field(wp_unslash($_POST['first_choice'] ?? 'true'));
if(!in_array($post_first_choice, array('true', 'webauthn', 'false'), true)){
$post_first_choice = 'true';
}
if($post_first_choice !== wwa_get_option('first_choice')){
wwa_add_log($res_id, 'network first_choice: "'.wwa_get_option('first_choice').'"->"'.$post_first_choice.'"');
}
wwa_update_option('first_choice', $post_first_choice);
$post_user_verification = sanitize_text_field(wp_unslash($_POST['user_verification'] ?? 'false'));
if(!in_array($post_user_verification, array('true', 'false'), true)){
$post_user_verification = 'false';
}
if($post_user_verification !== wwa_get_option('user_verification')){
wwa_add_log($res_id, 'network user_verification: "'.wwa_get_option('user_verification').'"->"'.$post_user_verification.'"');
}
wwa_update_option('user_verification', $post_user_verification);
$post_usernameless_login = sanitize_text_field(wp_unslash($_POST['usernameless_login'] ?? 'false'));
if(!in_array($post_usernameless_login, array('true', 'false'), true)){
$post_usernameless_login = 'false';
}
if($post_usernameless_login !== wwa_get_option('usernameless_login')){
wwa_add_log($res_id, 'network usernameless_login: "'.wwa_get_option('usernameless_login').'"->"'.$post_usernameless_login.'"');
}
wwa_update_option('usernameless_login', $post_usernameless_login);
$post_allow_type = sanitize_text_field(wp_unslash($_POST['allow_authenticator_type'] ?? 'none'));
if(!in_array($post_allow_type, array('none', 'platform', 'cross-platform'), true)){
$post_allow_type = 'none';
}
if($post_allow_type !== wwa_get_option('allow_authenticator_type')){
wwa_add_log($res_id, 'network allow_authenticator_type: "'.wwa_get_option('allow_authenticator_type').'"->"'.$post_allow_type.'"');
}
wwa_update_option('allow_authenticator_type', $post_allow_type);
$post_show_type = sanitize_text_field(wp_unslash($_POST['show_authenticator_type'] ?? 'false'));
if(!in_array($post_show_type, array('true', 'false'), true)){
$post_show_type = 'false';
}
if($post_show_type !== wwa_get_option('show_authenticator_type')){
wwa_add_log($res_id, 'network show_authenticator_type: "'.wwa_get_option('show_authenticator_type').'"->"'.$post_show_type.'"');
}
wwa_update_option('show_authenticator_type', $post_show_type);
$raw_ror = wp_unslash($_POST['ror_origins'] ?? '');
$ror_lines = explode("\n", $raw_ror);
$sanitized_ror = array();
foreach($ror_lines as $line){
$line = trim($line);
if($line === ''){
continue;
}
$parsed = wp_parse_url($line);
if(isset($parsed['scheme']) && isset($parsed['host'])){
$origin = $parsed['scheme'] . '://' . $parsed['host'];
if(isset($parsed['port'])){
$origin .= ':' . $parsed['port'];
}
$sanitized_ror[] = $origin;
}
}
$post_ror_origins = implode("\n", $sanitized_ror);
if($post_ror_origins !== wwa_get_option('ror_origins')){
wwa_add_log($res_id, 'network ror_origins: "'.str_replace("\n", ', ', wwa_get_option('ror_origins')).'"->"'.str_replace("\n", ', ', $post_ror_origins).'"');
}
wwa_update_option('ror_origins', $post_ror_origins);
wp_safe_redirect(add_query_arg('updated', 'true', network_admin_url('settings.php?page=wwa_network_admin')));
exit;
}
// Display network settings page
function wwa_display_network_settings(){
if(!current_user_can('manage_network_options')){
wp_die(esc_html__('You do not have sufficient permissions to access this page.'));
}
wp_enqueue_script('wwa_admin', plugins_url('js/admin.js', __FILE__), array(), get_option('wwa_version')['version']);
wp_localize_script('wwa_admin', 'php_vars', array(
'ajax_url' => admin_url('admin-ajax.php'),
'_ajax_nonce' => wp_create_nonce('wwa_admin_ajax'),
'i18n_1' => __('User verification is disabled by default because some mobile devices do not support it (especially on Android devices). But we <strong>recommend you to enable it</strong> if possible to further secure your login.', 'wp-webauthn'),
'i18n_2' => __('Log count: ', 'wp-webauthn'),
'i18n_3' => __('Loading failed, maybe try refreshing?', 'wp-webauthn')
));
wp_enqueue_style('wwa_admin', plugins_url('css/admin.css', __FILE__));
$wwa_not_allowed = false;
if(!function_exists('mb_substr') || !function_exists('gmp_intval') || !function_exists('sodium_crypto_sign_detached') || !wwa_check_ssl() && (wp_parse_url(site_url(), PHP_URL_HOST) !== 'localhost' && wp_parse_url(site_url(), PHP_URL_HOST) !== '127.0.0.1')){
$wwa_not_allowed = true;
}
?>
<div class="wrap">
<h1>WP-WebAuthn <?php esc_html_e('Network Settings', 'wp-webauthn');?></h1>
<?php if(isset($_GET['updated']) && $_GET['updated'] === 'true'){ ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e('Settings saved.', 'wp-webauthn');?></p></div>
<?php } ?>
<p class="description"><?php esc_html_e('These settings apply to all sites in the network.', 'wp-webauthn');?></p>
<form method="post" action="<?php echo esc_url(network_admin_url('edit.php?action=wwa_network_options_update')); ?>">
<?php wp_nonce_field('wwa_network_options_update', 'wwa_network_options_nonce'); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="first_choice"><?php esc_html_e('Preferred login method', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_first_choice=wwa_get_option('first_choice');?>
<select name="first_choice" id="first_choice">
<option value="true"<?php if($wwa_v_first_choice !== 'false' && !($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed)){?> selected<?php }?>><?php esc_html_e('Prefer WebAuthn', 'wp-webauthn');?></option>
<option value="false"<?php if($wwa_v_first_choice === 'false'){?> selected<?php }?>><?php esc_html_e('Prefer password', 'wp-webauthn');?></option>
<option value="webauthn"<?php if($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed){?> selected<?php }if($wwa_not_allowed){?> disabled<?php }?>><?php esc_html_e('WebAuthn Only', 'wp-webauthn');?></option>
</select>
<p class="description"><?php echo wp_kses(__('When using "WebAuthn Only", password login will be completely disabled. Please make sure your browser supports WebAuthn, otherwise you may unable to login.<br>User that doesn\'t have any registered authenticator (e.g. new user) will unable to login when using "WebAuthn Only".<br>When the browser does not support WebAuthn, the login method will default to password if password login is not disabled.', 'wp-webauthn'), array('br' => array()));?></p>
</td>
</tr>
<tr>
<th scope="row"></th>
</tr>
<tr>
<th scope="row"><label for="user_verification"><?php esc_html_e('Require user verification', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_uv=wwa_get_option('user_verification');?>
<fieldset id="wwa-uv-field">
<label><input type="radio" name="user_verification" value="true" <?php if($wwa_v_uv === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="user_verification" value="false" <?php if($wwa_v_uv === 'false'){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php echo wp_kses(__('User verification can improve security, but is not fully supported by mobile devices. <br> If you cannot register or verify your authenticators, please consider disabling user verification.', 'wp-webauthn'), array('br' => array()));?></p>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><label for="usernameless_login"><?php esc_html_e('Allow to login without username', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_ul=wwa_get_option('usernameless_login');?>
<fieldset>
<label><input type="radio" name="usernameless_login" value="true" <?php if($wwa_v_ul === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="usernameless_login" value="false" <?php if($wwa_v_ul === 'false' || $wwa_v_ul === false){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php echo wp_kses(__('Allow users to register authenticator with usernameless authentication feature and login without username.<br><strong>User verification will be enabled automatically when authenticating with usernameless authentication feature.</strong><br>Some authenticators and some browsers <strong>DO NOT</strong> support this feature.', 'wp-webauthn'), array('br' => array(), 'strong' => array()));?></p>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><label for="allow_authenticator_type"><?php esc_html_e('Allow a specific type of authenticator', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_at=wwa_get_option('allow_authenticator_type');?>
<select name="allow_authenticator_type" id="allow_authenticator_type">
<option value="none"<?php if($wwa_v_at === 'none' || $wwa_v_at === false){?> selected<?php }?>><?php esc_html_e('Any', 'wp-webauthn');?></option>
<option value="platform"<?php if($wwa_v_at === 'platform'){?> selected<?php }?>><?php esc_html_e('Platform (e.g. Passkey or built-in sensors)', 'wp-webauthn');?></option>
<option value="cross-platform"<?php if($wwa_v_at === 'cross-platform'){?> selected<?php }?>><?php esc_html_e('Roaming (e.g. USB security keys)', 'wp-webauthn');?></option>
</select>
<p class="description"><?php esc_html_e('If a type is selected, the browser will only prompt for authenticators of selected type when authenticating and user can only register authenticators of selected type.', 'wp-webauthn');?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="show_authenticator_type"><?php esc_html_e('Allow users to choose authenticator type', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_sat=wwa_get_option('show_authenticator_type');?>
<fieldset>
<label><input type="radio" name="show_authenticator_type" value="true" <?php if($wwa_v_sat === 'true'){?>checked="checked"<?php }?>> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="show_authenticator_type" value="false" <?php if($wwa_v_sat === 'false' || $wwa_v_sat === false){?>checked="checked"<?php }?>> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php echo wp_kses(__('When enabled, users can select the authenticator type when registering.<br>The "Allow a specific type" restriction above still applies regardless of this setting.', 'wp-webauthn'), array('br' => array()));?></p>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"></th>
</tr>
<!-- Feature not fully ready <tr>
<th scope="row"><label for="ror_origins"><?php esc_html_e('Related origins', 'wp-webauthn');?></label></th>
<td>
<?php $wwa_v_ror = wwa_get_option('ror_origins');?>
<textarea name="ror_origins" id="ror_origins" rows="4" cols="50" class="large-text code"><?php echo esc_textarea($wwa_v_ror ? $wwa_v_ror : '');?></textarea>
<p class="description"><?php echo wp_kses(__('Allow cross-site passkey usages (<a href="https://passkeys.dev/docs/advanced/related-origins/" target="_blank">Related Origin Requests</a>). May be useful for multi-site networks.<br> Enter one origin per line (e.g. <code>https://example.com</code>). Leave empty to disable.', 'wp-webauthn'), array('a' => array('href' => array(), 'target' => array()), 'br' => array(), 'code' => array()));?></p>
</td>
</tr>-->
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}

View File

@ -1,8 +1,15 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
$wwa_term = wwa_get_option('terminology') === 'webauthn';
// Insert CSS and JS // Insert CSS and JS
wp_enqueue_script('wwa_profile', plugins_url('js/profile.js', __FILE__)); wp_enqueue_script('wwa_profile', plugins_url('js/profile.js', __FILE__), array(), get_option('wwa_version')['version']);
wp_localize_script('wwa_profile', 'php_vars', array( wp_localize_script('wwa_profile', 'php_vars', array(
'ajax_url' => admin_url('admin-ajax.php'), 'ajax_url' => admin_url('admin-ajax.php'),
'_ajax_nonce' => wp_create_nonce('wwa_ajax'),
'user_id' => $user->ID, 'user_id' => $user->ID,
'i18n_1' => __('Initializing...', 'wp-webauthn'), 'i18n_1' => __('Initializing...', 'wp-webauthn'),
'i18n_2' => __('Please follow instructions to finish registration...', 'wp-webauthn'), 'i18n_2' => __('Please follow instructions to finish registration...', 'wp-webauthn'),
@ -19,7 +26,7 @@ wp_localize_script('wwa_profile', 'php_vars', array(
'i18n_13' => __('Please follow instructions to finish verification...', 'wp-webauthn'), 'i18n_13' => __('Please follow instructions to finish verification...', 'wp-webauthn'),
'i18n_14' => __('Verifying...', 'wp-webauthn'), 'i18n_14' => __('Verifying...', 'wp-webauthn'),
'i18n_15' => '<span class="wwa-failed">'.__('Verification failed', 'wp-webauthn').'</span>', 'i18n_15' => '<span class="wwa-failed">'.__('Verification failed', 'wp-webauthn').'</span>',
'i18n_16' => '<span class="wwa-success">'.__('Verification passed! You can now log in through WebAuthn', 'wp-webauthn').'</span>', 'i18n_16' => '<span class="wwa-success">'.(wwa_get_option('terminology') === 'webauthn' ? __('Verification passed! You can now log in through WebAuthn', 'wp-webauthn') : __('Verification passed! You can now log in with this passkey', 'wp-webauthn')).'</span>',
'i18n_17' => __('No registered authenticators', 'wp-webauthn'), 'i18n_17' => __('No registered authenticators', 'wp-webauthn'),
'i18n_18' => __('Confirm removal of authenticator: ', 'wp-webauthn'), 'i18n_18' => __('Confirm removal of authenticator: ', 'wp-webauthn'),
'i18n_19' => __('Removing...', 'wp-webauthn'), 'i18n_19' => __('Removing...', 'wp-webauthn'),
@ -30,38 +37,40 @@ wp_localize_script('wwa_profile', 'php_vars', array(
'i18n_25' => __('No', 'wp-webauthn'), 'i18n_25' => __('No', 'wp-webauthn'),
'i18n_26' => __(' (Unavailable)', 'wp-webauthn'), 'i18n_26' => __(' (Unavailable)', 'wp-webauthn'),
'i18n_27' => __('The site administrator has disabled usernameless login feature.', 'wp-webauthn'), 'i18n_27' => __('The site administrator has disabled usernameless login feature.', 'wp-webauthn'),
'i18n_28' => __('After removing this authenticator, you will not be able to login with WebAuthn', 'wp-webauthn'), // translators: %s: 'WebAuthn' or 'passkey'
'i18n_28' => sprintf(__('After removing this authenticator, you will not be able to login with %s', 'wp-webauthn'), $wwa_term ? 'WebAuthn' : __('Passkey', 'wp-webauthn')),
'i18n_29' => __(' (Disabled)', 'wp-webauthn'), 'i18n_29' => __(' (Disabled)', 'wp-webauthn'),
'i18n_30' => __('The site administrator only allow platform authenticators currently.', 'wp-webauthn'), 'i18n_30' => __('The site administrator only allow platform authenticators currently.', 'wp-webauthn'),
'i18n_31' => __('The site administrator only allow roaming authenticators currently.', 'wp-webauthn') 'i18n_31' => __('The site administrator only allow roaming authenticators currently.', 'wp-webauthn')
)); ));
wp_enqueue_style('wwa_profile', plugins_url('css/admin.css', __FILE__)); wp_enqueue_style('wwa_profile', plugins_url('css/admin.css', __FILE__));
wp_localize_script('wwa_profile', 'configs', array('usernameless' => (wwa_get_option('usernameless_login') === false ? "false" : wwa_get_option('usernameless_login')), 'allow_authenticator_type' => (wwa_get_option('allow_authenticator_type') === false ? "none" : wwa_get_option('allow_authenticator_type')))); wp_localize_script('wwa_profile', 'configs', array(
'usernameless' => (wwa_get_option('usernameless_login') === false ? "false" : wwa_get_option('usernameless_login')),
'allow_authenticator_type' => (wwa_get_option('allow_authenticator_type') === false ? "none" : wwa_get_option('allow_authenticator_type')),
'show_authenticator_type' => (wwa_get_option('show_authenticator_type') === false ? "true" : wwa_get_option('show_authenticator_type'))
));
?> ?>
<br> <br>
<h2 id="wwa-webauthn-start">WebAuthn</h2> <h2 id="wwa-webauthn-start"><?php if($wwa_term){ ?>WebAuthn<?php }else{ esc_html_e('Passkeys', 'wp-webauthn'); }?></h2>
<?php <?php
if(isset($_GET['wwa_registered']) && $_GET['wwa_registered'] === 'true'){ if(isset($_GET['wwa_registered']) && $_GET['wwa_registered'] === 'true'){
$count = 0; $count = 0;
if(user_can($user, 'read')){ if(user_can($user, 'read')){
$user_ids = wwa_get_option("user_id"); global $wpdb;
if(isset($user_ids[$user->user_login])){ $count = intval($wpdb->get_var($wpdb->prepare(
$user_id = $user_ids[$user->user_login]; "SELECT COUNT(*) FROM {$wpdb->wwa_credentials} WHERE user_id = %d AND registered_blog_id = %d",
$count = 0; $user->ID, get_current_blog_id()
$data = json_decode(wwa_get_option("user_credentials_meta"), true); )));
foreach($data as $key => $value){
if($user_id === $value["user"]){
$count++;
break;
}
}
}
} }
if($count === 0){ if($count === 0){
?> ?>
<div id="wp-webauthn-message-container"> <div id="wp-webauthn-message-container">
<div class="notice notice-info is-dismissible" role="alert" id="wp-webauthn-message"> <div class="notice notice-info is-dismissible" role="alert" id="wp-webauthn-message">
<p><?php _e('You\'ve successfully registered! Now you can register your authenticators below.', 'wp-webauthn')?></p> <p><?php
$wwa_term_plural = $wwa_term ? __('authenticators', 'wp-webauthn') : __('passkeys', 'wp-webauthn');
/* translators: %1$s: 'authenticators' or 'passkeys' */
echo esc_html(sprintf(__('You\'ve successfully registered! Now you can register your %1$s below.', 'wp-webauthn'), $wwa_term_plural));
?></p>
</div> </div>
</div> </div>
<?php <?php
@ -73,103 +82,112 @@ if(!function_exists("mb_substr") || !function_exists("gmp_intval") || !wwa_check
?> ?>
<div id="wp-webauthn-error-container"> <div id="wp-webauthn-error-container">
<div class="notice notice-error is-dismissible" role="alert" id="wp-webauthn-error"> <div class="notice notice-error is-dismissible" role="alert" id="wp-webauthn-error">
<p><?php _e('This site is not correctly configured to use WebAuthn. Please contact the site administrator.', 'wp-webauthn')?></p> <p><?php
/* translators: %s: 'WebAuthn' or 'passkey' */
echo esc_html(sprintf(__('This site is not correctly configured to use %s. Please contact the site administrator.', 'wp-webauthn'), $wwa_term ? 'WebAuthn' : __('Passkey', 'wp-webauthn')));
?></p>
</div> </div>
</div> </div>
<?php } ?> <?php } ?>
<table class="form-table"> <table class="form-table">
<tr class="user-rich-editing-wrap"> <tr class="user-rich-editing-wrap">
<th scope="row"><?php _e('WebAuthn Only', 'wp-webauthn');?></th> <th scope="row"><?php $wwa_term ? esc_html_e('WebAuthn Only', 'wp-webauthn') : esc_html_e('Passkey Only', 'wp-webauthn'); ?></th>
<td> <td>
<label for="webauthn_only"> <label for="webauthn_only">
<?php $wwa_v_first_choice = wwa_get_option('first_choice');?> <?php $wwa_v_first_choice = wwa_get_option('first_choice');?>
<input name="webauthn_only" type="checkbox" id="webauthn_only" value="true"<?php if(!$wwa_not_allowed){if($wwa_v_first_choice === 'webauthn'){echo ' disabled checked';}else{if(get_the_author_meta('webauthn_only', $user->ID) === 'true'){echo ' checked';}}}else{echo ' disabled';} ?>> <?php _e('Disable password login for this account', 'wp-webauthn');?> <input name="webauthn_only" type="checkbox" id="webauthn_only" value="true"<?php if(!$wwa_not_allowed){if($wwa_v_first_choice === 'webauthn'){echo ' disabled checked';}else{if(get_user_meta($user->ID, 'wwa_webauthn_only', true) === 'true'){echo ' checked';}}}else{echo ' disabled';} ?>> <?php esc_html_e('Disable password login for this account', 'wp-webauthn');?>
</label> </label>
<p class="description"><?php _e('When checked, password login will be completely disabled. Please make sure your browser supports WebAuthn and you have a registered authenticator, otherwise you may unable to login.', 'wp-webauthn');if($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed){?><br><strong><?php _e('The site administrator has disabled password login for the whole site.', 'wp-webauthn');?></strong><?php }?></p> <p class="description"><?php $wwa_term ? esc_html_e('When checked, password login will be completely disabled. Please make sure your browser supports WebAuthn and you have a registered authenticator, otherwise you may unable to login.', 'wp-webauthn') : esc_html_e('When checked, password login will be completely disabled. Please make sure you have a registered passkey, otherwise you may unable to login.', 'wp-webauthn');if(is_multisite()){?><br><?php esc_html_e('This setting applies to your account across all sites in the network.', 'wp-webauthn');} if($wwa_v_first_choice === 'webauthn' && !$wwa_not_allowed){?><br><strong><?php esc_html_e('The site administrator has disabled password login for the whole site.', 'wp-webauthn');?></strong><?php }?></p>
</td> </td>
</tr> </tr>
</table> </table>
<h3><?php _e('Registered WebAuthn Authenticators', 'wp-webauthn');?></h3> <h3><?php $wwa_term ? esc_html_e('Registered WebAuthn Authenticators', 'wp-webauthn') : esc_html_e('Registered Passkeys', 'wp-webauthn'); ?></h3>
<div class="wwa-table"> <div class="wwa-table">
<table class="wp-list-table widefat fixed striped"> <table class="wp-list-table widefat fixed striped">
<thead> <thead>
<tr> <tr>
<th><?php _e('Identifier', 'wp-webauthn');?></th> <th><?php esc_html_e('Identifier', 'wp-webauthn');?></th>
<th><?php _e('Type', 'wp-webauthn');?></th> <?php if(wwa_get_option('show_authenticator_type') !== 'false'){?><th class="wwa-type-th"><?php esc_html_e('Type', 'wp-webauthn');?></th><?php }?>
<th><?php _ex('Registered', 'time', 'wp-webauthn');?></th> <th><?php echo esc_html(_x('Registered', 'time', 'wp-webauthn'));?></th>
<th><?php _e('Last used', 'wp-webauthn');?></th> <th><?php esc_html_e('Last used', 'wp-webauthn');?></th>
<th class="wwa-usernameless-th"><?php _e('Usernameless', 'wp-webauthn');?></th> <th class="wwa-usernameless-th"><?php esc_html_e('Usernameless', 'wp-webauthn');?></th>
<th><?php _e('Action', 'wp-webauthn');?></th> <th><?php esc_html_e('Action', 'wp-webauthn');?></th>
</tr> </tr>
</thead> </thead>
<tbody id="wwa-authenticator-list"> <tbody id="wwa-authenticator-list">
<tr> <tr>
<td colspan="5"><?php _e('Loading...', 'wp-webauthn');?></td> <td colspan="<?php echo esc_attr(wwa_get_option('show_authenticator_type') !== 'false' ? '5' : '4'); ?>"><?php esc_html_e('Loading...', 'wp-webauthn');?></td>
</tr> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<th><?php _e('Identifier', 'wp-webauthn');?></th> <th><?php esc_html_e('Identifier', 'wp-webauthn');?></th>
<th><?php _e('Type', 'wp-webauthn');?></th> <?php if(wwa_get_option('show_authenticator_type') !== 'false'){?><th class="wwa-type-th"><?php esc_html_e('Type', 'wp-webauthn');?></th><?php }?>
<th><?php _ex('Registered', 'time', 'wp-webauthn');?></th> <th><?php echo esc_html(_x('Registered', 'time', 'wp-webauthn'));?></th>
<th><?php _e('Last used', 'wp-webauthn');?></th> <th><?php esc_html_e('Last used', 'wp-webauthn');?></th>
<th class="wwa-usernameless-th"><?php _e('Usernameless', 'wp-webauthn');?></th> <th class="wwa-usernameless-th"><?php esc_html_e('Usernameless', 'wp-webauthn');?></th>
<th><?php _e('Action', 'wp-webauthn');?></th> <th><?php esc_html_e('Action', 'wp-webauthn');?></th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
<p id="wwa_usernameless_tip"></p> <p id="wwa_usernameless_tip"></p>
<p id="wwa_type_tip"></p> <p id="wwa_type_tip"></p>
<button id="wwa-add-new-btn" class="button" title="<?php _e('Register New Authenticator', 'wp-webauthn');?>"<?php if($wwa_not_allowed){echo ' disabled';}?>><?php _e('Register New Authenticator', 'wp-webauthn');?></button>&nbsp;&nbsp;<button id="wwa-verify-btn" class="button" title="<?php _e('Verify Authenticator', 'wp-webauthn');?>"><?php _e('Verify Authenticator', 'wp-webauthn');?></button> <button id="wwa-add-new-btn" class="button" title="<?php $wwa_term ? esc_attr_e('Register New Authenticator', 'wp-webauthn') : esc_attr_e('Register New Passkey', 'wp-webauthn'); ?>"<?php if($wwa_not_allowed){echo ' disabled';}?>><?php $wwa_term ? esc_html_e('Register New Authenticator', 'wp-webauthn') : esc_html_e('Register New Passkey', 'wp-webauthn'); ?></button>&nbsp;&nbsp;<button id="wwa-verify-btn" class="button" title="<?php $wwa_term ? esc_attr_e('Verify Authenticator', 'wp-webauthn') : esc_attr_e('Verify Passkey', 'wp-webauthn'); ?>"><?php $wwa_term ? esc_html_e('Verify Authenticator', 'wp-webauthn') : esc_html_e('Verify Passkey', 'wp-webauthn'); ?></button>
<div id="wwa-new-block" tabindex="-1"> <div id="wwa-new-block" tabindex="-1">
<button class="button button-small wwa-cancel"><?php _e('Close');?></button> <button class="button button-small wwa-cancel"><?php esc_html_e('Close');?></button>
<h2><?php _e('Register New Authenticator', 'wp-webauthn');?></h2> <h2><?php $wwa_term ? esc_html_e('Register New Authenticator', 'wp-webauthn') : esc_html_e('Register New Passkey', 'wp-webauthn'); ?></h2>
<?php /* translators: %s: user login name */ ?> <?php
<p class="description"><?php printf(__('You are about to associate an authenticator with the current account <strong>%s</strong>.<br>You can register multiple authenticators for an account.', 'wp-webauthn'), $user->user_login);?></p> $wwa_term_singular = esc_html($wwa_term ? __('an authenticator', 'wp-webauthn') : __('a passkey', 'wp-webauthn'));
$wwa_term_plural = esc_html($wwa_term ? __('authenticators', 'wp-webauthn') : __('passkeys', 'wp-webauthn'));
/* translators: %1$s: 'an authenticator' or 'a passkey', %2$s: user login name, %3$s: 'authenticators' or 'passkeys' */
?>
<p class="description"><?php echo wp_kses(sprintf(__('You are about to associate %1$s with the current account <strong>%2$s</strong>.<br>You can register multiple %3$s for an account.', 'wp-webauthn'), $wwa_term_singular, esc_html($user->user_login), $wwa_term_plural), array('strong' => array(), 'br' => array()));?></p>
<table class="form-table"> <table class="form-table">
<?php if(wwa_get_option('show_authenticator_type') !== 'false'){?>
<tr> <tr>
<th scope="row"><label for="wwa_authenticator_type"><?php _e('Type of authenticator', 'wp-webauthn');?></label></th> <th scope="row"><label for="wwa_authenticator_type"><?php esc_html_e('Type of authenticator', 'wp-webauthn');?></label></th>
<td> <td>
<?php <?php
$allowed_type = wwa_get_option('allow_authenticator_type') === false ? 'none' : wwa_get_option('allow_authenticator_type'); $allowed_type = wwa_get_option('allow_authenticator_type') === false ? 'none' : wwa_get_option('allow_authenticator_type');
?> ?>
<select name="wwa_authenticator_type" id="wwa_authenticator_type"> <select name="wwa_authenticator_type" id="wwa_authenticator_type" form="wwa-registration">
<option value="none" id="type-none" class="sub-type"<?php if($allowed_type !== 'none'){echo ' disabled';}?>><?php _e('Any', 'wp-webauthn');?></option> <option value="none" id="type-none" class="sub-type"<?php if($allowed_type !== 'none'){echo ' disabled';}?>><?php esc_html_e('Any', 'wp-webauthn');?></option>
<option value="platform" id="type-platform" class="sub-type"<?php if($allowed_type === 'cross-platform'){echo ' disabled';}?>><?php _e('Platform (e.g. built-in fingerprint sensors)', 'wp-webauthn');?></option> <option value="platform" id="type-platform" class="sub-type"<?php if($allowed_type === 'cross-platform'){echo ' disabled';}?>><?php esc_html_e('Platform (e.g. built-in fingerprint sensors)', 'wp-webauthn');?></option>
<option value="cross-platform" id="type-cross-platform" class="sub-type"<?php if($allowed_type === 'platform'){echo ' disabled';}?>><?php _e('Roaming (e.g. USB security keys)', 'wp-webauthn');?></option> <option value="cross-platform" id="type-cross-platform" class="sub-type"<?php if($allowed_type === 'platform'){echo ' disabled';}?>><?php esc_html_e('Roaming (e.g. USB security keys)', 'wp-webauthn');?></option>
</select> </select>
<p class="description"><?php _e('If a type is selected, the browser will only prompt for authenticators of selected type. <br> Regardless of the type, you can only log in with the very same authenticators you\'ve registered.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('If a type is selected, the browser will only prompt for authenticators of selected type. <br> Regardless of the type, you can only log in with the very same authenticators you\'ve registered.', 'wp-webauthn'), array('br' => array()));?></p>
</td> </td>
</tr> </tr>
<?php }?>
<tr> <tr>
<th scope="row"><label for="wwa_authenticator_name"><?php _e('Authenticator Identifier', 'wp-webauthn');?></label></th> <th scope="row"><label for="wwa_authenticator_name"><?php esc_html_e('Authenticator Identifier', 'wp-webauthn');?></label></th>
<td> <td>
<input name="wwa_authenticator_name" type="text" id="wwa_authenticator_name" class="regular-text"> <input name="wwa_authenticator_name" type="text" id="wwa_authenticator_name" class="regular-text" form="wwa-registration">
<p class="description"><?php _e('An easily identifiable name for the authenticator. <strong>DOES NOT</strong> affect the authentication process in anyway.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('An easily identifiable name for the authenticator. <strong>DOES NOT</strong> affect the authentication process in anyway.', 'wp-webauthn'), array('strong' => array()));?></p>
</td> </td>
</tr> </tr>
<?php if(wwa_get_option('usernameless_login') === "true"){?> <?php if(wwa_get_option('usernameless_login') === "true"){?>
<tr> <tr>
<th scope="row"><label for="wwa_authenticator_usernameless"><?php _e('Login without username', 'wp-webauthn');?></th> <th scope="row"><label for="wwa_authenticator_usernameless"><?php esc_html_e('Login without username', 'wp-webauthn');?></th>
<td> <td>
<fieldset> <fieldset>
<label><input type="radio" name="wwa_authenticator_usernameless" class="wwa_authenticator_usernameless" value="true"> <?php _e("Enable", "wp-webauthn");?></label><br> <label><input type="radio" name="wwa_authenticator_usernameless" class="wwa_authenticator_usernameless" value="true" form="wwa-registration"> <?php esc_html_e("Enable", "wp-webauthn");?></label><br>
<label><input type="radio" name="wwa_authenticator_usernameless" class="wwa_authenticator_usernameless" value="false" checked="checked"> <?php _e("Disable", "wp-webauthn");?></label><br> <label><input type="radio" name="wwa_authenticator_usernameless" class="wwa_authenticator_usernameless" value="false" checked="checked" form="wwa-registration"> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php _e('If registered authenticator with this feature, you can login without enter your username.<br>Some authenticators like U2F-only authenticators and some browsers <strong>DO NOT</strong> support this feature.<br>A record will be stored in the authenticator permanently untill you reset it.', 'wp-webauthn');?></p> <p class="description"><?php echo wp_kses(__('If registered authenticator with this feature, you can login without enter your username.<br>Some authenticators like U2F-only authenticators and some browsers <strong>DO NOT</strong> support this feature.<br>A record will be stored in the authenticator permanently untill you reset it.', 'wp-webauthn'), array('br' => array(), 'strong' => array()));?></p>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
<?php }?> <?php }?>
</table> </table>
<button id="wwa-bind" class="button"><?php _e('Start Registration', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="wwa-show-progress"></span> <button id="wwa-bind" class="button"><?php esc_html_e('Start Registration', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="wwa-show-progress"></span>
</div> </div>
<div id="wwa-verify-block" tabindex="-1"> <div id="wwa-verify-block" tabindex="-1">
<button class="button button-small wwa-cancel"><?php _e('Close');?></button> <button class="button button-small wwa-cancel"><?php esc_html_e('Close');?></button>
<h2><?php _e('Verify Authenticator', 'wp-webauthn');?></h2> <h2><?php $wwa_term ? esc_html_e('Verify Authenticator', 'wp-webauthn') : esc_html_e('Verify Passkey', 'wp-webauthn'); ?></h2>
<p class="description"><?php _e('Click Test Login to verify that the registered authenticators are working.', 'wp-webauthn');?></p> <p class="description"><?php esc_html_e('Click Test Login to verify that the registered authenticators are working.', 'wp-webauthn');?></p>
<button id="wwa-test" class="button"><?php _e('Test Login', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="wwa-show-test"></span> <button id="wwa-test" class="button"><?php esc_html_e('Test Login', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="wwa-show-test"></span>
<?php if(wwa_get_option('usernameless_login') === "true"){?> <?php if(wwa_get_option('usernameless_login') === "true"){?>
<br><br><button id="wwa-test_usernameless" class="button"><?php _e('Test Login (usernameless)', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="wwa-show-test-usernameless"></span> <br><br><button id="wwa-test_usernameless" class="button"><?php esc_html_e('Test Login (usernameless)', 'wp-webauthn');?></button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="wwa-show-test-usernameless"></span>
<?php }?> <?php }?>
</div> </div>

View File

@ -1,14 +1,22 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
function wwa_localize_frontend(){ function wwa_localize_frontend(){
wwa_init_new_options();
wp_enqueue_script('wwa_frontend_js', plugins_url('js/frontend.js', __FILE__), array(), get_option('wwa_version')['version'], true); wp_enqueue_script('wwa_frontend_js', plugins_url('js/frontend.js', __FILE__), array(), get_option('wwa_version')['version'], true);
wp_localize_script('wwa_frontend_js', 'wwa_php_vars', array( wp_localize_script('wwa_frontend_js', 'wwa_php_vars', array(
'ajax_url' => admin_url('admin-ajax.php'), 'ajax_url' => admin_url('admin-ajax.php'),
'_ajax_nonce' => wp_create_nonce('wwa_ajax'),
'admin_url' => admin_url(), 'admin_url' => admin_url(),
'usernameless' => (wwa_get_option('usernameless_login') === false ? "false" : wwa_get_option('usernameless_login')), 'usernameless' => (wwa_get_option('usernameless_login') === false ? "false" : wwa_get_option('usernameless_login')),
'remember_me' => (wwa_get_option('remember_me') === false ? "false" : wwa_get_option('remember_me')), 'remember_me' => (wwa_get_option('remember_me') === false ? "false" : wwa_get_option('remember_me')),
'allow_authenticator_type' => (wwa_get_option('allow_authenticator_type') === false ? "none" : wwa_get_option('allow_authenticator_type')), 'allow_authenticator_type' => (wwa_get_option('allow_authenticator_type') === false ? "none" : wwa_get_option('allow_authenticator_type')),
'show_authenticator_type' => (wwa_get_option('show_authenticator_type') === false ? "true" : wwa_get_option('show_authenticator_type')),
'terminology' => (wwa_get_option('terminology') === false ? 'passkey' : wwa_get_option('terminology')),
'i18n_1' => __('Ready', 'wp-webauthn'), 'i18n_1' => __('Ready', 'wp-webauthn'),
'i18n_2' => __('Authenticate with WebAuthn', 'wp-webauthn'), 'i18n_2' => wwa_get_option('terminology') === 'webauthn' ? __('Authenticate with WebAuthn', 'wp-webauthn') : __('Authenticate with a passkey', 'wp-webauthn'),
'i18n_3' => __('Hold on...', 'wp-webauthn'), 'i18n_3' => __('Hold on...', 'wp-webauthn'),
'i18n_4' => __('Please proceed...', 'wp-webauthn'), 'i18n_4' => __('Please proceed...', 'wp-webauthn'),
'i18n_5' => __('Authenticating...', 'wp-webauthn'), 'i18n_5' => __('Authenticating...', 'wp-webauthn'),
@ -49,16 +57,16 @@ function wwa_localize_frontend(){
// Login form // Login form
function wwa_login_form_shortcode($vals){ function wwa_login_form_shortcode($vals){
extract(shortcode_atts( $atts = shortcode_atts(
array( array(
'traditional' => 'true', 'traditional' => 'true',
'username' => '', 'username' => '',
'auto_hide' => 'true', 'auto_hide' => 'true',
'to' => '' 'to' => ''
), $vals) ), $vals
); );
if($auto_hide === "true" && current_user_can("read")){ if($atts['auto_hide'] === "true" && current_user_can("read")){
return ''; return '';
} }
@ -70,24 +78,24 @@ function wwa_login_form_shortcode($vals){
$html_form = '<div class="wwa-login-form">'; $html_form = '<div class="wwa-login-form">';
$args = array('echo' => false, 'value_username' => sanitize_user($username)); $args = array('echo' => false, 'value_username' => sanitize_user($atts['username']));
$to_wwa = ''; $to_wwa = '';
if($to !== ""){ if($atts['to'] !== ""){
$args['redirect'] = sanitize_url($to); $args['redirect'] = sanitize_url($atts['to']);
$to_wwa = '<input type="hidden" name="wwa-redirect-to" class="wwa-redirect-to" id="wwa-redirect-to" value="'.$args["redirect"].'">'; $to_wwa = '<input type="hidden" name="wwa-redirect-to" class="wwa-redirect-to" id="wwa-redirect-to" value="'.$args["redirect"].'">';
} }
if($traditional === 'true' && wwa_get_option('first_choice') !== 'webauthn'){ if($atts['traditional'] === 'true' && wwa_get_option('first_choice') !== 'webauthn'){
$html_form .= '<div class="wwa-login-form-traditional">'.wp_login_form($args).'<br><a class="wwa-t2w" href="#"><span>'.__('Authenticate with WebAuthn', 'wp-webauthn').'</span></a></div>'; $html_form .= '<div class="wwa-login-form-traditional">'.wp_login_form($args).'<br><a class="wwa-t2w" href="#"><span>'.(wwa_get_option('terminology') === 'webauthn' ? __('Authenticate with WebAuthn', 'wp-webauthn') : __('Authenticate with a passkey', 'wp-webauthn')).'</span></a></div>';
} }
$html_form .= ' $html_form .= '
<div class="wwa-login-form-webauthn"> <div class="wwa-login-form-webauthn">
<p class="wwa-login-username"> <p class="wwa-login-username">
<label for="wwa-user-name">'.(wwa_get_option('email_login') !== 'true' ? __('Username', 'wp-webauthn') : __('Username or Email Address')).'</label> <label for="wwa-user-name">'.(wwa_get_option('email_login') !== 'true' ? __('Username', 'wp-webauthn') : __('Username or Email Address')).'</label>
<input type="text" name="wwa-user-name" id="wwa-user-name" class="wwa-user-name" value="'.esc_attr(sanitize_user($username, true)).'" size="20"> <input type="text" name="wwa-user-name" id="wwa-user-name" class="wwa-user-name" value="'.esc_attr(sanitize_user($atts['username'], true)).'" size="20">
</p> </p>
<div class="wp-webauthn-notice">'.__('Authenticate with WebAuthn', 'wp-webauthn').'</div> <div class="wp-webauthn-notice">'.(wwa_get_option('terminology') === 'webauthn' ? __('Authenticate with WebAuthn', 'wp-webauthn') : __('Authenticate with a passkey', 'wp-webauthn')).'</div>
<p class="wwa-login-submit-p">'.$to_wwa.'<div class="wwa-form-left">'.((wwa_get_option('remember_me') === false ? 'false' : wwa_get_option('remember_me') !== 'false') ? '<label class="wwa-remember-label"><input name="wwa-rememberme" type="checkbox" id="wwa-rememberme" value="forever"> '.__('Remember Me').'</label>' : '').'<a class="wwa-w2t" href="#">'.__('Authenticate with password', 'wp-webauthn').'</a></div><input type="button" name="wwa-login-submit" id="wwa-login-submit" class="wwa-login-submit button button-primary" value="'.__('Auth', 'wp-webauthn').'"></p> <p class="wwa-login-submit-p">'.$to_wwa.'<div class="wwa-form-left">'.((wwa_get_option('remember_me') === false ? 'false' : wwa_get_option('remember_me') !== 'false') ? '<label class="wwa-remember-label"><input name="wwa-rememberme" type="checkbox" id="wwa-rememberme" value="forever"> '.__('Remember Me').'</label>' : '').'<a class="wwa-w2t" href="#">'.__('Authenticate with password', 'wp-webauthn').'</a></div><input type="button" name="wwa-login-submit" id="wwa-login-submit" class="wwa-login-submit button button-primary" value="'.__('Auth', 'wp-webauthn').'"></p>
</div> </div>
</div>'; </div>';
@ -98,15 +106,15 @@ add_shortcode('wwa_login_form', 'wwa_login_form_shortcode');
// Register form // Register form
function wwa_register_form_shortcode($vals){ function wwa_register_form_shortcode($vals){
extract(shortcode_atts( $atts = shortcode_atts(
array( array(
'display' => 'true' 'display' => 'true'
), $vals) ), $vals
); );
// If always display // If always display
if(!current_user_can("read")){ if(!current_user_can("read")){
if($display === "true"){ if($atts['display'] === "true"){
return '<div class="wwa-register-form"><p class="wwa-bind">'.__('You haven\'t logged in yet.', 'wp-webauthn').'</p></div>'; return '<div class="wwa-register-form"><p class="wwa-bind">'.__('You haven\'t logged in yet.', 'wp-webauthn').'</p></div>';
}else{ }else{
return ''; return '';
@ -120,15 +128,17 @@ function wwa_register_form_shortcode($vals){
wp_enqueue_style('wwa_frondend_css', plugins_url('css/frontend.css', __FILE__), array(), get_option('wwa_version')['version']); wp_enqueue_style('wwa_frondend_css', plugins_url('css/frontend.css', __FILE__), array(), get_option('wwa_version')['version']);
$allowed_type = wwa_get_option('allow_authenticator_type') === false ? 'none' : wwa_get_option('allow_authenticator_type'); $allowed_type = wwa_get_option('allow_authenticator_type') === false ? 'none' : wwa_get_option('allow_authenticator_type');
return ' $show_type = wwa_get_option('show_authenticator_type') !== 'false';
<div class="wwa-register-form"> $type_selector = $show_type ? '
<label for="wwa-authenticator-type">'.__('Type of authenticator', 'wp-webauthn').'</label> <label for="wwa-authenticator-type">'.__('Type of authenticator', 'wp-webauthn').'</label>
<select name="wwa-authenticator-type" class="wwa-authenticator-type" id="wwa-authenticator-type"> <select name="wwa-authenticator-type" class="wwa-authenticator-type" id="wwa-authenticator-type">
<option value="none" class="wwa-type-none"'.($allowed_type !== 'none' ? ' disabled' : '').'>'.__('Any', 'wp-webauthn').'</option> <option value="none" class="wwa-type-none"'.($allowed_type !== 'none' ? ' disabled' : '').'>'.__('Any', 'wp-webauthn').'</option>
<option value="platform" class="wwa-type-platform"'.($allowed_type === 'cross-platform' ? ' disabled' : '').'>'.__('Platform (e.g. built-in fingerprint sensors)', 'wp-webauthn').'</option> <option value="platform" class="wwa-type-platform"'.($allowed_type === 'cross-platform' ? ' disabled' : '').'>'.__('Platform (e.g. built-in fingerprint sensors)', 'wp-webauthn').'</option>
<option value="cross-platform" class="wwa-type-cross-platform"'.($allowed_type === 'platform' ? ' disabled' : '').'>'.__('Roaming (e.g. USB security keys)', 'wp-webauthn').'</option> <option value="cross-platform" class="wwa-type-cross-platform"'.($allowed_type === 'platform' ? ' disabled' : '').'>'.__('Roaming (e.g. USB security keys)', 'wp-webauthn').'</option>
</select> </select>
<p class="wwa-bind-name-description">'.__('If a type is selected, the browser will only prompt for authenticators of selected type. <br> Regardless of the type, you can only log in with the very same authenticators you\'ve registered.', 'wp-webauthn').'</p> <p class="wwa-bind-name-description">'.__('If a type is selected, the browser will only prompt for authenticators of selected type. <br> Regardless of the type, you can only log in with the very same authenticators you\'ve registered.', 'wp-webauthn').'</p>' : '';
return '
<div class="wwa-register-form">'.$type_selector.'
<label for="wwa-authenticator-name">'.__('Authenticator identifier', 'wp-webauthn').'</label> <label for="wwa-authenticator-name">'.__('Authenticator identifier', 'wp-webauthn').'</label>
<input required name="wwa-authenticator-name" type="text" class="wwa-authenticator-name" id="wwa-authenticator-name"> <input required name="wwa-authenticator-name" type="text" class="wwa-authenticator-name" id="wwa-authenticator-name">
<p class="wwa-bind-name-description">'.__('An easily identifiable name for the authenticator. <strong>DOES NOT</strong> affect the authentication process in anyway.', 'wp-webauthn').'</p>'.( <p class="wwa-bind-name-description">'.__('An easily identifiable name for the authenticator. <strong>DOES NOT</strong> affect the authentication process in anyway.', 'wp-webauthn').'</p>'.(
@ -140,15 +150,15 @@ add_shortcode('wwa_register_form', 'wwa_register_form_shortcode');
// Verify button // Verify button
function wwa_verify_button_shortcode($vals){ function wwa_verify_button_shortcode($vals){
extract(shortcode_atts( $atts = shortcode_atts(
array( array(
'display' => 'true' 'display' => 'true'
), $vals) ), $vals
); );
// If always display // If always display
if(!current_user_can("read")){ if(!current_user_can("read")){
if($display === "true"){ if($atts['display'] === "true"){
return '<p class="wwa-test">'.__('You haven\'t logged in yet.', 'wp-webauthn').'</p>'; return '<p class="wwa-test">'.__('You haven\'t logged in yet.', 'wp-webauthn').'</p>';
}else{ }else{
return ''; return '';
@ -166,23 +176,26 @@ add_shortcode('wwa_verify_button', 'wwa_verify_button_shortcode');
// Authenticator list // Authenticator list
function wwa_list_shortcode($vals){ function wwa_list_shortcode($vals){
extract(shortcode_atts( $atts = shortcode_atts(
array( array(
'display' => 'true' 'display' => 'true'
), $vals) ), $vals
); );
$thead = '<div class="wwa-table-container"><table class="wwa-list-table"><thead><tr><th>'.__('Identifier', 'wp-webauthn').'</th><th>'.__('Type', 'wp-webauthn').'</th><th>'._x('Registered', 'time', 'wp-webauthn').'</th><th>'.__('Last used', 'wp-webauthn').'</th><th class="wwa-usernameless-th">'.__('Usernameless', 'wp-webauthn').'</th><th>'.__('Action', 'wp-webauthn').'</th></tr></thead><tbody class="wwa-authenticator-list">'; $show_type = wwa_get_option('show_authenticator_type') !== 'false';
$tbody = '<tr><td colspan="5">'.__('Loading...', 'wp-webauthn').'</td></tr>'; $type_th = $show_type ? '<th class="wwa-type-th">'.__('Type', 'wp-webauthn').'</th>' : '';
$tfoot = '</tbody><tfoot><tr><th>'.__('Identifier', 'wp-webauthn').'</th><th>'.__('Type', 'wp-webauthn').'</th><th>'._x('Registered', 'time', 'wp-webauthn').'</th><th>'.__('Last used', 'wp-webauthn').'</th><th class="wwa-usernameless-th">'.__('Usernameless', 'wp-webauthn').'</th><th>'.__('Action', 'wp-webauthn').'</th></tr></tfoot></table></div><p class="wwa-authenticator-list-usernameless-tip"></p><p class="wwa-authenticator-list-type-tip"></p>'; $loading_colspan = $show_type ? '5' : '4';
$thead = '<div class="wwa-table-container"><table class="wwa-list-table"><thead><tr><th>'.__('Identifier', 'wp-webauthn').'</th>'.$type_th.'<th>'._x('Registered', 'time', 'wp-webauthn').'</th><th>'.__('Last used', 'wp-webauthn').'</th><th class="wwa-usernameless-th">'.__('Usernameless', 'wp-webauthn').'</th><th>'.__('Action', 'wp-webauthn').'</th></tr></thead><tbody class="wwa-authenticator-list">';
$tbody = '<tr><td colspan="'.$loading_colspan.'">'.__('Loading...', 'wp-webauthn').'</td></tr>';
$tfoot = '</tbody><tfoot><tr><th>'.__('Identifier', 'wp-webauthn').'</th>'.$type_th.'<th>'._x('Registered', 'time', 'wp-webauthn').'</th><th>'.__('Last used', 'wp-webauthn').'</th><th class="wwa-usernameless-th">'.__('Usernameless', 'wp-webauthn').'</th><th>'.__('Action', 'wp-webauthn').'</th></tr></tfoot></table></div><p class="wwa-authenticator-list-usernameless-tip"></p><p class="wwa-authenticator-list-type-tip"></p>';
// If always display // If always display
if(!current_user_can("read")){ if(!current_user_can("read")){
if($display === "true"){ if($atts['display'] === "true"){
// Load CSS // Load CSS
wp_enqueue_style('wwa_frondend_css', plugins_url('css/frontend.css', __FILE__), array(), get_option('wwa_version')['version']); wp_enqueue_style('wwa_frondend_css', plugins_url('css/frontend.css', __FILE__), array(), get_option('wwa_version')['version']);
return $thead.'<tr><td colspan="5">'.__('You haven\'t logged in yet.', 'wp-webauthn').'</td></tr>'.$tfoot; return $thead.'<tr><td colspan="'.$loading_colspan.'">'.__('You haven\'t logged in yet.', 'wp-webauthn').'</td></tr>'.$tfoot;
}else{ }else{
return ''; return '';
} }

View File

@ -1,5 +1,9 @@
<?php <?php
if (!defined('ABSPATH')) {
exit;
}
$wwa_version = array( $wwa_version = array(
'version' => '1.3.4', 'version' => '1.4.1',
'commit' => 'b7ef5ce' 'commit' => 'f5955ea'
); );