Skip to main content

cdp_client/
front.rs

1pub const FRONT_RESTORE: &str = "Error: the input landed, but the tab that was in front could not be brought back to the front.\n";
2
3#[derive(Debug, Clone, PartialEq)]
4pub struct Front {
5    own: String,
6    window: i64,
7    restore: Option<(String, bool)>,
8}
9
10impl Cdp {
11    pub fn bring_forward(&mut self, session: &str) -> Result<Front, String> {
12        let own = self.front_own(session)?;
13        let infos = self.front_infos()?;
14        let text = |info: &Value, key: &str| info.get(key).and_then(Value::as_str).unwrap_or("").to_string();
15        let mine = infos.iter().find(|info| text(info, "targetId") == own).cloned().ok_or_else(|| self.refusal())?;
16        if text(&mine, "type") != "page" || self.info_protected(&mine, &infos) { return Err(self.refusal()) }
17        let context = text(&mine, "browserContextId");
18        let window = self.front_window(&own).map_err(|_| self.refusal())?;
19        if self.front_visible(&own) != Some(false) { return Err(self.refusal()) }
20        let mut shared = 0;
21        let mut visible = Vec::new();
22        let mut opaque = Vec::new();
23        for info in &infos {
24            let id = text(info, "targetId");
25            let kind = text(info, "type");
26            if id == own || kind == "iframe" || WINDOWLESS.contains(&kind.as_str()) || top_chrome(info) { continue }
27            let other = text(info, "browserContextId");
28            if !context.is_empty() && !other.is_empty() && other != context { continue }
29            let open = !id.is_empty() && url::Url::parse(&text(info, "url")).is_ok() && !self.protected_targets.contains(&id) && !self.info_protected(info, &infos);
30            let same = match self.front_window(&id) {
31                Ok(found) => found == window,
32                Err(_) if open || kind != "page" => continue,
33                Err(_) => return Err(self.refusal()),
34            };
35            if !open && !same { return Err(self.refusal()) }
36            if !same { continue }
37            shared += 1;
38            match (open && kind == "page").then(|| self.front_visible(&id)).flatten() {
39                Some(true) => visible.push(id),
40                Some(false) => {}
41                None => opaque.push(info.clone()),
42            }
43        }
44        let restore = match (visible.as_slice(), opaque.as_slice()) {
45            _ if shared == 0 => None,
46            ([front], _) => Some((front.clone(), false)),
47            ([], [seven]) if text(seven, "type") == "page" && seven_page(&text(seven, "url")) => Some((text(seven, "targetId"), true)),
48            _ => return Err(self.refusal()),
49        };
50        self.front_own(session).ok().filter(|again| again == &own).ok_or_else(|| self.refusal())?;
51        self.raw_call("Target.activateTarget", json!({ "targetId": own }), None, DOCUMENT_CHECK)?;
52        Ok(Front { own, window, restore })
53    }
54
55    pub fn restore_front(&mut self, front: Front) -> Result<(), String> {
56        let Some((target, seven)) = front.restore else { return Ok(()) };
57        let infos = self.front_infos().map_err(|_| FRONT_RESTORE.to_string())?;
58        let text = |info: &Value, key: &str| info.get(key).and_then(Value::as_str).unwrap_or("").to_string();
59        let info = infos.iter().find(|info| text(info, "targetId") == target).cloned().ok_or(FRONT_RESTORE.to_string())?;
60        let allowed = if seven {
61            text(&info, "type") == "page" && seven_page(&text(&info, "url"))
62        } else {
63            !self.protected_targets.contains(&target) && !self.info_protected(&info, &infos)
64        };
65        if !allowed || !infos.iter().any(|info| text(info, "targetId") == front.own) || self.front_window(&target).ok() != Some(front.window) {
66            return Err(FRONT_RESTORE.to_string());
67        }
68        self.raw_call("Target.activateTarget", json!({ "targetId": target }), None, DOCUMENT_CHECK).map_err(|_| FRONT_RESTORE.to_string())?;
69        Ok(())
70    }
71
72    fn front_own(&mut self, session: &str) -> Result<String, String> {
73        let own = self.session_target(session).ok_or_else(|| self.refusal())?;
74        if self.guard_sessions.contains(session) || self.blocked.contains(session) || self.protected_targets.contains(&own) {
75            return Err(self.refusal());
76        }
77        self.document_state_for(&own, true, None).ok_or_else(|| self.refusal())?;
78        if self.blocked.contains(session) { return Err(self.refusal()) }
79        Ok(own)
80    }
81
82    fn front_infos(&mut self) -> Result<Vec<Value>, String> {
83        let listed = self.raw_call("Target.getTargets", json!({}), None, DOCUMENT_CHECK)?;
84        listed.get("targetInfos").and_then(Value::as_array).cloned().ok_or_else(|| self.refusal())
85    }
86
87    fn front_window(&mut self, target: &str) -> Result<i64, String> {
88        let found = self.raw_call("Browser.getWindowForTarget", json!({ "targetId": target }), None, DOCUMENT_CHECK)?;
89        let window = found.get("windowId").and_then(Value::as_i64).ok_or_else(|| self.refusal())?;
90        if found.pointer("/bounds/windowState").and_then(Value::as_str) == Some("minimized") { return Err(self.refusal()) }
91        Ok(window)
92    }
93
94    fn front_visible(&mut self, target: &str) -> Option<bool> {
95        let attached = self.raw_call("Target.attachToTarget", json!({ "targetId": target, "flatten": true }), None, DOCUMENT_CHECK).ok()?;
96        let session = attached.get("sessionId").and_then(Value::as_str).filter(|session| !session.is_empty())?.to_string();
97        self.guard_sessions.insert(session.clone());
98        let state = (|| {
99            let world = self.raw_call("Page.createIsolatedWorld", json!({ "frameId": target, "worldName": "slicc-front" }), Some(&session), DOCUMENT_CHECK).ok()?;
100            let context = world.get("executionContextId").cloned()?;
101            let read = self.raw_call("Runtime.evaluate", json!({ "expression": "document.visibilityState", "contextId": context, "returnByValue": true }), Some(&session), DOCUMENT_CHECK).ok()?;
102            read.pointer("/result/value").and_then(Value::as_str).map(|state| state == "visible")
103        })();
104        let _ = self.raw_call("Target.detachFromTarget", json!({ "sessionId": session }), None, DOCUMENT_CHECK);
105        self.guard_sessions.remove(&session);
106        state
107    }
108}
109
110fn seven_rules() -> &'static slicc_browsers::Seven {
111    static SEVEN: std::sync::OnceLock<slicc_browsers::Seven> = std::sync::OnceLock::new();
112    SEVEN.get_or_init(|| slicc_browsers::catalog().seven)
113}
114
115pub fn seven_page(text: &str) -> bool {
116    let Ok(url) = url::Url::parse(text) else { return false };
117    let rules = seven_rules();
118    if url.scheme() != rules.origin_scheme || !url.username().is_empty() || url.password().is_some() || url.port().is_some() { return false }
119    let Some(label) = url.host_str().and_then(|host| host.strip_suffix(".sliccy.ai")) else { return false };
120    let bytes = label.as_bytes();
121    !bytes.is_empty() && bytes.len() <= 63
122        && bytes.iter().all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
123        && bytes[0] != b'-' && bytes[bytes.len() - 1] != b'-'
124        && rules.is_seven_label(label)
125}