fix(profile): align account dialogs and contracts

This commit is contained in:
2026-10-06 01:09:04 +08:00
parent f646fa8573
commit 248912f610
17 changed files with 1339 additions and 832 deletions
+29 -31
View File
@@ -624,6 +624,8 @@
background: var(--main-bg-color, var(--surface-1, #fff));
}
.aksrt-settings-modal [hidden] { display: none !important; }
.aksrt-settings-modal .modal-colorful-header {
position: absolute;
top: 0;
@@ -643,54 +645,50 @@
cursor: pointer;
}
.aksrt-settings-modal form {
.aksrt-settings-modal > form {
position: relative;
z-index: 0;
margin-top: 120px;
padding: 18px 15px;
}
.aksrt-settings-modal__fields { padding: 0 2px; }
.aksrt-settings-modal .modal-field { position: relative; margin-bottom: 14px; }
.aksrt-settings-modal .modal-field label { display: block; margin-bottom: 6px; color: var(--muted-2-color, #777); font-size: 12px; font-weight: 400; }
.aksrt-settings-modal .modal-field input,
.aksrt-settings-modal .readonly-display {
display: block;
width: 100%;
min-height: 38px;
box-sizing: border-box;
padding: 8px 38px 8px 11px;
border: 1px solid var(--main-border-color, var(--color-line, rgba(50, 50, 50, .12)));
border-radius: var(--mini-radius, 4px);
background: var(--main-bg-color, var(--surface-1, #fff));
color: var(--color-ink, #333);
font-size: 13px;
outline: 0;
.aksrt-settings-modal__pane {
position: relative;
z-index: 0;
margin-top: 120px;
padding: 18px 15px;
}
.aksrt-settings-modal .readonly-display { padding-right: 11px; color: var(--muted-2-color, #777); word-break: break-all; }
.aksrt-settings-modal .modal-field input:focus { border-color: var(--focus-color, var(--color-primary, #2997f7)); box-shadow: 0 0 0 2px var(--focus-color-opacity1, rgba(41, 151, 247, .12)); }
.aksrt-settings-modal__pane form,
.aksrt-settings-modal .account-step form {
margin: 0;
padding: 0;
}
.aksrt-settings-modal .account-step-list {
position: relative;
z-index: 0;
margin-top: 120px;
padding: 18px 15px 0;
}
.aksrt-settings-modal .account-step-panels { position: relative; z-index: 0; }
.aksrt-settings-modal .account-step { display: none; padding: 0 15px 18px; }
.aksrt-settings-modal .account-step.is-active { display: block; }
.aksrt-settings-modal .account-step > p { margin: 0 0 12px; }
.aksrt-settings-modal .account-recovery-toggle { border: 0; background: transparent; cursor: pointer; }
.aksrt-settings-modal__fields { padding: 0 2px; }
.aksrt-settings-modal .toggle-pwd { position: absolute; right: 7px; bottom: 4px; width: 30px; height: 30px; padding: 0; border: 0; background: transparent; color: var(--muted-2-color, #999); cursor: pointer; }
.aksrt-settings-modal .toggle-pwd:hover { color: var(--color-ink, #333); }
.aksrt-settings-modal .code-input-group { display: flex; align-items: stretch; gap: 7px; }
.aksrt-settings-modal .code-input-group input { min-width: 0; padding-right: 11px; letter-spacing: 2px; }
.aksrt-settings-modal .code-send-btn { min-width: 112px; padding: 6px 10px; white-space: nowrap; }
.aksrt-settings-modal .code-send-btn.is-counting { pointer-events: none; opacity: .6; }
.aksrt-settings-modal .pwd-strength { height: 3px; margin-top: 6px; border-radius: 2px; background: var(--muted-border-color, #eee); overflow: hidden; }
.aksrt-settings-modal .pwd-strength__fill { width: 0; height: 100%; border-radius: inherit; transition: width .2s ease, background-color .2s ease; }
.aksrt-settings-modal .pwd-strength__fill.level-weak { width: 33%; background: var(--color-error, #ef4444); }
.aksrt-settings-modal .pwd-strength__fill.level-medium { width: 66%; background: var(--color-warning, #f59e0b); }
.aksrt-settings-modal .pwd-strength__fill.level-strong { width: 100%; background: var(--color-success, #10b981); }
.aksrt-settings-modal .pwd-strength__text { display: block; min-height: 16px; margin-top: 3px; color: var(--muted-2-color, #999); font-size: 11px; }
.aksrt-settings-modal .pwd-strength__text.level-weak { color: var(--color-error, #ef4444); }
.aksrt-settings-modal .pwd-strength__text.level-medium { color: var(--color-warning, #f59e0b); }
.aksrt-settings-modal .pwd-strength__text.level-strong { color: var(--color-success, #10b981); }
.aksrt-settings-modal .aksrt-captcha-wrap { margin: 12px 0 8px; }
@media (max-width: 768px) {
.aksrt-modal-runtime.aksrt-user-settings-modal.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog {
max-height: calc(100dvh - env(safe-area-inset-bottom));
}
.aksrt-settings-modal .code-input-group { gap: 6px; }
.aksrt-settings-modal .code-send-btn { min-width: 104px; }
}
+1 -1
View File
@@ -167,7 +167,7 @@
var submitLabel = button ? button.innerHTML : '';
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fa fa-spinner fa-spin mr6" aria-hidden="true"></i>正在提交…';
button.innerHTML = '<i class="fa fas fa-spinner fa-spin mr6" aria-hidden="true"></i>正在提交…';
}
toast('正在提交申请…', 'load', '', 'aksrt_identity_submit');
window.AksrtRequest.request(config.restBase, {
+319 -359
View File
@@ -718,7 +718,6 @@
var runtime = window.AksrtModalRuntime;
var template = document.querySelector('template[data-aksrt-settings-modal-template="' + kind + '"]');
if (!runtime || typeof runtime.open !== 'function' || !template) return;
var title = kind === 'password' ? '修改密码' : (kind === 'email' ? '更改邮箱' : '绑定手机');
runtime.open({
html: template.innerHTML,
kind: 'account-settings',
@@ -727,251 +726,348 @@
onContent: function (_, session) {
var shell = session && session.shell;
if (!shell) return;
shell.setAttribute('aria-label', title);
var dialog = shell.querySelector('[data-account-dialog]');
var title = dialog && dialog.querySelector('[data-account-password-title], .modal-colorful-header .text-center > div:last-child');
if (title && title.textContent.trim()) shell.setAttribute('aria-label', title.textContent.trim());
var firstInput = shell.querySelector('input:not([type="hidden"]), textarea, select');
if (firstInput) firstInput.focus();
bindModalFields(shell);
initBindPhone(shell);
bindDialog(shell);
},
});
}
function bindModalFields(scope) {
if (!scope) return;
scope.querySelectorAll('.toggle-pwd').forEach(function(btn) {
if (btn.dataset.aksrtSettingsInitialized === 'true') return;
btn.dataset.aksrtSettingsInitialized = 'true';
btn.addEventListener('click', function() {
var inp = scope.querySelector('#' + this.getAttribute('data-target'));
if (!inp) return;
inp.type = inp.type === 'password' ? 'text' : 'password';
this.classList.toggle('is-visible');
var icon = this.querySelector('i');
if (icon) {
icon.classList.toggle('fa-eye', inp.type === 'password');
icon.classList.toggle('fa-eye-slash', inp.type === 'text');
}
function setStep(dialog, key) {
dialog.querySelectorAll('[data-account-step]').forEach(function (panel) {
panel.classList.toggle('is-active', panel.getAttribute('data-account-step') === key);
});
var panels = Array.prototype.slice.call(dialog.querySelectorAll('[data-account-step]'));
var index = Math.max(0, panels.findIndex(function (panel) { return panel.getAttribute('data-account-step') === key; }));
dialog.querySelectorAll('.account-step-list li').forEach(function (item, itemIndex) {
item.classList.toggle('active', itemIndex === index);
item.classList.toggle('done', itemIndex < index);
});
var pInput = scope.querySelector('#pwd-new');
var sBar = scope.querySelector('#pwd-strength-bar');
var sTxt = scope.querySelector('#pwd-strength-text');
if (pInput && sBar && sTxt) {
pInput.addEventListener('input', function() {
var v = this.value;
var fill = sBar.querySelector('.pwd-strength__fill');
if (!v) { fill.className = 'pwd-strength__fill'; sTxt.className = 'pwd-strength__text'; sTxt.textContent = ''; return; }
var score = 0;
if (v.length >= 6) score++;
if (v.length >= 10) score++;
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) score++;
if (/\d/.test(v)) score++;
if (/[^a-zA-Z0-9]/.test(v)) score++;
var level = score <= 1 ? 'weak' : (score <= 3 ? 'medium' : 'strong');
var label = score <= 1 ? '弱' : (score <= 3 ? '中' : '强');
fill.className = 'pwd-strength__fill level-' + level;
sTxt.className = 'pwd-strength__text level-' + level;
sTxt.textContent = '密码强度: ' + label;
});
}
bindSend('pwd-send-code', 'pwd-form', 'change_password', scope);
bindSend('email-send-code', 'email-form', 'change_email', scope);
submitForm('pwd-form', 'aksrt_change_password', scope);
submitForm('email-form', 'aksrt_change_email', scope);
}
function resetSettingsModal(shell) {
if (!shell || !shell.classList.contains('aksrt-user-settings-modal')) return;
shell.dataset.aksrtSettingsModalClosed = 'true';
var forms = shell.querySelectorAll('form');
var sceneMap = { 'pwd-form': 'change_password', 'email-form': 'change_email', 'aksrt-bind-phone-form': 'bind_phone' };
forms.forEach(function(form) {
form.reset();
var scene = sceneMap[form.id];
if (scene) resetCaptcha(scene, form);
form.querySelectorAll('[data-aksrt-countdown]').forEach(function(button) {
function resetCountdown(button) {
if (!button) return;
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
button.__aksrtCountdownTimer = null;
button.disabled = false;
button.classList.remove('is-counting');
button.textContent = button.getAttribute('data-idle-label') || button.textContent;
button.textContent = button.getAttribute('data-idle-label') || '发送验证码';
}
function startCountdown(button, seconds) {
var idleText = button.textContent;
var left = Number(seconds || 60);
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
button.disabled = true;
button.classList.add('is-counting');
button.setAttribute('data-idle-label', idleText);
button.textContent = left + 's';
button.__aksrtCountdownTimer = window.setInterval(function () {
left -= 1;
if (left <= 0) { resetCountdown(button); return; }
button.textContent = left + 's';
}, 1000);
}
function settingsRestRoot() {
return String((window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || cfg.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '');
}
function settingsRestNonce() {
return (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || cfg.restNonce || '';
}
function formHasRequiredValues(form) {
var required = form.querySelectorAll('[required]');
for (var i = 0; i < required.length; i++) {
if (required[i].type === 'checkbox' && !required[i].checked) {
required[i].focus();
notyf(i18n.agreeTerms || '请先同意相关协议', 'warning');
return false;
}
if (required[i].type !== 'checkbox' && !String(required[i].value || '').trim()) {
required[i].focus();
notyf(i18n.fillRequired || '请填写必填项', 'warning');
return false;
}
}
return true;
}
function sendEmailCode(button, form, scene) {
if (!form || button.disabled) return;
if (scene === 'change_email' && !String((form.querySelector('[name="new_email"]') || {}).value || '').trim()) {
notyf('请输入邮箱', 'warning');
return;
}
runCaptcha('change_email', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var data = new URLSearchParams();
data.set('action', 'aksrt_send_account_email_code');
data.set('_settings_nonce', settingsNonce);
data.set('scene', scene);
new FormData(form).forEach(function (value, key) { if (value !== '' && !data.has(key)) data.set(key, value); });
Object.keys(captchaPayload || {}).forEach(function (key) { data.set(key, captchaPayload[key]); });
button.disabled = true;
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() });
}).then(function (response) { return response ? response.json() : null; }).then(function (payload) {
if (!payload) return;
if (!payload.success) throw new Error((payload.data && payload.data.message) || '发送失败');
notyf((payload.data && payload.data.message) || '验证码已发送', 'success');
startCountdown(button, payload.data && payload.data.countdown);
}).catch(function (error) {
resetCountdown(button);
notyf(error.message || '发送失败', 'danger');
}).finally(function () { resetCaptcha('change_email', form); });
}
function sendPhoneCode(button, form, stage) {
if (!form || button.disabled) return;
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
if (!phone) { notyf('请输入手机号', 'warning'); return; }
runCaptcha('bind_phone', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var restRoot = settingsRestRoot();
var nonce = settingsRestNonce();
var request = fetch(restRoot + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify(Object.assign({ phone: phone, stage: stage }, captchaPayload || {}))
});
button.disabled = true;
return request;
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '发送失败');
notyf(result.payload.message || '验证码已发送', 'success');
startCountdown(button, result.payload.data && result.payload.data.countdown);
}).catch(function (error) {
resetCountdown(button);
notyf(error.message || '发送失败', 'danger');
}).finally(function () { resetCaptcha('bind_phone', form); });
}
function postAjaxForm(form, action, scene, success) {
if (!formHasRequiredValues(form)) return;
var button = form.querySelector('[type="submit"]');
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
var original = label ? label.textContent : '';
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
runCaptcha(scene, form).then(function (captchaPayload) {
if (captchaPayload === null) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
return null;
}
var data = new FormData(form);
data.set('action', action);
data.set('_settings_nonce', settingsNonce);
Object.keys(captchaPayload || {}).forEach(function (key) { data.set(key, captchaPayload[key]); });
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: data });
}).then(function (response) { return response ? response.json() : null; }).then(function (payload) {
if (!payload) return;
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
if (!payload.success) throw new Error((payload.data && payload.data.message) || '操作失败');
notyf((payload.data && payload.data.message) || '操作成功', 'success');
if (typeof success === 'function') success(payload);
}).catch(function (error) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(error.message || '操作失败', 'danger');
}).finally(function () { resetCaptcha(scene, form); });
}
function postPhoneForm(form, endpoint, success) {
if (!formHasRequiredValues(form)) return;
var button = form.querySelector('[type="submit"]');
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
var original = label ? label.textContent : '';
runCaptcha('bind_phone', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var restRoot = settingsRestRoot();
var nonce = settingsRestNonce();
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
var payload = {};
new FormData(form).forEach(function (value, key) { payload[key] = value; });
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
return fetch(restRoot + endpoint, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce }, body: JSON.stringify(payload) });
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '操作失败');
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(result.payload.message || '操作成功', 'success');
if (typeof success === 'function') success(result.payload);
}).catch(function (error) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(error.message || '操作失败', 'danger');
}).finally(function () { resetCaptcha('bind_phone', form); });
}
function sendRecoveryCode(button, form) {
if (!form || button.disabled) return;
var target = String((form.querySelector('[name="target"]') || {}).value || '').trim();
if (!target) { notyf('请输入邮箱或手机号', 'warning'); return; }
var dialog = form.closest('[data-account-dialog="password"]');
button.disabled = true;
runCaptcha('lostpassword', form).then(function (captchaPayload) {
if (captchaPayload === null) { notyf('请先完成人机验证', 'warning'); resetCountdown(button); return null; }
var payload = { scene: 'resetpassword', target: target, channel: dialog ? (dialog.getAttribute('data-recovery-channel') || '') : '' };
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
return fetch(settingsRestRoot() + '/auth/code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify(payload)
});
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '发送失败');
notyf((result.payload.data && result.payload.data.message) || result.payload.message || '验证码已发送', 'success');
startCountdown(button, (result.payload.data && result.payload.data.countdown) || result.payload.countdown);
}).catch(function (error) {
resetCountdown(button);
notyf(error.message || '发送失败', 'danger');
}).finally(function () { resetCaptcha('lostpassword', form); });
}
function postRecoveryForm(form) {
if (!formHasRequiredValues(form)) return;
var password = form.querySelector('[name="password"]');
var confirm = form.querySelector('[name="password_confirm"]');
if (password && confirm && password.value !== confirm.value) { notyf('两次密码输入不一致', 'warning'); confirm.focus(); return; }
var button = form.querySelector('[type="submit"]');
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
var original = label ? label.textContent : '';
var dialog = form.closest('[data-account-dialog="password"]');
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
runCaptcha('lostpassword', form).then(function (captchaPayload) {
if (captchaPayload === null) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
return null;
}
var payload = {};
new FormData(form).forEach(function (value, key) { payload[key] = value; });
payload.channel = dialog ? (dialog.getAttribute('data-recovery-channel') || '') : '';
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
return fetch(settingsRestRoot() + '/auth/reset-password', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify(payload)
});
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '重设密码失败');
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(result.payload.message || (result.payload.data && result.payload.data.message) || '密码已重置', 'success');
window.setTimeout(function () {
var redirect = result.payload.redirect || (result.payload.data && result.payload.data.redirect);
if (redirect) window.location.assign(redirect);
else window.location.reload();
}, 700);
}).catch(function (error) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(error.message || '重设密码失败', 'danger');
}).finally(function () { resetCaptcha('lostpassword', form); });
}
function bindDialog(shell) {
var dialog = shell.querySelector('[data-account-dialog]');
if (!dialog || dialog.dataset.aksrtBound === 'true') return;
dialog.dataset.aksrtBound = 'true';
dialog.querySelectorAll('.toggle-pwd').forEach(function (button) {
button.addEventListener('click', function () {
var input = dialog.querySelector('#' + button.getAttribute('data-target'));
if (!input) return;
input.type = input.type === 'password' ? 'text' : 'password';
var icon = button.querySelector('i');
if (icon) { icon.classList.toggle('fa-eye', input.type === 'password'); icon.classList.toggle('fa-eye-slash', input.type === 'text'); }
});
});
var bar = shell.querySelector('#pwd-strength-bar');
var fill = bar && bar.querySelector('.pwd-strength__fill');
var text = shell.querySelector('#pwd-strength-text');
if (fill) fill.className = 'pwd-strength__fill';
if (text) { text.textContent = ''; text.className = 'pwd-strength__text'; }
var type = dialog.getAttribute('data-account-dialog');
if (type === 'password') {
var recoveryTarget = dialog.querySelector('#pwd-recovery-target');
if (recoveryTarget && dialog.dataset.recoveryTarget) recoveryTarget.value = dialog.dataset.recoveryTarget;
dialog.querySelectorAll('[data-account-recovery-open],[data-account-recovery-back]').forEach(function (button) {
button.addEventListener('click', function () {
var showRecovery = button.hasAttribute('data-account-recovery-open');
dialog.querySelectorAll('[data-account-password-pane]').forEach(function (pane) {
pane.hidden = pane.getAttribute('data-account-password-pane') === 'recovery' ? !showRecovery : showRecovery;
});
var title = dialog.querySelector('[data-account-password-title]');
if (title) {
title.textContent = title.getAttribute(showRecovery ? 'data-recovery-label' : 'data-change-label') || title.textContent;
var shell = dialog.closest('.aksrt-user-settings-modal');
if (shell) shell.setAttribute('aria-label', title.textContent);
}
var input = dialog.querySelector(showRecovery ? '#pwd-recovery-target' : '#pwd-old');
if (input) input.focus();
});
});
var passwordForm = dialog.querySelector('#pwd-form');
if (passwordForm) passwordForm.addEventListener('submit', function (event) {
event.preventDefault();
var password = passwordForm.querySelector('[name="password"]');
var confirm = passwordForm.querySelector('[name="password2"]');
if (password && confirm && password.value !== confirm.value) { notyf('两次密码输入不一致', 'warning'); confirm.focus(); return; }
postAjaxForm(passwordForm, 'aksrt_change_password', 'change_password', function () { window.setTimeout(function () { window.location.reload(); }, 700); });
});
var recoveryForm = dialog.querySelector('#pwd-recovery-form');
var recoveryCodeButton = dialog.querySelector('[data-account-recovery-send]');
if (recoveryCodeButton && recoveryForm) recoveryCodeButton.addEventListener('click', function () { sendRecoveryCode(recoveryCodeButton, recoveryForm); });
if (recoveryForm) recoveryForm.addEventListener('submit', function (event) { event.preventDefault(); postRecoveryForm(recoveryForm); });
} else if (type === 'email') {
dialog.querySelectorAll('.account-code-send').forEach(function (button) {
button.addEventListener('click', function () { sendEmailCode(button, button.closest('form'), button.getAttribute('data-account-code-scene')); });
});
var oldForm = dialog.querySelector('#email-old-verify-form');
var newForm = dialog.querySelector('#email-new-form');
if (oldForm) oldForm.addEventListener('submit', function (event) {
event.preventDefault();
postAjaxForm(oldForm, 'aksrt_verify_account_email', 'change_email', function () { setStep(dialog, 'email-new'); });
});
if (newForm) newForm.addEventListener('submit', function (event) {
event.preventDefault();
postAjaxForm(newForm, 'aksrt_change_email', 'change_email', function () {
if (dialog.querySelector('[data-account-step="email-success"]')) setStep(dialog, 'email-success');
window.setTimeout(function () { window.location.reload(); }, 700);
});
});
} else if (type === 'phone') {
dialog.querySelectorAll('[data-aksrt-sms-trigger]').forEach(function (button) {
button.addEventListener('click', function () { sendPhoneCode(button, button.closest('form'), button.getAttribute('data-aksrt-sms-scene')); });
});
var phoneOldForm = dialog.querySelector('#phone-old-verify-form');
var phoneNewForm = dialog.querySelector('#aksrt-bind-phone-form');
if (phoneOldForm) phoneOldForm.addEventListener('submit', function (event) {
event.preventDefault();
postPhoneForm(phoneOldForm, '/user/phone-verify', function () { setStep(dialog, 'phone-new'); });
});
if (phoneNewForm) phoneNewForm.addEventListener('submit', function (event) {
event.preventDefault();
postPhoneForm(phoneNewForm, '/user/phone', function () {
if (dialog.querySelector('[data-account-step="phone-success"]')) setStep(dialog, 'phone-success');
window.setTimeout(function () { window.location.reload(); }, 700);
});
});
}
}
function resetSettingsModal(shell) {
if (!shell || !shell.classList.contains('aksrt-user-settings-modal')) return;
shell.querySelectorAll('.account-code-send,[data-aksrt-sms-trigger],[data-account-recovery-send]').forEach(resetCountdown);
shell.querySelectorAll('form').forEach(function (form) { form.reset(); });
}
if (document.documentElement.dataset.aksrtSettingsModalCleanupInitialized !== 'true') {
document.documentElement.dataset.aksrtSettingsModalCleanupInitialized = 'true';
document.addEventListener('aksrt:modal:close', function(event) {
var shell = event.detail && event.detail.shell;
resetSettingsModal(shell);
});
document.addEventListener('aksrt:modal:close', function (event) { resetSettingsModal(event.detail && event.detail.shell); });
}
root.querySelectorAll('[data-aksrt-settings-modal]').forEach(function(btn) {
if (btn.dataset.aksrtSettingsInitialized === 'true') return;
btn.dataset.aksrtSettingsInitialized = 'true';
btn.addEventListener('click', function() {
openSettingsModal(this.getAttribute('data-aksrt-settings-modal'));
root.querySelectorAll('[data-aksrt-settings-modal]').forEach(function (button) {
if (button.dataset.aksrtSettingsInitialized === 'true') return;
button.dataset.aksrtSettingsInitialized = 'true';
button.addEventListener('click', function () { openSettingsModal(button.getAttribute('data-aksrt-settings-modal')); });
});
});
// 验证码发送
function getSendBtnIdleText(btnId) {
if (btnId === 'pwd-send-code') return i18n.sendToCurrentEmail || '发送到当前邮箱';
if (btnId === 'email-send-code') return i18n.sendToNewEmail || '发送到新邮箱';
return i18n.sendCode || '发送验证码';
}
function bindSend(btnId, formId, scene, scope) {
var container = scope || document;
var b = container.querySelector ? container.querySelector('#' + btnId) : document.getElementById(btnId);
if (!b) return;
if (b.dataset.aksrtSettingsInitialized === 'true') return;
b.dataset.aksrtSettingsInitialized = 'true';
b.addEventListener('click', function() {
if (this.classList.contains('is-counting')) return;
var self = this;
var form = container.querySelector ? container.querySelector('#' + formId) : document.getElementById(formId);
var toastId = scene === 'change_password'
? 'aksrt-settings-password-code-toast'
: 'aksrt-settings-email-code-toast';
runCaptcha(scene, form).then(function(captchaPayload) {
if (captchaPayload === null) return;
if (container.dataset && container.dataset.aksrtSettingsModalClosed === 'true') return;
self.disabled = true;
self.textContent = i18n.sending || '发送中...';
notyf(i18n.sending || '正在发送验证码…', 'load', '', toastId);
var data = new URLSearchParams();
data.set('action', 'aksrt_send_email_code');
data.set('_settings_nonce', settingsNonce);
data.set('scene', scene || '');
if (form) {
new FormData(form).forEach(function(value, key) {
if (data.has(key) || value === '' || value == null) return;
data.set(key, value);
});
}
Object.keys(captchaPayload || {}).forEach(function(key) { data.set(key, captchaPayload[key]); });
fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() })
.then(function(r) { return r.json(); }).then(function(res) {
if (res.success) {
notyf((res.data && res.data.message) || i18n.codeSent || '验证码已发送', 'success', 5, toastId);
if (container.dataset && container.dataset.aksrtSettingsModalClosed === 'true') return;
var sec = 60;
self.classList.add('is-counting');
self.textContent = sec + 's 后重发';
self.setAttribute('data-aksrt-countdown', '');
self.setAttribute('data-idle-label', getSendBtnIdleText(btnId));
var t = setInterval(function() {
sec--;
if (sec <= 0) {
clearInterval(t);
self.__aksrtCountdownTimer = null;
self.disabled = false;
self.classList.remove('is-counting');
self.textContent = getSendBtnIdleText(btnId);
} else {
self.textContent = sec + 's 后重发';
}
}, 1000);
self.__aksrtCountdownTimer = t;
} else {
notyf((res.data && res.data.message) || i18n.sendFailed || '发送失败', 'danger', 5, toastId);
self.disabled = false;
self.textContent = getSendBtnIdleText(btnId);
}
})
.catch(function() {
notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
self.disabled = false;
self.textContent = getSendBtnIdleText(btnId);
})
.finally(function() {
resetCaptcha(scene, form);
});
}).catch(function() {
resetCaptcha(scene, form);
notyf('人机验证失败,请重试', 'danger', 5, toastId);
});
});
}
// 表单提交
function submitForm(fid, act, scope) {
var container = scope || document;
var fm = container.querySelector ? container.querySelector('#' + fid) : document.getElementById(fid);
if (!fm) return;
if (fm.dataset.aksrtSettingsInitialized === 'true') return;
fm.dataset.aksrtSettingsInitialized = 'true';
fm.addEventListener('submit', function(e) {
e.preventDefault();
var btn = fm.querySelector('[type=submit]');
var submitLabel = btn && btn.querySelector('[data-aksrt-settings-submit-label]');
var orig = submitLabel ? submitLabel.textContent : '';
var toastId = fid === 'pwd-form'
? 'aksrt-settings-password-toast'
: 'aksrt-settings-email-toast';
var reqs = fm.querySelectorAll('[required]');
for (var i = 0; i < reqs.length; i++) {
if (!reqs[i].value.trim()) { reqs[i].focus(); notyf(i18n.fillRequired || '请填写必填项', 'warning'); return; }
}
if (fid === 'pwd-form') {
var pwdNew = fm.querySelector('#pwd-new');
var pwdConfirm = fm.querySelector('#pwd-new-confirm');
if (pwdNew && pwdConfirm && pwdNew.value !== pwdConfirm.value) {
pwdConfirm.focus();
notyf(i18n.passwordMismatch || '两次输入的新密码不一致', 'warning');
return;
}
}
if (btn) { btn.disabled = true; setSettingsSubmitLabel(btn, i18n.submitting || '提交中...'); }
notyf(i18n.submitting || '正在提交…', 'load', '', toastId);
var scene = fid === 'pwd-form' ? 'change_password' : 'change_email';
runCaptcha(scene, fm).then(function(captchaPayload) {
if (captchaPayload === null) {
if (btn) { btn.disabled = false; setSettingsSubmitLabel(btn, orig); }
return null;
}
if (container.dataset && container.dataset.aksrtSettingsModalClosed === 'true') return null;
var modalShell = fm.closest('.aksrt-user-settings-modal');
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return null;
var d = new FormData(fm);
d.set('action', act);
d.set('_settings_nonce', settingsNonce);
Object.keys(captchaPayload || {}).forEach(function(key) { d.set(key, captchaPayload[key]); });
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: d });
})
.then(function(r) { return r ? r.json() : null; })
.then(function(r) {
if (!r) return;
if (btn) { btn.disabled = false; setSettingsSubmitLabel(btn, orig); }
if (r.success) {
notyf((r.data && r.data.message) || '操作成功', 'success', 5, toastId);
var activeModal = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive();
if (activeModal && activeModal.classList.contains('aksrt-user-settings-modal') && activeModal.contains(fm)) {
window.AksrtModalRuntime.close('settings-submit');
}
if (r.data && r.data.redirect_to) setTimeout(function() { window.location.href = r.data.redirect_to; }, 2500);
} else {
notyf((r.data && r.data.message) || i18n.operationFailed || '操作失败', 'danger', 5, toastId);
}
})
.catch(function() {
if (btn) { btn.disabled = false; setSettingsSubmitLabel(btn, orig); }
notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
})
.finally(function() {
resetCaptcha(scene, fm);
});
});
}
root.querySelectorAll('.binding-item--linkable').forEach(function(link) {
if (link.dataset.aksrtBindCaptchaInitialized === 'true') return;
link.dataset.aksrtBindCaptchaInitialized = 'true';
@@ -1134,145 +1230,9 @@
initNotifications();
initNotificationPreferences();
initSettings();
initBindPhone();
initSettingsCallbackToast();
});
// 绑定手机
function initBindPhone(root) {
root = root || document;
var form = root.id === 'aksrt-bind-phone-form' ? root : root.querySelector('#aksrt-bind-phone-form');
if (!form) return;
if (form.dataset.aksrtSettingsInitialized === 'true') return;
form.dataset.aksrtSettingsInitialized = 'true';
function isValidSmsPhone(phone) {
if (!window.aksrtData || aksrtData.smsNumberLimitCh !== false) {
return /^1[3-9]\d{9}$/.test(phone);
}
return /^(?:\d{5,}|\+\d{6,}|\+?\d{2,}[-\s]\d{2,}[0-9\-\s]{0,15})$/.test(phone);
}
var sendButton = form.querySelector('[data-aksrt-sms-trigger]');
if (sendButton) sendButton.addEventListener('click', function () {
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
if (!isValidSmsPhone(phone)) { notyf('请输入正确的手机号', 'warning'); return; }
var toastId = 'aksrt-settings-phone-code-toast';
var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
sendButton.disabled = true;
notyf('正在发送验证码…', 'load', '', toastId);
runCaptcha('bind_phone', form).then(function(captchaPayload) {
if (captchaPayload === null) {
sendButton.disabled = false;
return null;
}
var modalShell = form.closest('.aksrt-user-settings-modal');
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return null;
return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify(Object.assign({ phone: phone }, captchaPayload || {}))
});
}).then(function (r) { return r ? r.json().then(function (j) { return { ok: r.ok, json: j }; }) : null; }).then(function (res) {
if (!res) return;
if (!res.ok || !res.json.success) throw new Error(res.json.message || '发送失败');
notyf(res.json.message || '验证码已发送', 'success', 5, toastId);
var modalShell = form.closest('.aksrt-user-settings-modal');
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return;
var left = Number((res.json.data && res.json.data.countdown) || 60);
sendButton.setAttribute('data-aksrt-countdown', '');
sendButton.setAttribute('data-idle-label', '获取验证码');
sendButton.classList.add('is-counting');
var timer = setInterval(function () {
left -= 1;
sendButton.textContent = left + 's';
if (left <= 0) {
clearInterval(timer);
sendButton.__aksrtCountdownTimer = null;
sendButton.disabled = false;
sendButton.classList.remove('is-counting');
sendButton.textContent = sendButton.getAttribute('data-idle-label');
}
}, 1000);
sendButton.__aksrtCountdownTimer = timer;
}).catch(function (error) { sendButton.disabled = false; notyf(error.message || '发送失败', 'danger', 5, toastId); })
.finally(function () { resetCaptcha('bind_phone', form); });
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var phoneEl = form.querySelector('[name="phone"]');
var codeEl = form.querySelector('[name="code"]');
var phone = phoneEl ? String(phoneEl.value || '').trim() : '';
var code = codeEl ? String(codeEl.value || '').trim() : '';
if (!isValidSmsPhone(phone)) {
notyf('请输入正确的手机号', 'warning');
return;
}
if (!/^\d{6}$/.test(code)) {
notyf('请输入短信验证码', 'warning');
return;
}
var submitBtn = form.querySelector('[type="submit"]');
var submitLabel = submitBtn && submitBtn.querySelector('[data-aksrt-settings-submit-label]');
var submitIdleText = submitLabel ? submitLabel.textContent : '';
if (submitBtn) {
submitBtn.disabled = true;
setSettingsSubmitLabel(submitBtn, '绑定中...');
}
function doBind(captcha) {
var toastId = 'aksrt-settings-phone-bind-toast';
notyf('正在绑定手机…', 'load', '', toastId);
var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce
},
body: JSON.stringify(Object.assign({ phone: phone, code: code }, captcha))
})
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); })
.then(function (res) {
if (res.ok && res.json && res.json.success) {
notyf(res.json.message || '绑定成功', 'success', 5, toastId);
var activeModal = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive();
if (activeModal && activeModal.classList.contains('aksrt-user-settings-modal') && activeModal.contains(form)) {
window.AksrtModalRuntime.close('settings-submit');
}
setTimeout(function () { window.location.reload(); }, 600);
return;
}
notyf((res.json && (res.json.message || res.json.error)) || '绑定失败', 'danger', 5, toastId);
if (submitBtn) {
submitBtn.disabled = false;
setSettingsSubmitLabel(submitBtn, submitIdleText);
}
})
.catch(function () {
notyf('网络错误', 'danger', 5, toastId);
if (submitBtn) {
submitBtn.disabled = false;
setSettingsSubmitLabel(submitBtn, submitIdleText);
}
});
}
runCaptcha('bind_phone', form).then(function(captchaPayload) {
if (captchaPayload === null) {
if (submitBtn) {
submitBtn.disabled = false;
setSettingsSubmitLabel(submitBtn, submitIdleText);
}
return null;
}
var modalShell = form.closest('.aksrt-user-settings-modal');
if (modalShell && modalShell.dataset.aksrtSettingsModalClosed === 'true') return null;
return doBind(captchaPayload);
}).finally(function() {
resetCaptcha('bind_phone', form);
});
});
}
})();
@@ -81,7 +81,7 @@ $config = [
</div>
<div class="but-average modal-buts uc-auth-modal__footer">
<button type="button" class="but" data-aksrt-modal-close><?php esc_html_e('取消', 'aksrt'); ?></button>
<button type="submit" class="but c-blue"><i class="fa fa-check mr6" aria-hidden="true"></i><?php esc_html_e('确认提交', 'aksrt'); ?></button>
<button type="submit" class="but c-blue"><i class="fa fas fa-check mr6" aria-hidden="true"></i><?php esc_html_e('确认提交', 'aksrt'); ?></button>
</div>
</form>
</div>
@@ -1,25 +1,29 @@
<?php
/**
* User center account settings rows and runtime modal templates.
* User center account settings rows and reference-theme account dialogs.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$account_user = isset($target_user) && $target_user instanceof WP_User
? $target_user
: wp_get_current_user();
$account_user = isset($target_user) && $target_user instanceof WP_User ? $target_user : wp_get_current_user();
$account_user_id = $account_user instanceof WP_User ? (int) $account_user->ID : (int) ($user_id ?? 0);
$account_email = $account_user instanceof WP_User ? (string) $account_user->user_email : '';
$account_phone = $account_user_id > 0 ? (string) get_user_meta($account_user_id, 'phone_number', true) : '';
$user_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$sms_settings = class_exists('\\AKSRT\\Sms\\SMS') ? \AKSRT\Sms\SMS::get_instance()->get_provider_config() : [];
$account_oauth_new = $account_user_id > 0 && get_user_meta($account_user_id, 'oauth_new', true);
$user_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$sms_settings = class_exists('\AKSRT\Sms\SMS') ? \AKSRT\Sms\SMS::get_instance()->get_provider_config() : [];
$phone_available = !empty($user_settings['bind_phone']) && (($sms_settings['sms_sdk'] ?? 'null') !== 'null');
$phone_visible = $account_phone !== '' || $phone_available;
$phone_label = $account_phone !== '' ? $account_phone : __('暂未绑定', 'aksrt');
$email_action = $account_email !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt');
$phone_action = $account_phone !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt');
$agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false, 'label_html' => ''];
$email_verify_required = $account_email !== '' && !empty($user_settings['email_set_captch']);
$reset_channel = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::reset_password_verification_type() : 'email';
$sign_in_available = !class_exists('\\AKSRT\\Settings\\Settings') || (!\AKSRT\Settings\Settings::close_sign() && !\AKSRT\Settings\Settings::close_signin());
$can_recover_password = $sign_in_available && ($reset_channel === 'email_phone'
? ($account_email !== '' || $account_phone !== '')
: ($reset_channel === 'phone' ? $account_phone !== '' : $account_email !== ''));
$recovery_target = $reset_channel === 'phone' ? $account_phone : ($account_email !== '' ? $account_email : $account_phone);
$recovery_channel = $reset_channel === 'email_phone' ? '' : $reset_channel;
$render_row = static function (string $icon, string $title, string $value, string $action, string $modal, string $icon_tone, string $action_tone, bool $interactive = true): void {
?>
@@ -28,116 +32,122 @@ $render_row = static function (string $icon, string $title, string $value, strin
<span class="aksrt-account-row__icon aksrt-account-row__icon--<?php echo esc_attr($icon_tone); ?>"><i class="fa fas <?php echo esc_attr($icon); ?>" aria-hidden="true"></i></span>
<strong class="aksrt-account-row__copy"><?php echo esc_html($title); ?></strong>
</div>
<?php if ($value !== '') : ?>
<div class="aksrt-account-row__value" title="<?php echo esc_attr($value); ?>"><?php echo esc_html($value); ?></div>
<?php endif; ?>
<?php if ($value !== '') : ?><div class="aksrt-account-row__value" title="<?php echo esc_attr($value); ?>"><?php echo esc_html($value); ?></div><?php endif; ?>
<?php if ($interactive) : ?>
<button type="button" class="but hollow <?php echo esc_attr($action_tone); ?> aksrt-account-row__action" data-aksrt-settings-modal="<?php echo esc_attr($modal); ?>">
<?php echo esc_html($action); ?>
</button>
<?php else : ?>
<span class="aksrt-account-row__action aksrt-account-row__action--unavailable"><?php esc_html_e('暂不可修改', 'aksrt'); ?></span>
<?php endif; ?>
<button type="button" class="but hollow <?php echo esc_attr($action_tone); ?> aksrt-account-row__action" data-aksrt-settings-modal="<?php echo esc_attr($modal); ?>"><?php echo esc_html($action); ?></button>
<?php else : ?><span class="aksrt-account-row__action aksrt-account-row__action--unavailable"><?php esc_html_e('暂不可修改', 'aksrt'); ?></span><?php endif; ?>
</div>
<?php
};
$render_email_form = static function () use ($user_settings, $agreement): void {
?>
<form id="email-new-form">
<div class="relative line-form mb10 modal-line-field"><input type="email" id="email-new" name="new_email" class="line-form-input" required><div class="scale-placeholder"><?php esc_html_e('请输入邮箱', 'aksrt'); ?></div><i class="line-form-line"></i></div>
<?php if (!empty($user_settings['email_set_captch'])) : ?><div class="relative line-form mb10 modal-line-field"><input type="text" id="email-code" name="code" class="line-form-input" maxlength="6" inputmode="numeric" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn account-code-send" data-account-code-scene="change_email"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div><?php endif; ?>
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('change_email'); ?><?php endif; ?>
<?php if (!empty($agreement['enabled'])) : ?><label class="form-checkbox muted-color mb20"><input type="checkbox" name="agree_terms" value="1"<?php echo !empty($agreement['required']) ? ' required' : ' checked'; ?>> <?php echo wp_kses_post($agreement['label_html']); ?></label><?php endif; ?>
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <?php esc_html_e('确认提交', 'aksrt'); ?></button>
</form>
<?php
};
$render_phone_form = static function () use ($agreement): void {
?>
<form id="aksrt-bind-phone-form">
<div class="relative line-form mb10 modal-line-field"><input type="tel" id="bind-phone-input" name="phone" inputmode="tel" autocomplete="tel" required><div class="scale-placeholder"><?php esc_html_e('手机号', 'aksrt'); ?></div><i class="line-form-line"></i></div>
<div class="relative line-form mb10 modal-line-field"><input type="text" id="bind-phone-code" name="code" class="line-form-input" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn" data-aksrt-sms-trigger data-aksrt-sms-scene="bind_new_phone"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div>
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?>
<?php if (!empty($agreement['enabled'])) : ?><label class="form-checkbox muted-color mb20"><input type="checkbox" name="agree_terms" value="1"<?php echo !empty($agreement['required']) ? ' required' : ' checked'; ?>> <?php echo wp_kses_post($agreement['label_html']); ?></label><?php endif; ?>
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <?php esc_html_e('确认提交', 'aksrt'); ?></button>
</form>
<?php
};
?>
<section class="aksrt-account-settings" data-aksrt-settings-root data-aksrt-captcha-scope>
<div class="aksrt-account-settings__section">
<div class="aksrt-account-settings__heading">
<div>
<h3 class="title-h-left"><b><?php esc_html_e('绑定邮箱', 'aksrt'); ?></b></h3>
<p><?php esc_html_e('绑定邮箱账号,及时接收订单、审核等重要信息', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-account-list">
<?php $render_row('fa-envelope', __('邮箱', 'aksrt'), $account_email !== '' ? $account_email : __('暂未绑定', 'aksrt'), $email_action, 'email', 'blue', $account_email !== '' ? 'c-yellow-2' : 'c-blue'); ?>
</div>
<div class="aksrt-account-settings__heading"><div><h3 class="title-h-left"><b><?php esc_html_e('绑定邮箱', 'aksrt'); ?></b></h3><p><?php esc_html_e('绑定邮箱账号,及时接收订单、审核等重要信息', 'aksrt'); ?></p></div></div>
<div class="aksrt-account-list"><?php $render_row('fa-envelope', __('邮箱', 'aksrt'), $account_email !== '' ? $account_email : __('暂未绑定', 'aksrt'), $account_email !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt'), 'email', 'blue', $account_email !== '' ? 'c-yellow-2' : 'c-blue'); ?></div>
</div>
<?php if ($phone_visible) : ?>
<?php if ($account_phone !== '' || $phone_available) : ?>
<div class="aksrt-account-settings__section">
<div class="aksrt-account-settings__heading">
<div>
<h3 class="title-h-left"><b><?php esc_html_e('绑定手机', 'aksrt'); ?></b></h3>
<p><?php esc_html_e('绑定手机号,提高账户安全性', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-account-list">
<?php $render_row('fa-phone', __('手机号', 'aksrt'), $phone_label, $phone_action, 'phone', 'blue-2', $account_phone !== '' ? 'c-yellow' : 'c-blue', $phone_available); ?>
</div>
<div class="aksrt-account-settings__heading"><div><h3 class="title-h-left"><b><?php esc_html_e('绑定手机', 'aksrt'); ?></b></h3><p><?php esc_html_e('绑定手机号,提高账户安全性', 'aksrt'); ?></p></div></div>
<div class="aksrt-account-list"><?php $render_row('fa-phone', __('手机号', 'aksrt'), $account_phone !== '' ? $account_phone : __('暂未绑定', 'aksrt'), $account_phone !== '' ? __('修改', 'aksrt') : __('绑定', 'aksrt'), 'phone', 'blue-2', $account_phone !== '' ? 'c-yellow' : 'c-blue', $phone_available); ?></div>
</div>
<?php endif; ?>
<?php require __DIR__ . '/settings-bindings.php'; ?>
<div class="aksrt-account-settings__section">
<div class="aksrt-account-settings__heading">
<div>
<h3 class="title-h-left"><b><?php esc_html_e('账户密码', 'aksrt'); ?></b></h3>
<p><?php esc_html_e('定期修改密码有助于账户安全', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-account-list">
<?php $render_row('fa-unlock-alt', __('账户密码', 'aksrt'), '', __('修改密码', 'aksrt'), 'password', 'purple', 'c-blue-2'); ?>
</div>
<div class="aksrt-account-settings__heading"><div><h3 class="title-h-left"><b><?php esc_html_e('账户密码', 'aksrt'); ?></b></h3><p><?php echo $account_oauth_new ? esc_html__('您还未设置过密码,请在此设置新密码', 'aksrt') : esc_html__('定期修改密码有助于账户安全', 'aksrt'); ?></p></div></div>
<div class="aksrt-account-list"><?php $render_row('fa-unlock-alt', __('账户密码', 'aksrt'), '', $account_oauth_new ? __('设置新密码', 'aksrt') : __('修改密码', 'aksrt'), 'password', 'purple', 'c-blue-2'); ?></div>
</div>
<template data-aksrt-settings-modal-template="password">
<div class="aksrt-settings-modal">
<div class="modal-colorful-header colorful-bg jb-yellow">
<button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<div class="colorful-make"></div>
<div class="text-center"><div class="em2x"><i class="fa fas fa-unlock-alt" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php esc_html_e('修改密码', 'aksrt'); ?></div></div>
</div>
<div class="aksrt-settings-modal" data-account-dialog="password" data-recovery-channel="<?php echo esc_attr($recovery_channel); ?>" data-recovery-target="<?php echo esc_attr($recovery_target); ?>">
<div class="modal-colorful-header colorful-bg jb-yellow"><button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-unlock-alt" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10" data-account-password-title data-change-label="<?php echo esc_attr($account_oauth_new ? __('设置密码', 'aksrt') : __('修改密码', 'aksrt')); ?>" data-recovery-label="<?php esc_attr_e('重设密码', 'aksrt'); ?>"><?php echo $account_oauth_new ? esc_html__('设置密码', 'aksrt') : esc_html__('修改密码', 'aksrt'); ?></div></div></div>
<div class="aksrt-settings-modal__pane" data-account-password-pane="change">
<form id="pwd-form" autocomplete="off" novalidate>
<div class="aksrt-settings-modal__fields">
<div class="modal-field"><label for="pwd-old"><?php esc_html_e('原密码', 'aksrt'); ?></label><input type="password" id="pwd-old" name="old_password" required minlength="1"><button type="button" class="toggle-pwd" data-target="pwd-old" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button></div>
<div class="modal-field"><label for="pwd-new"><?php esc_html_e('新密码', 'aksrt'); ?></label><input type="password" id="pwd-new" name="new_password" required minlength="6" placeholder="<?php esc_attr_e('至少6位字符', 'aksrt'); ?>"><button type="button" class="toggle-pwd" data-target="pwd-new" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button><div class="pwd-strength" id="pwd-strength-bar"><div class="pwd-strength__fill"></div></div><span class="pwd-strength__text" id="pwd-strength-text"></span></div>
<div class="modal-field"><label for="pwd-new-confirm"><?php esc_html_e('确认新密码', 'aksrt'); ?></label><input type="password" id="pwd-new-confirm" name="new_password_confirm" required minlength="6" placeholder="<?php esc_attr_e('再次输入新密码', 'aksrt'); ?>"><button type="button" class="toggle-pwd" data-target="pwd-new-confirm" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button></div>
<div class="modal-field modal-field--code"><label for="pwd-code"><?php esc_html_e('当前邮箱验证码', 'aksrt'); ?></label><div class="code-input-group"><input type="text" id="pwd-code" name="email_code" maxlength="6" inputmode="numeric" pattern="[0-9]*" placeholder="<?php esc_attr_e('6位数字验证码', 'aksrt'); ?>" required><button type="button" class="but hollow code-send-btn" id="pwd-send-code"><?php esc_html_e('发送到当前邮箱', 'aksrt'); ?></button></div></div>
<?php aksrt_render_captcha('change_password'); ?>
<?php if (!$account_oauth_new) : ?><div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-old" name="passwordold" class="line-form-input" tabindex="1" required><div class="scale-placeholder"><?php esc_html_e('请输入原密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-old" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div><?php endif; ?>
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-new" name="password" class="line-form-input" tabindex="2" required minlength="6"><div class="scale-placeholder"><?php echo $account_oauth_new ? esc_html__('设置新密码', 'aksrt') : esc_html__('请输入新密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-new" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-confirm" name="password2" class="line-form-input" tabindex="3" required minlength="6"><div class="scale-placeholder"><?php esc_html_e('请再次输入新密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-confirm" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('change_password'); ?><?php endif; ?>
</div>
<button type="submit" class="but jb-blue padding-lg btn-block" id="pwd-submit"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
<?php if ($account_oauth_new) : ?><input type="hidden" name="oauth_new" value="1"><?php endif; ?>
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
</form>
<?php if (!$account_oauth_new && $can_recover_password) : ?><div class="text-right mt6"><button type="button" class="em09 muted-2-color account-recovery-toggle" data-account-recovery-open><?php esc_html_e('忘记原密码?点击重设密码', 'aksrt'); ?></button></div><?php endif; ?>
</div>
<?php if (!$account_oauth_new && $can_recover_password) : ?>
<div class="aksrt-settings-modal__pane" data-account-password-pane="recovery" hidden>
<div class="text-right mb10"><button type="button" class="em09 but p2-10 account-recovery-toggle" data-account-recovery-back><i class="fa fas fa-angle-left em12" aria-hidden="true"></i> <?php esc_html_e('返回使用原密码修改密码', 'aksrt'); ?></button></div>
<form id="pwd-recovery-form" autocomplete="off" novalidate>
<div class="aksrt-settings-modal__fields">
<div class="relative line-form mb10 modal-line-field"><input type="text" id="pwd-recovery-target" name="target" value="<?php echo esc_attr($recovery_target); ?>" class="line-form-input" autocomplete="username" required><div class="scale-placeholder"><?php esc_html_e('邮箱或手机号', 'aksrt'); ?></div><i class="line-form-line"></i></div>
<div class="relative line-form mb10 modal-line-field"><input type="text" id="pwd-recovery-code" name="code" class="line-form-input" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn" data-account-recovery-send><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div>
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('lostpassword'); ?><?php endif; ?>
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-recovery-new" name="password" class="line-form-input" required minlength="6"><div class="scale-placeholder"><?php esc_html_e('设置新密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-recovery-new" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
<div class="relative line-form mb10 modal-line-field"><input type="password" id="pwd-recovery-confirm" name="password_confirm" class="line-form-input" required minlength="6"><div class="scale-placeholder"><?php esc_html_e('重复密码', 'aksrt'); ?></div><button type="button" class="toggle-pwd abs-right passw" data-target="pwd-recovery-confirm" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fas fa-eye" aria-hidden="true"></i></button><i class="line-form-line"></i></div>
</div>
<button type="submit" class="but jb-green padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
</form>
</div>
<?php endif; ?>
</div>
</template>
<template data-aksrt-settings-modal-template="email">
<div class="aksrt-settings-modal">
<div class="modal-colorful-header colorful-bg jb-blue">
<button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<div class="colorful-make"></div>
<div class="text-center"><div class="em2x"><i class="fa fas fa-envelope" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_email !== '' ? __('更改邮箱', 'aksrt') : __('绑定邮箱', 'aksrt')); ?></div></div>
<div class="aksrt-settings-modal" data-account-dialog="email">
<div class="modal-colorful-header colorful-bg jb-blue"><button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-envelope" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_email !== '' ? __('修改邮箱', 'aksrt') : __('绑定邮箱', 'aksrt')); ?></div></div></div>
<?php if ($email_verify_required) : ?>
<ul class="mb20 step-simple account-step-list" data-account-step-list="email"><li class="active"><span><?php esc_html_e('验证老邮箱', 'aksrt'); ?></span></li><li><span><?php esc_html_e('验证新邮箱', 'aksrt'); ?></span></li><li><span><?php esc_html_e('修改成功', 'aksrt'); ?></span></li></ul>
<div class="account-step-panels">
<div class="account-step is-active" data-account-step="email-old"><p class="muted-2-color"><?php printf(esc_html__('请在下方获取验证码以验证您的老邮箱 %s', 'aksrt'), '<span class="badg">' . esc_html($account_email) . '</span>'); ?></p><form id="email-old-verify-form"><div class="relative line-form mb10 modal-line-field"><input type="text" name="code" class="line-form-input" maxlength="6" inputmode="numeric" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn account-code-send" data-account-code-scene="verify_email"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div><?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('change_email'); ?><?php endif; ?><button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-shield-alt" aria-hidden="true"></i> <?php esc_html_e('立即验证', 'aksrt'); ?></button></form></div>
<div class="account-step" data-account-step="email-new"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新邮箱账号', 'aksrt'); ?></p><?php $render_email_form(); ?></div>
<div class="account-step" data-account-step="email-success"><div class="text-center padding-h10"><i class="fa fas fa-check-circle c-green em3x mb10" aria-hidden="true"></i><p><?php esc_html_e('邮箱修改成功', 'aksrt'); ?></p></div></div>
</div>
<form id="email-form" autocomplete="off" novalidate>
<div class="aksrt-settings-modal__fields">
<?php if ($account_email !== '') : ?><div class="modal-field"><label><?php esc_html_e('当前邮箱', 'aksrt'); ?></label><div class="readonly-display"><?php echo esc_html($account_email); ?></div></div><?php endif; ?>
<div class="modal-field"><label for="email-current-password"><?php esc_html_e('当前密码', 'aksrt'); ?></label><input type="password" id="email-current-password" name="current_password" required minlength="1" placeholder="<?php esc_attr_e('请输入当前登录密码', 'aksrt'); ?>"><button type="button" class="toggle-pwd" data-target="email-current-password" aria-label="<?php esc_attr_e('显示或隐藏密码', 'aksrt'); ?>"><i class="fa fa-eye" aria-hidden="true"></i></button></div>
<div class="modal-field"><label for="email-new"><?php esc_html_e('新邮箱', 'aksrt'); ?></label><input type="email" id="email-new" name="new_email" required placeholder="<?php esc_attr_e('请输入新的邮箱地址', 'aksrt'); ?>"></div>
<div class="modal-field modal-field--code"><label for="email-code"><?php esc_html_e('新邮箱验证码', 'aksrt'); ?></label><div class="code-input-group"><input type="text" id="email-code" name="email_code" maxlength="6" inputmode="numeric" pattern="[0-9]*" placeholder="<?php esc_attr_e('6位数字验证码', 'aksrt'); ?>" required><button type="button" class="but hollow code-send-btn" id="email-send-code"><?php esc_html_e('发送到新邮箱', 'aksrt'); ?></button></div><p class="settings-hint settings-hint--bottom"><?php esc_html_e('验证码将发送到你填写的新邮箱,验证通过后才会正式切换。', 'aksrt'); ?></p></div>
<?php aksrt_render_captcha('change_email'); ?>
</div>
<button type="submit" class="but jb-blue padding-lg btn-block" id="email-submit"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
</form>
<?php else : ?>
<div class="aksrt-settings-modal__pane"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新邮箱账号', 'aksrt'); ?></p><?php $render_email_form(); ?></div>
<?php endif; ?>
</div>
</template>
<?php if ($phone_available) : ?>
<template data-aksrt-settings-modal-template="phone">
<div class="aksrt-settings-modal">
<div class="modal-colorful-header colorful-bg jb-blue">
<button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<div class="colorful-make"></div>
<div class="text-center"><div class="em2x"><i class="fa fas fa-mobile-alt" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_phone !== '' ? __('修改手机', 'aksrt') : __('绑定手机', 'aksrt')); ?></div></div>
<div class="aksrt-settings-modal" data-account-dialog="phone">
<div class="modal-colorful-header colorful-bg jb-blue"><button type="button" class="close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-phone" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10"><?php echo esc_html($account_phone !== '' ? __('修改手机', 'aksrt') : __('绑定手机', 'aksrt')); ?></div></div></div>
<?php if ($account_phone !== '') : ?>
<ul class="mb20 step-simple account-step-list" data-account-step-list="phone"><li class="active"><span><?php esc_html_e('验证老手机', 'aksrt'); ?></span></li><li><span><?php esc_html_e('验证新手机', 'aksrt'); ?></span></li><li><span><?php esc_html_e('修改成功', 'aksrt'); ?></span></li></ul>
<div class="account-step-panels">
<div class="account-step is-active" data-account-step="phone-old"><p class="muted-2-color"><?php printf(esc_html__('请在下方获取验证码以验证您的老手机号 %s', 'aksrt'), '<br><span class="badg">' . esc_html($account_phone) . '</span>'); ?></p><form id="phone-old-verify-form"><input type="hidden" name="phone" value="<?php echo esc_attr($account_phone); ?>"><div class="relative line-form mb10 modal-line-field"><input type="text" name="code" class="line-form-input" maxlength="6" inputmode="numeric" required><div class="scale-placeholder"><?php esc_html_e('验证码', 'aksrt'); ?></div><button type="button" class="but c-blue code-send-btn account-code-send" data-aksrt-sms-trigger data-aksrt-sms-scene="verify_old_phone"><?php esc_html_e('发送验证码', 'aksrt'); ?></button><i class="line-form-line"></i></div><?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?><button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-shield-alt" aria-hidden="true"></i> <?php esc_html_e('立即验证', 'aksrt'); ?></button></form></div>
<div class="account-step" data-account-step="phone-new"><p class="muted-2-color"><?php esc_html_e('请输入您需要修改的新手机号码', 'aksrt'); ?></p><?php $render_phone_form(); ?></div>
<div class="account-step" data-account-step="phone-success"><div class="text-center padding-h10"><i class="fa fas fa-check-circle c-green em3x mb10" aria-hidden="true"></i><p><?php esc_html_e('手机号修改成功', 'aksrt'); ?></p></div></div>
</div>
<form id="aksrt-bind-phone-form" autocomplete="off" novalidate data-aksrt-bind-phone-form>
<div class="aksrt-settings-modal__fields">
<div class="modal-field"><label for="bind-phone-input"><?php esc_html_e('手机号', 'aksrt'); ?></label><input type="tel" id="bind-phone-input" name="phone" data-aksrt-phone-input inputmode="tel" autocomplete="tel" required placeholder="<?php esc_attr_e('请输入手机号', 'aksrt'); ?>"></div>
<div class="modal-field modal-field--code"><label for="bind-phone-code"><?php esc_html_e('短信验证码', 'aksrt'); ?></label><div class="code-input-group"><input type="text" id="bind-phone-code" name="code" maxlength="6" inputmode="numeric" autocomplete="one-time-code" required placeholder="<?php esc_attr_e('请输入验证码', 'aksrt'); ?>"><button type="button" class="but hollow code-send-btn" data-aksrt-sms-trigger data-aksrt-sms-scene="bind_phone" data-aksrt-sms-phone="#bind-phone-input"><?php esc_html_e('获取验证码', 'aksrt'); ?></button></div></div>
<?php if (function_exists('aksrt_render_captcha')) : ?><?php aksrt_render_captcha('bind_phone'); ?><?php endif; ?>
</div>
<button type="submit" class="but jb-blue padding-lg btn-block"><i class="fa fas fa-check" aria-hidden="true"></i> <span data-aksrt-settings-submit-label><?php esc_html_e('确认提交', 'aksrt'); ?></span></button>
</form>
<?php else : ?>
<div class="aksrt-settings-modal__pane"><p class="muted-2-color"><?php esc_html_e('请输入您需要绑定的手机号', 'aksrt'); ?></p><?php $render_phone_form(); ?></div>
<?php endif; ?>
</div>
</template>
<?php endif; ?>
+318 -275
View File
@@ -1,322 +1,365 @@
<?php
/**
* User profile/password/email AJAX handlers
*
* @package AKSRT
*/
/** User profile and account settings AJAX handlers. */
defined('ABSPATH') || exit;
if (!function_exists('aksrt_account_captcha_error_data')) :
if (!function_exists('aksrt_account_captcha_error_data')) {
function aksrt_account_captcha_error_data($result): array {
if (is_wp_error($result)) {
return [
'code' => $result->get_error_code() ?: 'aksrt_captcha_failed',
'message' => $result->get_error_message() ?: __('人机验证失败,请重试', 'aksrt'),
'code' => is_wp_error($result) ? ($result->get_error_code() ?: 'aksrt_captcha_failed') : 'aksrt_captcha_failed',
'message' => is_wp_error($result)
? ($result->get_error_message() ?: __('人机验证失败,请重试', 'aksrt'))
: __('人机验证失败,请重试', 'aksrt'),
];
}
return [
'code' => 'aksrt_captcha_failed',
'message' => __('人机验证失败,请重试', 'aksrt'),
];
}
endif;
// ============================================================
add_action('wp_ajax_aksrt_save_profile', function () {
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
add_action('wp_ajax_aksrt_save_profile', function (): void {
if (
!isset($_POST['_profile_nonce'])
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')
) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
$user_id = (int) ($_POST['user_id'] ?? 0);
$result = function_exists('aksrt_save_user_center_profile')
? aksrt_save_user_center_profile($user_id, $_POST)
: new WP_Error('aksrt_profile_unavailable', __('保存失败', 'aksrt'));
if (is_wp_error($result)) wp_send_json_error(['message' => $result->get_error_message()]);
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
});
function aksrt_account_code_key(int $user_id, string $scene, string $target): string {
return 'aksrt_account_code_' . $user_id . '_' . sanitize_key($scene) . '_' . md5(strtolower(trim($target)));
}
function aksrt_account_store_code(int $user_id, string $scene, string $target, string $code): void {
set_transient(
aksrt_account_code_key($user_id, $scene, $target),
['hash' => wp_hash_password($code), 'attempts' => 0],
5 * MINUTE_IN_SECONDS
);
}
function aksrt_account_consume_code(int $user_id, string $scene, string $target, string $code, bool $consume = true): bool {
$key = aksrt_account_code_key($user_id, $scene, $target);
$stored = get_transient($key);
if (!is_array($stored) || empty($stored['hash'])) {
return false;
}
$attempts = (int) ($stored['attempts'] ?? 0) + 1;
if ($attempts > 6) {
delete_transient($key);
return false;
}
if (!wp_check_password($code, (string) $stored['hash'])) {
$stored['attempts'] = $attempts;
set_transient($key, $stored, 5 * MINUTE_IN_SECONDS);
return false;
}
if ($consume) {
delete_transient($key);
}
return true;
}
function aksrt_account_proof_key(int $user_id, string $kind): string {
return 'aksrt_account_proof_' . sanitize_key($kind) . '_' . $user_id;
}
function aksrt_account_set_proof(int $user_id, string $kind, string $target): void {
set_transient(
aksrt_account_proof_key($user_id, $kind),
['target' => strtolower(trim($target))],
30 * MINUTE_IN_SECONDS
);
}
function aksrt_account_consume_proof(int $user_id, string $kind, string $target, bool $consume = true): bool {
$key = aksrt_account_proof_key($user_id, $kind);
$proof = get_transient($key);
if (!is_array($proof) || strtolower(trim((string) ($proof['target'] ?? ''))) !== strtolower(trim($target))) {
return false;
}
if ($consume) {
delete_transient($key);
}
return true;
}
function aksrt_account_rate_limited(int $user_id, string $scene): bool {
$key = 'aksrt_account_rate_' . sanitize_key($scene) . '_' . $user_id;
$count = (int) get_transient($key);
if ($count >= 8) {
return true;
}
set_transient($key, $count + 1, 10 * MINUTE_IN_SECONDS);
return false;
}
function aksrt_account_reset_rate(int $user_id, string $scene): void {
delete_transient('aksrt_account_rate_' . sanitize_key($scene) . '_' . $user_id);
}
function aksrt_account_email_verification_required(): bool {
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
return !empty($settings['email_set_captch']);
}
add_action('wp_ajax_aksrt_send_account_email_code', function (): void {
if (
!isset($_POST['_settings_nonce'])
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = get_current_user_id();
$user = wp_get_current_user();
if (!$user_id || !($user instanceof WP_User)) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$scene = sanitize_key((string) wp_unslash($_POST['scene'] ?? ''));
if (!in_array($scene, ['verify_email', 'change_email'], true)) {
wp_send_json_error(['message' => __('无效的验证场景', 'aksrt')]);
}
if (!aksrt_account_email_verification_required()) {
wp_send_json_error(['message' => __('邮箱验证码验证未开启', 'aksrt')]);
}
$target = $scene === 'verify_email'
? (string) $user->user_email
: sanitize_email((string) wp_unslash($_POST['new_email'] ?? ''));
if (!is_email($target)) {
wp_send_json_error(['message' => __('邮箱格式错误', 'aksrt')]);
}
if ($scene === 'change_email') {
if (strtolower($target) === strtolower((string) $user->user_email)) {
wp_send_json_error(['message' => __('修改邮箱不能与现邮箱相同', 'aksrt')]);
}
if (email_exists($target)) {
wp_send_json_error(['message' => __('该邮箱已有绑定账号', 'aksrt')]);
}
}
if (function_exists('aksrt_require_captcha')) {
$captcha = aksrt_require_captcha('change_email', wp_unslash($_POST));
if ($captcha !== true) {
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
}
}
$rate_key = 'aksrt_account_email_rate_' . $user_id . '_' . $scene . '_' . md5(strtolower($target));
if (get_transient($rate_key)) {
wp_send_json_error(['message' => __('操作过于频繁,请稍后再试', 'aksrt')]);
}
set_transient($rate_key, true, 60);
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$sent = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::send_event_email('change_code', $target, [
'{username}' => $user->display_name,
'{site_name}' => get_bloginfo('name'),
'{code}' => $code,
])
: false;
if (!$sent) {
delete_transient($rate_key);
wp_send_json_error(['message' => __('验证码邮件发送失败,请稍后重试', 'aksrt')]);
}
aksrt_account_store_code($user_id, $scene, $target, $code);
wp_send_json_success([
'message' => $scene === 'verify_email'
? __('验证码已发送到您的老邮箱', 'aksrt')
: __('验证码已发送到新邮箱', 'aksrt'),
'countdown' => 60,
]);
});
add_action('wp_ajax_aksrt_verify_account_email', function (): void {
if (
!isset($_POST['_settings_nonce'])
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = get_current_user_id();
$user = wp_get_current_user();
if (!$user_id || !($user instanceof WP_User) || !is_email((string) $user->user_email)) {
wp_send_json_error(['message' => __('当前账号未绑定有效邮箱', 'aksrt')]);
}
if (!aksrt_account_email_verification_required()) {
wp_send_json_error(['message' => __('邮箱验证码验证未开启', 'aksrt')]);
}
if (function_exists('aksrt_require_captcha')) {
$captcha = aksrt_require_captcha('change_email', wp_unslash($_POST));
if ($captcha !== true) {
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
}
}
$code = sanitize_text_field((string) wp_unslash($_POST['code'] ?? ''));
if (!aksrt_account_consume_code($user_id, 'verify_email', (string) $user->user_email, $code)) {
wp_send_json_error(['message' => __('验证码错误或已过期', 'aksrt')]);
}
aksrt_account_set_proof($user_id, 'email_old', (string) $user->user_email);
wp_send_json_success(['message' => __('验证成功', 'aksrt')]);
});
add_action('wp_ajax_aksrt_change_password', function (): void {
if (
!isset($_POST['_settings_nonce'])
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = get_current_user_id();
if (!$user_id) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$user = get_userdata($user_id);
if (!($user instanceof WP_User)) {
wp_send_json_error(['message' => __('账号信息读取失败,请刷新页面重试', 'aksrt')]);
}
$oauth_new = (bool) get_user_meta($user_id, 'oauth_new', true);
$password = trim((string) wp_unslash($_POST['password'] ?? ''));
$password2 = trim((string) wp_unslash($_POST['password2'] ?? ''));
if (function_exists('aksrt_require_captcha')) {
$captcha = aksrt_require_captcha('change_password', wp_unslash($_POST));
if ($captcha !== true) {
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
}
}
if (!$oauth_new) {
$old_password = trim((string) wp_unslash($_POST['passwordold'] ?? ''));
if ($old_password === '') {
wp_send_json_error(['message' => __('请输入原密码', 'aksrt')]);
}
if ($old_password === $password) {
wp_send_json_error(['message' => __('新密码和原密码不能相同', 'aksrt')]);
}
if (aksrt_account_rate_limited($user_id, 'change_password')) {
wp_send_json_error(['message' => __('错误次数过多,请稍后再试', 'aksrt')]);
}
if (!wp_check_password($old_password, $user->user_pass, $user_id)) {
wp_send_json_error(['message' => __('原密码错误', 'aksrt')]);
}
}
$valid = class_exists('\AKSRT\Auth\Service')
? \AKSRT\Auth\Service::validate_password($password, $password2)
: __('认证服务不可用', 'aksrt');
if ($valid !== true) {
wp_send_json_error(['message' => (string) $valid]);
}
$result = wp_update_user(['ID' => $user_id, 'user_pass' => $password]);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
delete_user_meta($user_id, 'oauth_new');
aksrt_account_reset_rate($user_id, 'change_password');
wp_send_json_success([
'message' => $oauth_new
? __('密码设置成功', 'aksrt')
: __('修改成功,下次请使用新密码登录', 'aksrt'),
'reload' => true,
]);
});
// ============================================================
// AJAX: 发送邮箱验证码(修改密码/更改邮箱通用)
// ============================================================
if (!function_exists('aksrt_get_email_code_key')) :
function aksrt_get_email_code_key(int $user_id, string $scene): string {
return 'aksrt_email_code_' . $scene . '_' . $user_id;
add_action('wp_ajax_aksrt_change_email', function (): void {
if (
!isset($_POST['_settings_nonce'])
|| !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_settings_nonce'])), 'aksrt_account_settings')
) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
endif;
if (!function_exists('aksrt_get_email_code_rate_key')) :
function aksrt_get_email_code_rate_key(int $user_id, string $scene, string $target = ''): string {
$suffix = $target !== '' ? '_' . md5(strtolower($target)) : '';
return 'aksrt_email_code_rate_' . $scene . '_' . $user_id . $suffix;
}
endif;
if (!function_exists('aksrt_store_email_code')) :
function aksrt_store_email_code(int $user_id, string $scene, string $code, string $target = ''): void {
set_transient(
aksrt_get_email_code_key($user_id, $scene),
[
'hash' => wp_hash_password($code),
'target' => $target,
'issued_at' => time(),
],
5 * MINUTE_IN_SECONDS
);
}
endif;
if (!function_exists('aksrt_consume_email_code')) :
function aksrt_consume_email_code(int $user_id, string $scene, string $code, string $target = ''): bool {
$key = aksrt_get_email_code_key($user_id, $scene);
$stored = get_transient($key);
if (!is_array($stored) || empty($stored['hash']) || !wp_check_password($code, $stored['hash'])) {
return false;
}
if ($target !== '' && strtolower((string) ($stored['target'] ?? '')) !== strtolower($target)) {
return false;
}
delete_transient($key);
return true;
$user_id = get_current_user_id();
$user = wp_get_current_user();
if (!$user_id || !($user instanceof WP_User)) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
endif;
add_action('wp_ajax_aksrt_send_email_code', function () {
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
}
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
}
$scene = isset( $_POST['scene'] ) ? sanitize_key( (string) $_POST['scene'] ) : '';
if ( ! in_array( $scene, array( 'change_password', 'change_email' ), true ) ) {
wp_send_json_error( array( 'message' => __( '无效的验证场景', 'aksrt' ) ) );
}
if ( function_exists( 'aksrt_require_captcha' ) ) {
$captcha_result = aksrt_require_captcha( $scene, wp_unslash( $_POST ) );
if ( $captcha_result !== true ) {
wp_send_json_error( aksrt_account_captcha_error_data( $captcha_result ) );
$new_email = sanitize_email((string) wp_unslash($_POST['new_email'] ?? ''));
if (!is_email($new_email)) {
wp_send_json_error(['message' => __('邮箱格式错误', 'aksrt')]);
}
if ($user->user_email !== '' && strtolower($new_email) === strtolower((string) $user->user_email)) {
wp_send_json_error(['message' => __('修改邮箱不能与现邮箱相同', 'aksrt')]);
}
$current_user = wp_get_current_user();
$target_email = '';
$success_message = '';
if ( $scene === 'change_password' ) {
$target_email = $current_user->user_email;
if ( empty( $target_email ) || ! is_email( $target_email ) ) {
wp_send_json_error( array( 'message' => __( '当前账号未绑定有效邮箱', 'aksrt' ) ) );
}
$success_message = __( '验证码已发送到您的注册邮箱', 'aksrt' );
} else {
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
}
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '请输入有效的新邮箱地址', 'aksrt' ) ) );
}
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
}
if (email_exists($new_email)) {
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
}
$target_email = $new_email;
$success_message = __( '验证码已发送到新邮箱,请完成验证后再提交', 'aksrt' );
}
// 频率限制:60秒内不能重复发送
$rate_key = aksrt_get_email_code_rate_key( $current_user->ID, $scene, $target_email );
if ( get_transient( $rate_key ) ) {
wp_send_json_error( array( 'message' => __( '操作过于频繁,请稍后再试', 'aksrt' ) ) );
}
set_transient( $rate_key, true, 60 );
// 生成验证码并同步发送,只有真正发信成功后才写入验证码缓存。
$code = str_pad( (string) random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );
$site_name = get_bloginfo( 'name' );
$sent = \AKSRT\Settings\Settings::send_event_email(
'change_code',
$target_email,
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{code}' => $code ]
);
if ( ! $sent ) {
delete_transient( $rate_key );
error_log( '[AKSRT] 验证码邮件发送失败 user_id=' . $current_user->ID . ' email=' . $target_email . ' scene=' . $scene );
wp_send_json_error( array( 'message' => __( '验证码邮件发送失败,请稍后重试', 'aksrt' ) ) );
}
aksrt_store_email_code( $current_user->ID, $scene, $code, $target_email );
wp_send_json_success( array( 'message' => $success_message ) );
});
// ============================================================
// AJAX: 修改密码
// ============================================================
add_action('wp_ajax_aksrt_change_password', function () {
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
wp_send_json_error(['message' => __('该邮箱已有绑定账号', 'aksrt')]);
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
$agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false];
if (!empty($agreement['required']) && empty($_POST['agree_terms'])) {
wp_send_json_error(['message' => __('请先同意相关协议', 'aksrt')]);
}
$old_password = isset( $_POST['old_password'] ) ? trim( wp_unslash( $_POST['old_password'] ) ) : '';
$new_password = isset( $_POST['new_password'] ) ? trim( wp_unslash( $_POST['new_password'] ) ) : '';
$new_password_confirm = isset( $_POST['new_password_confirm'] ) ? trim( wp_unslash( $_POST['new_password_confirm'] ) ) : '';
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
// 验证原密码
$user = get_userdata( $user_id );
if ( ! $user || ! wp_check_password( $old_password, $user->data->user_pass, $user_id ) ) {
wp_send_json_error( array( 'message' => __( '原密码不正确', 'aksrt' ) ) );
}
// 验证新密码强度
if ( mb_strlen( $new_password ) < 6 ) {
wp_send_json_error( array( 'message' => __( '新密码长度至少6位', 'aksrt' ) ) );
}
if ( mb_strlen( $new_password ) > 128 ) {
wp_send_json_error( array( 'message' => __( '新密码过长', 'aksrt' ) ) );
}
if ( $new_password !== $new_password_confirm ) {
wp_send_json_error( array( 'message' => __( '两次输入的新密码不一致', 'aksrt' ) ) );
}
// 人机验证
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha( 'change_password', wp_unslash( $_POST ) );
if ( $captcha_result !== true ) {
wp_send_json_error( aksrt_account_captcha_error_data( $captcha_result ) );
$captcha = aksrt_require_captcha('change_email', wp_unslash($_POST));
if ($captcha !== true) {
wp_send_json_error(aksrt_account_captcha_error_data($captcha));
}
}
// 验证邮箱验证码
if ( ! aksrt_consume_email_code( $user_id, 'change_password', $email_code, $user->user_email ) ) {
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
$verification_required = aksrt_account_email_verification_required();
$old_email = (string) $user->user_email;
if (
$old_email !== ''
&& $verification_required
&& !aksrt_account_consume_proof($user_id, 'email_old', $old_email, false)
) {
wp_send_json_error(['message' => __('请先验证老邮箱', 'aksrt')]);
}
// 设置新密码
wp_set_password( $new_password, $user_id );
if ( class_exists( 'WP_Session_Tokens' ) ) {
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
if (
$verification_required
&& !aksrt_account_consume_code(
$user_id,
'change_email',
$new_email,
sanitize_text_field((string) wp_unslash($_POST['code'] ?? '')),
false
)
) {
wp_send_json_error(['message' => __('验证码错误或已过期', 'aksrt')]);
}
// 清除登录态
wp_clear_auth_cookie();
wp_send_json_success( array(
'message' => __( '密码修改成功,即将跳转到登录页…', 'aksrt' ),
'redirect_to' => aksrt_get_login_url(),
));
});
// ============================================================
// AJAX: 更改邮箱
// ============================================================
add_action('wp_ajax_aksrt_change_email', function () {
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
}
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
$current_user = wp_get_current_user();
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
}
// 验证新邮箱格式
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '请输入有效的邮箱地址', 'aksrt' ) ) );
}
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
}
// 检查邮箱是否已被使用
if ( email_exists( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
$result = wp_update_user(['ID' => $user_id, 'user_email' => $new_email]);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message() ?: __('邮箱更新失败,请稍后重试', 'aksrt')]);
}
// 人机验证
if ( function_exists( 'aksrt_require_captcha' ) ) {
$captcha_result = aksrt_require_captcha( 'change_email', wp_unslash( $_POST ) );
if ( $captcha_result !== true ) {
wp_send_json_error( aksrt_account_captcha_error_data( $captcha_result ) );
if ($old_email !== '' && $verification_required) {
delete_transient(aksrt_account_proof_key($user_id, 'email_old'));
}
if ($verification_required) {
delete_transient(aksrt_account_code_key($user_id, 'change_email', $new_email));
}
// 验证邮箱验证码
if ( ! aksrt_consume_email_code( $user_id, 'change_email', $email_code, $new_email ) ) {
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
update_user_meta($user_id, 'aksrt_email_verified', 1);
if ($old_email !== '' && is_email($old_email) && class_exists('\AKSRT\Settings\Settings')) {
\AKSRT\Settings\Settings::send_event_email('change_notice', $old_email, [
'{username}' => $user->display_name,
'{site_name}' => get_bloginfo('name'),
'{new_email}' => $new_email,
]);
}
// 更新邮箱
$old_email = $current_user->user_email;
$update_result = wp_update_user( [
'ID' => $user_id,
'user_email' => $new_email,
wp_send_json_success([
'message' => $old_email !== '' ? __('邮箱修改成功', 'aksrt') : __('邮箱绑定成功', 'aksrt'),
'reload' => true,
]);
if ( is_wp_error( $update_result ) ) {
wp_send_json_error( array( 'message' => $update_result->get_error_message() ?: __( '邮箱更新失败,请稍后重试', 'aksrt' ) ) );
}
if ( is_email( $old_email ) ) {
$site_name = get_bloginfo( 'name' );
\AKSRT\Settings\Settings::send_event_email(
'change_notice',
$old_email,
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{new_email}' => $new_email ]
);
}
$verify_url = '';
$verify_message = '';
update_user_meta( $user_id, 'aksrt_email_verified', 1 );
if ( class_exists( 'WP_Session_Tokens' ) ) {
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
}
// 清除登录态
wp_clear_auth_cookie();
wp_send_json_success( array(
'message' => $verify_message !== ''
? __( '邮箱修改成功,请前往新邮箱完成验证后重新登录…', 'aksrt' )
: __( '邮箱修改成功,即将跳转到登录页…', 'aksrt' ),
'redirect_to' => $verify_url !== '' ? $verify_url : aksrt_get_login_url(),
'verify_url' => $verify_url,
));
});
+5
View File
@@ -205,6 +205,11 @@ class UserController extends ControllerBase {
'callback' => [$this, 'user_phone_code'],
'permission_callback' => static function () { return is_user_logged_in(); },
]);
register_rest_route(self::API_NAMESPACE, '/user/phone-verify', [
'methods' => 'POST',
'callback' => [$this, 'user_verify_phone'],
'permission_callback' => static function () { return is_user_logged_in(); },
]);
register_rest_route(self::API_NAMESPACE, '/user/phone', [
'methods' => 'POST',
'callback' => [$this, 'user_bind_phone'],
+62 -13
View File
@@ -80,7 +80,7 @@ trait UserControllerAuthPhoneOperations {
], $ttl);
}
private function auth_consume_code(string $scene, string $target, string $code): bool {
private function auth_consume_code(string $scene, string $target, string $code, bool $consume = true): bool {
$key = $this->auth_code_key($scene, $target);
$stored = get_transient($key);
if (!is_array($stored) || empty($stored['hash'])) {
@@ -101,7 +101,9 @@ trait UserControllerAuthPhoneOperations {
set_transient($key, $stored, $remaining);
return false;
}
if ($consume) {
delete_transient($key);
}
return true;
}
@@ -459,38 +461,85 @@ trait UserControllerAuthPhoneOperations {
public function user_phone_code(\WP_REST_Request $request): \WP_REST_Response {
$payload = $this->auth_payload($request);
if (!$this->auth_verify_rest_nonce($request, $payload)) {
return $this->error(__('安全验证失败', 'aksrt'), 403);
}
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403);
$stage = sanitize_key((string) ($payload['stage'] ?? 'bind_new_phone'));
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) {
return $this->error(__('手机号格式不正确', 'aksrt'));
if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) return $this->error(__('手机号格式不正确', 'aksrt'));
$user_id = get_current_user_id();
$current_phone = (string) get_user_meta($user_id, 'phone_number', true);
if ($stage === 'verify_old_phone') {
if ($current_phone === '' || !hash_equals($current_phone, $phone)) return $this->error(__('当前绑定手机号已变化,请刷新页面重试', 'aksrt'), 409);
} elseif ($stage === 'bind_new_phone') {
if ($current_phone !== '' && !hash_equals($current_phone, $phone)) {
$proof = get_transient('aksrt_account_phone_old_' . $user_id);
if (!is_array($proof) || !hash_equals($current_phone, (string) ($proof['phone'] ?? ''))) return $this->error(__('请先验证老手机号', 'aksrt'), 403);
}
$users = get_users(['meta_key' => 'phone_number', 'meta_value' => $phone, 'number' => 1, 'fields' => 'ID']);
if (!empty($users) && (int) $users[0] !== $user_id) return $this->error(__('该手机号已被其他账号绑定', 'aksrt'), 409);
if ($current_phone !== '' && hash_equals($current_phone, $phone)) return $this->error(__('手机号不能与当前绑定号码相同', 'aksrt'), 409);
} else {
return $this->error(__('无效的验证步骤', 'aksrt'));
}
$captcha_error = $this->auth_require_captcha('bind_phone', $payload);
if ($captcha_error) return $captcha_error;
if (!class_exists('\AKSRT\Sms\SMS')) return $this->error(__('短信服务不可用', 'aksrt'), 503);
$rate_subject = $stage . '|' . $user_id . '|' . $phone;
if ($this->auth_rate_limited('account-phone-code', $rate_subject, 5, HOUR_IN_SECONDS)) return $this->error(__('验证码请求过于频繁,请稍后再试', 'aksrt'), 429);
$code = (string) random_int(100000, 999999);
$result = \AKSRT\Sms\SMS::get_instance()->send_code($phone, $code);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('发送失败', 'aksrt')));
}
$this->auth_store_code('bind_phone', $phone, $code, 30 * MINUTE_IN_SECONDS);
if (empty($result['success'])) return $this->error((string) ($result['message'] ?? __('发送失败', 'aksrt')));
$this->auth_store_code('account_' . $stage . '_' . $user_id, $phone, $code, 30 * MINUTE_IN_SECONDS);
return $this->success(['message' => __('验证码短信已发送', 'aksrt'), 'countdown' => (int) ($result['remaining_time'] ?? 60)]);
}
public function user_verify_phone(\WP_REST_Request $request): \WP_REST_Response {
$payload = $this->auth_payload($request);
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403);
$captcha_error = $this->auth_require_captcha('bind_phone', $payload);
if ($captcha_error) return $captcha_error;
$user_id = get_current_user_id();
$current_phone = (string) get_user_meta($user_id, 'phone_number', true);
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
$code = sanitize_text_field((string) ($payload['code'] ?? ''));
if ($current_phone === '' || !hash_equals($current_phone, $phone)) return $this->error(__('当前绑定手机号已变化,请刷新页面重试', 'aksrt'), 409);
if (!$this->auth_consume_code('account_verify_old_phone_' . $user_id, $current_phone, $code)) return $this->error(__('验证码错误或已过期', 'aksrt'));
set_transient('aksrt_account_phone_old_' . $user_id, ['phone' => $current_phone], 30 * MINUTE_IN_SECONDS);
return $this->success(['message' => __('手机号验证成功', 'aksrt')]);
}
public function user_bind_phone(\WP_REST_Request $request): \WP_REST_Response {
$payload = $this->auth_payload($request);
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
$captcha_error = $this->auth_require_captcha('bind_phone', $payload);
if ($captcha_error) return $captcha_error;
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403);
$user_id = get_current_user_id();
$current_phone = (string) get_user_meta($user_id, 'phone_number', true);
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
$code = sanitize_text_field((string) ($payload['code'] ?? ''));
if (!$this->auth_consume_code('bind_phone', $phone, $code)) {
return $this->error(__('验证码错误或已过期', 'aksrt'));
if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) return $this->error(__('手机号格式不正确', 'aksrt'));
if ($current_phone !== '') {
$proof = get_transient('aksrt_account_phone_old_' . $user_id);
if (!is_array($proof) || !hash_equals($current_phone, (string) ($proof['phone'] ?? ''))) return $this->error(__('请先验证老手机号', 'aksrt'), 403);
if (hash_equals($current_phone, $phone)) return $this->error(__('手机号不能与当前绑定号码相同', 'aksrt'), 409);
}
$users = get_users(['meta_key' => 'phone_number', 'meta_value' => $phone, 'number' => 1, 'fields' => 'ID']);
if (!empty($users) && (int) $users[0] !== $user_id) return $this->error(__('该手机号已被其他账号绑定', 'aksrt'), 409);
$agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false];
if (!empty($agreement['required']) && empty($payload['agree_terms'])) return $this->error(__('请先同意相关协议', 'aksrt'), 400);
$code_scene = 'account_bind_new_phone_' . $user_id;
if (!$this->auth_consume_code($code_scene, $phone, $code, false)) return $this->error(__('验证码错误或已过期', 'aksrt'));
if (!class_exists('\AKSRT\Auth\Service')) return $this->error(__('认证服务不可用', 'aksrt'), 500);
$result = \AKSRT\Auth\Service::bind_phone($payload, true);
return !empty($result['success']) ? $this->success(['message' => $result['message'] ?? __('绑定成功', 'aksrt')]) : $this->error((string) ($result['error'] ?? __('绑定失败', 'aksrt')));
if (empty($result['success'])) return $this->error((string) ($result['error'] ?? __('绑定失败', 'aksrt')));
delete_transient($this->auth_code_key($code_scene, $phone));
delete_transient('aksrt_account_phone_old_' . $user_id);
return $this->success(['message' => $result['message'] ?? __('绑定成功', 'aksrt')]);
}
public function test_sms_send(\WP_REST_Request $request): \WP_REST_Response {
@@ -0,0 +1,111 @@
'use strict';
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('account dialogs expose configured email steps, phone steps, and reference password branches', () => {
const template = read('inc/helpers/user-center-renderers/settings-account.php');
const javascript = read('assets/js/routes/user-center.js');
const css = read('assets/css/routes/user-center/settings.css');
assert.match(template, /\$email_verify_required = \$account_email !== '' && !empty\(\$user_settings\['email_set_captch'\]\)/);
assert.match(template, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"[\s\S]*?data-account-step="email-success"/);
assert.match(template, /if \(\$email_verify_required\)/);
assert.match(template, /if \(\$email_verify_required\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/);
assert.match(template, /if \(!empty\(\$user_settings\['email_set_captch'\]\)\)/);
assert.match(template, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"[\s\S]*?data-account-step="phone-success"/);
assert.match(template, /if \(\$account_phone !== ''\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/);
assert.match(template, /data-aksrt-sms-scene="verify_old_phone"/);
assert.match(template, /data-aksrt-sms-scene="bind_new_phone"/);
assert.match(template, /if \(!\$account_oauth_new\)[\s\S]*?name="passwordold"/);
assert.match(template, /if \(!\$account_oauth_new && \$can_recover_password\)/);
assert.match(template, /data-account-password-pane="recovery"/);
assert.match(javascript, /\/auth\/code/);
assert.match(javascript, /\/auth\/reset-password/);
assert.doesNotMatch(template + javascript + css, /pwd-strength|pwd-send-code|email-send-code/);
const emailConditionStart = template.indexOf('<?php if ($email_verify_required) : ?>');
const emailElse = template.indexOf('<?php else : ?>', emailConditionStart);
const emailEnd = template.indexOf('<?php endif; ?>', emailElse);
const emailSteps = template.slice(emailConditionStart, emailElse);
const emailDirect = template.slice(emailElse, emailEnd);
const phoneConditionStart = template.indexOf('<?php if ($account_phone !== \'\') : ?>');
const phoneElse = template.indexOf('<?php else : ?>', phoneConditionStart);
const phoneEnd = template.indexOf('<?php endif; ?>', phoneElse);
const phoneSteps = template.slice(phoneConditionStart, phoneElse);
const phoneDirect = template.slice(phoneElse, phoneEnd);
assert.match(emailSteps, /account-step-list/);
assert.doesNotMatch(emailDirect, /account-step-list|data-account-step=/);
assert.match(phoneSteps, /account-step-list/);
assert.doesNotMatch(phoneDirect, /account-step-list|data-account-step=/);
});
test('account PHP harness exercises email and password state transitions', () => {
const harness = path.join(root, 'tests', 'widget-system', 'account-settings-harness.php');
const output = execFileSync('php', [harness], { encoding: 'utf8' });
assert.match(output, /Account settings harness passed/);
});
test('email update keeps both proofs through validation failures and consumes them only after the write', () => {
const ajax = read('inc/modules/user-parts/account-ajax.php');
const send = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_send_account_email_code'"), ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"));
const verify = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"));
const change = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_email'"));
assert.match(send, /\['verify_email', 'change_email'\]/);
assert.match(send, /email_exists\(\$target\)/);
assert.match(verify, /aksrt_require_captcha\(\s*'change_email'/);
assert.match(verify, /aksrt_account_set_proof\(\$user_id, 'email_old'/);
assert.match(change, /aksrt_account_email_verification_required\(\)/);
assert.match(change, /aksrt_account_consume_proof\([^;]+false\)/);
assert.match(change, /aksrt_account_consume_code\([\s\S]*?false\s*\)/);
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_proof_key"));
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_code_key"));
assert.match(change, /\$agreement\['required'\]/);
assert.doesNotMatch(ajax, /wp_ajax_aksrt_send_email_code/);
});
test('phone replacement requires old-number proof and a new-number code while reusing bind side effects', () => {
const controller = read('src/Admin/UserControllerAuthPhoneOperations.php');
const routes = read('src/Admin/UserController.php');
const binding = read('src/Auth/ServicePart1b.php');
const code = controller.slice(controller.indexOf('public function user_phone_code('), controller.indexOf('public function user_verify_phone('));
const verify = controller.slice(controller.indexOf('public function user_verify_phone('), controller.indexOf('public function user_bind_phone('));
const bind = controller.slice(controller.indexOf('public function user_bind_phone('), controller.indexOf('public function test_sms_send('));
assert.match(routes, /\/user\/phone-verify[\s\S]*?user_verify_phone/);
assert.match(code, /verify_old_phone/);
assert.match(code, /bind_new_phone/);
assert.match(verify, /auth_consume_code\('account_verify_old_phone_' \./);
assert.match(verify, /aksrt_account_phone_old_/);
assert.match(bind, /aksrt_account_phone_old_/);
assert.match(bind, /auth_consume_code\(\$code_scene, \$phone, \$code, false\)/);
assert.ok(bind.indexOf("Auth\\Service::bind_phone") < bind.indexOf("delete_transient($this->auth_code_key($code_scene, $phone))"));
assert.match(bind, /aksrt_get_auth_agreement_view/);
assert.match(bind, /Auth\\Service::bind_phone\(\$payload, true\)/);
assert.match(binding, /aksrt_user_phone_changed/);
assert.doesNotMatch(controller, /auth_consume_code\('bind_phone'/);
});
test('password change retains old-password limits and captcha; recovery uses shared auth endpoints', () => {
const ajax = read('inc/modules/user-parts/account-ajax.php');
const javascript = read('assets/js/routes/user-center.js');
const password = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_email'"));
assert.match(password, /get_user_meta\(\$user_id, 'oauth_new', true\)/);
assert.match(password, /wp_check_password\(\$old/);
assert.match(password, /aksrt_account_rate_limited\(\$user_id, 'change_password'\)/);
assert.match(password, /aksrt_require_captcha\('change_password'/);
assert.match(password, /Auth\\Service::validate_password\(\$password, \$password2\)/);
assert.doesNotMatch(password, /email.*code|password.*strength/i);
assert.match(javascript, /scene: 'resetpassword', target: target, channel:/);
assert.match(javascript, /\/auth\/code/);
assert.match(javascript, /\/auth\/reset-password/);
assert.match(javascript, /data-recovery-channel/);
});
@@ -0,0 +1,281 @@
<?php
/** Account email and password transition contract harness. */
declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('MINUTE_IN_SECONDS', 60);
$GLOBALS['aksrt_account_settings'] = ['email_set_captch' => true];
$GLOBALS['aksrt_account_user_id'] = 7;
$GLOBALS['aksrt_account_user'] = null;
$GLOBALS['aksrt_account_meta'] = [];
$GLOBALS['aksrt_account_transients'] = [];
$GLOBALS['aksrt_account_actions'] = [];
$GLOBALS['aksrt_account_mail'] = [];
$GLOBALS['aksrt_account_existing_emails'] = [];
$GLOBALS['aksrt_account_captcha_calls'] = [];
$GLOBALS['aksrt_account_captcha_fail'] = false;
$GLOBALS['aksrt_account_update_fail'] = false;
final class WP_User {
public function __construct(
public int $ID,
public string $user_email,
public string $display_name,
public string $user_pass
) {}
}
final class WP_Error {
public function __construct(
private string $code = '',
private string $message = '',
private mixed $data = null
) {}
public function get_error_code(): string { return $this->code; }
public function get_error_message(): string { return $this->message; }
public function get_error_data(): mixed { return $this->data; }
}
final class JsonResponseException extends \RuntimeException {
public function __construct(public array $payload) { parent::__construct('json_response'); }
}
function add_action(string $hook, callable $callback): void {
$GLOBALS['aksrt_account_actions'][$hook] = $callback;
}
function __(string $text, string $domain = ''): string { return $text; }
function is_wp_error(mixed $value): bool { return $value instanceof WP_Error; }
function is_email(string $email): bool { return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); }
function sanitize_email(string $email): string { return trim($email); }
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
function sanitize_text_field(string $value): string { return trim(strip_tags($value)); }
function wp_unslash(mixed $value): mixed { return $value; }
function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'valid' && $action === 'aksrt_account_settings'; }
function get_current_user_id(): int { return (int) $GLOBALS['aksrt_account_user_id']; }
function wp_get_current_user(): mixed { return $GLOBALS['aksrt_account_user']; }
function get_userdata(int $user_id): ?WP_User { return $user_id === 7 ? $GLOBALS['aksrt_account_user'] : null; }
function get_bloginfo(string $show = ''): string { return 'AKSRT Test'; }
function wp_hash_password(string $password): string { return 'hash:' . $password; }
function wp_check_password(string $password, string $hash, int $user_id = 0): bool { return hash_equals('hash:' . $password, $hash); }
function email_exists(string $email): int|false { return $GLOBALS['aksrt_account_existing_emails'][strtolower($email)] ?? false; }
function set_transient(string $key, mixed $value, int $expiration = 0): bool {
$GLOBALS['aksrt_account_transients'][$key] = [
'value' => $value,
'expires' => $expiration > 0 ? time() + $expiration : 0,
];
return true;
}
function get_transient(string $key): mixed {
$row = $GLOBALS['aksrt_account_transients'][$key] ?? null;
if (!is_array($row)) return false;
if (!empty($row['expires']) && $row['expires'] <= time()) {
unset($GLOBALS['aksrt_account_transients'][$key]);
return false;
}
return $row['value'] ?? false;
}
function delete_transient(string $key): bool {
unset($GLOBALS['aksrt_account_transients'][$key]);
return true;
}
function get_user_meta(int $user_id, string $key, bool $single = false): mixed {
return $GLOBALS['aksrt_account_meta'][$user_id][$key] ?? '';
}
function update_user_meta(int $user_id, string $key, mixed $value): bool {
$GLOBALS['aksrt_account_meta'][$user_id][$key] = $value;
return true;
}
function delete_user_meta(int $user_id, string $key): bool {
unset($GLOBALS['aksrt_account_meta'][$user_id][$key]);
return true;
}
function wp_update_user(array $data): int|WP_Error {
if ($GLOBALS['aksrt_account_update_fail']) {
return new WP_Error('update_failed', 'update failed');
}
$user = $GLOBALS['aksrt_account_user'];
if (isset($data['user_email'])) $user->user_email = (string) $data['user_email'];
if (isset($data['user_pass'])) $user->user_pass = wp_hash_password((string) $data['user_pass']);
return (int) $user->ID;
}
function aksrt_require_captcha(string $scene, array $payload): true|WP_Error {
$GLOBALS['aksrt_account_captcha_calls'][] = $scene;
return $GLOBALS['aksrt_account_captcha_fail'] ? new WP_Error('captcha_failed', 'captcha failed') : true;
}
function aksrt_get_auth_agreement_view(): array { return ['enabled' => true, 'required' => true]; }
function wp_send_json_error(array $data): never { throw new JsonResponseException(['success' => false, 'data' => $data]); }
function wp_send_json_success(array $data): never { throw new JsonResponseException(['success' => true, 'data' => $data]); }
function account_settings_request(string $action, array $data = []): array {
$callback = $GLOBALS['aksrt_account_actions'][$action] ?? null;
if (!is_callable($callback)) throw new \RuntimeException('Missing AJAX action: ' . $action);
$_POST = array_merge(['_settings_nonce' => 'valid'], $data);
try {
$callback();
} catch (JsonResponseException $response) {
return $response->payload;
}
throw new \RuntimeException('AJAX action returned without a JSON response: ' . $action);
}
function account_settings_check(bool $condition, string $message): void {
if (!$condition) throw new \RuntimeException($message);
}
function account_settings_code_key(string $scene, string $target): string {
return 'aksrt_account_code_7_' . $scene . '_' . md5(strtolower(trim($target)));
}
function account_settings_rate_key(string $scene, string $target): string {
return 'aksrt_account_email_rate_7_' . $scene . '_' . md5(strtolower(trim($target)));
}
function account_settings_code(string $scene, string $target): string {
$stored = get_transient(account_settings_code_key($scene, $target));
account_settings_check(is_array($stored), 'expected verification code was not stored for ' . $scene);
return substr((string) ($stored['hash'] ?? ''), strlen('hash:'));
}
function account_settings_proof_key(): string {
return 'aksrt_account_proof_email_old_7';
}
require dirname(__DIR__, 2) . '/inc/modules/user-parts/account-ajax.php';
}
namespace AKSRT\Settings {
final class Settings {
public static function get_all(): array { return $GLOBALS['aksrt_account_settings']; }
public static function send_event_email(string $event, string $to, array $values): bool {
$GLOBALS['aksrt_account_mail'][] = ['event' => $event, 'to' => $to, 'values' => $values];
return true;
}
}
}
namespace AKSRT\Auth {
final class Service {
public static function validate_password(string $password, string $confirm = ''): true|string {
if (strlen($password) < 6) return 'password too short';
if ($confirm !== '' && $password !== $confirm) return 'password mismatch';
return true;
}
}
}
namespace {
$GLOBALS['aksrt_account_user'] = new WP_User(7, 'old@example.test', 'Test User', 'hash:OldPass123');
$send_action = 'wp_ajax_aksrt_send_account_email_code';
$verify_action = 'wp_ajax_aksrt_verify_account_email';
$change_email_action = 'wp_ajax_aksrt_change_email';
$change_password_action = 'wp_ajax_aksrt_change_password';
$saved_user = $GLOBALS['aksrt_account_user'];
$GLOBALS['aksrt_account_user'] = null;
account_settings_check(empty(account_settings_request($send_action, ['scene' => 'verify_email'])['success']), 'email-code request did not handle a missing user object');
account_settings_check(empty(account_settings_request($verify_action, ['code' => '123456'])['success']), 'old-email verification did not handle a missing user object');
account_settings_check(empty(account_settings_request($change_email_action, ['new_email' => 'new@example.test'])['success']), 'email update did not handle a missing user object');
account_settings_check(empty(account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123'])['success']), 'password update did not handle a missing user object');
$GLOBALS['aksrt_account_user'] = $saved_user;
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
account_settings_check(!empty($old_send['success']), 'old email verification code could not be sent');
$old_code = account_settings_code('verify_email', 'old@example.test');
$bad_old_code = account_settings_request($verify_action, ['code' => 'wrong']);
account_settings_check(empty($bad_old_code['success']), 'email verification accepted an incorrect old-email code');
$old_code_row = get_transient(account_settings_code_key('verify_email', 'old@example.test'));
account_settings_check(($old_code_row['attempts'] ?? 0) === 1, 'incorrect old-email attempt was not recorded');
$GLOBALS['aksrt_account_transients'][account_settings_code_key('verify_email', 'old@example.test')]['expires'] = time() - 1;
$expired_old_code = account_settings_request($verify_action, ['code' => $old_code]);
account_settings_check(empty($expired_old_code['success']), 'email verification accepted an expired old-email code');
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'expired old-email code was not discarded');
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired old-email code issued an old-email proof');
delete_transient(account_settings_rate_key('verify_email', 'old@example.test'));
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
account_settings_check(!empty($old_send['success']), 'old email code could not be resent after expiry');
$old_code = account_settings_code('verify_email', 'old@example.test');
$verified_old = account_settings_request($verify_action, ['code' => $old_code]);
account_settings_check(!empty($verified_old['success']), 'email verification rejected the correct old-email code');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'old-email proof was not issued');
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'valid old-email code was not consumed');
$new_email = 'new@example.test';
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
account_settings_check(!empty($new_send['success']), 'new email verification code could not be sent');
$new_code = account_settings_code('change_email', $new_email);
$GLOBALS['aksrt_account_existing_emails']['taken@example.test'] = 19;
$duplicate = account_settings_request($change_email_action, ['new_email' => 'taken@example.test', 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(empty($duplicate['success']), 'email update accepted an address already owned by another account');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'duplicate-email validation consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'duplicate-email validation consumed the new-email code');
unset($GLOBALS['aksrt_account_existing_emails']['taken@example.test']);
$bad_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => 'wrong', 'agree_terms' => '1']);
account_settings_check(empty($bad_new_code['success']), 'email update accepted an incorrect new-email code');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'incorrect-code validation consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'incorrect-code validation consumed the new-email code');
$GLOBALS['aksrt_account_transients'][account_settings_code_key('change_email', $new_email)]['expires'] = time() - 1;
$expired_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(empty($expired_new_code['success']), 'email update accepted an expired new-email code');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired new-email code validation consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'expired new-email code was not discarded');
delete_transient(account_settings_rate_key('change_email', $new_email));
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
account_settings_check(!empty($new_send['success']), 'new email code could not be resent after expiry');
$new_code = account_settings_code('change_email', $new_email);
$GLOBALS['aksrt_account_update_fail'] = true;
$write_failure = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(empty($write_failure['success']), 'email update reported success after the user write failed');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'failed database write consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'failed database write consumed the new-email code');
$GLOBALS['aksrt_account_update_fail'] = false;
$changed = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(!empty($changed['success']), 'email update rejected valid old and new proofs');
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $new_email, 'email update did not persist the new address');
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'successful update retained the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'successful update retained the new-email code');
account_settings_check((int) get_user_meta(7, 'aksrt_email_verified', true) === 1, 'successful update did not mark the email verified');
account_settings_check(end($GLOBALS['aksrt_account_mail'])['event'] === 'change_notice', 'successful update did not notify the previous email');
$GLOBALS['aksrt_account_settings']['email_set_captch'] = false;
$direct_email = account_settings_request($change_email_action, ['new_email' => 'direct@example.test', 'agree_terms' => '1']);
account_settings_check(!empty($direct_email['success']), 'email update without configured verification still required a code');
account_settings_check($GLOBALS['aksrt_account_user']->user_email === 'direct@example.test', 'direct email update did not persist');
$GLOBALS['aksrt_account_settings']['email_set_captch'] = true;
$GLOBALS['aksrt_account_user']->user_email = '';
$first_email = 'first@example.test';
$first_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $first_email]);
account_settings_check(!empty($first_send['success']), 'first email binding did not send a new-address code');
$first_code = account_settings_code('change_email', $first_email);
$first_bind = account_settings_request($change_email_action, ['new_email' => $first_email, 'code' => $first_code, 'agree_terms' => '1']);
account_settings_check(!empty($first_bind['success']), 'first email binding incorrectly required old-email verification');
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $first_email, 'first email binding did not persist');
$GLOBALS['aksrt_account_captcha_fail'] = true;
$captcha_rejected = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
account_settings_check(empty($captcha_rejected['success']), 'password change bypassed human verification');
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:OldPass123', 'captcha rejection changed the password');
$GLOBALS['aksrt_account_captcha_fail'] = false;
for ($attempt = 0; $attempt < 8; $attempt++) {
$wrong_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'wrong']);
account_settings_check(empty($wrong_password['success']), 'password change accepted an incorrect old password');
}
$limited_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
account_settings_check(empty($limited_password['success']), 'password change did not apply its old-password attempt limit');
delete_transient('aksrt_account_rate_change_password_7');
$changed_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
account_settings_check(!empty($changed_password['success']), 'password change rejected the correct old password');
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:NewPass123', 'password change did not persist the new password');
account_settings_check(get_transient('aksrt_account_rate_change_password_7') === false, 'successful password change did not reset its error limit');
update_user_meta(7, 'oauth_new', 1);
$oauth_password = account_settings_request($change_password_action, ['password' => 'OAuthPass123', 'password2' => 'OAuthPass123']);
account_settings_check(!empty($oauth_password['success']), 'OAuth first-time password setup incorrectly required an old password');
account_settings_check(get_user_meta(7, 'oauth_new', true) === '', 'OAuth first-time password setup retained the new-account marker');
echo "Account settings harness passed\n";
}
@@ -42,16 +42,16 @@ test('password and email account flows verify captcha once per distinct HTTP req
return account.slice(start, end === -1 ? account.length : end);
};
const send = methodBody('wp_ajax_aksrt_send_email_code', 'wp_ajax_aksrt_change_password');
const send = methodBody('wp_ajax_aksrt_send_account_email_code', 'wp_ajax_aksrt_verify_account_email');
const password = methodBody('wp_ajax_aksrt_change_password', 'wp_ajax_aksrt_change_email');
const email = methodBody('wp_ajax_aksrt_change_email');
assert.equal((send.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(send, /aksrt_require_captcha\( \$scene,/);
assert.match(send, /aksrt_require_captcha\(\s*'change_email'/);
assert.equal((password.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(password, /aksrt_require_captcha\( 'change_password'/);
assert.match(password, /aksrt_require_captcha\(\s*'change_password'/);
assert.equal((email.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(email, /aksrt_require_captcha\( 'change_email'/);
assert.match(email, /aksrt_require_captcha\(\s*'change_email'/);
});
test('legacy public SMS-code AJAX surface is removed while SMS service remains', () => {
@@ -59,6 +59,5 @@ test('legacy public SMS-code AJAX surface is removed while SMS service remains',
assert.doesNotMatch(sms, /aksrt_send_sms_code/);
assert.doesNotMatch(sms, /function ajax_send_code\s*\(/);
assert.match(sms, /public function send_code\(string \$phone\): array/);
assert.match(sms, /public function verify_code\(string \$phone, string \$code\): array/);
assert.match(sms, /public function send_code\(string \$phone, string \$code\): array/);
});
@@ -193,7 +193,7 @@ test('auth and account security flows consume fresh captcha tickets', () => {
const center = read('assets/js/routes/user-center.js');
assert.match(center, /runCaptcha\('bind_phone', form\)[\s\S]*\/user\/phone-code/);
assert.match(center, /runCaptcha\('bind_phone', form\)[\s\S]*\/user\/phone/);
assert.match(center, /runCaptcha\(scene, fm\)[\s\S]*resetCaptcha\(scene, fm\)/);
assert.match(center, /function postAjaxForm\(form, action, scene, success\)[\s\S]*?runCaptcha\(scene, form\)[\s\S]*?finally\(function \(\) \{ resetCaptcha\(scene, form\); \}\)/);
assert.match(center, /\/oauth\/bind-pass/);
assert.match(center, /bind_verification_required:\s*'请先完成人机验证后再绑定'/);
});
@@ -104,6 +104,7 @@ test('wallet reward content opens in the shared modal runtime once', () => {
const authorBar = read('templates/parts/single-author-bar.php');
const singleActions = read('inc/helpers/aksrt-single-render.php');
const scripts = read('src/Core/ScriptsPart1b.php');
const scriptsPart1c = read('src/Core/ScriptsPart1c.php');
assert.match(reward, /runtime\.open\(\{/);
assert.match(reward, /className: 'aksrt-reward-modal modal-mini full-sm'/);
@@ -122,7 +123,8 @@ test('wallet reward content opens in the shared modal runtime once', () => {
assert.match(helper, /data-aksrt-reward-submit/);
assert.match(authorBar, /aksrt_render_rewards_template_for_user/);
assert.match(singleActions, /aksrt_render_rewards_template_for_user/);
assert.match(scripts, /'needs_reward'\s*=>\s*'single-post\/rewards'/);
assert.match(scriptsPart1c, /\$requirements\['needs_reward'\]\s*=\s*\$is_article && !empty\(\$settings\['rewards_enabled'\]\)/);
assert.match(scriptsPart1c, /\$singular_assets\['needs_reward'\][\s\S]*?\$reward_js[\s\S]*?'aksrt-reward'/);
assert.doesNotMatch(helper + authorBar + singleActions, /aksrt_render_rewards_modal_for_user|reward-modal__qr/);
});
@@ -490,8 +490,9 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="password"/);
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="email"/);
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="phone"/);
const settingsSubmitButtons = settingsAccount.match(/<button type="submit" class="but jb-blue padding-lg btn-block"[^>]*><i class="fa fas fa-check" aria-hidden="true"><\/i> <span data-aksrt-settings-submit-label><\?php esc_html_e\('确认提交', 'aksrt'\); \?><\/span><\/button>/g) || [];
assert.equal(settingsSubmitButtons.length, 3, 'password, email, and phone dialogs use the reference submit button with the FA5 icon');
assert.match(settingsAccount, /id="pwd-form"[\s\S]*?data-aksrt-settings-submit-label/);
assert.match(settingsAccount, /id="email-new-form"[\s\S]*?but jb-blue padding-lg btn-block/);
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?but jb-blue padding-lg btn-block/);
assert.doesNotMatch(settingsAccount, /aksrt-settings-modal__footer|确认修改|确认更改|确认绑定/);
assert.ok(settingsAccount.indexOf('绑定邮箱') < settingsAccount.indexOf('绑定手机'));
assert.ok(settingsAccount.indexOf('绑定手机') < settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'"));
@@ -504,13 +505,20 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
assert.match(userCenterJs, /className: 'aksrt-user-settings-modal modal-mini'/);
assert.match(userCenterJs, /mobileBottom: true/);
assert.match(userCenterJs, /onContent: function \(_, session\)/);
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(shell\)/);
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(event\.detail && event\.detail\.shell\)/);
assert.match(userCenterJs, /resetCaptcha\(scene, form\)/);
assert.match(userCenterJs, /function setSettingsSubmitLabel\(button, text\)[\s\S]*?label\.textContent = text/);
assert.match(userCenterJs, /setSettingsSubmitLabel\(submitBtn, submitIdleText\)/);
assert.match(userCenterJs, /aksrt_change_password/);
assert.match(userCenterJs, /aksrt_change_email/);
assert.match(userCenterJs, /AksrtModalRuntime\.close\('settings-submit'\)/);
assert.match(settingsAccount, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"/);
assert.match(settingsAccount, /\$email_verify_required/);
assert.match(settingsAccount, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"/);
assert.match(settingsAccount, /data-account-recovery-open[\s\S]*?data-account-password-pane="recovery"/);
assert.match(settingsAccount, /aksrt_render_captcha\('change_password'\)/);
assert.match(userCenterJs, /\/auth\/code/);
assert.match(userCenterJs, /\/auth\/reset-password/);
assert.match(userCenterJs, /\/user\/phone-verify/);
assert.doesNotMatch(settingsAccount + userCenterJs + settingsCss, /pwd-strength|pwd-send-code|email-send-code|aksrt_send_email_code/);
assert.match(settingsCss, /\.aksrt-modal-runtime\.aksrt-user-settings-modal:not\(\.aksrt-modal-runtime--bottom\)[\s\S]*?width: 400px/);
assert.match(settingsCss, /\.aksrt-settings-modal \.modal-colorful-header[\s\S]*?height: 120px/);
assert.match(settingsAccount, /class="colorful-make"/);
+8 -13
View File
@@ -340,30 +340,25 @@ test('settings actions use rehydrated fixed-id notyf flows', () => {
const context = read('inc/helpers/aksrt-user-center.php');
const page = read('templates/pages/page-profile.php');
[
'aksrt-settings-password-code-toast',
'aksrt-settings-email-code-toast',
'aksrt-settings-password-toast',
'aksrt-settings-email-toast',
'aksrt-settings-oauth-unbind-toast',
'aksrt-settings-phone-bind-toast',
].forEach((id) => assert.match(userCenter, new RegExp("['\"]" + id + "['\"]")));
assert.match(userCenter, /aksrt-settings-oauth-unbind-toast/);
assert.doesNotMatch(userCenter, /aksrt-settings-(?:password|email|phone)-(?:code-)?toast/);
assert.match(userCenter, /notyf\([^;]+?'load'[^;]+?toastId\);/);
assert.match(userCenter, /notyf\([^;]+?'success'[^;]+?toastId\);/);
assert.match(userCenter, /notyf\([^;]+?'danger'[^;]+?toastId\);/);
assert.match(userCenter, /auto_fun\.aksrt-user-center'[\s\S]*?initSettings\(\);[\s\S]*?initBindPhone\(\);/);
assert.match(userCenter, /auto_fun\.aksrt-user-center'[\s\S]*?initSettings\(\);[\s\S]*?initSettingsCallbackToast\(\);/);
assert.doesNotMatch(userCenter, /initBindPhone\(/);
assert.match(userCenter, /aksrtSettingsInitialized/);
assert.match(userCenter, /oauth_action[\s\S]*?notyf\(message, type\)[\s\S]*?history\.replaceState/);
assert.doesNotMatch(userCenter, /oauth_unbound/);
assert.match(auth, /\[data-identity-root\], \[data-aksrt-settings-root\]/);
assert.doesNotMatch(auth, /\.settings-section/);
assert.match(userCenter, /'aksrt-settings-phone-code-toast'/);
assert.match(userCenter, /notyf\('正在发送验证码…', 'load', '', toastId\)/);
assert.match(userCenter, /notyf\([^;]+?'success', 5, toastId\)/);
assert.match(userCenter, /notyf\([^;]+?'danger', 5, toastId\)/);
assert.match(userCenter, /\/user\/phone-code/);
assert.match(userCenter, /\/user\/phone-verify/);
const accountSettings = read('inc/helpers/user-center-renderers/settings-account.php');
assert.match(accountSettings, /data-aksrt-sms-scene="verify_old_phone"/);
assert.match(accountSettings, /data-aksrt-sms-scene="bind_new_phone"/);
assert.match(settings, /require \$dir \. '\/settings-account\.php';/);
assert.doesNotMatch(context, /account_modals_html|'account-modals'/);
@@ -198,27 +198,31 @@ test('native user profile omits ledger tables and renders canonical images eager
assert.doesNotMatch(level, /Level::get_badge_html/);
});
test('identity status, type, and name are directly editable on the native profile', () => {
test('identity marker and profile are editable on the native profile while applications use the review queue', () => {
const auth = read('src/User/AuthApply.php');
const verify = read('src/Admin/Native/UsersMenusVerify.php');
const transitions = read('src/Identity/TransitionService.php');
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'handle_admin_profile_update'\]\)/);
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'save_admin_profile'\]\)/);
assert.match(auth, /current_user_can\('aksrt_review_identity'\)/);
assert.match(auth, /wp_nonce_field\('aksrt_identity_profile_' \.[\s\S]*?'aksrt_identity_profile_nonce'\)/);
assert.match(auth, /name="aksrt_identity_application_uuid"/);
assert.match(auth, /name="aksrt_identity_type"/);
assert.match(auth, /value="personal"[\s\S]*?个人认证/);
assert.match(auth, /value="enterprise"[\s\S]*?企业认证/);
assert.match(auth, /name="aksrt_identity_public_name"/);
assert.match(auth, /姓名 \/ 主体全称/);
assert.match(auth, /name="aksrt_identity_status"/);
assert.match(auth, /'manual_pending' => __\('待审核'/);
assert.match(auth, /'verified' => __\('已认证'/);
assert.match(auth, /'rejected' => __\('已驳回'/);
assert.match(auth, /create_admin_application\(\$repo, \$user_id, \$fields\)/);
assert.match(auth, /'type' => \$fields\['type'\]/);
assert.match(auth, /self::review\(\$user_id, \$application_uuid, \$target_status, \$note, get_current_user_id\(\), \$fields\)/);
assert.match(transitions, /public function admin_transition/);
assert.match(transitions, /\? in_array\(\$next, \['manual_pending', 'verified', 'rejected', 'cancelled'\], true\)/);
assert.match(transitions, /'verified' => \['cancelled'\]/);
assert.match(auth, /wp_nonce_field\('aksrt_auth_profile_' \.[\s\S]*?'aksrt_auth_profile_nonce'\)/);
assert.match(auth, /name="aksrt_auth_enabled"/);
assert.match(auth, /name="aksrt_auth_name"/);
assert.match(auth, /name="aksrt_auth_desc"/);
assert.match(auth, /name="aksrt_auth_time"/);
assert.match(auth, /update_user_meta\(\$user_id, 'auth', 1\)/);
assert.match(auth, /update_user_meta\(\$user_id, 'auth_info'/);
assert.doesNotMatch(auth, /name="aksrt_identity_(?:application_uuid|type|status|public_name)"/);
assert.match(verify, /'general',[\s\S]*?'manual'/);
assert.match(verify, /if \(\$status === 'manual_pending'\)/);
assert.match(verify, /'verified' => __\('通过申请'/);
assert.match(verify, /'rejected' => __\('拒绝申请'/);
assert.match(verify, /AuthApply::review\(\$user_id, \$uuid, \$status, \$note, get_current_user_id\(\), \$fields\)/);
assert.match(auth, /\['verified', 'rejected'\]/);
assert.match(auth, /\(string\) \$application\['status'\] !== 'manual_pending'/);
assert.match(transitions, /'manual_pending' => 'pending'/);
assert.match(transitions, /'verified' => 'verified'/);
assert.match(transitions, /'rejected' => 'rejected'/);
assert.match(transitions, /default => ''/);
});
@@ -5,10 +5,13 @@ declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('MINUTE_IN_SECONDS', 60);
define('HOUR_IN_SECONDS', 3600);
$GLOBALS['aksrt_profile_meta'] = [];
$GLOBALS['aksrt_profile_users'] = [];
$GLOBALS['aksrt_nonce_valid'] = true;
$GLOBALS['aksrt_can_edit_user'] = true;
$GLOBALS['aksrt_current_user_id'] = 7;
$GLOBALS['aksrt_transients'] = [];
$GLOBALS['aksrt_expected_sms_code'] = '246810';
@@ -34,7 +37,7 @@ namespace {
function is_admin(): bool { return true; }
function current_user_can(string $capability, ...$args): bool { return $GLOBALS['aksrt_can_edit_user']; }
function is_user_logged_in(): bool { return true; }
function get_current_user_id(): int { return 7; }
function get_current_user_id(): int { return (int) ($GLOBALS['aksrt_current_user_id'] ?? 7); }
function get_user_meta(int $user_id, string $key, bool $single = false) {
$value = $GLOBALS['aksrt_profile_meta'][$user_id][$key] ?? '';
return $single ? $value : ($value === '' ? [] : [$value]);
@@ -48,7 +51,7 @@ namespace {
foreach ($GLOBALS['aksrt_profile_meta'] as $user_id => $meta) {
if (in_array((int) $user_id, array_map('intval', $args['exclude'] ?? []), true)) continue;
if (($meta[$args['meta_key'] ?? ''] ?? null) !== ($args['meta_value'] ?? null)) continue;
$results[] = ($args['fields'] ?? '') === 'ids' ? (int) $user_id : new WP_User((int) $user_id);
$results[] = in_array(($args['fields'] ?? ''), ['ids', 'ID'], true) ? (int) $user_id : new WP_User((int) $user_id);
if (count($results) >= (int) ($args['number'] ?? 1)) break;
}
return $results;
@@ -99,6 +102,12 @@ namespace AKSRT\Sms {
}
}
namespace AKSRT\Settings {
final class Settings {
public static function get_all(): array { return ['bind_phone' => true]; }
}
}
namespace AKSRT\Auth {
require dirname(__DIR__, 2) . '/src/Auth/ServicePart1b.php';
final class PhoneHarnessService { use ServicePart1b; }
@@ -150,10 +159,25 @@ namespace {
check(empty($same_phone['success']), 'binding accepted the current phone as a change');
$phone_to_bind = '13800000010';
$otp_key = 'aksrt_auth_code_' . md5('bind_phone|' . strtolower(trim($phone_to_bind)));
$otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($phone_to_bind)));
$operations = new \AKSRT\Admin\PhoneOperationsHarness();
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$old_phone = '13800000007';
$old_otp_key = 'aksrt_auth_code_' . md5('account_verify_old_phone_7|' . strtolower(trim($old_phone)));
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$GLOBALS['aksrt_expected_sms_code'] = '246810';
$wrong_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '000000']));
check(empty($wrong_old_code->data['success']), 'REST phone replacement accepted an incorrect old-number code');
check(($GLOBALS['aksrt_transients'][$old_otp_key]['attempts'] ?? 0) === 1, 'incorrect old-number code attempt was not recorded');
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() - 1];
$expired_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810']));
check(empty($expired_old_code->data['success']), 'REST phone replacement accepted an expired old-number code');
check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'expired old-number code was not discarded');
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$verified_old_phone = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810']));
check(!empty($verified_old_phone->data['success']), 'REST phone replacement rejected the correct old-number code');
check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'valid old-number code was not consumed');
check(($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']['phone'] ?? '') === $old_phone, 'old-number proof was not issued for the current account');
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$wrong_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '000000']));
check(empty($wrong_code->data['success']), 'REST phone binding accepted an incorrect SMS code');
check(($GLOBALS['aksrt_transients'][$otp_key]['attempts'] ?? 0) === 1, 'incorrect SMS code attempt was not recorded');
@@ -161,11 +185,29 @@ namespace {
$expired_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
check(empty($expired_code->data['success']), 'REST phone binding accepted an expired SMS code');
check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'expired SMS code was not discarded');
$duplicate_phone = '13800000008';
$duplicate_otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($duplicate_phone)));
$GLOBALS['aksrt_transients'][$duplicate_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$duplicate_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $duplicate_phone, 'code' => '246810']));
check(empty($duplicate_code->data['success']), 'REST phone binding accepted a number owned by another user');
check(isset($GLOBALS['aksrt_transients'][$duplicate_otp_key]), 'duplicate-number validation consumed the valid SMS code');
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$valid_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
check(!empty($valid_code->data['success']), 'REST phone binding rejected the correct SMS code');
check(get_user_meta(7, 'phone_number', true) === $phone_to_bind, 'REST phone binding did not store the canonical phone meta');
check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'valid SMS code was not consumed');
check(!isset($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']), 'successful phone change did not consume the old-number proof');
$initial_bind_user_id = 9;
$initial_bind_phone = '13800000011';
$initial_bind_code_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_9|' . strtolower(trim($initial_bind_phone)));
$GLOBALS['aksrt_current_user_id'] = $initial_bind_user_id;
$GLOBALS['aksrt_transients'][$initial_bind_code_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
$initial_bind = $operations->user_bind_phone(new WP_REST_Request(['phone' => $initial_bind_phone, 'code' => '246810']));
check(!empty($initial_bind->data['success']), 'first-time phone binding incorrectly required an old-number proof');
check(get_user_meta($initial_bind_user_id, 'phone_number', true) === $initial_bind_phone, 'first-time phone binding did not store the canonical phone meta');
check(!isset($GLOBALS['aksrt_transients'][$initial_bind_code_key]), 'successful first-time phone binding did not consume its SMS code');
$GLOBALS['aksrt_current_user_id'] = 7;
$_POST = [
'aksrt_profile_admin_nonce' => 'ok',