Skip to content

Commit 3a2c3e7

Browse files
feat: Add ordered navigation handler for plugins, closes #7306 (#7439)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
1 parent b727735 commit 3a2c3e7

File tree

9 files changed

+115
-23
lines changed

9 files changed

+115
-23
lines changed

.changes/on-navigation-plugin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": 'minor:feat'
3+
---
4+
5+
Added `PluginBuilder::on_navigation`.
6+
Added `Plugin::on_navigation`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri-runtime": patch:breaking
3+
---
4+
5+
The `PendingWindow#navigation_handler` closure now receives a `&Url` argument instead of `Url`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch:breaking
3+
---
4+
5+
The `Window#on_navigation` closure now receives a `&Url` argument instead of `Url`.

core/tauri-runtime-wry/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3125,7 +3125,9 @@ fn create_webview<T: UserEvent>(
31253125
}
31263126
if let Some(navigation_handler) = pending.navigation_handler {
31273127
webview_builder = webview_builder.with_navigation_handler(move |url| {
3128-
Url::parse(&url).map(&navigation_handler).unwrap_or(true)
3128+
Url::parse(&url)
3129+
.map(|url| navigation_handler(&url))
3130+
.unwrap_or(true)
31293131
});
31303132
}
31313133
if let Some(user_agent) = webview_attributes.user_agent {

core/tauri-runtime/src/window.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type UriSchemeProtocol =
2626

2727
type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
2828

29+
type NavigationHandler = dyn Fn(&Url) -> bool + Send;
30+
2931
/// UI scaling utilities.
3032
pub mod dpi;
3133

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

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

238240
/// The resolved URL to load on the webview.
239241
pub url: String,

core/tauri/src/manager.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,8 @@ impl<R: Runtime> WindowManager<R> {
11141114
#[cfg(feature = "isolation")]
11151115
let pattern = self.pattern().clone();
11161116
let navigation_handler = pending.navigation_handler.take();
1117+
let manager = self.inner.clone();
1118+
let label = pending.label.clone();
11171119
pending.navigation_handler = Some(Box::new(move |url| {
11181120
// always allow navigation events for the isolation iframe and do not emit them for consumers
11191121
#[cfg(feature = "isolation")]
@@ -1125,7 +1127,17 @@ impl<R: Runtime> WindowManager<R> {
11251127
}
11261128
}
11271129
if let Some(handler) = &navigation_handler {
1128-
handler(url)
1130+
if !handler(url) {
1131+
return false;
1132+
}
1133+
}
1134+
let window = manager.windows.lock().unwrap().get(&label).cloned();
1135+
if let Some(w) = window {
1136+
manager
1137+
.plugins
1138+
.lock()
1139+
.expect("poisoned plugin store")
1140+
.on_navigation(&w, url)
11291141
} else {
11301142
true
11311143
}

core/tauri/src/plugin.rs

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ use crate::{
1111
use serde::de::DeserializeOwned;
1212
use serde_json::Value as JsonValue;
1313
use tauri_macros::default_runtime;
14+
use url::Url;
1415

15-
use std::{collections::HashMap, fmt, result::Result as StdResult, sync::Arc};
16+
use std::{fmt, result::Result as StdResult, sync::Arc};
1617

1718
/// Mobile APIs.
1819
#[cfg(mobile)]
@@ -48,6 +49,12 @@ pub trait Plugin<R: Runtime>: Send {
4849
#[allow(unused_variables)]
4950
fn created(&mut self, window: Window<R>) {}
5051

52+
/// Callback invoked when webview tries to navigate to the given Url. Returning falses cancels navigation.
53+
#[allow(unused_variables)]
54+
fn on_navigation(&mut self, window: &Window<R>, url: &Url) -> bool {
55+
true
56+
}
57+
5158
/// Callback invoked when the webview performs a navigation to a page.
5259
#[allow(unused_variables)]
5360
fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {}
@@ -66,6 +73,7 @@ pub trait Plugin<R: Runtime>: Send {
6673
type SetupHook<R, C> = dyn FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<()> + Send;
6774
type OnWebviewReady<R> = dyn FnMut(Window<R>) + Send;
6875
type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
76+
type OnNavigation<R> = dyn Fn(&Window<R>, &Url) -> bool + Send;
6977
type OnPageLoad<R> = dyn FnMut(Window<R>, PageLoadPayload) + Send;
7078
type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;
7179

@@ -192,6 +200,7 @@ pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
192200
invoke_handler: Box<InvokeHandler<R>>,
193201
setup: Option<Box<SetupHook<R, C>>>,
194202
js_init_script: Option<String>,
203+
on_navigation: Box<OnNavigation<R>>,
195204
on_page_load: Box<OnPageLoad<R>>,
196205
on_webview_ready: Box<OnWebviewReady<R>>,
197206
on_event: Box<OnEvent<R>>,
@@ -206,6 +215,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
206215
setup: None,
207216
js_init_script: None,
208217
invoke_handler: Box::new(|_| false),
218+
on_navigation: Box::new(|_, _| true),
209219
on_page_load: Box::new(|_, _| ()),
210220
on_webview_ready: Box::new(|_| ()),
211221
on_event: Box::new(|_, _| ()),
@@ -313,6 +323,31 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
313323
self
314324
}
315325

326+
/// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation.
327+
///
328+
/// #Example
329+
///
330+
/// ```
331+
/// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
332+
///
333+
/// fn init<R: Runtime>() -> TauriPlugin<R> {
334+
/// Builder::new("example")
335+
/// .on_navigation(|window, url| {
336+
/// // allow the production URL or localhost on dev
337+
/// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost"))
338+
/// })
339+
/// .build()
340+
/// }
341+
/// ```
342+
#[must_use]
343+
pub fn on_navigation<F>(mut self, on_navigation: F) -> Self
344+
where
345+
F: Fn(&Window<R>, &Url) -> bool + Send + 'static,
346+
{
347+
self.on_navigation = Box::new(on_navigation);
348+
self
349+
}
350+
316351
/// Callback invoked when the webview performs a navigation to a page.
317352
///
318353
/// # Examples
@@ -426,6 +461,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
426461
invoke_handler: self.invoke_handler,
427462
setup: self.setup,
428463
js_init_script: self.js_init_script,
464+
on_navigation: self.on_navigation,
429465
on_page_load: self.on_page_load,
430466
on_webview_ready: self.on_webview_ready,
431467
on_event: self.on_event,
@@ -441,6 +477,7 @@ pub struct TauriPlugin<R: Runtime, C: DeserializeOwned = ()> {
441477
invoke_handler: Box<InvokeHandler<R>>,
442478
setup: Option<Box<SetupHook<R, C>>>,
443479
js_init_script: Option<String>,
480+
on_navigation: Box<OnNavigation<R>>,
444481
on_page_load: Box<OnPageLoad<R>>,
445482
on_webview_ready: Box<OnWebviewReady<R>>,
446483
on_event: Box<OnEvent<R>>,
@@ -484,6 +521,10 @@ impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
484521
(self.on_webview_ready)(window)
485522
}
486523

524+
fn on_navigation(&mut self, window: &Window<R>, url: &Url) -> bool {
525+
(self.on_navigation)(window, url)
526+
}
527+
487528
fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
488529
(self.on_page_load)(window, payload)
489530
}
@@ -500,22 +541,21 @@ impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
500541
/// Plugin collection type.
501542
#[default_runtime(crate::Wry, wry)]
502543
pub(crate) struct PluginStore<R: Runtime> {
503-
store: HashMap<&'static str, Box<dyn Plugin<R>>>,
544+
store: Vec<Box<dyn Plugin<R>>>,
504545
}
505546

506547
impl<R: Runtime> fmt::Debug for PluginStore<R> {
507548
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549+
let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect();
508550
f.debug_struct("PluginStore")
509-
.field("plugins", &self.store.keys())
551+
.field("plugins", &plugins)
510552
.finish()
511553
}
512554
}
513555

514556
impl<R: Runtime> Default for PluginStore<R> {
515557
fn default() -> Self {
516-
Self {
517-
store: HashMap::new(),
518-
}
558+
Self { store: Vec::new() }
519559
}
520560
}
521561

@@ -524,12 +564,18 @@ impl<R: Runtime> PluginStore<R> {
524564
///
525565
/// Returns `true` if a plugin with the same name is already in the store.
526566
pub fn register<P: Plugin<R> + 'static>(&mut self, plugin: P) -> bool {
527-
self.store.insert(plugin.name(), Box::new(plugin)).is_some()
567+
let len = self.store.len();
568+
self.store.retain(|p| p.name() != plugin.name());
569+
let result = len != self.store.len();
570+
self.store.push(Box::new(plugin));
571+
result
528572
}
529573

530574
/// Removes the plugin with the given name from the store.
531575
pub fn unregister(&mut self, plugin: &'static str) -> bool {
532-
self.store.remove(plugin).is_some()
576+
let len = self.store.len();
577+
self.store.retain(|p| p.name() != plugin);
578+
len != self.store.len()
533579
}
534580

535581
/// Initializes all plugins in the store.
@@ -538,7 +584,7 @@ impl<R: Runtime> PluginStore<R> {
538584
app: &AppHandle<R>,
539585
config: &PluginConfig,
540586
) -> crate::Result<()> {
541-
self.store.values_mut().try_for_each(|plugin| {
587+
self.store.iter_mut().try_for_each(|plugin| {
542588
plugin
543589
.initialize(
544590
app,
@@ -552,7 +598,7 @@ impl<R: Runtime> PluginStore<R> {
552598
pub(crate) fn initialization_script(&self) -> String {
553599
self
554600
.store
555-
.values()
601+
.iter()
556602
.filter_map(|p| p.initialization_script())
557603
.fold(String::new(), |acc, script| {
558604
format!("{acc}\n(function () {{ {script} }})();")
@@ -563,35 +609,45 @@ impl<R: Runtime> PluginStore<R> {
563609
pub(crate) fn created(&mut self, window: Window<R>) {
564610
self
565611
.store
566-
.values_mut()
612+
.iter_mut()
567613
.for_each(|plugin| plugin.created(window.clone()))
568614
}
569615

616+
pub(crate) fn on_navigation(&mut self, window: &Window<R>, url: &Url) -> bool {
617+
for plugin in self.store.iter_mut() {
618+
if !plugin.on_navigation(window, url) {
619+
return false;
620+
}
621+
}
622+
true
623+
}
624+
570625
/// Runs the on_page_load hook for all plugins in the store.
571626
pub(crate) fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
572627
self
573628
.store
574-
.values_mut()
629+
.iter_mut()
575630
.for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone()))
576631
}
577632

578633
/// Runs the on_event hook for all plugins in the store.
579634
pub(crate) fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
580635
self
581636
.store
582-
.values_mut()
637+
.iter_mut()
583638
.for_each(|plugin| plugin.on_event(app, event))
584639
}
585640

586641
/// Runs the plugin `extend_api` hook if it exists. Returns whether the invoke message was handled or not.
587642
///
588643
/// The message is not handled when the plugin exists **and** the command does not.
589644
pub(crate) fn extend_api(&mut self, plugin: &str, invoke: Invoke<R>) -> bool {
590-
if let Some(plugin) = self.store.get_mut(plugin) {
591-
plugin.extend_api(invoke)
592-
} else {
593-
invoke.resolver.reject(format!("plugin {plugin} not found"));
594-
true
645+
for p in self.store.iter_mut() {
646+
if p.name() == plugin {
647+
return p.extend_api(invoke);
648+
}
595649
}
650+
invoke.resolver.reject(format!("plugin {plugin} not found"));
651+
true
596652
}
597653
}

core/tauri/src/window.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ use std::{
6060
};
6161

6262
pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
63-
pub(crate) type NavigationHandler = dyn Fn(Url) -> bool + Send;
63+
pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send;
6464

6565
#[derive(Clone, Serialize)]
6666
struct WindowCreatedEvent {
@@ -306,7 +306,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
306306
/// Ok(())
307307
/// });
308308
/// ```
309-
pub fn on_navigation<F: Fn(Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
309+
pub fn on_navigation<F: Fn(&Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
310310
self.navigation_handler.replace(Box::new(f));
311311
self
312312
}

examples/api/src-tauri/tauri-plugin-sample/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
4646

4747
Ok(())
4848
})
49+
.on_navigation(|window, url| {
50+
println!("navigation {} {url}", window.label());
51+
true
52+
})
4953
.build()
5054
}

0 commit comments

Comments
 (0)