Compare commits

...

6 Commits

Author SHA1 Message Date
Lucas Fernandes Nogueira
c1969e642e Merge branch 'dev' into feat/on-new-window-config 2025-10-14 08:35:36 -03:00
Lucas Nogueira
bfa2964114 clippy 2025-10-14 08:35:24 -03:00
Lucas Nogueira
e32108a453 use glob patterns by default 2025-10-14 08:08:24 -03:00
Lucas Nogueira
710256b20c code review suggestions 2025-10-14 07:32:57 -03:00
Lucas Nogueira
0df3eee3d8 fix ci 2025-10-12 19:00:18 -03:00
Lucas Nogueira
d885a5059f feat(core): added WindowConfig::on_new_window, closes #14263 2025-10-12 18:51:38 -03:00
16 changed files with 646 additions and 133 deletions

View File

@@ -0,0 +1,6 @@
---
"@tauri-apps/api": minor:feat
---
Added `onNewWindow` option to configure whether to allow the webview to open URLs when `window.open` is used.

View File

@@ -0,0 +1,6 @@
---
"tauri": minor:feat
"tauri-utils": minor:feat
---
Added `WindowConfig::on_new_window` to statically configure `WebviewBuilder::on_new_window`.

View File

@@ -602,6 +602,17 @@
"$ref": "#/definitions/ScrollBarStyle"
}
]
},
"onNewWindow": {
"description": "Action to perform when a new window is requested to be created.",
"default": {
"action": "deny"
},
"allOf": [
{
"$ref": "#/definitions/OnNewWindow"
}
]
}
},
"additionalProperties": false
@@ -1140,6 +1151,95 @@
}
]
},
"OnNewWindow": {
"description": "Action to perform when a new window is requested to be created.",
"oneOf": [
{
"description": "Allow the window to be created using the default webview implementation.",
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"allowDefault"
]
},
"urls": {
"description": "Only allow URLs matching the given pattern list when set.\n\n By default it is a glob pattern, but can use the full [URLPattern] spec if the `url-pattern` feature is enabled.\n\n [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/UrlScope"
}
}
},
"additionalProperties": false
},
{
"description": "Allow the window to be created using a Tauri window.",
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"allowTauriWindow"
]
},
"urls": {
"description": "Only allow URLs matching the given pattern list when set.\n\n By default it is a glob pattern, but can use the full [URLPattern] spec if the `url-pattern` feature is enabled.\n\n [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/UrlScope"
}
}
},
"additionalProperties": false
},
{
"description": "Deny the window from being created.",
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"deny"
]
}
},
"additionalProperties": false
}
]
},
"UrlScope": {
"description": "A scope to match URLs.",
"anyOf": [
{
"description": "A [`GlobPattern`] to match URLs.",
"allOf": [
{
"$ref": "#/definitions/GlobPattern"
}
]
}
]
},
"GlobPattern": {
"type": "string"
},
"SecurityConfig": {
"description": "Security configuration.\n\n See more: <https://v2.tauri.app/reference/config/#securityconfig>",
"type": "object",

View File

@@ -602,6 +602,17 @@
"$ref": "#/definitions/ScrollBarStyle"
}
]
},
"onNewWindow": {
"description": "Action to perform when a new window is requested to be created.",
"default": {
"action": "deny"
},
"allOf": [
{
"$ref": "#/definitions/OnNewWindow"
}
]
}
},
"additionalProperties": false
@@ -1140,6 +1151,95 @@
}
]
},
"OnNewWindow": {
"description": "Action to perform when a new window is requested to be created.",
"oneOf": [
{
"description": "Allow the window to be created using the default webview implementation.",
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"allowDefault"
]
},
"urls": {
"description": "Only allow URLs matching the given pattern list when set.\n\n By default it is a glob pattern, but can use the full [URLPattern] spec if the `url-pattern` feature is enabled.\n\n [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/UrlScope"
}
}
},
"additionalProperties": false
},
{
"description": "Allow the window to be created using a Tauri window.",
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"allowTauriWindow"
]
},
"urls": {
"description": "Only allow URLs matching the given pattern list when set.\n\n By default it is a glob pattern, but can use the full [URLPattern] spec if the `url-pattern` feature is enabled.\n\n [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/UrlScope"
}
}
},
"additionalProperties": false
},
{
"description": "Deny the window from being created.",
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"deny"
]
}
},
"additionalProperties": false
}
]
},
"UrlScope": {
"description": "A scope to match URLs.",
"anyOf": [
{
"description": "A [`GlobPattern`] to match URLs.",
"allOf": [
{
"$ref": "#/definitions/GlobPattern"
}
]
}
]
},
"GlobPattern": {
"type": "string"
},
"SecurityConfig": {
"description": "Security configuration.\n\n See more: <https://v2.tauri.app/reference/config/#securityconfig>",
"type": "object",

View File

@@ -75,3 +75,4 @@ config-json5 = ["json5"]
config-toml = []
resources = ["walkdir"]
html-manipulation = ["dep:html5ever", "dep:kuchiki"]
url-pattern = []

View File

@@ -29,15 +29,13 @@ use std::{
fs,
num::NonZeroU64,
path::PathBuf,
str::FromStr,
sync::Arc,
};
use thiserror::Error;
use url::Url;
use crate::{
config::{CapabilityEntry, Config},
platform::Target,
url::UrlPattern,
};
pub use self::{identifier::*, value::*};
@@ -273,64 +271,6 @@ pub struct PermissionSet {
pub permissions: Vec<String>,
}
/// UrlPattern for [`ExecutionContext::Remote`].
#[derive(Debug, Clone)]
pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
impl FromStr for RemoteUrlPattern {
type Err = urlpattern::quirks::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.search.replace("*".to_string());
}
if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.hash.replace("*".to_string());
}
if init
.pathname
.as_ref()
.map(|p| p.is_empty() || p == "/")
.unwrap_or(true)
{
init.pathname.replace("*".to_string());
}
let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
Ok(Self(Arc::new(pattern), s.to_string()))
}
}
impl RemoteUrlPattern {
#[doc(hidden)]
pub fn as_str(&self) -> &str {
&self.1
}
/// Test if a given URL matches the pattern.
pub fn test(&self, url: &Url) -> bool {
self
.0
.test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
.unwrap_or_default()
}
}
impl PartialEq for RemoteUrlPattern {
fn eq(&self, other: &Self) -> bool {
self.0.protocol() == other.0.protocol()
&& self.0.username() == other.0.username()
&& self.0.password() == other.0.password()
&& self.0.hostname() == other.0.hostname()
&& self.0.port() == other.0.port()
&& self.0.pathname() == other.0.pathname()
&& self.0.search() == other.0.search()
&& self.0.hash() == other.0.hash()
}
}
impl Eq for RemoteUrlPattern {}
/// Execution context of an IPC call.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum ExecutionContext {
@@ -340,7 +280,7 @@ pub enum ExecutionContext {
/// Remote URL is trying to use the IPC.
Remote {
/// The URL trying to access the IPC (URL pattern).
url: RemoteUrlPattern,
url: UrlPattern,
},
}
@@ -420,46 +360,6 @@ pub fn read_allowed_commands() -> Option<AllowedCommands> {
Some(json)
}
#[cfg(test)]
mod tests {
use crate::acl::RemoteUrlPattern;
#[test]
fn url_pattern_domain_wildcard() {
let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_path_wildcard() {
let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_scheme_wildcard() {
let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
}
}
#[cfg(feature = "build")]
mod build_ {
use std::convert::identity;

View File

@@ -61,7 +61,9 @@ fn add_description(schema: Schema, description: impl Into<String>) -> Schema {
/// Items to help with parsing content into a [`Config`].
pub mod parse;
use crate::{acl::capability::Capability, TitleBarStyle, WindowEffect, WindowEffectState};
use crate::{
acl::capability::Capability, url::UrlScope, TitleBarStyle, WindowEffect, WindowEffectState,
};
pub use self::parse::parse;
@@ -1647,6 +1649,35 @@ pub enum ScrollBarStyle {
FluentOverlay,
}
/// Action to perform when a new window is requested to be created.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, tag = "action")]
#[non_exhaustive]
pub enum OnNewWindow {
/// Allow the window to be created using the default webview implementation.
AllowDefault {
/// Only allow URLs matching the given pattern list when set.
///
/// By default it is a glob pattern, but can use the full [URLPattern] spec if the `url-pattern` feature is enabled.
///
/// [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
urls: Option<Vec<UrlScope>>,
},
/// Allow the window to be created using a Tauri window.
AllowTauriWindow {
/// Only allow URLs matching the given pattern list when set.
///
/// By default it is a glob pattern, but can use the full [URLPattern] spec if the `url-pattern` feature is enabled.
///
/// [URLPattern]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
urls: Option<Vec<UrlScope>>,
},
/// Deny the window from being created.
#[default]
Deny,
}
/// The window configuration object.
///
/// See more: <https://v2.tauri.app/reference/config/#windowconfig>
@@ -1995,6 +2026,9 @@ pub struct WindowConfig {
/// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation.
#[serde(default, alias = "scroll-bar-style")]
pub scroll_bar_style: ScrollBarStyle,
/// Action to perform when a new window is requested to be created.
#[serde(default, alias = "on-window-requested")]
pub on_new_window: OnNewWindow,
}
impl Default for WindowConfig {
@@ -2057,6 +2091,7 @@ impl Default for WindowConfig {
data_directory: None,
data_store_identifier: None,
scroll_bar_style: ScrollBarStyle::Default,
on_new_window: OnNewWindow::default(),
}
}
}
@@ -3524,6 +3559,24 @@ mod build {
}
}
impl ToTokens for OnNewWindow {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::config::OnNewWindow };
tokens.append_all(match self {
Self::AllowDefault { urls } => {
let urls = opt_vec_lit(urls.as_ref(), url_scope_lit);
quote! { #prefix::AllowDefault { urls: #urls } }
}
Self::AllowTauriWindow { urls } => {
let urls = opt_vec_lit(urls.as_ref(), url_scope_lit);
quote! { #prefix::AllowTauriWindow { urls: #urls } }
}
Self::Deny => quote! { #prefix::Deny },
})
}
}
impl ToTokens for WindowConfig {
fn to_tokens(&self, tokens: &mut TokenStream) {
let label = str_lit(&self.label);
@@ -3583,6 +3636,7 @@ mod build {
let data_directory = opt_lit(self.data_directory.as_ref().map(path_buf_lit).as_ref());
let data_store_identifier = opt_vec_lit(self.data_store_identifier, identity);
let scroll_bar_style = &self.scroll_bar_style;
let on_new_window = &self.on_new_window;
literal_struct!(
tokens,
@@ -3643,7 +3697,8 @@ mod build {
disable_input_accessory_view,
data_directory,
data_store_identifier,
scroll_bar_style
scroll_bar_style,
on_new_window
);
}
}

View File

@@ -35,6 +35,7 @@ pub mod plugin;
pub mod resources;
#[cfg(feature = "build")]
pub mod tokens;
pub mod url;
#[cfg(feature = "build")]
pub mod build;

View File

@@ -11,6 +11,8 @@ use quote::{quote, ToTokens};
use serde_json::Value as JsonValue;
use url::Url;
use crate::url::{GlobPattern, UrlPattern, UrlScope};
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
///
/// All fields must represent a binding of the same name that implements `ToTokens`.
@@ -92,6 +94,35 @@ pub fn url_lit(url: &Url) -> TokenStream {
quote! { #url.parse().unwrap() }
}
/// Creates a [`UrlPattern`] constructor `TokenStream`.
pub fn url_pattern_lit(url: &UrlPattern) -> TokenStream {
let url = url.as_str();
quote! { #url.parse().unwrap() }
}
/// Creates a [`GlobPattern`] constructor `TokenStream`.
pub fn glob_pattern_lit(pattern: &GlobPattern) -> TokenStream {
let pattern = pattern.0.as_str();
quote! { #pattern.parse().unwrap() }
}
/// Creates a [`UrlScope`] constructor `TokenStream`.
pub fn url_scope_lit(url: &UrlScope) -> TokenStream {
let prefix = quote! { ::tauri::utils::url::UrlScope };
match url {
#[cfg(feature = "url-pattern")]
UrlScope::UrlPattern(url) => {
let url = url.as_str();
quote! { #prefix::UrlPattern(#url.parse().unwrap()) }
}
#[cfg(not(feature = "url-pattern"))]
UrlScope::Glob(glob) => {
let pattern = glob.0.as_str();
quote! { #prefix::Glob(#pattern.parse().unwrap()) }
}
}
}
/// Create a map constructor, mapping keys and values with other `TokenStream`s.
///
/// This function is pretty generic because the types of keys AND values get transformed.

View File

@@ -0,0 +1,209 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! URL helpers.
use std::{str::FromStr, sync::Arc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url;
/// A scope to match URLs.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum UrlScope {
/// A [`UrlPattern`] to match URLs.
///
/// This is only available if the `url-pattern` feature is enabled.
#[cfg(feature = "url-pattern")]
UrlPattern(UrlPattern),
/// A [`GlobPattern`] to match URLs.
#[cfg(not(feature = "url-pattern"))]
Glob(GlobPattern),
}
impl UrlScope {
/// Test if a given URL is matched by the scope.
pub fn test(&self, url: &Url) -> bool {
match self {
#[cfg(feature = "url-pattern")]
Self::UrlPattern(pattern) => pattern.test(url),
#[cfg(not(feature = "url-pattern"))]
Self::Glob(pattern) => pattern.0.matches(url.as_str()),
}
}
}
/// A [`glob::Pattern`] to match URLs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobPattern(pub(crate) glob::Pattern);
#[cfg(feature = "schema")]
impl schemars::JsonSchema for GlobPattern {
fn schema_name() -> String {
"GlobPattern".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(_gen)
}
}
impl FromStr for GlobPattern {
type Err = glob::PatternError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
glob::Pattern::new(s).map(Self)
}
}
impl Serialize for GlobPattern {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}
impl<'de> Deserialize<'de> for GlobPattern {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
glob::Pattern::new(&s)
.map(Self)
.map_err(serde::de::Error::custom)
}
}
/// UrlPattern to match URLs.
#[derive(Debug, Clone)]
pub struct UrlPattern(Arc<urlpattern::UrlPattern>, String);
#[cfg(feature = "schema")]
impl schemars::JsonSchema for UrlPattern {
fn schema_name() -> String {
"UrlPattern".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(_gen)
}
}
impl Serialize for UrlPattern {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.1)
}
}
impl<'de> Deserialize<'de> for UrlPattern {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl FromStr for UrlPattern {
type Err = urlpattern::quirks::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.search.replace("*".to_string());
}
if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.hash.replace("*".to_string());
}
if init
.pathname
.as_ref()
.map(|p| p.is_empty() || p == "/")
.unwrap_or(true)
{
init.pathname.replace("*".to_string());
}
let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
Ok(Self(Arc::new(pattern), s.to_string()))
}
}
impl UrlPattern {
#[doc(hidden)]
pub fn as_str(&self) -> &str {
&self.1
}
/// Test if a given URL is matched by the pattern.
pub fn test(&self, url: &Url) -> bool {
self
.0
.test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
.unwrap_or_default()
}
}
impl PartialEq for UrlPattern {
fn eq(&self, other: &Self) -> bool {
self.0.protocol() == other.0.protocol()
&& self.0.username() == other.0.username()
&& self.0.password() == other.0.password()
&& self.0.hostname() == other.0.hostname()
&& self.0.port() == other.0.port()
&& self.0.pathname() == other.0.pathname()
&& self.0.search() == other.0.search()
&& self.0.hash() == other.0.hash()
}
}
impl Eq for UrlPattern {}
#[cfg(test)]
mod tests {
use super::UrlPattern;
#[test]
fn url_pattern_domain_wildcard() {
let pattern: UrlPattern = "http://*".parse().unwrap();
assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
let pattern: UrlPattern = "http://*.tauri.app".parse().unwrap();
assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_path_wildcard() {
let pattern: UrlPattern = "http://localhost/*".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_scheme_wildcard() {
let pattern: UrlPattern = "*://localhost".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
}
}

View File

@@ -221,6 +221,7 @@ image-png = ["image/png"]
macos-proxy = ["tauri-runtime-wry?/macos-proxy"]
dynamic-acl = []
specta = ["dep:specta"]
url-pattern = ["tauri-utils/url-pattern"]
[[example]]
name = "commands"

File diff suppressed because one or more lines are too long

View File

@@ -37,6 +37,7 @@
//! - **macos-proxy**: Adds support for [`WebviewBuilder::proxy_url`] on macOS. Requires macOS 14+.
//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/%5E2.0.0-rc.9/specta/attr.specta.html) with Tauri arguments such as [`State`](crate::State), [`Window`](crate::Window) and [`AppHandle`](crate::AppHandle)
//! - **dynamic-acl** *(enabled by default)*: Enables you to add ACLs at runtime, notably it enables the [`Manager::add_capability`] function.
//! - **url-pattern**: Enables using the [URLPattern] spec for URL scopes on JavaScript APIs.
//!
//! ## Cargo allowlist features
//!

View File

@@ -31,7 +31,7 @@ use tauri_runtime::{
WebviewDispatch,
};
pub use tauri_utils::config::Color;
use tauri_utils::config::{BackgroundThrottlingPolicy, WebviewUrl, WindowConfig};
use tauri_utils::config::{BackgroundThrottlingPolicy, OnNewWindow, WebviewUrl, WindowConfig};
pub use url::Url;
use crate::{
@@ -48,6 +48,7 @@ use crate::{
ResourceTable, Runtime, Window,
};
use std::sync::atomic::AtomicUsize;
use std::{
borrow::Cow,
hash::{Hash, Hasher},
@@ -274,13 +275,18 @@ unstable_struct!(
pub(crate) webview_attributes: WebviewAttributes,
pub(crate) web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
pub(crate) navigation_handler: Option<Box<NavigationHandler>>,
pub(crate) new_window_handler: Option<Box<NewWindowHandler<R>>>,
pub(crate) on_new_window_handler: Option<OnNewWindowHandler<R>>,
pub(crate) on_page_load_handler: Option<Box<OnPageLoad<R>>>,
pub(crate) document_title_changed_handler: Option<Box<OnDocumentTitleChanged<R>>>,
pub(crate) download_handler: Option<Arc<DownloadHandler<R>>>,
}
);
pub(crate) enum OnNewWindowHandler<R: Runtime> {
Fn(Box<NewWindowHandler<R>>),
Flag(OnNewWindow),
}
#[cfg_attr(not(feature = "unstable"), allow(dead_code))]
impl<R: Runtime> WebviewBuilder<R> {
/// Initializes a webview builder with the given webview label and URL to load.
@@ -352,7 +358,7 @@ async fn create_window(app: tauri::AppHandle) {
webview_attributes: WebviewAttributes::new(url),
web_resource_request_handler: None,
navigation_handler: None,
new_window_handler: None,
on_new_window_handler: None,
on_page_load_handler: None,
document_title_changed_handler: None,
download_handler: None,
@@ -431,7 +437,7 @@ async fn create_window(app: tauri::AppHandle) {
webview_attributes: WebviewAttributes::from(&config),
web_resource_request_handler: None,
navigation_handler: None,
new_window_handler: None,
on_new_window_handler: Some(OnNewWindowHandler::Flag(config.on_new_window)),
on_page_load_handler: None,
document_title_changed_handler: None,
download_handler: None,
@@ -590,7 +596,9 @@ tauri::Builder::default()
mut self,
f: F,
) -> Self {
self.new_window_handler.replace(Box::new(f));
self
.on_new_window_handler
.replace(OnNewWindowHandler::Fn(Box::new(f)));
self
}
@@ -704,30 +712,93 @@ tauri::Builder::default()
) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
let mut pending = PendingWebview::new(self.webview_attributes, self.label.clone())?;
pending.navigation_handler = self.navigation_handler.take();
pending.new_window_handler = self.new_window_handler.take().map(|handler| {
Box::new(
move |url, features: NewWindowFeatures| match handler(url, features) {
NewWindowResponse::Allow => tauri_runtime::webview::NewWindowResponse::Allow,
#[cfg(mobile)]
NewWindowResponse::Create { window: _ } => {
tauri_runtime::webview::NewWindowResponse::Allow
let app_handle = manager.app_handle().clone();
let label = window_label.to_string();
pending.new_window_handler =
self
.on_new_window_handler
.take()
.map(|on_new_window| match on_new_window {
OnNewWindowHandler::Fn(handler) => {
Box::new(
move |url, features: NewWindowFeatures| match handler(url, features) {
NewWindowResponse::Allow => tauri_runtime::webview::NewWindowResponse::Allow,
#[cfg(mobile)]
NewWindowResponse::Create { window: _ } => {
tauri_runtime::webview::NewWindowResponse::Allow
}
#[cfg(desktop)]
NewWindowResponse::Create { window } => {
tauri_runtime::webview::NewWindowResponse::Create {
window_id: window.window.window.id,
}
}
NewWindowResponse::Deny => tauri_runtime::webview::NewWindowResponse::Deny,
},
)
as Box<
dyn Fn(Url, NewWindowFeatures) -> tauri_runtime::webview::NewWindowResponse
+ Send
+ Sync
+ 'static,
>
}
#[cfg(desktop)]
NewWindowResponse::Create { window } => {
tauri_runtime::webview::NewWindowResponse::Create {
window_id: window.window.window.id,
}
OnNewWindowHandler::Flag(on_new_window) => {
let created_window_count = AtomicUsize::new(0);
Box::new(move |url, features| match &on_new_window {
OnNewWindow::AllowDefault { urls: Some(urls) } => {
if urls.iter().any(|pattern| pattern.test(&url)) {
tauri_runtime::webview::NewWindowResponse::Allow
} else {
tauri_runtime::webview::NewWindowResponse::Deny
}
}
#[cfg(mobile)]
OnNewWindow::AllowTauriWindow { urls: _ } => {
tauri_runtime::webview::NewWindowResponse::Deny
}
#[cfg(desktop)]
OnNewWindow::AllowTauriWindow { urls: Some(urls) } => {
if urls.iter().any(|pattern| pattern.test(&url)) {
let number =
created_window_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let builder = WebviewWindowBuilder::new(
&app_handle,
format!("{label}-created-{number}"),
WebviewUrl::External("about:blank".parse().unwrap()),
)
.window_features(features)
.on_document_title_changed(|window, title| {
window.set_title(&title).unwrap();
})
.title(url.as_str());
match builder.build() {
Ok(window) => tauri_runtime::webview::NewWindowResponse::Create {
window_id: window.window.window.id,
},
Err(e) => {
log::error!("failed to create window: {:?}", e);
tauri_runtime::webview::NewWindowResponse::Deny
}
}
} else {
tauri_runtime::webview::NewWindowResponse::Deny
}
}
OnNewWindow::AllowDefault { urls: None } => {
tauri_runtime::webview::NewWindowResponse::Allow
}
#[cfg(desktop)]
OnNewWindow::AllowTauriWindow { urls: None } => {
tauri_runtime::webview::NewWindowResponse::Allow
}
OnNewWindow::Deny => tauri_runtime::webview::NewWindowResponse::Deny,
_ => tauri_runtime::webview::NewWindowResponse::Deny,
})
}
NewWindowResponse::Deny => tauri_runtime::webview::NewWindowResponse::Deny,
},
)
as Box<
dyn Fn(Url, NewWindowFeatures) -> tauri_runtime::webview::NewWindowResponse
+ Send
+ Sync
+ 'static,
>
});
});
if let Some(document_title_changed_handler) = self.document_title_changed_handler.take() {
let label = pending.label.clone();

View File

@@ -199,6 +199,10 @@ class Webview {
// @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions
if (!options?.skip) {
if (typeof options?.onNewWindow === 'string') {
options.onNewWindow = { action: options.onNewWindow }
}
invoke('plugin:webview|create_webview', {
windowLabel: window.label,
options: {
@@ -897,8 +901,31 @@ interface WebviewOptions {
* - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation.
*/
scrollBarStyle?: ScrollBarStyle
/**
* Action to perform when a new window is requested to be created.
*
* 'allowDefault' lets the webview open the URL using the native implementation.
* 'allowTauriWindow' creates a Tauri window to load the URL.
* Additionally you can provide a list of filters to only allow URLs matching certain glob patterns.
* It can leverage the {@link https://developer.mozilla.org/en-US/docs/Web/API/URLPattern|URL pattern spec} if the `url-pattern` Cargo feature is enabled.
*
* A new window is requested to be opened by the {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open|window.open API}.
*
* ## Platform-specific
*
* - **Android / iOS**: Not supported.
* */
onNewWindow?: OnNewWindow
}
type OnNewWindow =
| 'allowDefault'
| 'allowTauriWindow'
| 'deny'
| { action: 'allowDefault'; urls?: string[] }
| { action: 'allowTauriWindow'; urls?: string[] }
| { action: 'deny' }
export { Webview, getCurrentWebview, getAllWebviews }
export type { DragDropEvent, WebviewOptions, Color }
export type { DragDropEvent, WebviewOptions, Color, OnNewWindow }

View File

@@ -83,6 +83,10 @@ class WebviewWindow {
// @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions
if (!options?.skip) {
if (typeof options?.onNewWindow === 'string') {
options.onNewWindow = { action: options.onNewWindow }
}
invoke('plugin:webview|create_webview_window', {
options: {
...options,