request->url(true); return function_exists('cms_lang_url') ? cms_lang_url($url) : $url; } protected function normalizeLang(string $lang): string { $lang = strtolower(trim($lang)); if ($lang === '' && function_exists('cms_current_region_lang')) { $lang = strtolower(trim((string)cms_current_region_lang('cn'))); } return $lang !== '' ? $lang : 'cn'; } protected function getInput(string $name, int $maxLength = 255): string { $value = trim((string)$this->request->post($name . '/s', '')); $value = strip_tags($value); $value = str_replace(["\0", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D", "\x1E", "\x1F", "\x7F"], '', $value); return mb_substr($value, 0, $maxLength, 'UTF-8'); } protected function validPhone(string $phone): bool { $length = mb_strlen($phone, 'UTF-8'); if ($length < 6 || $length > 40) { return false; } $digits = 0; foreach (preg_split('//u', $phone, -1, PREG_SPLIT_NO_EMPTY) as $char) { if ($char >= '0' && $char <= '9') { $digits++; continue; } if (!in_array($char, ['+', '-', ' ', '(', ')'], true)) { return false; } } return $digits >= 6; } protected function validLooseText(string $value, int $maxLength): bool { if (mb_strlen($value, 'UTF-8') > $maxLength) { return false; } foreach (['<', '>', '`', '{', '}', '[', ']', '$', '\\'] as $char) { if (strpos($value, $char) !== false) { return false; } } return true; } protected function validFormToken(string $token): bool { $expected = md5(session_id() . 'cooperation_lead'); return $token !== '' && hash_equals($expected, $token); } protected function validOption(string $type, string $value, string $lang): bool { if ($value === '') { return false; } return CooperationOption::where('type', $type) ->where('value', $value) ->where('lang', 'in', [$lang, 'cn']) ->where('status', 1) ->count() > 0; } protected function isDuplicateLead(string $email, string $phone): bool { $dayAgo = time() - 86400; return CooperationLead::where(function ($query) use ($email, $phone) { $query->where('email', $email)->whereOr('phone', $phone); })->where('createtime', '>=', $dayAgo)->count() > 0; } protected function duplicateLeadMessage(bool $isCn): string { return $isCn ? '您已提交过信息,我们会尽快与您联系,请勿重复提交。' : 'You have already submitted your information. We will contact you soon. Please do not submit repeatedly.'; } protected function checkSubmitLimit(string $email, string $phone): bool { $ip = (string)$this->request->ip(); $now = time(); $minuteAgo = $now - 60; $hourAgo = $now - 3600; $dayAgo = $now - 86400; if (CooperationLead::where('ip', $ip)->where('createtime', '>=', $minuteAgo)->count() >= 1) { return false; } if (CooperationLead::where('ip', $ip)->where('createtime', '>=', $hourAgo)->count() >= 5) { return false; } if (CooperationLead::where('ip', $ip)->where('createtime', '>=', $dayAgo)->count() >= 20) { return false; } if (CooperationLead::where('email', $email)->where('createtime', '>=', $hourAgo)->count() >= 3) { return false; } if (CooperationLead::where('phone', $phone)->where('createtime', '>=', $hourAgo)->count() >= 3) { return false; } return true; } protected function riskCacheKey(string $suffix): string { return 'cooperation_risk_' . md5((string)$this->request->ip() . '_' . $suffix); } protected function requestCount(string $key, int $ttl): int { $count = (int)Cache::get($key, 0) + 1; Cache::set($key, $count, $ttl); return $count; } protected function riskSnapshot(string $email = '', string $phone = '', bool $countRequest = false): array { $ip = (string)$this->request->ip(); $now = time(); $minuteAgo = $now - 60; $hourAgo = $now - 3600; $dayAgo = $now - 86400; $minuteRequests = $countRequest ? $this->requestCount($this->riskCacheKey('minute'), 60) : (int)Cache::get($this->riskCacheKey('minute'), 0); $dayRequests = $countRequest ? $this->requestCount($this->riskCacheKey('day'), 86400) : (int)Cache::get($this->riskCacheKey('day'), 0); $ipMinuteLeads = CooperationLead::where('ip', $ip)->where('createtime', '>=', $minuteAgo)->count(); $ipHourLeads = CooperationLead::where('ip', $ip)->where('createtime', '>=', $hourAgo)->count(); $ipDayLeads = CooperationLead::where('ip', $ip)->where('createtime', '>=', $dayAgo)->count(); $emailHourLeads = $email !== '' ? CooperationLead::where('email', $email)->where('createtime', '>=', $hourAgo)->count() : 0; $phoneHourLeads = $phone !== '' ? CooperationLead::where('phone', $phone)->where('createtime', '>=', $hourAgo)->count() : 0; $needCaptcha = $minuteRequests >= 3 || $dayRequests >= 20 || $ipMinuteLeads >= 1 || $ipHourLeads >= 3 || $ipDayLeads >= 10 || $emailHourLeads >= 2 || $phoneHourLeads >= 2; return [ 'need_captcha' => $needCaptcha ? 1 : 0, 'minute_requests' => $minuteRequests, 'day_requests' => $dayRequests, 'ip_minute_leads' => $ipMinuteLeads, 'ip_hour_leads' => $ipHourLeads, 'ip_day_leads' => $ipDayLeads, 'email_hour_leads' => $emailHourLeads, 'phone_hour_leads' => $phoneHourLeads, ]; } protected function needCaptcha(string $email = '', string $phone = '', bool $countRequest = false): bool { $risk = $this->riskSnapshot($email, $phone, $countRequest); return !empty($risk['need_captcha']); } protected function checkCaptcha(bool $isCn): void { $verify = trim((string)$this->request->post('verify/s', '')); if ($verify === '') { $this->error($isCn ? '请输入验证码' : 'Please enter the verification code', null, ['need_captcha' => 1]); } if (!Captcha::check($verify)) { $this->error($isCn ? '验证码错误,请重新输入' : 'Incorrect verification code, please try again', null, ['need_captcha' => 1]); } } public function captcha() { return Captcha::create(); } protected function containsSpam(string $text): bool { $lower = mb_strtolower($text, 'UTF-8'); $badWords = ['http://', 'https://', 'normalizeLang((string)$this->request->param('lang/s', '')); $this->success('ok', null, [ 'lang' => $lang, 'countries' => CooperationOption::getOptions('country', $lang), 'country_regions' => CooperationOption::getOptions('country_region', $lang), 'country_tree' => CooperationOption::getCountryTree($lang), 'industries' => CooperationOption::getOptions('industry', $lang), 'need_captcha' => $this->needCaptcha() ? 1 : 0, ]); } public function submit() { if (!$this->request->isPost()) { $this->error('请求方式错误'); } $name = $this->getInput('name', 80); $phone = $this->getInput('phone', 40); $email = $this->getInput('email', 120); $company = $this->getInput('company', 120); $country = $this->getInput('country', 100); $industry = $this->getInput('industry', 100); $content = $this->getInput('content', 300); $sourcePage = trim((string)$this->request->post('source_page/s', $this->frontendCurrentUrl())); $sourcePage = mb_substr($sourcePage, 0, 255, 'UTF-8'); $lang = $this->normalizeLang((string)$this->request->post('lang/s', '')); $privacyAgree = (int)$this->request->post('privacy_agree/d', 0); $formToken = trim((string)$this->request->post('form_token/s', '')); $honeypot = trim((string)$this->request->post('website/s', '')); $isCn = in_array($lang, ['cn', 'zh', 'zh-cn'], true); $needCaptcha = $this->needCaptcha($email, $phone, true); if ($needCaptcha) { $this->checkCaptcha($isCn); } if ($name === '') { $this->error($isCn ? '请填写姓名' : 'Please enter your name'); } if (!$this->validLooseText($name, 80) || !$this->validLooseText($company, 120)) { $this->error($isCn ? '输入内容包含不支持的字符' : 'Unsupported characters detected'); } if ($phone === '' || !$this->validPhone($phone)) { $this->error($isCn ? '请填写正确的电话' : 'Please enter a valid phone number'); } if ($email === '' || !Validate::is($email, 'email')) { $this->error($isCn ? '电子邮箱格式不正确' : 'Please enter a valid email address'); } if ($company === '') { $this->error($isCn ? '请填写公司名称' : 'Please enter your company name'); } if ($country === '') { $this->error($isCn ? '请选择国家/地区' : 'Please select a country/region'); } if ($industry === '') { $this->error($isCn ? '请选择所在行业' : 'Please select your industry'); } if ($content === '') { $this->error($isCn ? '请填写咨询内容' : 'Please enter your inquiry'); } if ($privacyAgree !== 1) { $this->error($isCn ? '请先同意隐私政策' : 'Please agree to the privacy policy first'); } if ($this->isDuplicateLead($email, $phone)) { $this->success($this->duplicateLeadMessage($isCn), null, [ 'duplicate' => 1, 'lang' => $lang, ]); } if ($honeypot !== '') { $this->error($isCn ? '提交失败,请稍后再试' : 'Submission failed, please try again later'); } if (!$this->validFormToken($formToken)) { $this->error($isCn ? '页面已过期,请刷新后重试' : 'The page has expired, please refresh and try again'); } if (!$this->checkSubmitLimit($email, $phone)) { $this->error($isCn ? '提交过于频繁,请稍后再试' : 'Too many submissions, please try again later'); } if (!$this->validOption('country', $country, $lang)) { $this->error($isCn ? '请选择有效的国家/地区' : 'Please choose a valid country/region'); } if (!$this->validOption('industry', $industry, $lang)) { $this->error($isCn ? '请选择有效的行业' : 'Please choose a valid industry'); } if (mb_strlen($content, 'UTF-8') > 300) { $this->error($isCn ? '咨询内容请控制在300字以内' : 'Please keep your message within 300 characters'); } if ($this->containsSpam($content . ' ' . $name . ' ' . $company)) { $this->error($isCn ? '提交内容未通过安全校验' : 'The submitted content did not pass security validation'); } $ip = (string)$this->request->ip(); $userAgent = mb_substr((string)$this->request->server('HTTP_USER_AGENT'), 0, 255, 'UTF-8'); $ipLimitWindowSeconds = 60; $ipLimitWindowMax = 10; $ipLimitDailyMax = 50; $now = time(); $dayKey = date('Ymd', $now); $ipWindowKey = 'cooperation_submit_ip_window_' . md5($ip); $ipDailyKey = 'cooperation_submit_ip_daily_' . md5($ip . '_' . $dayKey); $ipWindowHits = (int)Cache::get($ipWindowKey, 0); if ($ipWindowHits >= $ipLimitWindowMax) { $this->error($isCn ? '提交过于频繁,请稍后再试' : 'Too many submissions. Please try again later.'); } $ipDailyHits = (int)Cache::get($ipDailyKey, 0); if ($ipDailyHits >= $ipLimitDailyMax) { $this->error($isCn ? '今日提交次数已达上限,请明天再试' : 'Daily submission limit reached. Please try again tomorrow.'); } Cache::set($ipWindowKey, $ipWindowHits + 1, $ipLimitWindowSeconds); Cache::set($ipDailyKey, $ipDailyHits + 1, 86400); Db::startTrans(); try { $lead = new CooperationLead(); $lead->save([ 'name' => $name, 'phone' => $phone, 'email' => $email, 'company' => $company, 'country' => $country, 'industry' => $industry, 'content' => $content, 'lang' => $lang, 'source_page' => $sourcePage, 'ip' => $ip, 'user_agent' => $userAgent, 'privacy_agree' => 1, 'privacy_agree_time' => $now, 'privacy_agree_ip' => $ip, 'status' => 0, 'admin_remark' => '', 'createtime' => $now, 'updatetime' => $now, ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); $this->error(($isCn ? '提交失败:' : 'Submission failed: ') . $e->getMessage()); } $this->success($isCn ? '提交成功,我们会尽快与您联系' : 'Submitted successfully. We will contact you soon.', null, [ 'id' => $lead->id, 'lang' => $lang, ]); } }