mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-15 10:58:54 +02:00
Compare commits
6 Commits
dev
...
feat/on-ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1969e642e | ||
|
|
bfa2964114 | ||
|
|
e32108a453 | ||
|
|
710256b20c | ||
|
|
0df3eee3d8 | ||
|
|
d885a5059f |
6
.changes/on-new-window-requested-api.md
Normal file
6
.changes/on-new-window-requested-api.md
Normal 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.
|
||||
|
||||
6
.changes/on-new-window-requested-config.md
Normal file
6
.changes/on-new-window-requested-config.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri": minor:feat
|
||||
"tauri-utils": minor:feat
|
||||
---
|
||||
|
||||
Added `WindowConfig::on_new_window` to statically configure `WebviewBuilder::on_new_window`.
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -75,3 +75,4 @@ config-json5 = ["json5"]
|
||||
config-toml = []
|
||||
resources = ["walkdir"]
|
||||
html-manipulation = ["dep:html5ever", "dep:kuchiki"]
|
||||
url-pattern = []
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
209
crates/tauri-utils/src/url.rs
Normal file
209
crates/tauri-utils/src/url.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
//!
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user