From 3a2c3e74710bef9a14932dce74c351cca6215429 Mon Sep 17 00:00:00 2001 From: Jeffrey Hutchins Date: Mon, 24 Jul 2023 11:36:16 -0600 Subject: [PATCH] feat: Add ordered navigation handler for plugins, closes #7306 (#7439) Co-authored-by: Lucas Nogueira --- .changes/on-navigation-plugin.md | 6 ++ .../runtime-navigation-handler-url-arg.md | 5 + .changes/window-on-navigation-arg.md | 5 + core/tauri-runtime-wry/src/lib.rs | 4 +- core/tauri-runtime/src/window.rs | 4 +- core/tauri/src/manager.rs | 14 ++- core/tauri/src/plugin.rs | 92 +++++++++++++++---- core/tauri/src/window.rs | 4 +- .../src-tauri/tauri-plugin-sample/src/lib.rs | 4 + 9 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 .changes/on-navigation-plugin.md create mode 100644 .changes/runtime-navigation-handler-url-arg.md create mode 100644 .changes/window-on-navigation-arg.md diff --git a/.changes/on-navigation-plugin.md b/.changes/on-navigation-plugin.md new file mode 100644 index 000000000000..32b53447286c --- /dev/null +++ b/.changes/on-navigation-plugin.md @@ -0,0 +1,6 @@ +--- +"tauri": 'minor:feat' +--- + +Added `PluginBuilder::on_navigation`. +Added `Plugin::on_navigation`. diff --git a/.changes/runtime-navigation-handler-url-arg.md b/.changes/runtime-navigation-handler-url-arg.md new file mode 100644 index 000000000000..b6f48f6893a4 --- /dev/null +++ b/.changes/runtime-navigation-handler-url-arg.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime": patch:breaking +--- + +The `PendingWindow#navigation_handler` closure now receives a `&Url` argument instead of `Url`. diff --git a/.changes/window-on-navigation-arg.md b/.changes/window-on-navigation-arg.md new file mode 100644 index 000000000000..79ebb0e7eca6 --- /dev/null +++ b/.changes/window-on-navigation-arg.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +The `Window#on_navigation` closure now receives a `&Url` argument instead of `Url`. diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index b8413c4a7571..ded01af7b3b0 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -3125,7 +3125,9 @@ fn create_webview( } if let Some(navigation_handler) = pending.navigation_handler { webview_builder = webview_builder.with_navigation_handler(move |url| { - Url::parse(&url).map(&navigation_handler).unwrap_or(true) + Url::parse(&url) + .map(|url| navigation_handler(&url)) + .unwrap_or(true) }); } if let Some(user_agent) = webview_attributes.user_agent { diff --git a/core/tauri-runtime/src/window.rs b/core/tauri-runtime/src/window.rs index fbb7899f9b92..b04fd4ff598d 100644 --- a/core/tauri-runtime/src/window.rs +++ b/core/tauri-runtime/src/window.rs @@ -26,6 +26,8 @@ type UriSchemeProtocol = type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync; +type NavigationHandler = dyn Fn(&Url) -> bool + Send; + /// UI scaling utilities. pub mod dpi; @@ -233,7 +235,7 @@ pub struct PendingWindow> { pub menu_ids: Arc>>, /// A handler to decide if incoming url is allowed to navigate. - pub navigation_handler: Option bool + Send>>, + pub navigation_handler: Option>, /// The resolved URL to load on the webview. pub url: String, diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 2db699983845..752eed9c62d5 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -1114,6 +1114,8 @@ impl WindowManager { #[cfg(feature = "isolation")] let pattern = self.pattern().clone(); let navigation_handler = pending.navigation_handler.take(); + let manager = self.inner.clone(); + let label = pending.label.clone(); pending.navigation_handler = Some(Box::new(move |url| { // always allow navigation events for the isolation iframe and do not emit them for consumers #[cfg(feature = "isolation")] @@ -1125,7 +1127,17 @@ impl WindowManager { } } if let Some(handler) = &navigation_handler { - handler(url) + if !handler(url) { + return false; + } + } + let window = manager.windows.lock().unwrap().get(&label).cloned(); + if let Some(w) = window { + manager + .plugins + .lock() + .expect("poisoned plugin store") + .on_navigation(&w, url) } else { true } diff --git a/core/tauri/src/plugin.rs b/core/tauri/src/plugin.rs index c7a67913cec4..a5cb7907e760 100644 --- a/core/tauri/src/plugin.rs +++ b/core/tauri/src/plugin.rs @@ -11,8 +11,9 @@ use crate::{ use serde::de::DeserializeOwned; use serde_json::Value as JsonValue; use tauri_macros::default_runtime; +use url::Url; -use std::{collections::HashMap, fmt, result::Result as StdResult, sync::Arc}; +use std::{fmt, result::Result as StdResult, sync::Arc}; /// Mobile APIs. #[cfg(mobile)] @@ -48,6 +49,12 @@ pub trait Plugin: Send { #[allow(unused_variables)] fn created(&mut self, window: Window) {} + /// Callback invoked when webview tries to navigate to the given Url. Returning falses cancels navigation. + #[allow(unused_variables)] + fn on_navigation(&mut self, window: &Window, url: &Url) -> bool { + true + } + /// Callback invoked when the webview performs a navigation to a page. #[allow(unused_variables)] fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) {} @@ -66,6 +73,7 @@ pub trait Plugin: Send { type SetupHook = dyn FnOnce(&AppHandle, PluginApi) -> Result<()> + Send; type OnWebviewReady = dyn FnMut(Window) + Send; type OnEvent = dyn FnMut(&AppHandle, &RunEvent) + Send; +type OnNavigation = dyn Fn(&Window, &Url) -> bool + Send; type OnPageLoad = dyn FnMut(Window, PageLoadPayload) + Send; type OnDrop = dyn FnOnce(AppHandle) + Send; @@ -192,6 +200,7 @@ pub struct Builder { invoke_handler: Box>, setup: Option>>, js_init_script: Option, + on_navigation: Box>, on_page_load: Box>, on_webview_ready: Box>, on_event: Box>, @@ -206,6 +215,7 @@ impl Builder { setup: None, js_init_script: None, invoke_handler: Box::new(|_| false), + on_navigation: Box::new(|_, _| true), on_page_load: Box::new(|_, _| ()), on_webview_ready: Box::new(|_| ()), on_event: Box::new(|_, _| ()), @@ -313,6 +323,31 @@ impl Builder { self } + /// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation. + /// + /// #Example + /// + /// ``` + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_navigation(|window, url| { + /// // allow the production URL or localhost on dev + /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_navigation(mut self, on_navigation: F) -> Self + where + F: Fn(&Window, &Url) -> bool + Send + 'static, + { + self.on_navigation = Box::new(on_navigation); + self + } + /// Callback invoked when the webview performs a navigation to a page. /// /// # Examples @@ -426,6 +461,7 @@ impl Builder { invoke_handler: self.invoke_handler, setup: self.setup, js_init_script: self.js_init_script, + on_navigation: self.on_navigation, on_page_load: self.on_page_load, on_webview_ready: self.on_webview_ready, on_event: self.on_event, @@ -441,6 +477,7 @@ pub struct TauriPlugin { invoke_handler: Box>, setup: Option>>, js_init_script: Option, + on_navigation: Box>, on_page_load: Box>, on_webview_ready: Box>, on_event: Box>, @@ -484,6 +521,10 @@ impl Plugin for TauriPlugin { (self.on_webview_ready)(window) } + fn on_navigation(&mut self, window: &Window, url: &Url) -> bool { + (self.on_navigation)(window, url) + } + fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { (self.on_page_load)(window, payload) } @@ -500,22 +541,21 @@ impl Plugin for TauriPlugin { /// Plugin collection type. #[default_runtime(crate::Wry, wry)] pub(crate) struct PluginStore { - store: HashMap<&'static str, Box>>, + store: Vec>>, } impl fmt::Debug for PluginStore { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect(); f.debug_struct("PluginStore") - .field("plugins", &self.store.keys()) + .field("plugins", &plugins) .finish() } } impl Default for PluginStore { fn default() -> Self { - Self { - store: HashMap::new(), - } + Self { store: Vec::new() } } } @@ -524,12 +564,18 @@ impl PluginStore { /// /// Returns `true` if a plugin with the same name is already in the store. pub fn register + 'static>(&mut self, plugin: P) -> bool { - self.store.insert(plugin.name(), Box::new(plugin)).is_some() + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin.name()); + let result = len != self.store.len(); + self.store.push(Box::new(plugin)); + result } /// Removes the plugin with the given name from the store. pub fn unregister(&mut self, plugin: &'static str) -> bool { - self.store.remove(plugin).is_some() + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin); + len != self.store.len() } /// Initializes all plugins in the store. @@ -538,7 +584,7 @@ impl PluginStore { app: &AppHandle, config: &PluginConfig, ) -> crate::Result<()> { - self.store.values_mut().try_for_each(|plugin| { + self.store.iter_mut().try_for_each(|plugin| { plugin .initialize( app, @@ -552,7 +598,7 @@ impl PluginStore { pub(crate) fn initialization_script(&self) -> String { self .store - .values() + .iter() .filter_map(|p| p.initialization_script()) .fold(String::new(), |acc, script| { format!("{acc}\n(function () {{ {script} }})();") @@ -563,15 +609,24 @@ impl PluginStore { pub(crate) fn created(&mut self, window: Window) { self .store - .values_mut() + .iter_mut() .for_each(|plugin| plugin.created(window.clone())) } + pub(crate) fn on_navigation(&mut self, window: &Window, url: &Url) -> bool { + for plugin in self.store.iter_mut() { + if !plugin.on_navigation(window, url) { + return false; + } + } + true + } + /// Runs the on_page_load hook for all plugins in the store. pub(crate) fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { self .store - .values_mut() + .iter_mut() .for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone())) } @@ -579,7 +634,7 @@ impl PluginStore { pub(crate) fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { self .store - .values_mut() + .iter_mut() .for_each(|plugin| plugin.on_event(app, event)) } @@ -587,11 +642,12 @@ impl PluginStore { /// /// The message is not handled when the plugin exists **and** the command does not. pub(crate) fn extend_api(&mut self, plugin: &str, invoke: Invoke) -> bool { - if let Some(plugin) = self.store.get_mut(plugin) { - plugin.extend_api(invoke) - } else { - invoke.resolver.reject(format!("plugin {plugin} not found")); - true + for p in self.store.iter_mut() { + if p.name() == plugin { + return p.extend_api(invoke); + } } + invoke.resolver.reject(format!("plugin {plugin} not found")); + true } } diff --git a/core/tauri/src/window.rs b/core/tauri/src/window.rs index b6dc32922124..57fc821f71a6 100644 --- a/core/tauri/src/window.rs +++ b/core/tauri/src/window.rs @@ -60,7 +60,7 @@ use std::{ }; pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync; -pub(crate) type NavigationHandler = dyn Fn(Url) -> bool + Send; +pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; #[derive(Clone, Serialize)] struct WindowCreatedEvent { @@ -306,7 +306,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// Ok(()) /// }); /// ``` - pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { + pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { self.navigation_handler.replace(Box::new(f)); self } diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs b/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs index d7b9af348200..39b2d9a3f5c9 100644 --- a/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs +++ b/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs @@ -46,5 +46,9 @@ pub fn init() -> TauriPlugin { Ok(()) }) + .on_navigation(|window, url| { + println!("navigation {} {url}", window.label()); + true + }) .build() }