Skip to content

Commit

Permalink
feat: Add ordered navigation handler for plugins, closes #7306 (#7439)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
  • Loading branch information
jhutchins and lucasfernog authored Jul 24, 2023
1 parent b727735 commit 3a2c3e7
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 23 deletions.
6 changes: 6 additions & 0 deletions .changes/on-navigation-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"tauri": 'minor:feat'
---

Added `PluginBuilder::on_navigation`.
Added `Plugin::on_navigation`.
5 changes: 5 additions & 0 deletions .changes/runtime-navigation-handler-url-arg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri-runtime": patch:breaking
---

The `PendingWindow#navigation_handler` closure now receives a `&Url` argument instead of `Url`.
5 changes: 5 additions & 0 deletions .changes/window-on-navigation-arg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---

The `Window#on_navigation` closure now receives a `&Url` argument instead of `Url`.
4 changes: 3 additions & 1 deletion core/tauri-runtime-wry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3125,7 +3125,9 @@ fn create_webview<T: UserEvent>(
}
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 {
Expand Down
4 changes: 3 additions & 1 deletion core/tauri-runtime/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -233,7 +235,7 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
pub menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,

/// A handler to decide if incoming url is allowed to navigate.
pub navigation_handler: Option<Box<dyn Fn(Url) -> bool + Send>>,
pub navigation_handler: Option<Box<NavigationHandler>>,

/// The resolved URL to load on the webview.
pub url: String,
Expand Down
14 changes: 13 additions & 1 deletion core/tauri/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ impl<R: Runtime> WindowManager<R> {
#[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")]
Expand All @@ -1125,7 +1127,17 @@ impl<R: Runtime> WindowManager<R> {
}
}
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
}
Expand Down
92 changes: 74 additions & 18 deletions core/tauri/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -48,6 +49,12 @@ pub trait Plugin<R: Runtime>: Send {
#[allow(unused_variables)]
fn created(&mut self, window: Window<R>) {}

/// 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<R>, 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<R>, payload: PageLoadPayload) {}
Expand All @@ -66,6 +73,7 @@ pub trait Plugin<R: Runtime>: Send {
type SetupHook<R, C> = dyn FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<()> + Send;
type OnWebviewReady<R> = dyn FnMut(Window<R>) + Send;
type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
type OnNavigation<R> = dyn Fn(&Window<R>, &Url) -> bool + Send;
type OnPageLoad<R> = dyn FnMut(Window<R>, PageLoadPayload) + Send;
type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;

Expand Down Expand Up @@ -192,6 +200,7 @@ pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R, C>>>,
js_init_script: Option<String>,
on_navigation: Box<OnNavigation<R>>,
on_page_load: Box<OnPageLoad<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
Expand All @@ -206,6 +215,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
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(|_, _| ()),
Expand Down Expand Up @@ -313,6 +323,31 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
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<R: Runtime>() -> TauriPlugin<R> {
/// 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<F>(mut self, on_navigation: F) -> Self
where
F: Fn(&Window<R>, &Url) -> bool + Send + 'static,
{
self.on_navigation = Box::new(on_navigation);
self
}

/// Callback invoked when the webview performs a navigation to a page.
///
/// # Examples
Expand Down Expand Up @@ -426,6 +461,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
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,
Expand All @@ -441,6 +477,7 @@ pub struct TauriPlugin<R: Runtime, C: DeserializeOwned = ()> {
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R, C>>>,
js_init_script: Option<String>,
on_navigation: Box<OnNavigation<R>>,
on_page_load: Box<OnPageLoad<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
Expand Down Expand Up @@ -484,6 +521,10 @@ impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
(self.on_webview_ready)(window)
}

fn on_navigation(&mut self, window: &Window<R>, url: &Url) -> bool {
(self.on_navigation)(window, url)
}

fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
(self.on_page_load)(window, payload)
}
Expand All @@ -500,22 +541,21 @@ impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
/// Plugin collection type.
#[default_runtime(crate::Wry, wry)]
pub(crate) struct PluginStore<R: Runtime> {
store: HashMap<&'static str, Box<dyn Plugin<R>>>,
store: Vec<Box<dyn Plugin<R>>>,
}

impl<R: Runtime> fmt::Debug for PluginStore<R> {
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<R: Runtime> Default for PluginStore<R> {
fn default() -> Self {
Self {
store: HashMap::new(),
}
Self { store: Vec::new() }
}
}

Expand All @@ -524,12 +564,18 @@ impl<R: Runtime> PluginStore<R> {
///
/// Returns `true` if a plugin with the same name is already in the store.
pub fn register<P: Plugin<R> + '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.
Expand All @@ -538,7 +584,7 @@ impl<R: Runtime> PluginStore<R> {
app: &AppHandle<R>,
config: &PluginConfig,
) -> crate::Result<()> {
self.store.values_mut().try_for_each(|plugin| {
self.store.iter_mut().try_for_each(|plugin| {
plugin
.initialize(
app,
Expand All @@ -552,7 +598,7 @@ impl<R: Runtime> PluginStore<R> {
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} }})();")
Expand All @@ -563,35 +609,45 @@ impl<R: Runtime> PluginStore<R> {
pub(crate) fn created(&mut self, window: Window<R>) {
self
.store
.values_mut()
.iter_mut()
.for_each(|plugin| plugin.created(window.clone()))
}

pub(crate) fn on_navigation(&mut self, window: &Window<R>, 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<R>, payload: PageLoadPayload) {
self
.store
.values_mut()
.iter_mut()
.for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone()))
}

/// Runs the on_event hook for all plugins in the store.
pub(crate) fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
self
.store
.values_mut()
.iter_mut()
.for_each(|plugin| plugin.on_event(app, event))
}

/// Runs the plugin `extend_api` hook if it exists. Returns whether the invoke message was handled or not.
///
/// 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<R>) -> 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
}
}
4 changes: 2 additions & 2 deletions core/tauri/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -306,7 +306,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
/// Ok(())
/// });
/// ```
pub fn on_navigation<F: Fn(Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
pub fn on_navigation<F: Fn(&Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
self.navigation_handler.replace(Box::new(f));
self
}
Expand Down
4 changes: 4 additions & 0 deletions examples/api/src-tauri/tauri-plugin-sample/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {

Ok(())
})
.on_navigation(|window, url| {
println!("navigation {} {url}", window.label());
true
})
.build()
}

0 comments on commit 3a2c3e7

Please sign in to comment.