use serde_json::json;
use crate::cdp::client::CdpClient;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DialogPolicy {
Accept,
Dismiss,
Manual,
}
impl DialogPolicy {
pub fn parse(s: &str) -> Result<Self, crate::BoxError> {
match s.to_ascii_lowercase().as_str() {
"accept" => Ok(Self::Accept),
"dismiss" => Ok(Self::Dismiss),
"manual" => Ok(Self::Manual),
other => {
Err(format!("Unknown --dialog {other:?}. Use \"accept\", \"dismiss\", or \"manual\".").into())
}
}
}
#[must_use]
pub const fn auto_handles(self) -> bool {
!matches!(self, Self::Manual)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct DialogResponse {
pub accept: bool,
pub prompt_text: Option<String>,
}
#[must_use]
pub fn dialog_decision(policy: DialogPolicy, dialog_type: &str, text: Option<&str>) -> DialogResponse {
match policy {
DialogPolicy::Accept => DialogResponse {
accept: true,
prompt_text: (dialog_type == "prompt").then(|| text.unwrap_or("").to_string()),
},
DialogPolicy::Dismiss | DialogPolicy::Manual => {
DialogResponse { accept: false, prompt_text: None }
}
}
}
pub async fn apply_stealth(client: &CdpClient) {
let _ = client.enable("Network").await;
let _ = client
.send(
"Page.addScriptToEvaluateOnNewDocument",
json!({ "source": STEALTH_PATCHES_JS }),
)
.await;
let _ = client
.send(
"Runtime.evaluate",
json!({"expression": "Object.defineProperty(navigator, 'webdriver', { get: () => undefined });"}),
)
.await;
let _ = client
.send(
"Network.setUserAgentOverride",
json!({
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"acceptLanguage": "en-US,en;q=0.9",
"platform": "MacIntel"
}),
)
.await;
}
const STEALTH_PATCHES_JS: &str = r#"
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
// Mask chrome.runtime (headless doesn't have it)
if (!window.chrome) window.chrome = {};
if (!window.chrome.runtime) window.chrome.runtime = { connect: () => {}, sendMessage: () => {} };
// Mask Permissions API inconsistency (headless returns "prompt" for notifications)
const origQuery = window.Permissions && Permissions.prototype.query;
if (origQuery) {
Permissions.prototype.query = (params) => (
params.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: origQuery.call(Permissions.prototype, params)
);
}
// Mask webGL vendor/renderer (headless gives "Google Inc." / "ANGLE")
const getParam = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {
if (param === 37445) return 'Intel Inc.';
if (param === 37446) return 'Intel Iris OpenGL Engine';
return getParam.call(this, param);
};
// Fix CDP input leak: screenX/screenY == pageX/pageY reveals automation.
const __screenOffset = { x: Math.floor(Math.random() * 100) + 50, y: Math.floor(Math.random() * 100) + 80 };
const origMouseEvent = MouseEvent;
window.MouseEvent = class extends origMouseEvent {
constructor(type, init = {}) {
if (init.screenX !== undefined) init.screenX += __screenOffset.x;
if (init.screenY !== undefined) init.screenY += __screenOffset.y;
super(type, init);
}
};
"#;
#[cfg(test)]
mod tests {
use super::{dialog_decision, DialogPolicy, DialogResponse};
#[test]
fn policy_parse_is_case_insensitive() {
assert_eq!(DialogPolicy::parse("Accept").unwrap(), DialogPolicy::Accept);
assert_eq!(DialogPolicy::parse("DISMISS").unwrap(), DialogPolicy::Dismiss);
assert_eq!(DialogPolicy::parse("manual").unwrap(), DialogPolicy::Manual);
assert!(DialogPolicy::parse("nope").is_err());
}
#[test]
fn only_manual_skips_handler() {
assert!(DialogPolicy::Accept.auto_handles());
assert!(DialogPolicy::Dismiss.auto_handles());
assert!(!DialogPolicy::Manual.auto_handles());
}
#[test]
fn accept_confirms_alert_and_confirm_without_prompt_text() {
for t in ["alert", "confirm"] {
assert_eq!(
dialog_decision(DialogPolicy::Accept, t, Some("ignored")),
DialogResponse { accept: true, prompt_text: None }
);
}
}
#[test]
fn accept_supplies_prompt_text() {
assert_eq!(
dialog_decision(DialogPolicy::Accept, "prompt", Some("hello")),
DialogResponse { accept: true, prompt_text: Some("hello".into()) }
);
assert_eq!(
dialog_decision(DialogPolicy::Accept, "prompt", None),
DialogResponse { accept: true, prompt_text: Some(String::new()) }
);
}
#[test]
fn accept_proceeds_through_beforeunload() {
assert_eq!(
dialog_decision(DialogPolicy::Accept, "beforeunload", None),
DialogResponse { accept: true, prompt_text: None }
);
}
#[test]
fn dismiss_cancels_every_type() {
for t in ["alert", "confirm", "prompt", "beforeunload"] {
assert_eq!(
dialog_decision(DialogPolicy::Dismiss, t, Some("x")),
DialogResponse { accept: false, prompt_text: None }
);
}
}
#[test]
fn manual_falls_through_to_cancel() {
assert_eq!(
dialog_decision(DialogPolicy::Manual, "confirm", Some("x")),
DialogResponse { accept: false, prompt_text: None }
);
}
}