feat: 手动实现表单邮箱验证码功能
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
<?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_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);
|
||||
}
|
||||
|
||||
$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);
|
||||
@@ -63,6 +63,10 @@ function itstudio_join_get_default_settings() {
|
||||
'photo_public_notice' => 0,
|
||||
'photo_inactive' => 0,
|
||||
'signup_form_shortcode' => '',
|
||||
'otp_enabled' => 0,
|
||||
'otp_form_id' => '',
|
||||
'otp_email_field_id' => '',
|
||||
'otp_code_field_id' => '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,6 +183,10 @@ function itstudio_join_sanitize_settings($input) {
|
||||
'result_second_interview_records' => itstudio_join_sanitize_records_value($input['result_second_interview_records'] ?? ''),
|
||||
'result_admission_records' => itstudio_join_sanitize_records_value($input['result_admission_records'] ?? ''),
|
||||
'signup_form_shortcode' => itstudio_join_sanitize_shortcode_value($input['signup_form_shortcode'] ?? ''),
|
||||
'otp_enabled' => !empty($input['otp_enabled']) ? 1 : 0,
|
||||
'otp_form_id' => absint($input['otp_form_id'] ?? 0),
|
||||
'otp_email_field_id' => absint($input['otp_email_field_id'] ?? 0),
|
||||
'otp_code_field_id' => absint($input['otp_code_field_id'] ?? 0),
|
||||
);
|
||||
|
||||
foreach (itstudio_join_get_photo_field_map() as $field_key) {
|
||||
@@ -2385,6 +2393,23 @@ function itstudio_join_render_settings_page() {
|
||||
<p class="description">示例:<code>[formidable id="12"]</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">邮箱 OTP 验证</th>
|
||||
<td>
|
||||
<label for="itstudio_join_otp_enabled">
|
||||
<input type="checkbox" id="itstudio_join_otp_enabled" name="itstudio_join_settings[otp_enabled]" value="1" <?php checked(!empty($settings['otp_enabled'])); ?>>
|
||||
在报名表单提交前启用邮箱验证码
|
||||
</label>
|
||||
<p class="description">启用后,前台会在指定验证码字段旁自动显示“发送验证码”按钮,不需要手动添加 HTML 按钮。</p>
|
||||
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_form_id">Formidable 表单 ID</label></p>
|
||||
<input type="number" min="1" step="1" id="itstudio_join_otp_form_id" name="itstudio_join_settings[otp_form_id]" value="<?php echo esc_attr((string) ($settings['otp_form_id'] ?? '')); ?>" class="small-text">
|
||||
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_email_field_id">邮箱字段 ID</label></p>
|
||||
<input type="number" min="1" step="1" id="itstudio_join_otp_email_field_id" name="itstudio_join_settings[otp_email_field_id]" value="<?php echo esc_attr((string) ($settings['otp_email_field_id'] ?? '')); ?>" class="small-text">
|
||||
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_code_field_id">验证码字段 ID</label></p>
|
||||
<input type="number" min="1" step="1" id="itstudio_join_otp_code_field_id" name="itstudio_join_settings[otp_code_field_id]" value="<?php echo esc_attr((string) ($settings['otp_code_field_id'] ?? '')); ?>" class="small-text">
|
||||
<p class="description">验证码字段建议使用 Short Text,设置为仅允许数字、最大长度 6。邮箱字段必须是 Email 类型。验证码有效期 10 分钟,同一邮箱 60 秒内不能重复发送,最多尝试 5 次。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button('保存设置'); ?>
|
||||
</form>
|
||||
|
||||
@@ -23,7 +23,9 @@ define('ITSTUDIO_RECRUITMENT_PATH', plugin_dir_path(__FILE__));
|
||||
|
||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/recruitment-articles.php';
|
||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/join-system.php';
|
||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/email-otp.php';
|
||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/terms.php';
|
||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/formidable-async-patch.php';
|
||||
|
||||
function itstudio_recruitment_activate() {
|
||||
flush_rewrite_rules();
|
||||
|
||||
@@ -777,3 +777,7 @@ function itstudio_join_fallback() {
|
||||
exit;
|
||||
}
|
||||
add_action('template_redirect', 'itstudio_join_fallback', 9);
|
||||
if (file_exists(get_template_directory() . '/includes/formidable-async-patch.php')) {
|
||||
require_once get_template_directory() . '/includes/formidable-async-patch.php';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user