use anyhow::{anyhow, Result};
use chromiumoxide::cdp::js_protocol::runtime::EvaluateParams;
use chromiumoxide::Page;
pub async fn evaluate_in_all_frames<T>(page: &Page, expression: &str) -> Result<Vec<T>>
where
T: serde::de::DeserializeOwned,
{
let frame_ids = page.frames().await?;
let mut out = Vec::with_capacity(frame_ids.len());
for fid in frame_ids {
if let Some(ctx) = page.frame_execution_context(fid.clone()).await? {
let params = EvaluateParams::builder()
.expression(expression)
.context_id(ctx)
.build()
.map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
let eval = page.evaluate_expression(params).await?;
if let Ok(v) = eval.into_value::<T>() {
out.push(v);
}
}
}
Ok(out)
}
pub async fn evaluate_in_frames_first<T, F>(
page: &Page,
expression: &str,
filter: F,
default: T,
) -> Result<T>
where
T: serde::de::DeserializeOwned + Clone,
F: Fn(&T) -> bool,
{
let all = evaluate_in_all_frames::<T>(page, expression).await?;
Ok(all.into_iter().find(filter).unwrap_or(default))
}
pub async fn find_element_centre_in_frames(
page: &Page,
selector: &str,
) -> Result<Option<(f64, f64)>> {
let frame_ids = page.frames().await?;
let main_frame = page.mainframe().await?;
let mut iframe_offsets: std::collections::HashMap<String, (f64, f64)> =
std::collections::HashMap::new();
if let Some(ref main) = main_frame {
let js = r#"
(function() {
const out = [];
for (const f of document.querySelectorAll('iframe')) {
const r = f.getBoundingClientRect();
out.push({ src: f.src, id: f.id, x: r.left, y: r.top });
}
return out;
})()
"#;
if let Some(ctx) = page.frame_execution_context(main.clone()).await? {
let params = EvaluateParams::builder()
.expression(js)
.context_id(ctx)
.build()
.map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
let eval = page.evaluate_expression(params).await?;
if let Ok(vals) = eval.into_value::<Vec<serde_json::Value>>() {
for v in vals {
let key = v["src"].as_str().or_else(|| v["id"].as_str()).unwrap_or("");
if let (Some(x), Some(y)) = (v["x"].as_f64(), v["y"].as_f64()) {
iframe_offsets.insert(key.to_string(), (x, y));
}
}
}
}
}
let escaped = selector.replace('\\', "\\\\").replace('\'', "\\'");
let js = format!(
r#"(function() {{
const el = document.querySelector('{}');
if (!el) return null;
const r = el.getBoundingClientRect();
return {{ x: r.left + r.width / 2, y: r.top + r.height / 2, url: window.location.href }};
}})()"#,
escaped
);
for fid in frame_ids {
if let Some(ctx) = page.frame_execution_context(fid.clone()).await? {
let params = EvaluateParams::builder()
.expression(&js)
.context_id(ctx)
.build()
.map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
let eval = page.evaluate_expression(params).await?;
if let Ok(val) = eval.into_value::<serde_json::Value>() {
if let (Some(x), Some(y)) = (val["x"].as_f64(), val["y"].as_f64()) {
let url = val["url"].as_str().unwrap_or("");
let (offset_x, offset_y) = if Some(fid) == main_frame.clone() {
(0.0, 0.0)
} else {
iframe_offsets.get(url).copied().unwrap_or((0.0, 0.0))
};
return Ok(Some((x + offset_x, y + offset_y)));
}
}
}
}
Ok(None)
}
pub async fn verify_token_in_frames(
page: &Page,
token_input_name: &str,
) -> Result<bool> {
let js = format!(
r#"!!document.querySelector('input[name="{}"][value]:not([value=""])')"#,
token_input_name.replace('"', "\\\"")
);
let results = evaluate_in_all_frames::<bool>(page, &js).await?;
Ok(results.into_iter().any(|v| v))
}
#[cfg(test)]
mod tests {
#[test]
fn selector_escaping_handles_quotes() {
let sel = "[data-test='value']";
let escaped = sel.replace('\\', "\\\\").replace('\'', "\\'");
assert!(escaped.contains("\\'"));
}
#[test]
fn token_name_escaping_handles_quotes() {
let name = r#"g-recaptcha-response"#;
let escaped = name.replace('"', "\\\"");
assert!(!escaped.contains('"'));
}
}