!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);