tauri_runtime_cef/
dialog.rs1use crate::{FrameNavigationState, NativeDocumentToken};
17use std::sync::{Arc, Mutex};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
20#[serde(rename_all = "lowercase")]
21#[non_exhaustive]
22pub enum NativeDialogKind {
23 Alert,
24 Confirm,
25 Prompt,
26 BeforeUnload,
27}
28
29#[derive(Clone)]
32pub struct NativeDialogToken(Arc<()>);
33impl PartialEq for NativeDialogToken {
34 fn eq(&self, other: &Self) -> bool {
35 Arc::ptr_eq(&self.0, &other.0)
36 }
37}
38impl Eq for NativeDialogToken {}
39impl std::fmt::Debug for NativeDialogToken {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("NativeDialogToken").finish_non_exhaustive()
42 }
43}
44
45#[derive(Clone, Debug)]
46#[non_exhaustive]
47pub struct NativeDialogSnapshot {
48 pub token: NativeDialogToken,
49 pub kind: NativeDialogKind,
50 pub has_browser_handler: bool,
53}
54
55#[derive(Clone, Debug, Default)]
56#[non_exhaustive]
57pub struct NativeDialogObservation {
58 pub known: bool,
60 pub dialog: Option<NativeDialogSnapshot>,
61}
62
63#[derive(Default)]
64struct ObservedDialog {
65 document: Option<NativeDocumentToken>,
66 observation: NativeDialogObservation,
67}
68
69#[derive(Clone)]
70pub(crate) struct DialogState {
71 browser: FrameNavigationState,
72 state: Arc<Mutex<ObservedDialog>>,
73}
74impl DialogState {
75 pub(crate) fn new(browser: FrameNavigationState) -> Self {
76 Self {
77 browser,
78 state: Arc::default(),
79 }
80 }
81 pub(crate) fn accepts_browser(&self, id: i32) -> bool {
82 self.browser.has_browser_id(id)
83 }
84 pub(crate) fn snapshot(&self, document: Option<&NativeDocumentToken>) -> NativeDialogObservation {
85 self
86 .state
87 .lock()
88 .ok()
89 .filter(|state| state.document.as_ref() == document)
90 .map(|state| state.observation.clone())
91 .unwrap_or_default()
92 }
93 pub(crate) fn on_event(&self, method: &str, params: &[u8]) {
94 let observation = match method {
95 "Page.javascriptDialogOpening" => {
96 #[derive(serde::Deserialize)]
97 struct Opening {
98 #[serde(rename = "type")]
99 kind: NativeDialogKind,
100 #[serde(rename = "hasBrowserHandler")]
101 has_browser_handler: bool,
102 }
103 let opening = (params.len() <= 262_144)
106 .then(|| serde_json::from_slice::<Opening>(params).ok())
107 .flatten();
108 opening
109 .map(|opening| NativeDialogObservation {
110 known: true,
111 dialog: Some(NativeDialogSnapshot {
112 token: NativeDialogToken(Arc::new(())),
113 kind: opening.kind,
114 has_browser_handler: opening.has_browser_handler,
115 }),
116 })
117 .unwrap_or_default()
118 }
119 "Page.javascriptDialogClosed" => NativeDialogObservation {
120 known: true,
121 dialog: None,
122 },
123 _ => return,
124 };
125 let document = self.browser.document();
126 if let Ok(mut state) = self.state.lock() {
127 *state = ObservedDialog {
128 document,
129 observation,
130 };
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 fn ready() -> FrameNavigationState {
139 let browser = FrameNavigationState::new();
140 for kind in [
141 crate::FrameEventKind::Created,
142 crate::FrameEventKind::Attached,
143 crate::FrameEventKind::MainFrameChanged,
144 crate::FrameEventKind::LoadingStateChanged { is_loading: false },
145 ] {
146 browser.on_frame_event(&crate::FrameEvent {
147 browser_id: 1,
148 frame_id: "main".into(),
149 is_main: true,
150 kind,
151 });
152 }
153 browser
154 }
155 const CONFIRM: &[u8] = br#"{"type":"confirm","hasBrowserHandler":true,"message":"non-secret fixture","defaultPrompt":"omitted fixture"}"#;
156 #[test]
157 fn exact_dialog_lifetimes_rotate_on_reopen_and_do_not_cross_browsers() {
158 let browser = ready();
159 let state = DialogState::new(browser.clone());
160 let document = browser.document().unwrap();
161 assert!(!state.snapshot(Some(&document)).known);
162 state.on_event("Page.javascriptDialogOpening", CONFIRM);
163 let first = state.snapshot(Some(&document)).dialog.unwrap();
164 assert_eq!(first.kind, NativeDialogKind::Confirm);
165 assert!(first.has_browser_handler);
166 state.on_event("Page.javascriptDialogClosed", b"unread prompt value");
167 assert!(state.snapshot(Some(&document)).known);
168 assert!(state.snapshot(Some(&document)).dialog.is_none());
169 state.on_event("Page.javascriptDialogOpening", CONFIRM);
170 assert_ne!(
171 state.snapshot(Some(&document)).dialog.unwrap().token,
172 first.token
173 );
174 let other = DialogState::new(ready());
175 other.on_event("Page.javascriptDialogOpening", CONFIRM);
176 assert_ne!(
177 other
178 .state
179 .lock()
180 .unwrap()
181 .observation
182 .dialog
183 .as_ref()
184 .unwrap()
185 .token,
186 first.token
187 );
188 assert!(!state.accepts_browser(2));
189 }
190 #[test]
191 fn navigation_unknown_protocol_and_missing_handler_facts_fail_closed() {
192 let browser = ready();
193 let state = DialogState::new(browser.clone());
194 let document = browser.document().unwrap();
195 state.on_event("Page.javascriptDialogOpening", CONFIRM);
196 browser.close();
197 assert!(!state.snapshot(browser.document().as_ref()).known);
198 state.on_event("Page.javascriptDialogOpening", br#"{"type":"confirm"}"#);
199 assert!(!state.snapshot(None).known);
200 state.on_event(
201 "Page.javascriptDialogOpening",
202 br#"{"type":"unknown","hasBrowserHandler":true}"#,
203 );
204 assert!(state.snapshot(None).dialog.is_none());
205 assert!(state.snapshot(Some(&document)).dialog.is_none());
206 }
207}