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}