use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;
use super::{parse_detection_result, CaptchaInfo, Detector};
pub struct MathCaptchaDetector;
impl MathCaptchaDetector {
pub const JS: &'static str = r#"(function() {
// Tier 1: explicit input names + dedicated containers.
const direct = document.querySelector(
"input[name='math_captcha'], input[id*='math_captcha'], input[name='captcha-result'], .math-captcha-question, .cptch_input"
);
if (direct) {
return { kind: 'custom:wp_math_captcha', site_key: null, container: 'input[name="math_captcha"]' };
}
// Tier 2: textual arithmetic prompt + a numeric input nearby.
const equationRe = /\b\d+\s*[+\-×−x\*]\s*\d+\s*=/;
const candidates = document.querySelectorAll(
".equation, .captcha-equation, [class*='equation'], [class*='math'], [data-captcha-question]"
);
for (const c of candidates) {
const text = (c.textContent || '').trim();
if (equationRe.test(text)) {
return { kind: 'custom:wp_math_captcha', site_key: null, container: '.equation' };
}
}
// Tier 3: scan the whole document body for an arithmetic
// prompt — bounded to the first 4KB so a 50MB SPA doesn't
// become a regex bomb.
const body = (document.body && document.body.innerText) || '';
if (equationRe.test(body.slice(0, 4096))) {
const numericInput = document.querySelector(
"input[type='number'], input[type='text'][name*='captcha' i]"
);
if (numericInput) {
return { kind: 'custom:wp_math_captcha', site_key: null, container: 'input[type=number]' };
}
}
return { kind: 'none', site_key: null, container: null };
})()"#;
}
#[async_trait]
impl Detector for MathCaptchaDetector {
fn name(&self) -> &'static str {
"math_captcha"
}
fn priority(&self) -> i32 {
80
}
async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
let raw = page.evaluate(Self::JS).await?;
let val = raw.into_value::<serde_json::Value>()?;
Ok(parse_detection_result(val))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn js_matches_explicit_math_input_name() {
assert!(MathCaptchaDetector::JS.contains("input[name='math_captcha']"));
assert!(MathCaptchaDetector::JS.contains("input[name='captcha-result']"));
assert!(MathCaptchaDetector::JS.contains(".math-captcha-question"));
assert!(MathCaptchaDetector::JS.contains(".cptch_input"));
}
#[test]
fn js_returns_custom_kind() {
assert!(MathCaptchaDetector::JS.contains("custom:wp_math_captcha"));
}
#[test]
fn js_includes_textual_equation_regex() {
assert!(MathCaptchaDetector::JS.contains("equationRe"));
}
#[test]
fn detector_runs_after_vendor_detectors() {
let d = MathCaptchaDetector;
assert_eq!(d.priority(), 80);
}
#[test]
fn detector_name_stable() {
let d = MathCaptchaDetector;
assert_eq!(d.name(), "math_captcha");
}
}