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}