Files
ITStudioMainSite/plugins/itstudio-recruitment/includes/email-otp.php
T

288 lines
11 KiB
PHP

<?php
/**
* 招新报名表单邮箱 OTP:前台发码、限流、服务端校验和一次性消费。
*/
if (!defined('ABSPATH')) {
exit;
}
function itstudio_join_otp_get_config() {
$settings = itstudio_join_get_settings();
$form_id = absint($settings['otp_form_id'] ?? 0);
if ($form_id <= 0 && preg_match('/\[formidable\s+[^\]]*\bid=["\']?(\d+)/i', (string) ($settings['signup_form_shortcode'] ?? ''), $matches)) {
$form_id = absint($matches[1]);
}
return array(
'enabled' => !empty($settings['otp_enabled']),
'form_id' => $form_id,
'email_field_id' => absint($settings['otp_email_field_id'] ?? 0),
'code_field_id' => absint($settings['otp_code_field_id'] ?? 0),
'ttl' => 10 * MINUTE_IN_SECONDS,
'cooldown' => 60,
'max_attempts' => 5,
);
}
function itstudio_join_otp_is_configured() {
$config = itstudio_join_otp_get_config();
return !empty($config['enabled'])
&& $config['form_id'] > 0
&& $config['email_field_id'] > 0
&& $config['code_field_id'] > 0;
}
function itstudio_join_otp_normalize_email($email) {
return strtolower(trim(sanitize_email((string) $email)));
}
function itstudio_join_otp_get_client_ip() {
return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : '';
}
function itstudio_join_otp_key($email, $form_id) {
return 'itstudio_join_otp_' . md5((string) $form_id . '|' . itstudio_join_otp_normalize_email($email));
}
function itstudio_join_otp_ip_key($ip) {
return 'itstudio_join_otp_ip_' . md5((string) $ip);
}
function itstudio_join_otp_hash_code($email, $code) {
return hash_hmac('sha256', itstudio_join_otp_normalize_email($email) . '|' . (string) $code, wp_salt('auth'));
}
function itstudio_join_otp_random_code() {
return (string) wp_rand(100000, 999999);
}
function itstudio_join_otp_email_exists_in_form($form_id, $field_id, $email) {
global $wpdb;
$form_id = absint($form_id);
$field_id = absint($field_id);
$email = itstudio_join_otp_normalize_email($email);
if ($form_id <= 0 || $field_id <= 0 || $email === '') {
return false;
}
$items_table = $wpdb->prefix . 'frm_items';
$metas_table = $wpdb->prefix . 'frm_item_metas';
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(1)
FROM {$metas_table} AS meta
INNER JOIN {$items_table} AS item ON item.id = meta.item_id
WHERE item.form_id = %d
AND item.is_draft = 0
AND meta.field_id = %d
AND LOWER(TRIM(meta.meta_value)) = %s",
$form_id,
$field_id,
$email
)
);
return (int) $count > 0;
}
function itstudio_join_otp_json_error($message_cn, $message_en, $status = 400, $extra = array()) {
wp_send_json_error(
array_merge(
array(
'message_cn' => $message_cn,
'message_en' => $message_en,
),
$extra
),
$status
);
}
function itstudio_join_otp_send_ajax() {
if (!check_ajax_referer('itstudio_join_otp', 'nonce', false)) {
itstudio_join_otp_json_error('请求已过期,请刷新页面后重试。', 'Request expired. Please refresh the page and try again.', 403);
}
$config = itstudio_join_otp_get_config();
if (!itstudio_join_otp_is_configured()) {
itstudio_join_otp_json_error('邮箱验证码尚未完成配置,请联系管理员。', 'Email verification is not configured yet. Please contact the administrator.', 503);
}
$email = itstudio_join_otp_normalize_email(isset($_POST['email']) ? wp_unslash($_POST['email']) : '');
if ($email === '' || !is_email($email)) {
itstudio_join_otp_json_error('请输入有效的邮箱地址。', 'Please enter a valid email address.', 400);
}
if (itstudio_join_otp_email_exists_in_form($config['form_id'], $config['email_field_id'], $email)) {
itstudio_join_otp_json_error('该邮箱已经报名,无需重复提交。', 'This email has already been used to apply.', 409);
}
$ip = itstudio_join_otp_get_client_ip();
$otp_key = itstudio_join_otp_key($email, $config['form_id']);
$existing = get_transient($otp_key);
$now = time();
if (is_array($existing) && !empty($existing['last_sent']) && ($now - (int) $existing['last_sent']) < $config['cooldown']) {
$wait = max(1, $config['cooldown'] - ($now - (int) $existing['last_sent']));
itstudio_join_otp_json_error("请等待 {$wait} 秒后再发送。", "Please wait {$wait} seconds before requesting another code.", 429, array('wait' => $wait));
}
if ($ip !== '') {
$ip_key = itstudio_join_otp_ip_key($ip);
$ip_last_sent = (int) get_transient($ip_key);
if ($ip_last_sent > 0 && ($now - $ip_last_sent) < 10) {
$wait = max(1, 10 - ($now - $ip_last_sent));
itstudio_join_otp_json_error("请求过于频繁,请等待 {$wait} 秒。", "Too many requests. Please wait {$wait} seconds.", 429, array('wait' => $wait));
}
set_transient($ip_key, $now, 10);
}
$code = itstudio_join_otp_random_code();
$payload = array(
'code_hash' => itstudio_join_otp_hash_code($email, $code),
'expires' => $now + $config['ttl'],
'last_sent' => $now,
'attempts' => 0,
'form_id' => $config['form_id'],
'email' => $email,
);
set_transient($otp_key, $payload, $config['ttl']);
$site_name = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
$subject = sprintf('%s 报名邮箱验证码', $site_name !== '' ? $site_name : '爱特工作室');
$message = "您好!\n\n您的报名邮箱验证码是:{$code}\n\n验证码 10 分钟内有效,且只能使用一次。如非本人操作,请忽略此邮件。\n\n{$site_name}";
$sent = wp_mail($email, $subject, $message, array('Content-Type: text/plain; charset=UTF-8'));
if (!$sent) {
delete_transient($otp_key);
itstudio_join_otp_json_error('邮件发送失败,请稍后重试。', 'The email could not be sent. Please try again later.', 500);
}
wp_send_json_success(
array(
'message_cn' => '验证码已发送,请检查邮箱及垃圾邮件。',
'message_en' => 'Verification code sent. Please check your inbox.',
'cooldown' => $config['cooldown'],
'ttl' => $config['ttl'],
)
);
}
add_action('wp_ajax_itstudio_join_otp_send', 'itstudio_join_otp_send_ajax');
add_action('wp_ajax_nopriv_itstudio_join_otp_send', 'itstudio_join_otp_send_ajax');
function itstudio_join_otp_validate_field($errors, $posted_field, $posted_value) {
if (!is_object($posted_field)) {
return $errors;
}
$config = itstudio_join_otp_get_config();
if (!itstudio_join_otp_is_configured()
|| (int) ($posted_field->form_id ?? 0) !== $config['form_id']
|| (int) ($posted_field->id ?? 0) !== $config['code_field_id']) {
return $errors;
}
$email = itstudio_join_otp_normalize_email(isset($_POST['item_meta'][$config['email_field_id']]) ? wp_unslash($_POST['item_meta'][$config['email_field_id']]) : '');
$code = is_scalar($posted_value) ? trim((string) $posted_value) : '';
$field_key = 'field' . $config['code_field_id'];
if ($email === '' || !is_email($email)) {
$errors[$field_key] = '请输入有效的邮箱并先获取验证码。';
return $errors;
}
if (!preg_match('/^\d{6}$/', $code)) {
$errors[$field_key] = '请输入 6 位数字验证码。';
return $errors;
}
$otp_key = itstudio_join_otp_key($email, $config['form_id']);
$payload = get_transient($otp_key);
if (!is_array($payload) || empty($payload['code_hash']) || empty($payload['expires'])) {
$errors[$field_key] = '验证码不存在或已失效,请重新获取。';
return $errors;
}
if (time() > (int) $payload['expires']) {
delete_transient($otp_key);
$errors[$field_key] = '验证码已过期,请重新获取。';
return $errors;
}
if ((int) ($payload['attempts'] ?? 0) >= $config['max_attempts']) {
delete_transient($otp_key);
$errors[$field_key] = '验证码尝试次数过多,请重新获取。';
return $errors;
}
if (!hash_equals((string) $payload['code_hash'], itstudio_join_otp_hash_code($email, $code))) {
$payload['attempts'] = (int) ($payload['attempts'] ?? 0) + 1;
$remaining = max(0, (int) $payload['expires'] - time());
if ($remaining > 0) {
set_transient($otp_key, $payload, $remaining);
}
$errors[$field_key] = '验证码错误,请重试。';
return $errors;
}
$payload['verified_at'] = time();
$remaining = max(1, (int) $payload['expires'] - time());
set_transient($otp_key, $payload, $remaining);
return $errors;
}
add_filter('frm_validate_field_entry', 'itstudio_join_otp_validate_field', 20, 3);
function itstudio_join_otp_consume_after_entry($entry_id, $form_id) {
$config = itstudio_join_otp_get_config();
if (!itstudio_join_otp_is_configured() || (int) $form_id !== $config['form_id']) {
return;
}
$email = itstudio_join_otp_normalize_email(isset($_POST['item_meta'][$config['email_field_id']]) ? wp_unslash($_POST['item_meta'][$config['email_field_id']]) : '');
if ($email !== '' && is_email($email)) {
delete_transient(itstudio_join_otp_key($email, $config['form_id']));
}
}
add_action('frm_after_create_entry', 'itstudio_join_otp_consume_after_entry', 20, 2);
function itstudio_join_otp_enqueue_assets() {
if (!itstudio_is_join_page_context() || !itstudio_join_otp_is_configured() || !shortcode_exists('formidable')) {
return;
}
$config = itstudio_join_otp_get_config();
wp_enqueue_style(
'itstudio-join-otp',
plugins_url('../assets/otp.css', __FILE__),
array('itstudio-join-page'),
ITSTUDIO_RECRUITMENT_VERSION
);
wp_enqueue_script(
'itstudio-join-otp',
plugins_url('../assets/otp.js', __FILE__),
array(),
ITSTUDIO_RECRUITMENT_VERSION,
true
);
wp_localize_script(
'itstudio-join-otp',
'itstudioJoinOtp',
array(
'formId' => $config['form_id'],
'emailFieldId' => $config['email_field_id'],
'codeFieldId' => $config['code_field_id'],
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('itstudio_join_otp'),
'cooldown' => $config['cooldown'],
'labels' => array(
'send' => '发送验证码',
'resend' => '重新发送 (%s秒)',
'sending' => '发送中…',
'emailRequired' => '请先填写邮箱地址。',
'invalidEmail' => '请输入有效的邮箱地址。',
'networkError' => '网络错误,请稍后重试。',
),
)
);
}
add_action('wp_enqueue_scripts', 'itstudio_join_otp_enqueue_assets', 40);