Skip to main content

tauri_runtime_cef/
dialog.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Native protocol observations of CEF's existing JavaScript dialog UI.
6//! No custom dialog handler, callback, message text, or prompt value is retained.
7//!
8//! These events reach the runtime through the DevTools `Page` domain it enables
9//! on every browser. Enabling that domain observes dialogs without taking them
10//! over: Chromium notifies each enabled `PageHandler` and *then* still runs the
11//! browser's own dialog manager, and CEF always supplies one for its browsers.
12//! A dialog only stalls a page when no browser handler exists at all, which the
13//! protocol itself reports as `hasBrowserHandler == false`. The runtime
14//! therefore never has to answer a dialog with `Page.handleJavaScriptDialog`.
15
16use 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/// Opaque lifetime of one native JavaScript dialog notification. Compare with
30/// the current UI-thread snapshot before submitting any dialog action.
31#[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  /// Chromium reports a non-DevTools dialog handler. This does not establish
51  /// native visibility, the exact button label, or the current prompt value.
52  pub has_browser_handler: bool,
53}
54
55#[derive(Clone, Debug, Default)]
56#[non_exhaustive]
57pub struct NativeDialogObservation {
58  /// A dialog event was observed for this document. False never means absent.
59  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        // Dialog payloads contain arbitrary page text. Bound parsing and retain
104        // only the native kind/handler facts; malformed input revokes old refs.
105        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}