Skip to main content

cdp_client/
dialog.rs

1#[derive(Clone, PartialEq)]
2struct Dialog {
3    url: String,
4    frame: String,
5    epoch: u64,
6}
7
8#[derive(Clone, PartialEq)]
9struct DialogDocument {
10    url: String,
11    loader: String,
12}
13
14pub const DIALOG_UNOBSERVED: &str = "Error: No dialog visible to this session. Dialogs already open before attachment require the foreground dialog holder (follow-up #529); arm dialog-accept or dialog-dismiss before triggering a dialog.\n";
15
16impl Cdp {
17    pub fn dialog_targets(&mut self) -> Result<Vec<String>, String> {
18        let result = self.raw_call("Target.getTargets", json!({}), None, DOCUMENT_CHECK)?;
19        let infos = result.get("targetInfos").and_then(Value::as_array).ok_or_else(|| self.refusal())?;
20        Ok(infos.iter().filter(|info| {
21            info.get("type").and_then(Value::as_str) == Some("page")
22                && info.get("url").and_then(Value::as_str).is_some_and(|url| self.dialog_url(url))
23                && !self.info_protected(info, infos)
24        }).filter_map(|info| info.get("targetId").and_then(Value::as_str).filter(|id| !id.is_empty()).map(str::to_string)).collect())
25    }
26
27    pub fn attach_dialog(&mut self, target: &str) -> Result<String, String> {
28        self.dialog_target(target)?;
29        let result = self.raw_call("Target.attachToTarget", json!({"targetId":target,"flatten":true}), None, DOCUMENT_CHECK)?;
30        let session = result.get("sessionId").and_then(Value::as_str).filter(|session| !session.is_empty())
31            .ok_or("Target.attachToTarget returned no sessionId\n".to_string())?.to_string();
32        self.session_targets.insert(session.clone(), target.to_string());
33        self.dialog_sessions.insert(session.clone());
34        let observed = (|| {
35            let url = self.dialog_target(target)?;
36            if self.dialog_history(&session)? != url { return Err(self.refusal()) }
37            self.send("Page.enable", json!({}), Some(&session))?;
38            Ok(())
39        })();
40        if let Err(message) = observed {
41            let _ = self.raw_call("Target.detachFromTarget", json!({"sessionId":session}), None, DOCUMENT_CHECK);
42            self.dialog_sessions.remove(&session);
43            self.dialogs.remove(&session);
44            self.dialog_documents.remove(&session);
45            self.session_targets.remove(&session);
46            return Err(message);
47        }
48        Ok(session)
49    }
50
51    pub fn dialog_ready(&mut self, session: &str) -> Result<Value, String> {
52        if !self.dialog_sessions.contains(session) || self.blocked.contains(session) { return Err(self.refusal()) }
53        let target = self.session_target(session).ok_or_else(|| self.refusal())?;
54        let url = self.dialog_target(&target)?;
55        if self.dialog_history(session)? != url { return Err(self.refusal()) }
56        let loader = self.dialog_documents.get(session).filter(|document| document.url == url).map(|document| document.loader.as_str());
57        Ok(json!({"target":target,"loaderId":loader,"loaderIdSource":if loader.is_some() {"frame-navigated"} else {"unavailable"},"status":if loader.is_some() {"document-verified"} else {"observing"}}))
58    }
59
60    pub fn dialog_attached(&self, session: &str, target: &str) -> bool {
61        self.dialog_sessions.contains(session) && self.session_target(session).as_deref() == Some(target)
62    }
63
64    pub fn dialog_id(&self, session: &str) -> Option<u64> {
65        self.dialogs.get(session).map(|dialog| dialog.epoch)
66    }
67
68    pub fn verify_dialog(&mut self, session: &str, epoch: u64) -> Result<(), String> {
69        if self.dialog_id(session) != Some(epoch) { return Err(DIALOG_UNOBSERVED.into()) }
70        self.check_dialog(session)
71    }
72
73    pub fn wait_dialog(&mut self, session: &str, timeout: Duration) -> Result<(), String> {
74        let deadline = Instant::now() + timeout;
75        loop {
76            if self.blocked.contains(session) || !self.dialog_sessions.contains(session) {
77                return Err(self.refusal());
78            }
79            if let Some(dialog) = self.dialogs.get(session) {
80                let target = self.session_target(session).ok_or_else(|| self.refusal())?;
81                if dialog.frame != target || !self.dialog_url(&dialog.url) { return Err(self.refusal()) }
82                return Ok(());
83            }
84            if Instant::now() >= deadline || !self.pump(deadline)? {
85                return Err(DIALOG_UNOBSERVED.into());
86            }
87        }
88    }
89
90    fn dialog_url(&self, text: &str) -> bool {
91        url::Url::parse(text).is_ok_and(|url| matches!(url.scheme(), "http" | "https") && url.host_str().is_some())
92            && !self.guard.url(text)
93    }
94
95    fn dialog_target(&mut self, target: &str) -> Result<String, String> {
96        if target.is_empty() || self.protected_targets.contains(target) || self.abandoned.contains(target) {
97            return Err(self.refusal());
98        }
99        let result = self.raw_call("Target.getTargetInfo", json!({"targetId":target}), None, DOCUMENT_CHECK)?;
100        let info = result.get("targetInfo").ok_or_else(|| self.refusal())?;
101        let text = |key| info.get(key).and_then(Value::as_str).unwrap_or("");
102        if text("targetId") != target || text("type") != "page" || !self.dialog_url(text("url"))
103            || self.info_protected(info, &[]) { return Err(self.refusal()) }
104        let url = text("url").to_string();
105        self.targets.insert(target.to_string(), info.clone());
106        Ok(url)
107    }
108
109    fn dialog_history(&mut self, session: &str) -> Result<String, String> {
110        let history = self.raw_call("Page.getNavigationHistory", json!({}), Some(session), DOCUMENT_CHECK)?;
111        let index = history.get("currentIndex").and_then(Value::as_u64)
112            .and_then(|index| usize::try_from(index).ok()).ok_or_else(|| self.refusal())?;
113        let url = history.get("entries").and_then(Value::as_array)
114            .and_then(|entries| entries.get(index))
115            .and_then(|entry| entry.get("url")).and_then(Value::as_str)
116            .filter(|url| self.dialog_url(url)).ok_or_else(|| self.refusal())?;
117        Ok(url.to_string())
118    }
119
120    fn check_dialog(&mut self, session: &str) -> Result<(), String> {
121        if self.blocked.contains(session) || self.guard_sessions.contains(session) { return Err(self.refusal()) }
122        let target = self.session_target(session).ok_or_else(|| self.refusal())?;
123        let dialog = self.dialogs.get(session).cloned().ok_or(DIALOG_UNOBSERVED.to_string())?;
124        if dialog.frame != target || !self.dialog_url(&dialog.url) { return Err(self.refusal()) }
125        let url = self.dialog_target(&target)?;
126        let current = self.dialog_history(session)?;
127        if current != dialog.url || url != current || self.dialogs.get(session) != Some(&dialog)
128            || self.blocked.contains(session) || self.protected_targets.contains(&target) { return Err(self.refusal()) }
129        Ok(())
130    }
131
132    fn admit_dialog(&mut self, session: &str) -> Result<(), String> {
133        self.check_dialog(session)?;
134        self.dialogs.remove(session);
135        Ok(())
136    }
137
138    fn observe_dialog(&mut self, event: &Value) {
139        let method = event.get("method").and_then(Value::as_str).unwrap_or("");
140        if method == "Target.detachedFromTarget" {
141            if let Some(session) = event.pointer("/params/sessionId").and_then(Value::as_str) {
142                self.dialog_sessions.remove(session);
143                self.dialogs.remove(session);
144                self.dialog_documents.remove(session);
145            }
146            return;
147        }
148        let Some(session) = event.get("sessionId").and_then(Value::as_str) else { return };
149        match method {
150            "Page.javascriptDialogOpening" if self.session_target(session).is_some() => {
151                self.dialog_epoch += 1;
152                self.dialogs.insert(session.to_string(), Dialog {
153                    url: event.pointer("/params/url").and_then(Value::as_str).unwrap_or("").to_string(),
154                    frame: event.pointer("/params/frameId").and_then(Value::as_str).unwrap_or("").to_string(),
155                    epoch: self.dialog_epoch,
156                });
157            }
158            "Page.frameNavigated" => {
159                self.dialogs.remove(session);
160                self.dialog_documents.remove(session);
161                let frame = &event["params"]["frame"];
162                let text = |key| frame.get(key).and_then(Value::as_str).unwrap_or("");
163                let url = format!("{}{}", text("url"), text("urlFragment"));
164                let origin = url::Url::parse(&url).ok().map(|url| url.origin().ascii_serialization());
165                if self.session_target(session).as_deref() == Some(text("id")) && frame.get("parentId").is_none()
166                    && self.dialog_url(&url) && text("unreachableUrl").is_empty() && frame_text_known(text("loaderId"))
167                    && origin.as_deref() == Some(text("securityOrigin")) {
168                    self.dialog_documents.insert(session.to_string(), DialogDocument {url, loader:text("loaderId").to_string()});
169                }
170            }
171            "Page.frameStartedNavigating" => {
172                self.dialogs.remove(session);
173                self.dialog_documents.remove(session);
174            }
175            "Page.javascriptDialogClosed" => { self.dialogs.remove(session); }
176            _ => {}
177        }
178    }
179}