1use std::{
6 borrow::Cow,
7 collections::{HashMap, HashSet},
8 fmt,
9 fs::create_dir_all,
10 sync::{Arc, Mutex, MutexGuard},
11};
12
13use serde::Serialize;
14use serialize_to_javascript::{default_template, DefaultTemplate, Template};
15use tauri_runtime::{
16 webview::{DetachedWebview, InitializationScript, PendingWebview},
17 window::DragDropEvent,
18};
19use tauri_utils::config::WebviewUrl;
20use url::Url;
21
22use crate::{
23 app::{GlobalWebviewEventListener, OnPageLoad, UriSchemeResponder, WebviewEvent},
24 ipc::InvokeHandler,
25 pattern::PatternJavascript,
26 sealed::ManagerBase,
27 webview::PageLoadPayload,
28 EventLoopMessage, EventTarget, Manager, Runtime, Scopes, UriSchemeContext, Webview, Window,
29};
30
31use super::{
32 window::{DragDropPayload, DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT},
33 {AppManager, EmitPayload},
34};
35
36pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
41
42pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
43 include_str!("../../scripts/process-ipc-message-fn.js");
44
45#[cfg(feature = "isolation")]
46#[derive(Template)]
47#[default_template("../../scripts/isolation.js")]
48pub(crate) struct IsolationJavascript<'a> {
49 pub(crate) isolation_src: &'a str,
50 pub(crate) style: &'a str,
51}
52
53#[derive(Template)]
54#[default_template("../../scripts/ipc.js")]
55pub(crate) struct IpcJavascript<'a> {
56 pub(crate) isolation_origin: &'a str,
57}
58
59pub struct UriSchemeProtocol<R: Runtime> {
61 #[allow(clippy::type_complexity)]
63 pub protocol:
64 Box<dyn Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>,
65}
66
67pub struct WebviewManager<R: Runtime> {
68 pub webviews: Mutex<HashMap<String, Webview<R>>>,
69 pub invoke_handler: Box<InvokeHandler<R>>,
71 pub on_page_load: Option<Arc<OnPageLoad<R>>>,
73 pub uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
75 pub event_listeners: Arc<Vec<GlobalWebviewEventListener<R>>>,
77
78 pub invoke_initialization_script: String,
80
81 pub(crate) invoke_key: String,
83}
84
85impl<R: Runtime> fmt::Debug for WebviewManager<R> {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.debug_struct("WebviewManager")
88 .field(
89 "invoke_initialization_script",
90 &self.invoke_initialization_script,
91 )
92 .field("invoke_key", &self.invoke_key)
93 .finish()
94 }
95}
96
97impl<R: Runtime> WebviewManager<R> {
98 pub(crate) fn register_uri_scheme_protocol<N: Into<String>>(
99 &self,
100 uri_scheme: N,
101 protocol: Arc<UriSchemeProtocol<R>>,
102 ) {
103 let uri_scheme = uri_scheme.into();
104 self
105 .uri_scheme_protocols
106 .lock()
107 .unwrap()
108 .insert(uri_scheme, protocol);
109 }
110
111 pub(crate) fn webviews_lock(&self) -> MutexGuard<'_, HashMap<String, Webview<R>>> {
113 self.webviews.lock().expect("poisoned webview manager")
114 }
115
116 fn prepare_pending_webview<M: Manager<R>>(
117 &self,
118 mut pending: PendingWebview<EventLoopMessage, R>,
119 label: &str,
120 window_label: &str,
121 manager: &M,
122 ) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
123 let app_manager = manager.manager();
124
125 let plugin_init_scripts = app_manager
126 .plugins
127 .lock()
128 .expect("poisoned plugin store")
129 .initialization_script();
130
131 let pattern_init = PatternJavascript {
132 pattern: (&*app_manager.pattern).into(),
133 }
134 .render_default(&Default::default())?;
135
136 let mut webview_attributes = pending.webview_attributes;
137
138 let use_https_scheme = webview_attributes.use_https_scheme;
139
140 let ipc_init = IpcJavascript {
141 isolation_origin: &match &*app_manager.pattern {
142 #[cfg(feature = "isolation")]
143 crate::Pattern::Isolation { schema, .. } => {
144 crate::pattern::format_real_schema(schema, use_https_scheme)
145 }
146 _ => "".to_owned(),
147 },
148 }
149 .render_default(&Default::default())?;
150
151 let mut all_initialization_scripts: Vec<InitializationScript> = vec![];
152
153 fn main_frame_script(script: String) -> InitializationScript {
154 InitializationScript {
155 script,
156 for_main_frame_only: true,
157 }
158 }
159
160 all_initialization_scripts.push(main_frame_script(
161 r"
162 Object.defineProperty(window, 'isTauri', {
163 value: true,
164 });
165
166 if (!window.__TAURI_INTERNALS__) {
167 Object.defineProperty(window, '__TAURI_INTERNALS__', {
168 value: {
169 plugins: {}
170 }
171 })
172 }
173 "
174 .to_owned(),
175 ));
176 all_initialization_scripts.push(main_frame_script(self.invoke_initialization_script.clone()));
177 all_initialization_scripts.push(main_frame_script(format!(
178 r#"
179 Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{
180 value: {{
181 currentWindow: {{ label: {current_window_label} }},
182 currentWebview: {{ label: {current_webview_label} }}
183 }}
184 }})
185 "#,
186 current_window_label = serde_json::to_string(window_label)?,
187 current_webview_label = serde_json::to_string(&label)?,
188 )));
189 all_initialization_scripts.push(main_frame_script(self.initialization_script(
190 app_manager,
191 &ipc_init.into_string(),
192 &pattern_init.into_string(),
193 use_https_scheme,
194 )?));
195
196 all_initialization_scripts.extend(plugin_init_scripts);
197
198 #[cfg(feature = "isolation")]
199 if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern {
200 all_initialization_scripts.push(main_frame_script(
201 IsolationJavascript {
202 isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme),
203 style: tauri_utils::pattern::isolation::IFRAME_STYLE,
204 }
205 .render_default(&Default::default())?
206 .into_string(),
207 ));
208 }
209
210 if let Some(plugin_global_api_scripts) = &*app_manager.plugin_global_api_scripts {
211 for &script in plugin_global_api_scripts.iter() {
212 all_initialization_scripts.push(main_frame_script(script.to_owned()));
213 }
214 }
215
216 all_initialization_scripts.extend(webview_attributes.initialization_scripts);
218 webview_attributes.initialization_scripts = all_initialization_scripts;
219
220 pending.webview_attributes = webview_attributes;
221
222 let mut registered_scheme_protocols = Vec::new();
223
224 for (uri_scheme, protocol) in &*self.uri_scheme_protocols.lock().unwrap() {
225 registered_scheme_protocols.push(uri_scheme.clone());
226 let protocol = protocol.clone();
227 let app_handle = manager.app_handle().clone();
228
229 pending.register_uri_scheme_protocol(uri_scheme, move |webview_id, request, responder| {
230 let context = UriSchemeContext {
231 app_handle: &app_handle,
232 webview_label: webview_id,
233 };
234 (protocol.protocol)(context, request, UriSchemeResponder(responder))
235 });
236 }
237
238 let window_url = Url::parse(&pending.url).unwrap();
239 let window_origin = if window_url.scheme() == "data" {
240 "null".into()
241 } else if (cfg!(windows) || cfg!(target_os = "android"))
242 && window_url.scheme() != "http"
243 && window_url.scheme() != "https"
244 {
245 let https = if use_https_scheme { "https" } else { "http" };
246 format!("{https}://{}.localhost", window_url.scheme())
247 } else if let Some(host) = window_url.host() {
248 format!(
249 "{}://{}{}",
250 window_url.scheme(),
251 host,
252 window_url
253 .port()
254 .map(|p| format!(":{p}"))
255 .unwrap_or_default()
256 )
257 } else {
258 "null".into()
259 };
260
261 if !registered_scheme_protocols.contains(&"tauri".into()) {
262 let web_resource_request_handler = pending.web_resource_request_handler.take();
263 let protocol = crate::protocol::tauri::get(
264 manager.manager_owned(),
265 &window_origin,
266 web_resource_request_handler,
267 );
268 pending.register_uri_scheme_protocol("tauri", move |webview_id, request, responder| {
269 protocol(webview_id, request, UriSchemeResponder(responder))
270 });
271 registered_scheme_protocols.push("tauri".into());
272 }
273
274 if !registered_scheme_protocols.contains(&"ipc".into()) {
275 let protocol = crate::ipc::protocol::get(manager.manager_owned());
276 pending.register_uri_scheme_protocol("ipc", move |webview_id, request, responder| {
277 protocol(webview_id, request, UriSchemeResponder(responder))
278 });
279 registered_scheme_protocols.push("ipc".into());
280 }
281
282 let label = pending.label.clone();
283 let app_manager_ = manager.manager_owned();
284 let on_page_load_handler = pending.on_page_load_handler.take();
285 pending
286 .on_page_load_handler
287 .replace(Box::new(move |url, event| {
288 let payload = PageLoadPayload { url: &url, event };
289
290 if let Some(w) = app_manager_.get_webview(&label) {
291 if let Some(on_page_load) = &app_manager_.webview.on_page_load {
292 on_page_load(&w, &payload);
293 }
294
295 app_manager_
296 .plugins
297 .lock()
298 .unwrap()
299 .on_page_load(&w, &payload);
300 }
301
302 if let Some(handler) = &on_page_load_handler {
303 handler(url, event);
304 }
305 }));
306
307 #[cfg(feature = "protocol-asset")]
308 if !registered_scheme_protocols.contains(&"asset".into()) {
309 let asset_scope = app_manager
310 .state()
311 .get::<crate::Scopes>()
312 .asset_protocol
313 .clone();
314 let protocol = crate::protocol::asset::get(asset_scope, window_origin.clone());
315 pending.register_uri_scheme_protocol("asset", move |webview_id, request, responder| {
316 protocol(webview_id, request, UriSchemeResponder(responder))
317 });
318 }
319
320 #[cfg(feature = "isolation")]
321 if let crate::Pattern::Isolation {
322 assets,
323 schema,
324 key: _,
325 crypto_keys,
326 } = &*app_manager.pattern
327 {
328 let protocol = crate::protocol::isolation::get(
329 manager.manager_owned(),
330 schema,
331 assets.clone(),
332 *crypto_keys.aes_gcm().raw(),
333 window_origin,
334 use_https_scheme,
335 );
336 pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| {
337 protocol(webview_id, request, UriSchemeResponder(responder))
338 });
339 }
340
341 Ok(pending)
342 }
343
344 fn initialization_script(
345 &self,
346 app_manager: &AppManager<R>,
347 ipc_script: &str,
348 pattern_script: &str,
349 use_https_scheme: bool,
350 ) -> crate::Result<String> {
351 #[derive(Template)]
352 #[default_template("../../scripts/init.js")]
353 struct InitJavascript<'a> {
354 #[raw]
355 pattern_script: &'a str,
356 #[raw]
357 ipc_script: &'a str,
358 #[raw]
359 core_script: &'a str,
360 #[raw]
361 event_initialization_script: &'a str,
362 #[raw]
363 freeze_prototype: &'a str,
364 }
365
366 #[derive(Template)]
367 #[default_template("../../scripts/core.js")]
368 struct CoreJavascript<'a> {
369 os_name: &'a str,
370 protocol_scheme: &'a str,
371 invoke_key: &'a str,
372 }
373
374 let freeze_prototype = if app_manager.config.app.security.freeze_prototype {
375 include_str!("../../scripts/freeze_prototype.js")
376 } else {
377 ""
378 };
379
380 InitJavascript {
381 pattern_script,
382 ipc_script,
383 core_script: &CoreJavascript {
384 os_name: std::env::consts::OS,
385 protocol_scheme: if use_https_scheme { "https" } else { "http" },
386 invoke_key: self.invoke_key(),
387 }
388 .render_default(&Default::default())?
389 .into_string(),
390 event_initialization_script: &crate::event::event_initialization_script(
391 app_manager.listeners().function_name(),
392 app_manager.listeners().listeners_object_name(),
393 ),
394 freeze_prototype,
395 }
396 .render_default(&Default::default())
397 .map(|s| s.into_string())
398 .map_err(Into::into)
399 }
400
401 pub fn prepare_webview<M: Manager<R>>(
402 &self,
403 manager: &M,
404 mut pending: PendingWebview<EventLoopMessage, R>,
405 window_label: &str,
406 ) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
407 if self.webviews_lock().contains_key(&pending.label) {
408 return Err(crate::Error::WebviewLabelAlreadyExists(pending.label));
409 }
410
411 let app_manager = manager.manager();
412
413 #[allow(unused_mut)] let mut url = match &pending.webview_attributes.url {
415 WebviewUrl::App(path) => {
416 let app_url = app_manager.get_url(pending.webview_attributes.use_https_scheme);
417 let url = if PROXY_DEV_SERVER && is_local_network_url(&app_url) {
418 Cow::Owned(Url::parse("tauri://localhost").unwrap())
419 } else {
420 app_url
421 };
422 if path.to_str() != Some("index.html") {
424 url
425 .join(&path.to_string_lossy())
426 .map_err(crate::Error::InvalidUrl)
427 .unwrap()
429 } else {
430 url.into_owned()
431 }
432 }
433 WebviewUrl::External(url) => {
434 let config_url = app_manager.get_url(pending.webview_attributes.use_https_scheme);
435 let is_app_url = config_url.make_relative(url).is_some();
436 let mut url = url.clone();
437 if is_app_url && PROXY_DEV_SERVER && is_local_network_url(&url) {
438 Url::parse("tauri://localhost").unwrap()
439 } else {
440 url
441 }
442 }
443
444 WebviewUrl::CustomProtocol(url) => url.clone(),
445 _ => unimplemented!(),
446 };
447
448 #[cfg(not(feature = "webview-data-url"))]
449 if url.scheme() == "data" {
450 return Err(crate::Error::InvalidWebviewUrl(
451 "data URLs are not supported without the `webview-data-url` feature.",
452 ));
453 }
454
455 #[cfg(feature = "webview-data-url")]
456 if let Some(csp) = app_manager.csp() {
457 if url.scheme() == "data" {
458 if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
459 let (body, _) = data_url.decode_to_vec().unwrap();
460 let html = String::from_utf8_lossy(&body).into_owned();
461 if html.contains('<') && html.contains('>') {
463 let document = tauri_utils::html::parse(html);
464 tauri_utils::html::inject_csp(&document, &csp.to_string());
465 url.set_path(&format!("{},{document}", mime::TEXT_HTML));
466 }
467 }
468 }
469 }
470
471 pending.url = url.to_string();
472
473 #[cfg(target_os = "android")]
474 {
475 pending = pending.on_webview_created(move |ctx| {
476 let plugin_manager = ctx
477 .env
478 .call_method(
479 ctx.activity,
480 "getPluginManager",
481 "()Lapp/tauri/plugin/PluginManager;",
482 &[],
483 )?
484 .l()?;
485
486 ctx.env.call_method(
488 plugin_manager,
489 "onWebViewCreated",
490 "(Landroid/webkit/WebView;)V",
491 &[ctx.webview.into()],
492 )?;
493
494 Ok(())
495 });
496 }
497
498 let label = pending.label.clone();
499 pending = self.prepare_pending_webview(pending, &label, window_label, manager)?;
500
501 pending.ipc_handler = Some(crate::ipc::protocol::message_handler(
502 manager.manager_owned(),
503 ));
504
505 #[cfg(any(target_os = "linux", target_os = "windows"))]
508 if pending.webview_attributes.data_directory.is_none() {
509 let local_app_data = manager.path().resolve(
510 &app_manager.config.identifier,
511 crate::path::BaseDirectory::LocalData,
512 );
513 if let Ok(user_data_dir) = local_app_data {
514 pending.webview_attributes.data_directory = Some(user_data_dir);
515 }
516 }
517
518 if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
520 if !user_data_dir.exists() {
521 create_dir_all(user_data_dir)?;
522 }
523 }
524
525 #[cfg(all(desktop, not(target_os = "windows")))]
526 if pending.webview_attributes.zoom_hotkeys_enabled {
527 #[derive(Template)]
528 #[default_template("../webview/scripts/zoom-hotkey.js")]
529 struct HotkeyZoom<'a> {
530 os_name: &'a str,
531 }
532
533 pending
534 .webview_attributes
535 .initialization_scripts
536 .push(InitializationScript {
537 script: HotkeyZoom {
538 os_name: std::env::consts::OS,
539 }
540 .render_default(&Default::default())?
541 .into_string(),
542 for_main_frame_only: true,
543 })
544 }
545
546 #[cfg(feature = "isolation")]
547 let pattern = app_manager.pattern.clone();
548 let navigation_handler = pending.navigation_handler.take();
549 let app_manager = manager.manager_owned();
550 let label = pending.label.clone();
551 pending.navigation_handler = Some(Box::new(move |url| {
552 #[cfg(feature = "isolation")]
554 if let crate::Pattern::Isolation { schema, .. } = &*pattern {
555 if url.scheme() == schema
556 && url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN)
557 {
558 return true;
559 }
560 }
561 if let Some(handler) = &navigation_handler {
562 if !handler(url) {
563 return false;
564 }
565 }
566 let webview = app_manager.webview.webviews_lock().get(&label).cloned();
567 if let Some(w) = webview {
568 app_manager
569 .plugins
570 .lock()
571 .expect("poisoned plugin store")
572 .on_navigation(&w, url)
573 } else {
574 true
575 }
576 }));
577
578 Ok(pending)
579 }
580
581 pub(crate) fn attach_webview(
582 &self,
583 window: Window<R>,
584 webview: DetachedWebview<EventLoopMessage, R>,
585 use_https_scheme: bool,
586 ) -> Webview<R> {
587 let webview = Webview::new(window, webview, use_https_scheme);
588
589 let webview_event_listeners = self.event_listeners.clone();
590 let webview_ = webview.clone();
591 webview.on_webview_event(move |event| {
592 let _ = on_webview_event(&webview_, event);
593 for handler in webview_event_listeners.iter() {
594 handler(&webview_, event);
595 }
596 });
597
598 {
600 self
601 .webviews_lock()
602 .insert(webview.label().to_string(), webview.clone());
603 }
604
605 let manager = webview.manager_owned();
607 let webview_ = webview.clone();
608 let _ = webview.run_on_main_thread(move || {
610 manager
611 .plugins
612 .lock()
613 .expect("poisoned plugin store")
614 .webview_created(webview_);
615 });
616
617 #[cfg(all(target_os = "ios", feature = "wry"))]
618 {
619 webview
620 .with_webview(|w| {
621 unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) };
622 })
623 .expect("failed to run on_webview_created hook");
624 }
625
626 let event = crate::EventName::from_str("tauri://webview-created");
627 let payload = Some(crate::webview::CreatedEvent {
628 label: webview.label().into(),
629 });
630
631 let _ = webview
632 .manager
633 .emit(event, EmitPayload::Serialize(&payload));
634
635 webview
636 }
637
638 pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
639 let script = script.into();
640 let webviews = self.webviews_lock().values().cloned().collect::<Vec<_>>();
641 webviews
642 .iter()
643 .try_for_each(|webview| webview.eval(&script))
644 }
645
646 pub fn labels(&self) -> HashSet<String> {
647 self.webviews_lock().keys().cloned().collect()
648 }
649
650 pub(crate) fn invoke_key(&self) -> &str {
651 &self.invoke_key
652 }
653}
654
655impl<R: Runtime> Webview<R> {
656 fn emit_to_webview<S: Serialize>(
658 &self,
659 event: crate::EventName<&str>,
660 payload: &S,
661 ) -> crate::Result<()> {
662 let window_label = self.label();
663 let payload = EmitPayload::Serialize(payload);
664 self
665 .manager()
666 .emit_filter(event, payload, |target| match target {
667 EventTarget::Webview { label } | EventTarget::WebviewWindow { label } => {
668 label == window_label
669 }
670 _ => false,
671 })
672 }
673}
674
675fn on_webview_event<R: Runtime>(webview: &Webview<R>, event: &WebviewEvent) -> crate::Result<()> {
676 match event {
677 WebviewEvent::DragDrop(event) => match event {
678 DragDropEvent::Enter { paths, position } => {
679 let payload = DragDropPayload {
680 paths: Some(paths),
681 position,
682 };
683 webview.emit_to_webview(DRAG_ENTER_EVENT, &payload)?
684 }
685 DragDropEvent::Over { position } => {
686 let payload = DragDropPayload {
687 position,
688 paths: None,
689 };
690 webview.emit_to_webview(DRAG_OVER_EVENT, &payload)?
691 }
692 DragDropEvent::Drop { paths, position } => {
693 let scopes = webview.state::<Scopes>();
694 for path in paths {
695 if path.is_file() {
696 let _ = scopes.allow_file(path);
697 } else {
698 let _ = scopes.allow_directory(path, false);
699 }
700 }
701 let payload = DragDropPayload {
702 paths: Some(paths),
703 position,
704 };
705 webview.emit_to_webview(DRAG_DROP_EVENT, &payload)?
706 }
707 DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, &())?,
708 _ => unimplemented!(),
709 },
710 }
711
712 Ok(())
713}
714
715fn is_local_network_url(url: &url::Url) -> bool {
716 match url.host() {
717 Some(url::Host::Domain(s)) => s == "localhost",
718 Some(url::Host::Ipv4(_)) | Some(url::Host::Ipv6(_)) => true,
719 None => false,
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 #[test]
728 fn local_network_url() {
729 assert!(is_local_network_url(&"http://localhost".parse().unwrap()));
730 assert!(is_local_network_url(
731 &"http://127.0.0.1:8080".parse().unwrap()
732 ));
733 assert!(is_local_network_url(
734 &"https://192.168.3.17".parse().unwrap()
735 ));
736
737 assert!(!is_local_network_url(&"https://tauri.app".parse().unwrap()));
738 }
739}