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