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 {
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 {
margin-right: 5px;
width: 56px;
}
#wp-webauthn span {
line-height: 30px;
@ -47,8 +48,32 @@
#loginform.wwa-webauthn-only {
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){
#wp-webauthn span {
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,
type: 'GET',
data: {
action: 'wwa_get_log'
action: 'wwa_get_log',
_ajax_nonce: php_vars._ajax_nonce
},
success: function (data) {
if (typeof data === 'string') {
@ -71,7 +72,8 @@ jQuery('#clear_log').click((e) => {
url: php_vars.ajax_url,
type: 'GET',
data: {
action: 'wwa_clear_log'
action: 'wwa_clear_log',
_ajax_nonce: php_vars._ajax_nonce
},
success: function () {
updateLog();

View File

@ -8,44 +8,44 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
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')) {
// Not support, show a message
if (document.querySelectorAll('#login > h1').length > 0) {
let dom = document.createElement('p');
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)
}
}
wwa_dom('#loginform', (dom) => { dom.classList.add('wwa-webauthn-only') });
if (document.getElementsByClassName('user-pass-wrap').length > 0) {
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) });
}
} else {
// WordPress 5.2-
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) });
}
const targetDOM = document.getElementById('loginform').getElementsByTagName('p')[1];
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 (php_vars.webauthn_only !== 'true') {
if (wwa_login_php_vars.webauthn_only !== 'true') {
if (document.getElementsByClassName('user-pass-wrap').length > 0) {
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' });
}
} else {
// WordPress 5.2-
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' });
}
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-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');
// 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) {
return;
@ -64,9 +73,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (dom.length > 0) {
if (dom[0].getElementsByTagName('input').length > 0) {
// 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 {
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);
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';
button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_3;
wwa_disable_buttons();
@ -314,7 +315,7 @@ function wwa_bind() {
wwa_dom('wwa-authenticator-type', (dom) => { dom.disabled = true }, 'class');
wwa_dom('wwa-authenticator-usernameless', (dom) => { dom.disabled = true }, 'class');
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) {
button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_28;
let data = rawData;
@ -381,7 +382,7 @@ function wwa_bind() {
return publicKeyCredential;
}).then(JSON.stringify).then((AuthenticatorAttestationResponse) => {
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 (rawData === 'true') {
button_dom.nextElementSibling.innerHTML = wwa_php_vars.i18n_29;
@ -521,19 +522,33 @@ function wwa_verify() {
}
// 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() {
if (document.getElementsByClassName('wwa-authenticator-list').length === 0) {
return;
}
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) {
let data = rawData;
try {
data = JSON.parse(rawData);
} catch (e) {
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;
}
if (data.length === 0) {
@ -542,7 +557,7 @@ function updateList() {
} else {
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-type-tip', (dom) => { dom.innerText = '' }, 'class');
return;
@ -561,7 +576,7 @@ function updateList() {
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');
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');
}
} 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) {
let request = wwa_ajax();
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) {
updateList();
} 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 : ''))) {
wwa_dom(`wwa-key-${id}`, (dom) => { dom.innerText = wwa_php_vars.i18n_19 }, 'class');
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) {
updateList();
} else {

View File

@ -1,5 +1,8 @@
'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
const wwa_ajax = function () {
let xmlHttpReq = new XMLHttpRequest();
@ -70,19 +73,154 @@ const wwa_dom = (selector, callback = () => { }, method = 'query') => {
}
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', () => {
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');
if (placeholder) {
const previous = placeholder.previousSibling;
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();
} 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();
}
placeholder.remove();
}
placeholder.remove();
}
if (document.querySelectorAll('#lostpasswordform, #registerform, .admin-email-confirm-form, #resetpassform').length > 0) {
return;
@ -95,24 +233,25 @@ document.addEventListener('DOMContentLoaded', () => {
button_check.id = 'wp-webauthn-check';
button_check.type = 'button';
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');
if (php_vars.webauthn_only !== 'true') {
if (wwa_login_php_vars.webauthn_only !== 'true') {
button_toggle.id = 'wp-webauthn';
button_toggle.type = 'button';
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');
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_check, submit.nextElementSibling);
}
let notice = document.createElement('div');
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');
if (forgetmenot.length > 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-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_login', (dom) => { dom.focus() }, 'id');
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = `<span><span class="dashicons dashicons-shield-alt"></span> ${php_vars.i18n_2}</span>` }, 'class');
wwa_dom('user_login', (dom) => {
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-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')
if (inputDom.length > 0) {
if (document.getElementById('wwa-username-label')) {
// 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 {
inputDom[0].innerText = php_vars.i18n_10;
inputDom[0].innerText = wwa_login_php_vars.i18n_10;
}
}
} else {
if (document.getElementsByClassName('user-pass-wrap').length > 0) {
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' });
}
} else {
// WordPress 5.2-
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' });
}
document.getElementById('loginform').getElementsByTagName('p')[1].style.display = 'none';
}
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('user_login', (dom) => { dom.focus() }, 'id');
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = `<span><span class="dashicons dashicons-shield-alt"></span> ${php_vars.i18n_2}</span>` }, 'class');
wwa_dom('user_login', (dom) => {
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-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')
if (inputDom.length > 0) {
if (document.getElementById('wwa-username-label')) {
// 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 {
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;
}
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('p.message', (dom) => { dom.remove() });
if (document.querySelectorAll('#login > h1').length > 0) {
let dom = document.createElement('div');
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)
}
// Shake the login form, code from WordPress
@ -293,22 +457,28 @@ function check() {
wwa_shake(form, shake, 20);
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('#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();
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) {
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;
try {
data = JSON.parse(rawData);
} catch (e) {
console.warn(rawData);
if (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');
if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} 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('#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) {
if (php_vars.allow_authenticator_type === 'cross-platform') {
if (wwa_login_php_vars.allow_authenticator_type === 'cross-platform') {
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'];
}
}
@ -338,7 +508,7 @@ function check() {
delete data.clientID;
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;
}).then((data) => {
const publicKeyCredential = {
@ -355,10 +525,10 @@ function check() {
return publicKeyCredential;
}).then(JSON.stringify).then((AuthenticatorResponse) => {
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 (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) {
setTimeout(() => {
window.location.href = document.querySelectorAll('p.submit input[name="redirect_to"]')[0].value;
@ -370,24 +540,24 @@ function check() {
}, 200);
} else {
setTimeout(() => {
window.location.href = php_vars.admin_url
window.location.href = wwa_login_php_vars.admin_url
}, 200);
}
}
} else {
if (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');
if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} 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('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
}
} else {
if (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');
if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} 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('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
@ -395,23 +565,23 @@ function check() {
})
}).catch((error) => {
console.warn(error);
if (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');
if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} 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('#wp-webauthn-check, #wp-webauthn', (dom) => { dom.disabled = false });
})
} else {
if (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');
if (wwa_login_php_vars.usernameless === 'true' && document.getElementById('user_login').value === '') {
wwa_dom('wp-webauthn-notice', (dom) => { dom.innerHTML = wwa_login_php_vars.i18n_7 + wwa_login_php_vars.i18n_12 }, 'class');
} 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('#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
if (window.PublicKeyCredential === undefined || navigator.credentials.create === undefined || typeof navigator.credentials.create !== 'function') {
jQuery('#wwa-bind, #wwa-test').attr('disabled', 'disabled');
@ -24,12 +26,13 @@ function updateList() {
type: 'GET',
data: {
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) {
if (typeof data === 'string') {
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;
}
if (data.length === 0) {
@ -38,7 +41,12 @@ function updateList() {
} else {
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').hide();
jQuery('#wwa_type_tip').text('');
@ -59,7 +67,7 @@ function updateList() {
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);
if (has_usernameless || configs.usernameless === 'true') {
@ -67,6 +75,11 @@ function updateList() {
} else {
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') {
jQuery('#wwa_usernameless_tip').text(php_vars.i18n_27);
jQuery('#wwa_usernameless_tip').show();
@ -87,11 +100,23 @@ function updateList() {
}
},
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
*
* @param {string} input Base64URL coded string
@ -140,6 +165,10 @@ jQuery('.wwa-cancel').click((e) => {
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) => {
if (e.keyCode === 13) {
jQuery('#wwa-bind').trigger('click');
@ -160,16 +189,19 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').attr('disabled', 'disabled');
jQuery('#wwa_authenticator_name').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({
url: php_vars.ajax_url,
type: 'GET',
data: {
action: 'wwa_create',
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',
user_id: php_vars.user_id
user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
},
success: function (data) {
if (typeof data === 'string') {
@ -178,7 +210,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').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();
return;
}
@ -241,10 +275,11 @@ jQuery('#wwa-bind').click((e) => {
data: {
data: window.btoa(AuthenticatorAttestationResponse),
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',
clientid: clientID,
user_id: php_vars.user_id
user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
},
success: function (data) {
if (data.trim() === 'true') {
@ -254,7 +289,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa_authenticator_name').removeAttr('disabled');
jQuery('#wwa_authenticator_name').val('');
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();
} else {
// Register failed
@ -262,7 +299,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').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();
}
},
@ -271,7 +310,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').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();
}
})
@ -282,7 +323,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').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();
})
},
@ -291,7 +334,9 @@ jQuery('#wwa-bind').click((e) => {
jQuery('#wwa-bind').removeAttr('disabled');
jQuery('#wwa_authenticator_name').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();
}
})
@ -428,7 +473,8 @@ function renameAuthenticator(id, name) {
id: id,
name: new_name,
target: 'rename',
user_id: php_vars.user_id
user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
},
success: function () {
updateList();
@ -456,7 +502,8 @@ function removeAuthenticator(id, name) {
action: 'wwa_modify_authenticator',
id: id,
target: 'remove',
user_id: php_vars.user_id
user_id: php_vars.user_id,
_ajax_nonce: php_vars._ajax_nonce
},
success: function () {
updateList();

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
=== WP-WebAuthn ===
Contributors: axton
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
Tested up to: 6.6
Stable tag: 1.3.4
Requires PHP: 7.2
Tested up to: 6.9
Stable tag: 1.4.1
Requires PHP: 7.4
License: GPLv3
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.
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.**
**WebAuthn requires HTTPS connection or `localhost` to function normally.**
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 =
@ -80,6 +82,18 @@ To use FaceID or TouchID, you need to use iOS/iPadOS 14+.
== 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 =
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+.
@ -106,68 +120,7 @@ Fix: 2FA compatibility
Update: Translations
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 ==
= 1.2.5 =
Improvred HTTPS checking and updated German translation (by niiconn)
= 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
= 1.4.1 =
New "Passkey" terminology option, multisite support (beta), improved Passkey experience and more

View File

@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
throw new RuntimeException($err);
}
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
},
"require": {
"php": "^7.0 || ^8.0",
"php": "^7.1 || ^8.0",
"ext-simplexml": "*",
"ext-mbstring": "*",
"ext-ctype": "*",

View File

@ -42,7 +42,7 @@ abstract class Assert
* The assertion chain can be stateful, that means be careful when you reuse
* 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);
@ -55,7 +55,7 @@ abstract class Assert
* @param mixed $values
* @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();
}
@ -66,7 +66,7 @@ abstract class Assert
* @param mixed $value
* @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();
}

View File

@ -307,7 +307,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -331,7 +331,7 @@ class Assertion
*
* @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($value2, $message, $propertyPath);
@ -358,7 +358,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -382,7 +382,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -412,7 +412,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -434,7 +434,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -461,7 +461,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -488,7 +488,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -515,7 +515,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -537,7 +537,7 @@ class Assertion
*
* @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 (
\is_resource($value) ||
@ -577,7 +577,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -604,7 +604,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -631,7 +631,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -658,7 +658,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -683,7 +683,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -710,7 +710,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -737,7 +737,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -766,7 +766,7 @@ class Assertion
*
* @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);
@ -794,7 +794,7 @@ class Assertion
*
* @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);
@ -825,7 +825,7 @@ class Assertion
*
* @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);
@ -858,7 +858,7 @@ class Assertion
*
* @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);
@ -891,7 +891,7 @@ class Assertion
*
* @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);
@ -925,7 +925,7 @@ class Assertion
*
* @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::minLength($value, $minLength, $message, $propertyPath, $encoding);
@ -949,7 +949,7 @@ class Assertion
*
* @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);
@ -981,7 +981,7 @@ class Assertion
*
* @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);
@ -1015,7 +1015,7 @@ class Assertion
*
* @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);
@ -1047,7 +1047,7 @@ class Assertion
*
* @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);
@ -1072,7 +1072,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1097,7 +1097,7 @@ class Assertion
*
* @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);
}
@ -1115,7 +1115,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1140,7 +1140,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1167,7 +1167,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1194,7 +1194,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -1216,7 +1216,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -1233,7 +1233,7 @@ class Assertion
/**
* Assert that value is countable.
*
* @param array|Countable|ResourceBundle|SimpleXMLElement $value
* @param mixed $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
@ -1243,7 +1243,7 @@ class Assertion
*
* @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')) {
$assert = \is_countable($value);
@ -1272,7 +1272,7 @@ class Assertion
*
* @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);
@ -1297,7 +1297,7 @@ class Assertion
*
* @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);
@ -1321,7 +1321,7 @@ class Assertion
*
* @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) {
if (\array_search($value, $values, true) !== $key) {
@ -1346,7 +1346,7 @@ class Assertion
*
* @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);
@ -1371,7 +1371,7 @@ class Assertion
*
* @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::notEmpty($value[$key], $message, $propertyPath);
@ -1387,7 +1387,7 @@ class Assertion
*
* @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))) {
$message = \sprintf(
@ -1417,7 +1417,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1448,7 +1448,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -1472,7 +1472,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1502,7 +1502,7 @@ class Assertion
*
* @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);
@ -1534,7 +1534,7 @@ class Assertion
*
* @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);
@ -1565,7 +1565,7 @@ class Assertion
*
* @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);
@ -1590,7 +1590,7 @@ class Assertion
*
* @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::notEmpty($value, $message, $propertyPath);
@ -1615,7 +1615,7 @@ class Assertion
*
* @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);
@ -1639,7 +1639,7 @@ class Assertion
*
* @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);
@ -1663,7 +1663,7 @@ class Assertion
*
* @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);
@ -1692,7 +1692,7 @@ class Assertion
*
* @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);
@ -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/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);
@ -1772,7 +1772,7 @@ class Assertion
*
* @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 {
static::regex($value, '(^([a-zA-Z]{1}[a-zA-Z0-9]*)$)', $message, $propertyPath);
@ -1801,7 +1801,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -1828,7 +1828,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -1855,7 +1855,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1882,7 +1882,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -1905,7 +1905,7 @@ class Assertion
*
* @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 {
$reflection = new ReflectionClass($class);
@ -1948,7 +1948,7 @@ class Assertion
*
* @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()) {
$message = \sprintf(
@ -1972,7 +1972,7 @@ class Assertion
*
* @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);
@ -2002,7 +2002,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2028,7 +2028,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2052,7 +2052,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2076,7 +2076,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2147,7 +2147,7 @@ class Assertion
*
* @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);
@ -2167,7 +2167,7 @@ class Assertion
*
* @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);
@ -2196,7 +2196,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2219,7 +2219,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -2243,7 +2243,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -2267,7 +2267,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -2291,7 +2291,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -2317,7 +2317,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -2344,7 +2344,7 @@ class Assertion
*
* @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) {
$message = \sprintf(
@ -2368,7 +2368,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2394,7 +2394,7 @@ class Assertion
*
* @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($format, $message, $propertyPath);
@ -2422,7 +2422,7 @@ class Assertion
*
* @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)) {
static::classExists($value, $message, $propertyPath);
@ -2440,7 +2440,7 @@ class Assertion
*
* @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);
@ -2465,7 +2465,7 @@ class Assertion
*
* @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::allString($properties, $message, $propertyPath);
@ -2500,7 +2500,7 @@ class Assertion
*
* @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.');
@ -2527,7 +2527,7 @@ class Assertion
*
* @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');
@ -2544,7 +2544,7 @@ class Assertion
*
* @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);
@ -2564,7 +2564,7 @@ class Assertion
*
* @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)) {
$message = \sprintf(
@ -2589,7 +2589,7 @@ class Assertion
*
* @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);
@ -2617,7 +2617,7 @@ class Assertion
*
* @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);
if ($flag === null) {
@ -2648,7 +2648,7 @@ class Assertion
*
* @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);
@ -2667,7 +2667,7 @@ class Assertion
*
* @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);
@ -2680,7 +2680,7 @@ class Assertion
* @param mixed $constant
* @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)) {
$message = \sprintf(static::generateMessage($message ?: 'Value "%s" expected to be a defined constant.'), $constant);
@ -2699,7 +2699,7 @@ class Assertion
*
* @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)) {
$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 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->defaultMessage = $defaultMessage;

View File

@ -31,7 +31,7 @@ class InvalidArgumentException extends \InvalidArgumentException implements Asse
*/
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);

View File

@ -133,7 +133,7 @@ class LazyAssertion
*
* @return static
*/
public function that($value, string $propertyPath = null, $defaultMessage = null)
public function that($value, ?string $propertyPath = null, $defaultMessage = null)
{
$this->currentChainFailed = 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
* 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);
}
@ -44,7 +44,7 @@ function that($value, $defaultMessage = null, string $defaultPropertyPath = null
* @param string|callable|null $defaultMessage
* @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);
}
@ -58,7 +58,7 @@ function thatAll($values, $defaultMessage = null, string $defaultPropertyPath =
*
* @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);
}

View File

@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/
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
* @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;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@ -309,6 +320,24 @@ class InstalledVersions
{
self::$installed = $data;
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();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} 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 */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
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;
}

View File

@ -16,12 +16,12 @@ return array(
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
'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'),
'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'),
'Nyholm\\Psr7Server\\' => array($vendorDir . '/nyholm/psr7-server/src'),
'League\\Uri\\' => array($vendorDir . '/league/uri-interfaces/src', $vendorDir . '/league/uri/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'),
'League\\Uri\\' => array($vendorDir . '/league/uri/src', $vendorDir . '/league/uri-interfaces/src'),
'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\\KeyManagement\\' => array($vendorDir . '/web-token/jwt-key-mgmt'),
'Jose\\Component\\Core\\' => array($vendorDir . '/web-token/jwt-core'),

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ class PhpToken implements \Stringable
public $text;
/**
* @var int
* @var -1|positive-int
*/
public $line;
@ -38,6 +38,9 @@ class PhpToken implements \Stringable
*/
public $pos;
/**
* @param -1|positive-int $line
*/
public function __construct(int $id, string $text, int $line = -1, int $position = -1)
{
$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
{

View File

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

View File

@ -19,7 +19,15 @@ namespace Symfony\Component\Process;
*/
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.
@ -48,39 +56,48 @@ class ExecutableFinder
*/
public function find(string $name, ?string $default = null, array $extraDirs = [])
{
if (\ini_get('open_basedir')) {
$searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs);
$dirs = [];
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
);
// windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes
if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) {
return $name;
}
$suffixes = [''];
$dirs = array_merge(
explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
$extraDirs
);
$suffixes = [];
if ('\\' === \DIRECTORY_SEPARATOR) {
$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 ($dirs as $dir) {
if ('' === $dir) {
$dir = '.';
}
if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($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;
}
}

View File

@ -34,15 +34,8 @@ class PhpExecutableFinder
public function find(bool $includeArgs = true)
{
if ($php = getenv('PHP_BINARY')) {
if (!is_executable($php)) {
$command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --';
if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) {
if (!is_executable($php)) {
return false;
}
} else {
return false;
}
if (!is_executable($php) && !$php = $this->executableFinder->find($php)) {
return false;
}
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);
if (!\is_resource($this->process)) {
if (!$this->process) {
throw new RuntimeException('Unable to launch a new process.');
}
$this->status = self::STATUS_STARTED;
@ -1456,8 +1456,9 @@ class Process implements \IteratorAggregate
private function close(): int
{
$this->processPipes->close();
if (\is_resource($this->process)) {
if ($this->process) {
proc_close($this->process);
$this->process = null;
}
$this->exitcode = $this->processInformation['exitcode'];
$this->status = self::STATUS_TERMINATED;
@ -1591,7 +1592,14 @@ class Process implements \IteratorAggregate
$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) {
$cmd .= ' '.$offset.'>"'.$filename.'"';
}
@ -1637,7 +1645,7 @@ class Process implements \IteratorAggregate
if (str_contains($argument, "\0")) {
$argument = str_replace("\0", '?', $argument);
}
if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
if (!preg_match('/[()%!^"<>&|\s]/', $argument)) {
return $argument;
}
$argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);

View File

@ -3,12 +3,13 @@
Plugin Name: WP-WebAuthn
Plugin URI: https://flyhigher.top
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 URI: https://axton.cc
License: GPLv3
Text Domain: wp-webauthn
Domain Path: /languages
Network: true
*/
/* 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.
@ -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
*/
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_uninstall_hook(__FILE__, 'wwa_uninstall');
function wwa_init(){
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{
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(){
if(!get_option('wwa_init')){
// Init
wwa_create_table();
$site_domain = wp_parse_url(site_url(), PHP_URL_HOST);
$wwa_init_options = array(
'user_credentials' => '{}',
'user_credentials_meta' => '{}',
'user_id' => array(),
'first_choice' => 'true',
'website_name' => get_bloginfo('name'),
'website_domain' => $site_domain === NULL ? "" : $site_domain,
'remember_me' => 'false',
'email_login' => 'false',
'user_verification' => 'false',
'usernameless_login' => 'false',
'allow_authenticator_type' => 'none',
'password_reset' => 'off',
'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);
include('wwa-version.php');
update_option('wwa_version', $wwa_version);
update_option('wwa_log', array());
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{
include('wwa-version.php');
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
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');
if(isset($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){
$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[$option_name] = $option_value;
update_option('wwa_options',$options);
@ -83,3 +327,19 @@ include('wwa-menus.php');
include('wwa-functions.php');
include('wwa-ajax.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
if (!defined('ABSPATH')) {
exit;
}
// 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(
'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')
@ -25,7 +30,7 @@ if(!function_exists('sodium_crypto_sign_detached')){
$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')){
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;
}
// Only admin can change settings
@ -33,17 +38,20 @@ if(
(isset($_POST['wwa_ref']) && $_POST['wwa_ref'] === 'true')
&& check_admin_referer('wwa_options_update')
&& 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['email_login']) && ($_POST['email_login'] === 'true' || $_POST['email_login'] === 'false'))
&& (isset($_POST['user_verification']) && ($_POST['user_verification'] === 'true' || $_POST['user_verification'] === 'false'))
&& (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['user_verification']) && ($_POST['user_verification'] === 'true' || $_POST['user_verification'] === 'false')))
&& (is_multisite() || (isset($_POST['usernameless_login']) && ($_POST['usernameless_login'] === 'true' || $_POST['usernameless_login'] === 'false')))
&& (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['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['website_name'])
&& isset($_POST['website_domain'])
// && (is_multisite() || isset($_POST['ror_origins']))
){
$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, '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_update_option('logging', $post_logging);
$post_first_choice = sanitize_text_field(wp_unslash($_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.'"');
if(!is_multisite()){
$post_first_choice = sanitize_text_field(wp_unslash($_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']));
if($post_website_name !== wwa_get_option('website_name')){
@ -86,6 +100,31 @@ if(
}
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']));
if($post_remember_me !== wwa_get_option('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);
$post_user_verification = sanitize_text_field(wp_unslash($_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);
if(!is_multisite()){
$post_user_verification = sanitize_text_field(wp_unslash($_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);
$post_allow_authenticator_type = sanitize_text_field(wp_unslash($_POST['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_update_option('allow_authenticator_type', $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')){
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);
$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.'"');
$post_show_authenticator_type = sanitize_text_field(wp_unslash($_POST['show_authenticator_type']));
if($post_show_authenticator_type !== wwa_get_option('show_authenticator_type')){
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']));
if($post_password_reset !== wwa_get_option('password_reset')){
@ -128,13 +175,35 @@ if(
}
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');
}elseif((isset($_POST['wwa_ref']) && $_POST['wwa_ref'] === 'true')){
add_settings_error('wwa_settings', 'save_error', __('Settings NOT saved.', 'wp-webauthn'));
}
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
if(wwa_validate_privileges()){ ?>
@ -144,37 +213,73 @@ wp_nonce_field('wwa_options_update');
?>
<input type="hidden" name="wwa_ref" value="true">
<table class="form-table">
<?php if(!is_multisite()){ ?>
<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>
<?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 === 'true' || !$wwa_not_allowed){?> selected<?php }?>><?php _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="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="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 _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>
</tr>
<?php } ?>
<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>
<input required name="website_name" type="text" id="website_name" value="<?php echo wwa_get_option('website_name');?>" class="regular-text">
<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>
</td>
</tr>
<tr>
<th scope="row"><label for="website_domain"><?php _e('Website domain', 'wp-webauthn');?></label></th>
<td>
<input required name="website_domain" type="text" id="website_domain" value="<?php echo wwa_get_option('website_domain');?>" class="regular-text">
<p class="description"><?php _e('This field <strong>MUST</strong> be exactly the same with the current domain or parent domain.', 'wp-webauthn');?></p>
<?php $wwa_v_t=wwa_get_option('terminology');
if($wwa_v_t === false){
wwa_update_option('terminology', 'webauthn');
$wwa_v_t = 'webauthn';
}
?>
<fieldset>
<label><input type="radio" name="terminology" value="webauthn" <?php if($wwa_v_t === 'webauthn'){?>checked="checked"<?php }?>> WebAuthn</label><br>
<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>
</tr>
<tr>
<th scope="row"></th>
</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>
<?php $wwa_v_rm=wwa_get_option('remember_me');
if($wwa_v_rm === false){
@ -183,14 +288,14 @@ if($wwa_v_rm === false){
}
?>
<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="false" <?php if($wwa_v_rm === 'false'){?>checked="checked"<?php }?>> <?php _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>
<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 esc_html_e("Disable", "wp-webauthn");?></label><br>
<p class="description"><?php esc_html_e('Show the \'Remember Me\' checkbox beside the login form when using WebAuthn.', 'wp-webauthn');?></p>
</fieldset>
</td>
</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>
<?php $wwa_v_el=wwa_get_option('email_login');
if($wwa_v_el === false){
@ -199,25 +304,26 @@ if($wwa_v_el === false){
}
?>
<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="false" <?php if($wwa_v_el === 'false'){?>checked="checked"<?php }?>> <?php _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>
<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 esc_html_e("Disable", "wp-webauthn");?></label><br>
<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>
</td>
</tr>
<?php if(!is_multisite()){ ?>
<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>
<?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 _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>
<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>
<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 _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>
<?php $wwa_v_ul=wwa_get_option('usernameless_login');
if($wwa_v_ul === false){
@ -226,14 +332,14 @@ if($wwa_v_ul === false){
}
?>
<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="false" <?php if($wwa_v_ul === 'false'){?>checked="checked"<?php }?>> <?php _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>
<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 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 _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>
<?php $wwa_v_at=wwa_get_option('allow_authenticator_type');
if($wwa_v_at === false){
@ -242,18 +348,35 @@ if($wwa_v_at === false){
}
?>
<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="platform"<?php if($wwa_v_at === 'platform'){?> selected<?php }?>><?php _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="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 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 _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>
</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>
</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>
<?php $wwa_v_pr=wwa_get_option('password_reset');
if($wwa_v_pr === false){
@ -262,37 +385,53 @@ if($wwa_v_pr === false){
}
?>
<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="admin"<?php if($wwa_v_pr === 'admin'){?> selected<?php }?>><?php _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="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 esc_html_e('Everyone except administrators', 'wp-webauthn');?></option>
<option value="all"<?php if($wwa_v_pr === 'all'){?> selected<?php }?>><?php esc_html_e('Everyone', 'wp-webauthn');?></option>
</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>
</tr>
<tr>
<th scope="row"></th>
</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>
<?php $wwa_v_aur=wwa_get_option('after_user_registration');
if($wwa_v_aur === false){
wwa_update_option('after_user_registration', '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">
<option value="none"<?php if($wwa_v_aur === 'none'){?> selected<?php }?>><?php _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="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 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>
<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>
</tr>
<tr>
<th scope="row"></th>
</tr>
<?php do_action('wwa_admin_page_extra'); ?>
<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>
<?php $wwa_v_log=wwa_get_option('logging');
if($wwa_v_log === false){
@ -301,12 +440,12 @@ if($wwa_v_log === false){
}
?>
<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="false" <?php if($wwa_v_log === 'false'){?>checked="checked"<?php }?>> <?php _e("Disable", "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 esc_html_e("Disable", "wp-webauthn");?></label><br>
<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 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>
</td>
</tr>
@ -315,12 +454,12 @@ if($wwa_v_log === false){
if(wwa_get_option('logging') === 'true' || ($log !== false && count($log) > 0)){
?>
<div<?php if(wwa_get_option('logging') !== 'true'){?> id="wwa-remove-log"<?php }?>>
<h2><?php _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>
<p class="description"><?php _e('Automatic update every 5 seconds.', 'wp-webauthn');?></p>
<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 ? "" : esc_textarea(implode("\n", get_option("wwa_log")));?></textarea>
<p class="description"><?php esc_html_e('Automatic update every 5 seconds.', 'wp-webauthn');?></p>
<br>
</div>
<?php }}
/* 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>

View File

@ -1,4 +1,8 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
require_once('wp-webauthn-vendor/autoload.php');
use Webauthn\Server;
use Webauthn\PublicKeyCredentialRpEntity;
@ -7,169 +11,257 @@ use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialDescriptor;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
/**
* Store all publickeys and pubilckey metas
*/
class PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepositoryInterface {
// Get one credential by credential ID
private $registration_context = null;
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource {
$data = $this->read();
if(isset($data[base64_encode($publicKeyCredentialId)])){
return PublicKeyCredentialSource::createFromArray($data[base64_encode($publicKeyCredentialId)]);
global $wpdb;
$key = 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;
}
// Get one credential's meta by credential ID
public function findOneMetaByCredentialId(string $publicKeyCredentialId): ?array {
$meta = json_decode(wwa_get_option("user_credentials_meta"), true);
if(isset($meta[base64_encode($publicKeyCredentialId)])){
return $meta[base64_encode($publicKeyCredentialId)];
global $wpdb;
$key = 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;
}
// Get all credentials of one user
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
public function findAllForUserEntityByUserId(int $wp_user_id): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT credential_source FROM {$wpdb->wwa_credentials} WHERE user_id = %d",
$wp_user_id
));
$sources = [];
foreach($this->read() as $data){
$source = PublicKeyCredentialSource::createFromArray($data);
if($source->getUserHandle() === $publicKeyCredentialUserEntity->getId()){
$sources[] = $source;
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 $sources;
}
public function findCredentialsForUserEntityByType(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity, string $credentialType): array {
$credentialsForUserEntity = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
$credentialsByType = [];
foreach($credentialsForUserEntity as $credential){
if($this->findOneMetaByCredentialId($credential->getPublicKeyCredentialId())["authenticator_type"] === $credentialType){
$credentialsByType[] = $credential;
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
global $wpdb;
$handle = $publicKeyCredentialUserEntity->getId();
$wp_user_id = $wpdb->get_var($wpdb->prepare(
"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 saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, bool $usernameless = false): void {
$data = $this->read();
$data_key = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
$data[$data_key] = $publicKeyCredentialSource;
$this->write($data, $data_key, $usernameless);
}
// Update credential's last used
public function updateCredentialLastUsed(string $publicKeyCredentialId): void {
$credential = $this->findOneMetaByCredentialId($publicKeyCredentialId);
if($credential !== null){
$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);
$meta[base64_encode($publicKeyCredentialId)] = $credential;
wwa_update_option("user_credentials_meta", wp_json_encode($meta));
}
}
// 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"] : "-"
));
public function findCredentialsForUserEntityByType(int $wp_user_id, string $credentialType): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT credential_source FROM {$wpdb->wwa_credentials}
WHERE user_id = %d AND authenticator_type = %s",
$wp_user_id, $credentialType
));
$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 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 modifyAuthenticator(string $id, string $name, PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity, string $action, string $res_id): string {
$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.";
public function setRegistrationContext(int $user_id, string $name, string $type, bool $usernameless = false): void {
$this->registration_context = compact('user_id', 'name', 'type', 'usernameless');
}
// Rename a credential from database by credential ID
private function renameCredential(string $id, string $name, string $res_id): void {
$meta = json_decode(wwa_get_option("user_credentials_meta"), true);
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));
}
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void {
global $wpdb;
$cred_id = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
// Remove a credential from database by credential ID
private function removeCredential(string $id, string $res_id): void {
$data = $this->read();
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));
}
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->wwa_credentials} WHERE credential_id = %s",
$cred_id
));
// Read credential database
private function read(): array {
if(wwa_get_option("user_credentials") !== NULL){
try{
return json_decode(wwa_get_option("user_credentials"), true);
}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" => "-"
if($exists > 0){
$wpdb->update(
$wpdb->wwa_credentials,
array('credential_source' => wp_json_encode($publicKeyCredentialSource)),
array('credential_id' => $cred_id)
);
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
function wwa_ajax_create(){
check_ajax_referer('wwa_ajax');
$client_id = false;
try{
$res_id = wwa_generate_random_string(5);
$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."\"");
// Get user ID or create one
$user_key = "";
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
wwa_add_log($res_id, "ajax_create: User not initialized, initialize");
$user_array = wwa_get_option("user_id");
$user_key = hash("sha256", $user_info->user_login."-".$user_info->display_name."-".wwa_generate_random_string(10));
$user_array[$user_info->user_login] = $user_key;
wwa_update_option("user_id", $user_array);
}else{
$user_key = wwa_get_option("user_id")[$user_info->user_login];
$user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!$user_key){
$user_id_map = wwa_get_option("user_id");
if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = $user_id_map[$user_info->user_login];
update_user_meta($user_info->ID, 'wwa_user_handle', $user_key);
}else{
wwa_add_log($res_id, "ajax_create: User not initialized, initialize");
$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(
@ -287,7 +381,7 @@ function wwa_ajax_create(){
$credentialSourceRepository = new PublicKeyCredentialSourceRepository();
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
$credentialSources = $credentialSourceRepository->findAllForUserEntityByUserId($user_info->ID);
// Convert the Credential Sources into Public Key Credential Descriptors for excluding
$excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) {
@ -363,6 +457,7 @@ add_action("wp_ajax_wwa_create" , "wwa_ajax_create");
// Verify the attestation
function wwa_ajax_create_response(){
check_ajax_referer('wwa_ajax');
$client_id = false;
try{
$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["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: 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"])){
@ -487,13 +582,26 @@ function wwa_ajax_create_response(){
try {
$publicKeyCredentialSource = $server->loadAndCheckAttestationResponse(
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
);
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"]){
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
function wwa_ajax_auth_start(){
$client_id = false;
try{
$res_id = wwa_generate_random_string(5);
$client_id = strval(time()).wwa_generate_random_string(24);
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
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\"");
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
wwa_add_log($res_id, "ajax_auth: (ERROR)User not initialized, exit");
wwa_wp_die("User not inited.", $client_id);
}else{
$user_key = wwa_get_option("user_id")[$user_info->user_login];
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
$user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!$user_key){
$user_id_map = wwa_get_option("user_id");
if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = $user_id_map[$user_info->user_login];
}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{
if(wwa_get_option("usernameless_login") === "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_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."\"");
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
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{
$user_key = wwa_get_option("user_id")[$user_info->user_login];
$user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!$user_key){
$user_id_map = wwa_get_option("user_id");
if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = $user_id_map[$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{
$user_info = new stdClass();
@ -681,15 +799,40 @@ function wwa_ajax_auth_start(){
// Usernameless authentication, return empty allowed credentials list
wwa_add_log($res_id, "ajax_auth: Usernameless authentication, allowedCredentials => []");
$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{
// Get the list of authenticators associated to the user
// $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
$allow_authenticator_type = wwa_get_option("allow_authenticator_type");
if($allow_authenticator_type === false || $allow_authenticator_type === "none"){
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
}elseif($allow_authenticator_type !== false && $allow_authenticator_type !== "none"){
$credentialSources = $credentialSourceRepository->findAllForUserEntityByUserId($user_info->ID);
}else{
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
@ -768,7 +911,8 @@ function wwa_ajax_auth(){
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"])){
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);
}
$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){
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])){
wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not initialized, exit");
wwa_wp_die("User not inited.", $client_id);
}else{
$user_key = wwa_get_option("user_id")[$user_info->user_login];
$user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
$user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!$user_key){
$user_id_map = wwa_get_option("user_id");
if(is_array($user_id_map) && isset($user_id_map[$user_info->user_login])){
$user_key = $user_id_map[$user_info->user_login];
}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(
$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: 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"]));
@ -911,59 +1059,66 @@ function wwa_ajax_auth(){
$allow_authenticator_type = wwa_get_option("allow_authenticator_type");
if($allow_authenticator_type !== false && $allow_authenticator_type !== 'none'){
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);
}
}
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
$all_user = wwa_get_option("user_id");
$user_login_name = false;
foreach($all_user as $user => $user_id){
if($user_id === $credential_meta["user"]){
$user_login_name = $user;
break;
}
}
$resolved_user_handle = null;
$resolved_user_info = null;
// Match userHandle
if($credential_meta["user"] === base64_decode($data_array["response"]["userHandle"])){
// Found user
if($user_login_name !== false){
wwa_add_log($res_id, "ajax_auth_response: Found user => \"".$user_login_name."\", user_key => \"".$credential_meta["user"]."\"");
// Testing, verify user
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)Credential found, but user not match, exit");
wwa_wp_die("Bad request.", $client_id);
if($cred_row !== null){
$resolved_user_handle = $cred_row->user_handle;
$resolved_user_info = get_user_by('id', $cred_row->user_id);
}elseif(!get_option('wwa_credentials_migrated')){
wwa_add_log($res_id, "ajax_auth_response: Credential not in global table, trying pre-migration fallback");
$old_handle = $credential_meta["user"];
$all_user = wwa_get_option("user_id");
if(is_array($all_user)){
foreach($all_user as $login => $handle){
if($handle === $old_handle){
$resolved_user_info = get_user_by('login', $login);
$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);
}
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{
wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but usernameless => \"false\", exit");
wwa_wp_die("Bad request.", $client_id);
@ -974,12 +1129,14 @@ function wwa_ajax_auth(){
}
}else{
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"])));
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"]){
$rpEntity = new PublicKeyCredentialRpEntity(
@ -1004,7 +1161,11 @@ function wwa_ajax_auth(){
try {
$server->loadAndCheckAssertionResponse(
$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,
$serverRequest
);
@ -1084,6 +1245,7 @@ add_action("wp_ajax_nopriv_wwa_auth" , "wwa_ajax_auth");
// Get authenticator list
function wwa_ajax_authenticator_list(){
check_ajax_referer('wwa_ajax');
$res_id = wwa_generate_random_string(5);
wwa_init_new_options();
@ -1118,30 +1280,21 @@ function wwa_ajax_authenticator_list(){
header('Content-Type: application/json');
$user_key = "";
if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
// The user haven't bound any authenticator, return empty list
$user_key = get_user_meta($user_info->ID, 'wwa_user_handle', true);
if(!$user_key){
echo "[]";
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();
echo wp_json_encode($publicKeyCredentialSourceRepository->getShowList($userEntity));
echo wp_json_encode($publicKeyCredentialSourceRepository->getShowListByUserId($user_info->ID));
exit;
}
add_action("wp_ajax_wwa_authenticator_list" , "wwa_ajax_authenticator_list");
// Modify an authenticator
function wwa_ajax_modify_authenticator(){
check_ajax_referer('wwa_ajax');
try{
$res_id = wwa_generate_random_string(5);
@ -1192,30 +1345,25 @@ function wwa_ajax_modify_authenticator(){
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."\"");
$publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
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"){
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;
}catch(\Exception $exception){
@ -1234,6 +1382,7 @@ add_action("wp_ajax_wwa_modify_authenticator" , "wwa_ajax_modify_authenticator")
// Print log
function wwa_ajax_get_log(){
check_ajax_referer('wwa_admin_ajax');
if(!wwa_validate_privileges()){
wwa_wp_die("Bad Request.");
}
@ -1254,6 +1403,7 @@ add_action("wp_ajax_wwa_get_log" , "wwa_ajax_get_log");
// Clear log
function wwa_ajax_clear_log(){
check_ajax_referer('wwa_admin_ajax');
if(!wwa_validate_privileges()){
wwa_wp_die("Bad Request.");
}

View File

@ -1,5 +1,61 @@
<?php
// Two Factor
if(has_action('wp_login', array('Two_Factor_Core', 'wp_login')) !== false){
remove_action('wp_login', array('Two_Factor_Core', 'wp_login'), 10, 2);
if (!defined('ABSPATH')) {
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
if (!defined('ABSPATH')) {
exit;
}
// WordPress transient adapter
function wwa_set_temp_val($name, $value, $client_id){
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){
$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){
@ -37,6 +41,10 @@ function wwa_init_new_options(){
if(wwa_get_option('allow_authenticator_type') === false){
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){
wwa_update_option('remember_me', 'false');
}
@ -52,6 +60,12 @@ function wwa_init_new_options(){
if(wwa_get_option('after_user_registration') === false){
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
@ -80,7 +94,7 @@ function wwa_add_log($id, $content = '', $init = false){
if($log === false){
$log = array();
}
$log[] = '['.current_time('mysql').']['.$id.'] '.$content;
$log[] = '['.current_time('mysql').']['.$id.'] '.wp_strip_all_tags($content);
update_option('wwa_log', $log);
}
@ -104,49 +118,72 @@ function wwa_generate_call_trace($exception = false){
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){
$res_id = wwa_generate_random_string(5);
$user_data = get_userdata($user_id);
$all_user_meta = wwa_get_option('user_id');
$user_key = '';
// 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]);
}
if($user_data !== false){
wwa_add_log($res_id, "Deleted user credentials for => \"".$user_data->user_login."\"");
}
// Delete credentials
$all_credentials_meta = json_decode(wwa_get_option('user_credentials_meta'), true);
$all_credentials = json_decode(wwa_get_option('user_credentials'), true);
foreach($all_credentials_meta as $credential => $meta){
if($user_key === $meta['user']){
wwa_add_log($res_id, "Delete credential => \"".$credential."\"");
unset($all_credentials_meta[$credential]);
unset($all_credentials[$credential]);
}
if(is_multisite()){
wwa_cleanup_blog_credentials($user_id, get_current_blog_id());
}else{
wwa_cleanup_all_user_credentials($user_id);
}
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');
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
function wwa_login_js(){
wwa_init_new_options();
$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')){
$wwa_not_allowed = 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');
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'),
'admin_url' => admin_url(),
'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',
'password_reset' => ((wwa_get_option('password_reset') === false || wwa_get_option('password_reset') === 'off') ? 'false' : 'true'),
'separator' => apply_filters('login_link_separator', ' | '),
'terminology' => (wwa_get_option('terminology') === false ? 'passkey' : wwa_get_option('terminology')),
'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_4' => __('Please proceed...', 'wp-webauthn'),
'i18n_5' => __('Authenticating...', 'wp-webauthn'),
@ -167,7 +205,9 @@ function wwa_login_js(){
'i18n_9' => __('Username', 'wp-webauthn'),
'i18n_10' => __('Username or Email Address'),
'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'){
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)){
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 $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');
}
// Show a notice in admin pages
function wwa_no_authenticator_warning(){
if(is_network_admin()){
return;
}
$user_info = wp_get_current_user();
$first_choice = wwa_get_option('first_choice');
$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;
}
if($check_self){
// Check current user
$user_id = '';
$show_notice_flag = false;
if(!isset(wwa_get_option('user_id')[$user_info->user_login])){
$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){?>
global $wpdb;
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->wwa_credentials}
WHERE user_id = %d AND registered_blog_id = %d",
$user_info->ID, get_current_blog_id()
));
if(intval($count) === 0){ ?>
<div class="notice notice-warning">
<?php /* translators: %s: 'the site' or 'your account', and admin profile url */ ?>
<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>
<?php
$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>
<?php }
}
// Check other user
global $pagenow;
if($pagenow == 'user-edit.php' && isset($_GET['user_id']) && intval($_GET['user_id']) !== $user_info->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;
}
if(!current_user_can('edit_user', $user_id_wp)){
$other_user = get_user_by('id', $user_id_wp);
if($other_user === false){
return;
}
$user_info = get_user_by('id', $user_id_wp);
if($user_info === false){
if($first_choice !== 'webauthn' && get_user_meta($other_user->ID, 'wwa_webauthn_only', true) !== 'true'){
return;
}
if($first_choice !== 'webauthn' && get_the_author_meta('webauthn_only', $user_info->ID) !== 'true'){
return;
}
$user_id = '';
$show_notice_flag = false;
if(!isset(wwa_get_option('user_id')[$user_info->user_login])){
$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){ ?>
global $wpdb;
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->wwa_credentials}
WHERE user_id = %d AND registered_blog_id = %d",
$other_user->ID, get_current_blog_id()
));
if(intval($count) === 0){ ?>
<div class="notice notice-warning">
<?php /* translators: %s: 'the site' or 'your account' */ ?>
<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>
<?php
$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>
<?php }
}
@ -353,17 +380,27 @@ function wwa_settings_link($links_array, $plugin_file_name){
}
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){
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="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;
}
add_filter('plugin_row_meta', 'wwa_meta_link', 10, 2);
// Check if we are under HTTPS
function wwa_check_ssl() {
function wwa_check_ssl(){
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && $_SERVER['HTTPS'] !== '') {
return true;
}
@ -380,17 +417,37 @@ function wwa_check_ssl() {
}
// Check user privileges
function wwa_validate_privileges() {
$user = wp_get_current_user();
$allowed_roles = array('administrator');
if(array_intersect($allowed_roles, $user->roles)){
return true;
function wwa_validate_privileges(){
return current_user_can('manage_options');
}
// 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
function wwa_get_user($username) {
function wwa_get_user($username){
if(wwa_get_option('email_login') !== 'true'){
return get_user_by('login', $username);
}else{
@ -400,3 +457,54 @@ function wwa_get_user($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
if (!defined('ABSPATH')) {
exit;
}
// Add 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(){
include('wwa-admin-content.php');
@ -28,11 +32,11 @@ function wwa_save_user_profile_fields($user_id){
}
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'){
update_user_meta($user_id, 'webauthn_only', 'true');
update_user_meta($user_id, 'wwa_webauthn_only', 'true');
}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');
@ -48,3 +52,12 @@ function 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
if (!defined('ABSPATH')) {
exit;
}
$wwa_term = wwa_get_option('terminology') === 'webauthn';
// 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(
'ajax_url' => admin_url('admin-ajax.php'),
'_ajax_nonce' => wp_create_nonce('wwa_ajax'),
'user_id' => $user->ID,
'i18n_1' => __('Initializing...', '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_14' => __('Verifying...', 'wp-webauthn'),
'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_18' => __('Confirm removal of authenticator: ', '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_26' => __(' (Unavailable)', '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_30' => __('The site administrator only allow platform 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_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>
<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
if(isset($_GET['wwa_registered']) && $_GET['wwa_registered'] === 'true'){
$count = 0;
if(user_can($user, 'read')){
$user_ids = wwa_get_option("user_id");
if(isset($user_ids[$user->user_login])){
$user_id = $user_ids[$user->user_login];
$count = 0;
$data = json_decode(wwa_get_option("user_credentials_meta"), true);
foreach($data as $key => $value){
if($user_id === $value["user"]){
$count++;
break;
}
}
}
global $wpdb;
$count = intval($wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->wwa_credentials} WHERE user_id = %d AND registered_blog_id = %d",
$user->ID, get_current_blog_id()
)));
}
if($count === 0){
?>
<div id="wp-webauthn-message-container">
<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>
<?php
@ -73,103 +82,112 @@ if(!function_exists("mb_substr") || !function_exists("gmp_intval") || !wwa_check
?>
<div id="wp-webauthn-error-container">
<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>
<?php } ?>
<table class="form-table">
<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>
<label for="webauthn_only">
<?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>
<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>
</tr>
</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">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Identifier', 'wp-webauthn');?></th>
<th><?php _e('Type', 'wp-webauthn');?></th>
<th><?php _ex('Registered', 'time', 'wp-webauthn');?></th>
<th><?php _e('Last used', 'wp-webauthn');?></th>
<th class="wwa-usernameless-th"><?php _e('Usernameless', 'wp-webauthn');?></th>
<th><?php _e('Action', 'wp-webauthn');?></th>
<th><?php esc_html_e('Identifier', '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 echo esc_html(_x('Registered', 'time', 'wp-webauthn'));?></th>
<th><?php esc_html_e('Last used', 'wp-webauthn');?></th>
<th class="wwa-usernameless-th"><?php esc_html_e('Usernameless', 'wp-webauthn');?></th>
<th><?php esc_html_e('Action', 'wp-webauthn');?></th>
</tr>
</thead>
<tbody id="wwa-authenticator-list">
<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>
</tbody>
<tfoot>
<tr>
<th><?php _e('Identifier', 'wp-webauthn');?></th>
<th><?php _e('Type', 'wp-webauthn');?></th>
<th><?php _ex('Registered', 'time', 'wp-webauthn');?></th>
<th><?php _e('Last used', 'wp-webauthn');?></th>
<th class="wwa-usernameless-th"><?php _e('Usernameless', 'wp-webauthn');?></th>
<th><?php _e('Action', 'wp-webauthn');?></th>
<th><?php esc_html_e('Identifier', '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 echo esc_html(_x('Registered', 'time', 'wp-webauthn'));?></th>
<th><?php esc_html_e('Last used', 'wp-webauthn');?></th>
<th class="wwa-usernameless-th"><?php esc_html_e('Usernameless', 'wp-webauthn');?></th>
<th><?php esc_html_e('Action', 'wp-webauthn');?></th>
</tr>
</tfoot>
</table>
</div>
<p id="wwa_usernameless_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">
<button class="button button-small wwa-cancel"><?php _e('Close');?></button>
<h2><?php _e('Register New Authenticator', 'wp-webauthn');?></h2>
<?php /* translators: %s: user login name */ ?>
<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>
<button class="button button-small wwa-cancel"><?php esc_html_e('Close');?></button>
<h2><?php $wwa_term ? esc_html_e('Register New Authenticator', 'wp-webauthn') : esc_html_e('Register New Passkey', 'wp-webauthn'); ?></h2>
<?php
$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">
<?php if(wwa_get_option('show_authenticator_type') !== 'false'){?>
<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>
<?php
$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">
<option value="none" id="type-none" class="sub-type"<?php if($allowed_type !== 'none'){echo ' disabled';}?>><?php _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="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>
<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 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 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 esc_html_e('Roaming (e.g. USB security keys)', 'wp-webauthn');?></option>
</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>
</tr>
<?php }?>
<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>
<input name="wwa_authenticator_name" type="text" id="wwa_authenticator_name" class="regular-text">
<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>
<input name="wwa_authenticator_name" type="text" id="wwa_authenticator_name" class="regular-text" form="wwa-registration">
<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>
</tr>
<?php if(wwa_get_option('usernameless_login') === "true"){?>
<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>
<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="false" checked="checked"> <?php _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>
<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" form="wwa-registration"> <?php esc_html_e("Disable", "wp-webauthn");?></label><br>
<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>
</td>
</tr>
<?php }?>
</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 id="wwa-verify-block" tabindex="-1">
<button class="button button-small wwa-cancel"><?php _e('Close');?></button>
<h2><?php _e('Verify Authenticator', 'wp-webauthn');?></h2>
<p class="description"><?php _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 class="button button-small wwa-cancel"><?php esc_html_e('Close');?></button>
<h2><?php $wwa_term ? esc_html_e('Verify Authenticator', 'wp-webauthn') : esc_html_e('Verify Passkey', 'wp-webauthn'); ?></h2>
<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 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"){?>
<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 }?>
</div>

View File

@ -1,14 +1,22 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
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_localize_script('wwa_frontend_js', 'wwa_php_vars', array(
'ajax_url' => admin_url('admin-ajax.php'),
'_ajax_nonce' => wp_create_nonce('wwa_ajax'),
'admin_url' => admin_url(),
'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')),
'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_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_4' => __('Please proceed...', 'wp-webauthn'),
'i18n_5' => __('Authenticating...', 'wp-webauthn'),
@ -49,16 +57,16 @@ function wwa_localize_frontend(){
// Login form
function wwa_login_form_shortcode($vals){
extract(shortcode_atts(
$atts = shortcode_atts(
array(
'traditional' => 'true',
'username' => '',
'auto_hide' => 'true',
'to' => ''
), $vals)
), $vals
);
if($auto_hide === "true" && current_user_can("read")){
if($atts['auto_hide'] === "true" && current_user_can("read")){
return '';
}
@ -70,24 +78,24 @@ function wwa_login_form_shortcode($vals){
$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 = '';
if($to !== ""){
$args['redirect'] = sanitize_url($to);
if($atts['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"].'">';
}
if($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>';
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>'.(wwa_get_option('terminology') === 'webauthn' ? __('Authenticate with WebAuthn', 'wp-webauthn') : __('Authenticate with a passkey', 'wp-webauthn')).'</span></a></div>';
}
$html_form .= '
<div class="wwa-login-form-webauthn">
<p class="wwa-login-username">
<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>
<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>
</div>
</div>';
@ -98,15 +106,15 @@ add_shortcode('wwa_login_form', 'wwa_login_form_shortcode');
// Register form
function wwa_register_form_shortcode($vals){
extract(shortcode_atts(
$atts = shortcode_atts(
array(
'display' => 'true'
), $vals)
), $vals
);
// If always display
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>';
}else{
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']);
$allowed_type = wwa_get_option('allow_authenticator_type') === false ? 'none' : wwa_get_option('allow_authenticator_type');
return '
<div class="wwa-register-form">
$show_type = wwa_get_option('show_authenticator_type') !== 'false';
$type_selector = $show_type ? '
<label for="wwa-authenticator-type">'.__('Type of authenticator', 'wp-webauthn').'</label>
<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="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>
</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>
<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>'.(
@ -140,15 +150,15 @@ add_shortcode('wwa_register_form', 'wwa_register_form_shortcode');
// Verify button
function wwa_verify_button_shortcode($vals){
extract(shortcode_atts(
$atts = shortcode_atts(
array(
'display' => 'true'
), $vals)
), $vals
);
// If always display
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>';
}else{
return '';
@ -166,23 +176,26 @@ add_shortcode('wwa_verify_button', 'wwa_verify_button_shortcode');
// Authenticator list
function wwa_list_shortcode($vals){
extract(shortcode_atts(
$atts = shortcode_atts(
array(
'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">';
$tbody = '<tr><td colspan="5">'.__('Loading...', 'wp-webauthn').'</td></tr>';
$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>';
$show_type = wwa_get_option('show_authenticator_type') !== 'false';
$type_th = $show_type ? '<th class="wwa-type-th">'.__('Type', 'wp-webauthn').'</th>' : '';
$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(!current_user_can("read")){
if($display === "true"){
if($atts['display'] === "true"){
// Load CSS
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{
return '';
}

View File

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