use ff_rdp_core::{Grip, StorageActor, WebConsoleActor};
use serde_json::{Value, json};
use crate::cli::args::Cli;
use crate::error::AppError;
use crate::hints::{HintContext, HintSource};
use crate::output;
use crate::output_pipeline::OutputPipeline;
use super::connect_tab::{ConnectedTab, connect_direct};
use super::js_helpers::escape_selector;
const CMP_SELECTORS: &[&str] = &[
"#CybotCookiebotDialog",
"#onetrust-consent-sdk",
".cmp-container",
"[data-testid=\"uc-default-wall\"]",
"#didomi-host",
".qc-cmp-ui-container",
];
pub fn run(cli: &Cli, name: Option<&str>) -> Result<(), AppError> {
let mut ctx = connect_direct(cli)?;
let tab_actor = ctx.target_tab_actor().clone();
let cookies =
StorageActor::list_cookies(ctx.transport_mut(), &tab_actor).map_err(AppError::from)?;
let mut results: Vec<Value> = cookies
.iter()
.map(|c| {
let mut obj = serde_json::to_value(c).unwrap_or_default();
if c.expires == 0 {
obj["expires"] = json!("Session");
}
if let Some(o) = obj.as_object_mut() {
o.remove("lastAccessed");
o.remove("creationTime");
}
obj
})
.collect();
if let Some(filter_name) = name {
results.retain(|c| c.get("name").and_then(Value::as_str) == Some(filter_name));
}
let total = results.len();
let result_json = json!(results);
let mut meta = json!({"host": cli.host, "port": cli.port});
if total == 0
&& let Some(note) = detect_consent_banner(&mut ctx)
&& let Some(m) = meta.as_object_mut()
{
m.insert("note".to_string(), json!(note));
}
let mut envelope = output::envelope(&result_json, total, &meta);
if total == 0
&& let Some(obj) = envelope.as_object_mut()
{
obj.insert(
"hint".to_string(),
json!(
"No cookies found. The page may not set cookies, or try navigating first. \
If a consent banner is present, accept it or use `ff-rdp launch --temp-profile --auto-consent`."
),
);
}
let hint_ctx = HintContext::new(HintSource::Cookies);
OutputPipeline::from_cli(cli)?
.finalize_with_hints(&envelope, Some(&hint_ctx))
.map_err(AppError::from)
}
fn detect_consent_banner(ctx: &mut ConnectedTab) -> Option<String> {
let selectors_js = CMP_SELECTORS
.iter()
.map(|s| format!("'{}'", escape_selector(s)))
.collect::<Vec<_>>()
.join(",");
let js = format!(
r"(function() {{
var sels = [{selectors_js}];
for (var i = 0; i < sels.length; i++) {{
if (document.querySelector(sels[i])) return sels[i];
}}
return null;
}})()"
);
let console_actor = ctx.target.console_actor.clone();
let eval_result =
WebConsoleActor::evaluate_js_async(ctx.transport_mut(), &console_actor, &js).ok()?;
if eval_result.exception.is_some() {
return None;
}
match &eval_result.result {
Grip::Value(Value::String(selector)) => Some(format!(
"0 cookies found — a consent banner was detected ({selector}); \
cookies may appear after accepting consent"
)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cmp_selectors_are_valid_css() {
for sel in CMP_SELECTORS {
assert!(!sel.is_empty(), "CMP selector should not be empty");
assert!(
!sel.contains('\''),
"CMP selector should not contain single quotes: {sel}"
);
}
}
}