tauri/plugin.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//! The Tauri plugin extension to expand Tauri functionality.
6
7use crate::{
8 AppHandle, Error, RunEvent, Runtime, UriSchemeContext, Webview, Window,
9 app::UriSchemeResponder,
10 ipc::{Invoke, InvokeHandler, ScopeObject, ScopeValue},
11 manager::webview::UriSchemeProtocol,
12 utils::config::PluginConfig,
13 webview::PageLoadPayload,
14};
15use serde::{
16 Serialize, Serializer,
17 de::{Deserialize, DeserializeOwned, Deserializer, Error as DeError},
18};
19use serde_json::Value as JsonValue;
20use tauri_macros::default_runtime;
21use tauri_runtime::webview::InitializationScript;
22use thiserror::Error;
23use url::Url;
24
25use std::{
26 borrow::Cow,
27 collections::HashMap,
28 fmt::{self, Debug},
29 sync::Arc,
30};
31
32/// Mobile APIs.
33#[cfg(mobile)]
34pub mod mobile;
35
36/// The plugin interface.
37pub trait Plugin<R: Runtime>: Send {
38 /// The plugin name. Used as key on the plugin config object.
39 fn name(&self) -> &'static str;
40
41 /// Initializes the plugin.
42 #[allow(unused_variables)]
43 fn initialize(
44 &mut self,
45 app: &AppHandle<R>,
46 config: JsonValue,
47 ) -> Result<(), Box<dyn std::error::Error>> {
48 Ok(())
49 }
50
51 /// Add the provided JavaScript to a list of scripts that should be run after the global object has been created,
52 /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
53 ///
54 /// The script is wrapped into its own context with `(function () { /* your script here */ })();`,
55 /// so global variables must be assigned to `window` instead of implicitly declared.
56 ///
57 /// This is executed only on the main frame.
58 /// If you only want to run it in all frames, use [`Plugin::initialization_script_2`] to set that to false.
59 ///
60 /// ## Platform-specific
61 ///
62 /// - **Windows:** scripts are always added to subframes.
63 /// - **Android:** When [addDocumentStartJavaScript] is not supported,
64 /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
65 /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
66 ///
67 /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
68 /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
69 fn initialization_script(&self) -> Option<String> {
70 None
71 }
72
73 // TODO: Change `initialization_script` to this in v3
74 /// Same as [`Plugin::initialization_script`] but returns an [`InitializationScript`] instead
75 /// We plan to replace [`Plugin::initialization_script`] with this signature in v3
76 fn initialization_script_2(&self) -> Option<InitializationScript> {
77 self
78 .initialization_script()
79 .map(|script| InitializationScript {
80 script,
81 for_main_frame_only: true,
82 })
83 }
84
85 /// Callback invoked when the window is created.
86 #[allow(unused_variables)]
87 fn window_created(&mut self, window: Window<R>) {}
88
89 /// Callback invoked when the webview is created.
90 #[allow(unused_variables)]
91 fn webview_created(&mut self, webview: Webview<R>) {}
92
93 /// Callback invoked when webview tries to navigate to the given Url. Returning false cancels navigation.
94 #[allow(unused_variables)]
95 fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
96 true
97 }
98
99 /// Callback invoked when the webview performs a navigation to a page.
100 #[allow(unused_variables)]
101 fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {}
102
103 /// Callback invoked when the event loop receives a new event.
104 #[allow(unused_variables)]
105 fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {}
106
107 // TODO: Change this to `run_invoke_handler` in v3
108 /// Extend commands to [`crate::Builder::invoke_handler`].
109 #[allow(unused_variables)]
110 fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
111 false
112 }
113}
114
115type SetupHook<R, C> =
116 dyn FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<(), Box<dyn std::error::Error>> + Send;
117type OnWindowReady<R> = dyn FnMut(Window<R>) + Send;
118type OnWebviewReady<R> = dyn FnMut(Webview<R>) + Send;
119type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
120type OnNavigation<R> = dyn Fn(&Webview<R>, &Url) -> bool + Send;
121type OnPageLoad<R> = dyn FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send;
122type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;
123
124/// A handle to a plugin.
125#[derive(Debug)]
126#[allow(dead_code)]
127pub struct PluginHandle<R: Runtime> {
128 name: &'static str,
129 handle: AppHandle<R>,
130}
131
132impl<R: Runtime> Clone for PluginHandle<R> {
133 fn clone(&self) -> Self {
134 Self {
135 name: self.name,
136 handle: self.handle.clone(),
137 }
138 }
139}
140
141impl<R: Runtime> PluginHandle<R> {
142 /// Returns the application handle.
143 pub fn app(&self) -> &AppHandle<R> {
144 &self.handle
145 }
146}
147
148/// Api exposed to the plugin setup hook.
149#[derive(Clone)]
150#[allow(dead_code)]
151pub struct PluginApi<R: Runtime, C: DeserializeOwned> {
152 handle: AppHandle<R>,
153 name: &'static str,
154 raw_config: Arc<JsonValue>,
155 config: C,
156}
157
158impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
159 /// Returns the plugin configuration.
160 pub fn config(&self) -> &C {
161 &self.config
162 }
163
164 /// Returns the application handle.
165 pub fn app(&self) -> &AppHandle<R> {
166 &self.handle
167 }
168
169 /// Gets the global scope defined on the permissions that are part of the app ACL.
170 pub fn scope<T: ScopeObject>(&self) -> crate::Result<ScopeValue<T>> {
171 self
172 .handle
173 .manager
174 .runtime_authority
175 .lock()
176 .unwrap()
177 .scope_manager
178 .get_global_scope_typed(&self.handle, self.name)
179 }
180}
181
182/// Errors that can happen during [`Builder`].
183#[derive(Debug, Clone, Hash, PartialEq, Error)]
184#[non_exhaustive]
185pub enum BuilderError {
186 /// Plugin attempted to use a reserved name.
187 #[error("plugin uses reserved name: {0}")]
188 ReservedName(String),
189}
190
191const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
192
193/// Builds a [`TauriPlugin`].
194///
195/// This Builder offers a more concise way to construct Tauri plugins than implementing the Plugin trait directly.
196///
197/// # Conventions
198///
199/// When using the Builder Pattern it is encouraged to export a function called `init` that constructs and returns the plugin.
200/// While plugin authors can provide every possible way to construct a plugin,
201/// sticking to the `init` function convention helps users to quickly identify the correct function to call.
202///
203/// ```rust
204/// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
205///
206/// pub fn init<R: Runtime>() -> TauriPlugin<R> {
207/// Builder::new("example")
208/// .build()
209/// }
210/// ```
211///
212/// When plugins expose more complex configuration options, it can be helpful to provide a Builder instead:
213///
214/// ```rust
215/// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime};
216///
217/// pub struct Builder {
218/// option_a: String,
219/// option_b: String,
220/// option_c: bool
221/// }
222///
223/// impl Default for Builder {
224/// fn default() -> Self {
225/// Self {
226/// option_a: "foo".to_string(),
227/// option_b: "bar".to_string(),
228/// option_c: false
229/// }
230/// }
231/// }
232///
233/// impl Builder {
234/// pub fn new() -> Self {
235/// Default::default()
236/// }
237///
238/// pub fn option_a(mut self, option_a: String) -> Self {
239/// self.option_a = option_a;
240/// self
241/// }
242///
243/// pub fn option_b(mut self, option_b: String) -> Self {
244/// self.option_b = option_b;
245/// self
246/// }
247///
248/// pub fn option_c(mut self, option_c: bool) -> Self {
249/// self.option_c = option_c;
250/// self
251/// }
252///
253/// pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
254/// PluginBuilder::new("example")
255/// .setup(move |app_handle, api| {
256/// // use the options here to do stuff
257/// println!("a: {}, b: {}, c: {}", self.option_a, self.option_b, self.option_c);
258///
259/// Ok(())
260/// })
261/// .build()
262/// }
263/// }
264/// ```
265pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
266 name: &'static str,
267 invoke_handler: Box<InvokeHandler<R>>,
268 setup: Option<Box<SetupHook<R, C>>>,
269 js_init_script: Option<InitializationScript>,
270 on_navigation: Box<OnNavigation<R>>,
271 on_page_load: Box<OnPageLoad<R>>,
272 on_window_ready: Box<OnWindowReady<R>>,
273 on_webview_ready: Box<OnWebviewReady<R>>,
274 on_event: Box<OnEvent<R>>,
275 on_drop: Option<Box<OnDrop<R>>>,
276 uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
277}
278
279impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
280 /// Creates a new Plugin builder.
281 pub fn new(name: &'static str) -> Self {
282 Self {
283 name,
284 setup: None,
285 js_init_script: None,
286 invoke_handler: Box::new(|_| false),
287 on_navigation: Box::new(|_, _| true),
288 on_page_load: Box::new(|_, _| ()),
289 on_window_ready: Box::new(|_| ()),
290 on_webview_ready: Box::new(|_| ()),
291 on_event: Box::new(|_, _| ()),
292 on_drop: None,
293 uri_scheme_protocols: Default::default(),
294 }
295 }
296
297 /// Defines the JS message handler callback.
298 /// It is recommended you use the [tauri::generate_handler] to generate the input to this method, as the input type is not considered stable yet.
299 ///
300 /// # Examples
301 ///
302 /// ```rust
303 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
304 ///
305 /// #[tauri::command]
306 /// async fn foobar<R: Runtime>(app: tauri::AppHandle<R>, window: tauri::Window<R>) -> Result<(), String> {
307 /// println!("foobar");
308 ///
309 /// Ok(())
310 /// }
311 ///
312 /// fn init<R: Runtime>() -> TauriPlugin<R> {
313 /// Builder::new("example")
314 /// .invoke_handler(tauri::generate_handler![foobar])
315 /// .build()
316 /// }
317 ///
318 /// ```
319 /// [tauri::generate_handler]: ../macro.generate_handler.html
320 #[must_use]
321 pub fn invoke_handler<F>(mut self, invoke_handler: F) -> Self
322 where
323 F: Fn(Invoke<R>) -> bool + Send + Sync + 'static,
324 {
325 self.invoke_handler = Box::new(invoke_handler);
326 self
327 }
328
329 /// Sets the provided JavaScript to be run after the global object has been created,
330 /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
331 ///
332 /// The script is wrapped into its own context with `(function () { /* your script here */ })();`,
333 /// so global variables must be assigned to `window` instead of implicitly declared.
334 ///
335 /// Note that calling this function multiple times overrides previous values.
336 ///
337 /// This is executed only on the main frame.
338 /// If you only want to run it in all frames, use [`Self::js_init_script_on_all_frames`] instead.
339 ///
340 /// ## Platform-specific
341 ///
342 /// - **Windows:** scripts are always added to subframes.
343 /// - **Android:** When [addDocumentStartJavaScript] is not supported,
344 /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
345 /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
346 ///
347 /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
348 /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
349 ///
350 /// # Examples
351 ///
352 /// ```rust
353 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
354 ///
355 /// const INIT_SCRIPT: &str = r#"
356 /// if (window.location.origin === 'https://tauri.app') {
357 /// console.log("hello world from js init script");
358 ///
359 /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' };
360 /// }
361 /// "#;
362 ///
363 /// fn init<R: Runtime>() -> TauriPlugin<R> {
364 /// Builder::new("example")
365 /// .js_init_script(INIT_SCRIPT)
366 /// .build()
367 /// }
368 /// ```
369 #[must_use]
370 // TODO: Rename to `initialization_script` in v3
371 pub fn js_init_script(mut self, js_init_script: impl Into<String>) -> Self {
372 self.js_init_script = Some(InitializationScript {
373 script: js_init_script.into(),
374 for_main_frame_only: true,
375 });
376 self
377 }
378
379 /// Sets the provided JavaScript to be run after the global object has been created,
380 /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
381 ///
382 /// Since it runs on all top-level document and child frame page navigations,
383 /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
384 ///
385 /// Note that calling this function multiple times overrides previous values.
386 ///
387 /// This is executed on all frames, main frame and also sub frames.
388 /// If you only want to run it in the main frame, use [`Self::js_init_script`] instead.
389 ///
390 /// ## Platform-specific
391 ///
392 /// - **Windows:** scripts are always added to subframes.
393 /// - **Android:** When [addDocumentStartJavaScript] is not supported,
394 /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
395 /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
396 ///
397 /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
398 /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
399 #[must_use]
400 pub fn js_init_script_on_all_frames(mut self, js_init_script: impl Into<String>) -> Self {
401 self.js_init_script = Some(InitializationScript {
402 script: js_init_script.into(),
403 for_main_frame_only: false,
404 });
405 self
406 }
407
408 /// Define a closure that runs when the plugin is registered.
409 ///
410 /// # Examples
411 ///
412 /// ```rust
413 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime, Manager};
414 /// use std::path::PathBuf;
415 ///
416 /// #[derive(Debug, Default)]
417 /// struct PluginState {
418 /// dir: Option<PathBuf>
419 /// }
420 ///
421 /// fn init<R: Runtime>() -> TauriPlugin<R> {
422 /// Builder::new("example")
423 /// .setup(|app, api| {
424 /// app.manage(PluginState::default());
425 ///
426 /// Ok(())
427 /// })
428 /// .build()
429 /// }
430 /// ```
431 #[must_use]
432 pub fn setup<F>(mut self, setup: F) -> Self
433 where
434 F: FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<(), Box<dyn std::error::Error>>
435 + Send
436 + 'static,
437 {
438 self.setup.replace(Box::new(setup));
439 self
440 }
441
442 /// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation.
443 ///
444 /// #Example
445 ///
446 /// ```
447 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
448 ///
449 /// fn init<R: Runtime>() -> TauriPlugin<R> {
450 /// Builder::new("example")
451 /// .on_navigation(|webview, url| {
452 /// // allow the production URL or localhost on dev
453 /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost"))
454 /// })
455 /// .build()
456 /// }
457 /// ```
458 #[must_use]
459 pub fn on_navigation<F>(mut self, on_navigation: F) -> Self
460 where
461 F: Fn(&Webview<R>, &Url) -> bool + Send + 'static,
462 {
463 self.on_navigation = Box::new(on_navigation);
464 self
465 }
466
467 /// Callback invoked when the webview performs a navigation to a page.
468 ///
469 /// # Examples
470 ///
471 /// ```rust
472 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
473 ///
474 /// fn init<R: Runtime>() -> TauriPlugin<R> {
475 /// Builder::new("example")
476 /// .on_page_load(|webview, payload| {
477 /// println!("{:?} URL {} in webview {}", payload.event(), payload.url(), webview.label());
478 /// })
479 /// .build()
480 /// }
481 /// ```
482 #[must_use]
483 pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
484 where
485 F: FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send + 'static,
486 {
487 self.on_page_load = Box::new(on_page_load);
488 self
489 }
490
491 /// Callback invoked when the window is created.
492 ///
493 /// # Examples
494 ///
495 /// ```rust
496 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
497 ///
498 /// fn init<R: Runtime>() -> TauriPlugin<R> {
499 /// Builder::new("example")
500 /// .on_window_ready(|window| {
501 /// println!("created window {}", window.label());
502 /// })
503 /// .build()
504 /// }
505 /// ```
506 #[must_use]
507 pub fn on_window_ready<F>(mut self, on_window_ready: F) -> Self
508 where
509 F: FnMut(Window<R>) + Send + 'static,
510 {
511 self.on_window_ready = Box::new(on_window_ready);
512 self
513 }
514
515 /// Callback invoked when the webview is created.
516 ///
517 /// # Examples
518 ///
519 /// ```rust
520 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
521 ///
522 /// fn init<R: Runtime>() -> TauriPlugin<R> {
523 /// Builder::new("example")
524 /// .on_webview_ready(|webview| {
525 /// println!("created webview {}", webview.label());
526 /// })
527 /// .build()
528 /// }
529 /// ```
530 #[must_use]
531 pub fn on_webview_ready<F>(mut self, on_webview_ready: F) -> Self
532 where
533 F: FnMut(Webview<R>) + Send + 'static,
534 {
535 self.on_webview_ready = Box::new(on_webview_ready);
536 self
537 }
538
539 /// Callback invoked when the event loop receives a new event.
540 ///
541 /// # Examples
542 ///
543 /// ```rust
544 /// use tauri::{plugin::{Builder, TauriPlugin}, RunEvent, Runtime};
545 ///
546 /// fn init<R: Runtime>() -> TauriPlugin<R> {
547 /// Builder::new("example")
548 /// .on_event(|app_handle, event| {
549 /// match event {
550 /// RunEvent::ExitRequested { api, .. } => {
551 /// // Prevents the app from exiting.
552 /// // This will cause the core thread to continue running in the background even without any open windows.
553 /// api.prevent_exit();
554 /// }
555 /// // Ignore all other cases.
556 /// _ => {}
557 /// }
558 /// })
559 /// .build()
560 /// }
561 /// ```
562 #[must_use]
563 pub fn on_event<F>(mut self, on_event: F) -> Self
564 where
565 F: FnMut(&AppHandle<R>, &RunEvent) + Send + 'static,
566 {
567 self.on_event = Box::new(on_event);
568 self
569 }
570
571 /// Callback invoked when the plugin is dropped.
572 ///
573 /// # Examples
574 ///
575 /// ```rust
576 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
577 ///
578 /// fn init<R: Runtime>() -> TauriPlugin<R> {
579 /// Builder::new("example")
580 /// .on_drop(|app| {
581 /// println!("plugin has been dropped and is no longer running");
582 /// // you can run cleanup logic here
583 /// })
584 /// .build()
585 /// }
586 /// ```
587 #[must_use]
588 pub fn on_drop<F>(mut self, on_drop: F) -> Self
589 where
590 F: FnOnce(AppHandle<R>) + Send + 'static,
591 {
592 self.on_drop.replace(Box::new(on_drop));
593 self
594 }
595
596 /// Registers a URI scheme protocol available to all webviews.
597 ///
598 /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS,
599 /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows
600 /// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux.
601 ///
602 /// # Known limitations
603 ///
604 /// URI scheme protocols are registered when the webview is created. Due to this limitation, if the plugin is registered after a webview has been created, this protocol won't be available.
605 ///
606 /// # Arguments
607 ///
608 /// * `uri_scheme` The URI scheme to register, such as `example`.
609 /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`.
610 ///
611 /// # Examples
612 ///
613 /// ```rust
614 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
615 ///
616 /// fn init<R: Runtime>() -> TauriPlugin<R> {
617 /// Builder::new("myplugin")
618 /// .register_uri_scheme_protocol("myscheme", |_ctx, req| {
619 /// http::Response::builder().body(Vec::new()).unwrap()
620 /// })
621 /// .build()
622 /// }
623 /// ```
624 ///
625 /// # Warning
626 ///
627 /// Pages loaded from a custom protocol will have a different Origin on different platforms.
628 /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin`
629 /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the
630 /// different Origin headers across platforms:
631 ///
632 /// - macOS, iOS and Linux: `<scheme_name>://localhost/<path>` (so it will be `my-scheme://localhost/path/to/page).
633 /// - Windows and Android: `http://<scheme_name>.localhost/<path>` by default (so it will be `http://my-scheme.localhost/path/to/page`).
634 /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`].
635 #[must_use]
636 pub fn register_uri_scheme_protocol<
637 N: Into<String>,
638 T: Into<Cow<'static, [u8]>>,
639 H: Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>) -> http::Response<T>
640 + Send
641 + Sync
642 + 'static,
643 >(
644 mut self,
645 uri_scheme: N,
646 protocol_handler: H,
647 ) -> Self {
648 self.uri_scheme_protocols.insert(
649 uri_scheme.into(),
650 Arc::new(UriSchemeProtocol {
651 handler: Box::new(move |ctx, request, responder| {
652 responder.respond(protocol_handler(ctx, request))
653 }),
654 }),
655 );
656 self
657 }
658
659 /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you
660 /// to process the request in a separate thread and respond asynchronously.
661 ///
662 /// # Arguments
663 ///
664 /// * `uri_scheme` The URI scheme to register, such as `example`.
665 /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`.
666 ///
667 /// # Examples
668 ///
669 /// ```rust
670 /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
671 ///
672 /// fn init<R: Runtime>() -> TauriPlugin<R> {
673 /// Builder::new("myplugin")
674 /// .register_asynchronous_uri_scheme_protocol("app-files", |_ctx, request, responder| {
675 /// // skip leading `/`
676 /// let path = request.uri().path()[1..].to_string();
677 /// std::thread::spawn(move || {
678 /// if let Ok(data) = std::fs::read(path) {
679 /// responder.respond(
680 /// http::Response::builder()
681 /// .body(data)
682 /// .unwrap()
683 /// );
684 /// } else {
685 /// responder.respond(
686 /// http::Response::builder()
687 /// .status(http::StatusCode::BAD_REQUEST)
688 /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
689 /// .body("failed to read file".as_bytes().to_vec())
690 /// .unwrap()
691 /// );
692 /// }
693 /// });
694 /// })
695 /// .build()
696 /// }
697 /// ```
698 ///
699 /// # Warning
700 ///
701 /// Pages loaded from a custom protocol will have a different Origin on different platforms.
702 /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin`
703 /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the
704 /// different Origin headers across platforms:
705 ///
706 /// - macOS, iOS and Linux: `<scheme_name>://localhost/<path>` (so it will be `my-scheme://localhost/path/to/page).
707 /// - Windows and Android: `http://<scheme_name>.localhost/<path>` by default (so it will be `http://my-scheme.localhost/path/to/page`).
708 /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`].
709 #[must_use]
710 pub fn register_asynchronous_uri_scheme_protocol<
711 N: Into<String>,
712 H: Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync + 'static,
713 >(
714 mut self,
715 uri_scheme: N,
716 protocol_handler: H,
717 ) -> Self {
718 self.uri_scheme_protocols.insert(
719 uri_scheme.into(),
720 Arc::new(UriSchemeProtocol {
721 handler: Box::new(protocol_handler),
722 }),
723 );
724 self
725 }
726
727 /// Builds the [`TauriPlugin`].
728 pub fn try_build(self) -> Result<TauriPlugin<R, C>, BuilderError> {
729 if let Some(&reserved) = RESERVED_PLUGIN_NAMES.iter().find(|&r| r == &self.name) {
730 return Err(BuilderError::ReservedName(reserved.into()));
731 }
732
733 Ok(TauriPlugin {
734 name: self.name,
735 app: None,
736 invoke_handler: self.invoke_handler,
737 setup: self.setup,
738 js_init_script: self.js_init_script,
739 on_navigation: self.on_navigation,
740 on_page_load: self.on_page_load,
741 on_window_ready: self.on_window_ready,
742 on_webview_ready: self.on_webview_ready,
743 on_event: self.on_event,
744 on_drop: self.on_drop,
745 uri_scheme_protocols: self.uri_scheme_protocols,
746 })
747 }
748
749 /// Builds the [`TauriPlugin`].
750 ///
751 /// # Panics
752 ///
753 /// If the builder returns an error during [`Self::try_build`], then this method will panic.
754 pub fn build(self) -> TauriPlugin<R, C> {
755 self.try_build().expect("valid plugin")
756 }
757}
758
759/// Plugin struct that is returned by the [`Builder`]. Should only be constructed through the builder.
760pub struct TauriPlugin<R: Runtime, C: DeserializeOwned = ()> {
761 name: &'static str,
762 app: Option<AppHandle<R>>,
763 invoke_handler: Box<InvokeHandler<R>>,
764 setup: Option<Box<SetupHook<R, C>>>,
765 js_init_script: Option<InitializationScript>,
766 on_navigation: Box<OnNavigation<R>>,
767 on_page_load: Box<OnPageLoad<R>>,
768 on_window_ready: Box<OnWindowReady<R>>,
769 on_webview_ready: Box<OnWebviewReady<R>>,
770 on_event: Box<OnEvent<R>>,
771 on_drop: Option<Box<OnDrop<R>>>,
772 uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
773}
774
775impl<R: Runtime, C: DeserializeOwned> Drop for TauriPlugin<R, C> {
776 fn drop(&mut self) {
777 if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) {
778 on_drop(app);
779 }
780 }
781}
782
783impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
784 fn name(&self) -> &'static str {
785 self.name
786 }
787
788 fn initialize(
789 &mut self,
790 app: &AppHandle<R>,
791 config: JsonValue,
792 ) -> Result<(), Box<dyn std::error::Error>> {
793 self.app.replace(app.clone());
794 if let Some(s) = self.setup.take() {
795 (s)(
796 app,
797 PluginApi {
798 name: self.name,
799 handle: app.clone(),
800 raw_config: Arc::new(config.clone()),
801 config: serde_json::from_value(config).map_err(|err| {
802 format!(
803 "Error deserializing 'plugins.{}' within your Tauri configuration: {err}",
804 self.name
805 )
806 })?,
807 },
808 )?;
809 }
810
811 for (uri_scheme, protocol) in &self.uri_scheme_protocols {
812 app
813 .manager
814 .webview
815 .register_uri_scheme_protocol(uri_scheme, protocol.clone())
816 }
817 Ok(())
818 }
819
820 fn initialization_script(&self) -> Option<String> {
821 self
822 .js_init_script
823 .clone()
824 .map(|initialization_script| initialization_script.script)
825 }
826
827 fn initialization_script_2(&self) -> Option<InitializationScript> {
828 self.js_init_script.clone()
829 }
830
831 fn window_created(&mut self, window: Window<R>) {
832 (self.on_window_ready)(window)
833 }
834
835 fn webview_created(&mut self, webview: Webview<R>) {
836 (self.on_webview_ready)(webview)
837 }
838
839 fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
840 (self.on_navigation)(webview, url)
841 }
842
843 fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
844 (self.on_page_load)(webview, payload)
845 }
846
847 fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
848 (self.on_event)(app, event)
849 }
850
851 fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
852 (self.invoke_handler)(invoke)
853 }
854}
855
856/// Plugin collection type.
857#[default_runtime(crate::Wry, wry)]
858pub(crate) struct PluginStore<R: Runtime> {
859 store: Vec<Box<dyn Plugin<R>>>,
860}
861
862impl<R: Runtime> fmt::Debug for PluginStore<R> {
863 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864 let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect();
865 f.debug_struct("PluginStore")
866 .field("plugins", &plugins)
867 .finish()
868 }
869}
870
871impl<R: Runtime> Default for PluginStore<R> {
872 fn default() -> Self {
873 Self { store: Vec::new() }
874 }
875}
876
877impl<R: Runtime> PluginStore<R> {
878 /// Adds a plugin to the store.
879 ///
880 /// Returns `true` if a plugin with the same name is already in the store.
881 pub fn register(&mut self, plugin: Box<dyn Plugin<R>>) -> bool {
882 let len = self.store.len();
883 self.store.retain(|p| p.name() != plugin.name());
884 let result = len != self.store.len();
885 self.store.push(plugin);
886 result
887 }
888
889 /// Removes the plugin with the given name from the store.
890 pub fn unregister(&mut self, plugin: &str) -> bool {
891 let len = self.store.len();
892 self.store.retain(|p| p.name() != plugin);
893 len != self.store.len()
894 }
895
896 /// Initializes the given plugin.
897 pub(crate) fn initialize(
898 &self,
899 plugin: &mut Box<dyn Plugin<R>>,
900 app: &AppHandle<R>,
901 config: &PluginConfig,
902 ) -> crate::Result<()> {
903 initialize(plugin, app, config)
904 }
905
906 /// Initializes all plugins in the store.
907 pub(crate) fn initialize_all(
908 &mut self,
909 app: &AppHandle<R>,
910 config: &PluginConfig,
911 ) -> crate::Result<()> {
912 self
913 .store
914 .iter_mut()
915 .try_for_each(|plugin| initialize(plugin, app, config))
916 }
917
918 /// Generates an initialization script from all plugins in the store.
919 pub(crate) fn initialization_script(&self) -> Vec<InitializationScript> {
920 self
921 .store
922 .iter()
923 .filter_map(|p| p.initialization_script_2())
924 .map(
925 |InitializationScript {
926 script,
927 for_main_frame_only,
928 }| InitializationScript {
929 script: format!("(function () {{ {script} }})();"),
930 for_main_frame_only,
931 },
932 )
933 .collect()
934 }
935
936 /// Runs the created hook for all plugins in the store.
937 pub(crate) fn window_created(&mut self, window: Window<R>) {
938 self.store.iter_mut().for_each(|plugin| {
939 #[cfg(feature = "tracing")]
940 let _span = tracing::trace_span!("plugin::hooks::created", name = plugin.name()).entered();
941 plugin.window_created(window.clone())
942 })
943 }
944
945 /// Runs the webview created hook for all plugins in the store.
946 pub(crate) fn webview_created(&mut self, webview: Webview<R>) {
947 self
948 .store
949 .iter_mut()
950 .for_each(|plugin| plugin.webview_created(webview.clone()))
951 }
952
953 pub(crate) fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
954 for plugin in self.store.iter_mut() {
955 #[cfg(feature = "tracing")]
956 let _span =
957 tracing::trace_span!("plugin::hooks::on_navigation", name = plugin.name()).entered();
958 if !plugin.on_navigation(webview, url) {
959 return false;
960 }
961 }
962 true
963 }
964
965 /// Runs the on_page_load hook for all plugins in the store.
966 pub(crate) fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
967 self.store.iter_mut().for_each(|plugin| {
968 #[cfg(feature = "tracing")]
969 let _span =
970 tracing::trace_span!("plugin::hooks::on_page_load", name = plugin.name()).entered();
971 plugin.on_page_load(webview, payload)
972 })
973 }
974
975 /// Runs the on_event hook for all plugins in the store.
976 pub(crate) fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
977 self
978 .store
979 .iter_mut()
980 .for_each(|plugin| plugin.on_event(app, event))
981 }
982
983 /// Runs the plugin [`Plugin::extend_api`] hook if it exists. Returns whether the invoke message was handled or not.
984 ///
985 /// The message is not handled when the plugin exists **and** the command does not.
986 pub(crate) fn run_invoke_handler(&mut self, plugin: &str, invoke: Invoke<R>) -> bool {
987 for p in self.store.iter_mut() {
988 if p.name() == plugin {
989 #[cfg(feature = "tracing")]
990 let _span = tracing::trace_span!("plugin::hooks::ipc", name = plugin).entered();
991 return p.extend_api(invoke);
992 }
993 }
994 invoke.resolver.reject(format!("plugin {plugin} not found"));
995 true
996 }
997}
998
999#[cfg_attr(feature = "tracing", tracing::instrument(name = "plugin::hooks::initialize", skip(plugin, app), fields(name = plugin.name())))]
1000fn initialize<R: Runtime>(
1001 plugin: &mut Box<dyn Plugin<R>>,
1002 app: &AppHandle<R>,
1003 config: &PluginConfig,
1004) -> crate::Result<()> {
1005 plugin
1006 .initialize(
1007 app,
1008 config.0.get(plugin.name()).cloned().unwrap_or_default(),
1009 )
1010 .map_err(|e| Error::PluginInitialization(plugin.name().to_string(), e.to_string()))
1011}
1012
1013/// Permission state.
1014#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1015#[cfg_attr(feature = "specta", derive(specta::Type))]
1016pub enum PermissionState {
1017 /// Permission access has been granted.
1018 Granted,
1019 /// Permission access has been denied.
1020 Denied,
1021 /// Permission must be requested.
1022 #[default]
1023 Prompt,
1024 /// Permission must be requested, but you must explain to the user why your app needs that permission. **Android only**.
1025 PromptWithRationale,
1026}
1027
1028impl std::fmt::Display for PermissionState {
1029 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1030 match self {
1031 Self::Granted => write!(f, "granted"),
1032 Self::Denied => write!(f, "denied"),
1033 Self::Prompt => write!(f, "prompt"),
1034 Self::PromptWithRationale => write!(f, "prompt-with-rationale"),
1035 }
1036 }
1037}
1038
1039impl Serialize for PermissionState {
1040 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1041 where
1042 S: Serializer,
1043 {
1044 serializer.serialize_str(self.to_string().as_ref())
1045 }
1046}
1047
1048impl<'de> Deserialize<'de> for PermissionState {
1049 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1050 where
1051 D: Deserializer<'de>,
1052 {
1053 let s = <String as Deserialize>::deserialize(deserializer)?;
1054 match s.to_lowercase().as_str() {
1055 "granted" => Ok(Self::Granted),
1056 "denied" => Ok(Self::Denied),
1057 "prompt" => Ok(Self::Prompt),
1058 "prompt-with-rationale" => Ok(Self::PromptWithRationale),
1059 _ => Err(DeError::custom(format!("unknown permission state '{s}'"))),
1060 }
1061 }
1062}