migrate to using scope

pull/2019/head
amrbashir 9 months ago
parent 38369c832d
commit 42ab44cd5f
No known key found for this signature in database
GPG Key ID: BBD7A47A2003FF33

3
Cargo.lock generated

@ -6586,11 +6586,14 @@ version = "2.0.0"
dependencies = [ dependencies = [
"open", "open",
"regex", "regex",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror", "thiserror",
"url",
"urlpattern",
] ]
[[package]] [[package]]

@ -21,6 +21,8 @@ url = "2"
schemars = "0.8" schemars = "0.8"
dunce = "1" dunce = "1"
specta = "=2.0.0-rc.20" specta = "=2.0.0-rc.20"
glob = "0.3"
urlpattern = "0.3"
#tauri-specta = "=2.0.0-rc.11" #tauri-specta = "=2.0.0-rc.11"
[workspace.package] [workspace.package]

@ -16,7 +16,6 @@
}, },
"core:default", "core:default",
"fs:default", "fs:default",
"opener:default",
"core:window:allow-minimize", "core:window:allow-minimize",
"core:window:allow-maximize", "core:window:allow-maximize",
"core:window:allow-unmaximize", "core:window:allow-unmaximize",
@ -81,6 +80,7 @@
], ],
"deny": ["$APPDATA/db/*.stronghold"] "deny": ["$APPDATA/db/*.stronghold"]
}, },
"store:default" "store:default",
"opener:default"
] ]
} }

@ -27,7 +27,10 @@
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-row gap-2 items-center"> <form
class="flex flex-row gap-2 items-center"
on:submit|preventDefault={openPath}
>
<input <input
class="input grow" class="input grow"
placeholder="Type the path to watch..." placeholder="Type the path to watch..."
@ -42,6 +45,6 @@
{/each} {/each}
</select> </select>
<button class="btn" on:click={openPath}>Open</button> <button class="btn" type="submit">Open</button>
</div> </form>
</div> </div>

@ -34,7 +34,7 @@ thiserror = { workspace = true }
url = { workspace = true } url = { workspace = true }
anyhow = "1" anyhow = "1"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
glob = "0.3" glob = { workspace = true }
# TODO: Remove `serialization-compat-6` in v3 # TODO: Remove `serialization-compat-6` in v3
notify = { version = "7", optional = true, features = [ notify = { version = "7", optional = true, features = [
"serde", "serde",

@ -16,10 +16,23 @@ mod scope;
#[serde(untagged)] #[serde(untagged)]
#[allow(unused)] #[allow(unused)]
enum FsScopeEntry { enum FsScopeEntry {
/// FS scope path. /// A path that can be accessed by the webview when using the fs APIs.
/// FS scope path pattern.
///
/// The pattern can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
Value(PathBuf), Value(PathBuf),
Object { Object {
/// FS scope path. /// A path that can be accessed by the webview when using the fs APIs.
///
/// The pattern can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
path: PathBuf, path: PathBuf,
}, },
} }

@ -25,7 +25,7 @@ tauri-plugin = { workspace = true, features = ["build"] }
schemars = { workspace = true } schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
url = { workspace = true } url = { workspace = true }
urlpattern = "0.3" urlpattern = { workspace = true }
regex = "1" regex = "1"
[dependencies] [dependencies]
@ -35,7 +35,7 @@ tauri = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { version = "1", features = ["sync", "macros"] } tokio = { version = "1", features = ["sync", "macros"] }
tauri-plugin-fs = { path = "../fs", version = "2.0.3" } tauri-plugin-fs = { path = "../fs", version = "2.0.3" }
urlpattern = "0.3" urlpattern = { workspace = true }
regex = "1" regex = "1"
http = "1" http = "1"
reqwest = { version = "0.12", default-features = false } reqwest = { version = "0.12", default-features = false }

@ -24,13 +24,17 @@ ios = { level = "partial", notes = "Only allows to open URLs via `open`" }
[build-dependencies] [build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] } tauri-plugin = { workspace = true, features = ["build"] }
schemars = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html serde = { workspace = true }
urlpattern = { workspace = true }
regex = "1"
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tauri = { workspace = true } tauri = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
regex = "1"
open = { version = "5", features = ["shellexecute-on-windows"] } open = { version = "5", features = ["shellexecute-on-windows"] }
urlpattern = { workspace = true }
regex = "1"
url = { workspace = true }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_OPENER__=function(n){"use strict";async function e(n,e={},_){return window.__TAURI_INTERNALS__.invoke(n,e,_)}return"function"==typeof SuppressedError&&SuppressedError,n.open=async function(n,_){await e("plugin:opener|open",{path:n,with:_})},n.revealInDir=async function(){return e("plugin:opener|reveal_in_dir")},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})} if("__TAURI__"in window){var __TAURI_PLUGIN_OPENER__=function(n){"use strict";async function e(n,e={},_){return window.__TAURI_INTERNALS__.invoke(n,e,_)}return"function"==typeof SuppressedError&&SuppressedError,n.open=async function(n,_){await e("plugin:opener|open",{path:n,with:_})},n.revealInDir=async function(){return e("plugin:opener|reveal_item_in_dir")},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})}

@ -2,13 +2,66 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["open", "reveal_in_dir"]; use std::path::PathBuf;
#[path = "src/scope_entry.rs"]
#[allow(dead_code)]
mod scope;
/// Opener scope entry.
#[derive(schemars::JsonSchema)]
#[serde(untagged)]
#[allow(unused)]
enum OpenerScopeEntry {
Url {
/// A URL that can be opened by the webview when using the Opener APIs.
/// Wildcards can be used following the URL pattern standard.
///
/// See [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.
///
/// Examples:
///
/// - "https://*" : allows all HTTPS origin on port 443
///
/// - "https://*:*" : allows all HTTPS origin on any port
///
/// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path
///
/// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/"
url: String,
},
Path {
/// A path that can be opened by the webview when using the Opener APIs.
///
/// The pattern can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
path: PathBuf,
},
}
// Ensure `OpenerScopeEntry` and `scope::EntryRaw` is kept in sync
fn _f() {
match (scope::EntryRaw::Url { url: String::new() }) {
scope::EntryRaw::Url { url } => OpenerScopeEntry::Url { url },
scope::EntryRaw::Path { path } => OpenerScopeEntry::Path { path },
};
match (OpenerScopeEntry::Url { url: String::new() }) {
OpenerScopeEntry::Url { url } => scope::EntryRaw::Url { url },
OpenerScopeEntry::Path { path } => scope::EntryRaw::Path { path },
};
}
const COMMANDS: &[&str] = &["open", "reveal_item_in_dir"];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)
.global_api_script_path("./api-iife.js") .global_api_script_path("./api-iife.js")
.android_path("android") .android_path("android")
.ios_path("ios") .ios_path("ios")
.global_scope_schema(schemars::schema_for!(OpenerScopeEntry))
.build(); .build();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

@ -53,5 +53,5 @@ export async function open(path: string, openWith?: string): Promise<void> {
}) })
} }
export async function revealInDir() { export async function revealInDir() {
return invoke('plugin:opener|reveal_in_dir') return invoke('plugin:opener|reveal_item_in_dir')
} }

@ -0,0 +1,17 @@
"$schema" = "schemas/schema.json"
[[permission]]
identifier = "allow-default-urls"
description = "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
[[permission.scope.allow]]
url = "mailto:*"
[[permission.scope.allow]]
url = "tel:*"
[[permission.scope.allow]]
url = "http://*:*"
[[permission.scope.allow]]
url = "https://*:*"

@ -3,11 +3,11 @@
"$schema" = "../../schemas/schema.json" "$schema" = "../../schemas/schema.json"
[[permission]] [[permission]]
identifier = "allow-reveal-in-dir" identifier = "allow-reveal-item-in-dir"
description = "Enables the reveal_in_dir command without any pre-configured scope." description = "Enables the reveal_item_in_dir command without any pre-configured scope."
commands.allow = ["reveal_in_dir"] commands.allow = ["reveal_item_in_dir"]
[[permission]] [[permission]]
identifier = "deny-reveal-in-dir" identifier = "deny-reveal-item-in-dir"
description = "Denies the reveal_in_dir command without any pre-configured scope." description = "Denies the reveal_item_in_dir command without any pre-configured scope."
commands.deny = ["reveal_in_dir"] commands.deny = ["reveal_item_in_dir"]

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-reveal-item-in-dir"
description = "Enables the reveal_item_in_dir command without any pre-configured scope."
commands.allow = ["reveal_item_in_dir"]
[[permission]]
identifier = "deny-reveal-item-in-dir"
description = "Denies the reveal_item_in_dir command without any pre-configured scope."
commands.deny = ["reveal_item_in_dir"]

@ -1,9 +1,11 @@
## Default Permission ## Default Permission
This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application
as well as reveal file in directories using default file explorer
- `allow-open` - `allow-open`
- `allow-reveal-in-dir` - `allow-reveal-item-in-dir`
- `default-urls`
## Permission Table ## Permission Table
@ -43,12 +45,51 @@ Denies the open command without any pre-configured scope.
<tr> <tr>
<td> <td>
`opener:allow-reveal-in-dir` `opener:allow-reveal-item-in-dir`
</td>
<td>
Enables the reveal_item_in_dir command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:deny-reveal-item-in-dir`
</td>
<td>
Denies the reveal_item_in_dir command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:allow-reveal-item-in-dir`
</td>
<td>
Enables the reveal_item_in_dir command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:deny-reveal-item-in-dir`
</td> </td>
<td> <td>
Enables the reveal_in_dir command without any pre-configured scope. Denies the reveal_item_in_dir command without any pre-configured scope.
</td> </td>
</tr> </tr>
@ -56,12 +97,12 @@ Enables the reveal_in_dir command without any pre-configured scope.
<tr> <tr>
<td> <td>
`opener:deny-reveal-in-dir` `opener:default-urls`
</td> </td>
<td> <td>
Denies the reveal_in_dir command without any pre-configured scope. This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.
</td> </td>
</tr> </tr>

@ -1,4 +1,6 @@
"$schema" = "schemas/schema.json" "$schema" = "schemas/schema.json"
[default] [default]
permissions = ["allow-open", "allow-reveal-in-dir"] description = """This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application
as well as reveal file in directories using default file explorer"""
permissions = ["allow-open", "allow-reveal-item-in-dir", "allow-default-urls"]

@ -305,16 +305,32 @@
"const": "deny-open" "const": "deny-open"
}, },
{ {
"description": "Enables the reveal_in_dir command without any pre-configured scope.", "description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "allow-reveal-in-dir" "const": "allow-reveal-item-in-dir"
}, },
{ {
"description": "Denies the reveal_in_dir command without any pre-configured scope.", "description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "deny-reveal-in-dir" "const": "deny-reveal-item-in-dir"
}, },
{ {
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "allow-reveal-item-in-dir"
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "deny-reveal-item-in-dir"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "default-urls"
},
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer",
"type": "string", "type": "string",
"const": "default" "const": "default"
} }

@ -1,16 +1,38 @@
use tauri::{AppHandle, Runtime, State}; use tauri::{
ipc::{CommandScope, GlobalScope},
AppHandle, Runtime,
};
use crate::{open::Program, Opener}; use crate::{open::Program, scope::Scope, Error};
#[tauri::command] #[tauri::command]
pub async fn open<R: Runtime>( pub async fn open<R: Runtime>(
_app: AppHandle<R>, app: AppHandle<R>,
opener: State<'_, Opener<R>>, command_scope: CommandScope<crate::scope::Entry>,
global_scope: GlobalScope<crate::scope::Entry>,
path: String, path: String,
with: Option<Program>, with: Option<Program>,
) -> crate::Result<()> { ) -> crate::Result<()> {
opener.open(path, with) let scope = Scope::new(
&app,
command_scope
.allows()
.iter()
.chain(global_scope.allows())
.collect(),
command_scope
.denies()
.iter()
.chain(global_scope.denies())
.collect(),
);
if scope.is_allowed(&path)? {
crate::open::open(path, with)
} else {
Err(Error::NotAllowed(path))
}
} }
#[tauri::command] #[tauri::command]
pub async fn reveal_in_dir() {} pub async fn reveal_item_in_dir() {}

@ -1,62 +0,0 @@
use regex::Regex;
use serde::Deserialize;
/// Scope for the open command
pub struct OpenScope {
/// The validation regex that `shell > open` paths must match against.
pub open: Option<Regex>,
}
/// Configuration for the shell plugin.
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Config {
/// Open URL with the user's default application.
#[serde(default)]
pub open: OpenConfig,
}
impl Config {
pub fn open_scope(&self) -> OpenScope {
let open = match &self.open {
OpenConfig::Flag(false) => None,
OpenConfig::Flag(true) => {
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
}
OpenConfig::Validate(validator) => {
let regex = format!("^{validator}$");
let validator =
Regex::new(&regex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
Some(validator)
}
};
OpenScope { open }
}
}
/// Defines the `opener > open` api scope.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum OpenConfig {
/// If the opener open API should be enabled.
///
/// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used.
Flag(bool),
/// Enable the opener open API, with a custom regex that the opened path must match against.
///
/// The regex string is automatically surrounded by `^...$` to match the full string.
/// For example the `https?://\w+` regex would be registered as `^https?://\w+$`.
///
/// If using a custom regex to support a non-http(s) schema, care should be used to prevent values
/// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.
Validate(String),
}
impl Default for OpenConfig {
fn default() -> Self {
Self::Flag(false)
}
}

@ -10,18 +10,15 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
#[error(transparent)] #[error(transparent)]
Tauri(#[from] tauri::Error),
#[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("unknown program {0}")] #[error("unknown program {0}")]
UnknownProgramName(String), UnknownProgramName(String),
/// At least one argument did not pass input validation. #[error("Not allowed to open {0}")]
#[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] NotAllowed(String),
Validation {
/// Index of the variable.
index: usize,
/// Regex that the variable value failed to match.
validation: String,
},
} }
impl Serialize for Error { impl Serialize for Error {

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use config::OpenScope;
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime, AppHandle, Manager, Runtime,
@ -16,9 +15,10 @@ const PLUGIN_IDENTIFIER: &str = "app.tauri.opener";
tauri::ios_plugin_binding!(init_plugin_opener); tauri::ios_plugin_binding!(init_plugin_opener);
mod commands; mod commands;
mod config;
mod error; mod error;
mod open; mod open;
mod scope;
mod scope_entry;
pub use error::Error; pub use error::Error;
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
@ -28,21 +28,16 @@ pub struct Opener<R: Runtime> {
app: AppHandle<R>, app: AppHandle<R>,
#[cfg(mobile)] #[cfg(mobile)]
mobile_plugin_handle: PluginHandle<R>, mobile_plugin_handle: PluginHandle<R>,
open_scope: OpenScope,
} }
impl<R: Runtime> Opener<R> { impl<R: Runtime> Opener<R> {
/// Open a (url) path with a default or specific browser opening program. /// Open a (url) path with a default or specific browser opening program.
///
/// See [`crate::open::open`] for how it handles security-related measures.
#[cfg(desktop)] #[cfg(desktop)]
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> { pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
open::open(&self.open_scope, path.into(), with).map_err(Into::into) open::open(path.into(), with).map_err(Into::into)
} }
/// Open a (url) path with a default or specific browser opening program. /// Open a (url) path with a default or specific browser opening program.
///
/// See [`crate::open::open`] for how it handles security-related measures.
#[cfg(mobile)] #[cfg(mobile)]
pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> { pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
self.mobile_plugin_handle self.mobile_plugin_handle
@ -63,13 +58,10 @@ impl<R: Runtime, T: Manager<R>> crate::OpenerExt<R> for T {
} }
/// Initializes the plugin. /// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::<R, Option<config::Config>>::new("opener") Builder::new("opener")
.js_init_script(include_str!("init-iife.js").to_string()) .js_init_script(include_str!("init-iife.js").to_string())
.setup(|app, api| { .setup(|app, _api| {
let default_config = config::Config::default();
let config = api.config().as_ref().unwrap_or(&default_config);
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
@ -77,7 +69,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
app.manage(Opener { app.manage(Opener {
app: app.clone(), app: app.clone(),
open_scope: config.open_scope(),
#[cfg(mobile)] #[cfg(mobile)]
mobile_plugin_handle: handle, mobile_plugin_handle: handle,
}); });
@ -85,7 +76,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::open, commands::open,
commands::reveal_in_dir commands::reveal_item_in_dir
]) ])
.build() .build()
} }

@ -6,11 +6,10 @@
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::str::FromStr; use std::{fmt::Display, str::FromStr};
use crate::{config::OpenScope, Error};
/// Program to use on the [`open()`] call. /// Program to use on the [`open()`] call.
#[derive(Debug)]
pub enum Program { pub enum Program {
/// Use the `open` program. /// Use the `open` program.
Open, Open,
@ -36,6 +35,28 @@ pub enum Program {
Safari, Safari,
} }
impl Display for Program {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Open => "open",
Self::Start => "start",
Self::XdgOpen => "xdg-open",
Self::Gio => "gio",
Self::GnomeOpen => "gnome-open",
Self::KdeOpen => "kde-open",
Self::WslView => "wslview",
Self::Firefox => "firefox",
Self::Chrome => "chrome",
Self::Chromium => "chromium",
Self::Safari => "safari",
}
)
}
}
impl FromStr for Program { impl FromStr for Program {
type Err = super::Error; type Err = super::Error;
@ -118,18 +139,18 @@ impl Program {
/// Ok(()) /// Ok(())
/// }); /// });
/// ``` /// ```
pub fn open<P: AsRef<str>>(scope: &OpenScope, path: P, with: Option<Program>) -> crate::Result<()> { pub fn open<P: AsRef<str>>(path: P, with: Option<Program>) -> crate::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
// ensure we pass validation if the configuration has one // // ensure we pass validation if the configuration has one
if let Some(regex) = &scope.open { // if let Some(regex) = &scope.open {
if !regex.is_match(path) { // if !regex.is_match(path) {
return Err(Error::Validation { // return Err(Error::Validation {
index: 0, // index: 0,
validation: regex.as_str().into(), // validation: regex.as_str().into(),
}); // });
} // }
} // }
// The prevention of argument escaping is handled by the usage of std::process::Command::arg by // The prevention of argument escaping is handled by the usage of std::process::Command::arg by
// the `open` dependency. This behavior should be re-confirmed during upgrades of `open`. // the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.

@ -0,0 +1,146 @@
use std::{marker::PhantomData, path::PathBuf, sync::Arc};
use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime};
use url::Url;
use urlpattern::UrlPatternMatchInput;
use crate::{scope_entry::EntryRaw, Error};
#[derive(Debug)]
pub enum Entry {
Url(urlpattern::UrlPattern),
Path(Option<PathBuf>),
}
fn parse_url_pattern(
s: &str,
) -> std::result::Result<urlpattern::UrlPattern, urlpattern::quirks::Error> {
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());
}
urlpattern::UrlPattern::parse(init, Default::default())
}
impl ScopeObject for Entry {
type Error = Error;
fn deserialize<R: Runtime>(
app: &AppHandle<R>,
raw: Value,
) -> std::result::Result<Self, Self::Error> {
serde_json::from_value(raw.into())
.and_then(|raw| {
let entry = match raw {
EntryRaw::Url { url } => Entry::Url(parse_url_pattern(&url).map_err(|e| {
serde::de::Error::custom(format!(
"`{}` is not a valid URL pattern: {e}",
url
))
})?),
EntryRaw::Path { path } => {
let path = match app.path().parse(path) {
Ok(path) => Some(path),
#[cfg(not(target_os = "android"))]
Err(tauri::Error::UnknownPath) => None,
Err(err) => return Err(serde::de::Error::custom(err.to_string())),
};
Entry::Path(path)
}
};
Ok(entry)
})
.map_err(Into::into)
}
}
#[derive(Debug)]
pub struct Scope<'a, R: Runtime, M: Manager<R>> {
allowed: Vec<&'a Arc<Entry>>,
denied: Vec<&'a Arc<Entry>>,
manager: &'a M,
_marker: PhantomData<R>,
}
impl<'a, R: Runtime, M: Manager<R>> Scope<'a, R, M> {
pub(crate) fn new(
manager: &'a M,
allowed: Vec<&'a Arc<Entry>>,
denied: Vec<&'a Arc<Entry>>,
) -> Self {
Self {
manager,
allowed,
denied,
_marker: PhantomData,
}
}
pub fn is_allowed(&self, path_or_url: &str) -> crate::Result<bool> {
let url = Url::parse(path_or_url).ok();
match url {
Some(url) => Ok(self.is_url_allowed(url)),
None => self.is_path_allowed(path_or_url),
}
}
pub fn is_url_allowed(&self, url: Url) -> bool {
let denied = self.denied.iter().any(|entry| match entry.as_ref() {
Entry::Url(url_pattern) => url_pattern
.test(UrlPatternMatchInput::Url(url.clone()))
.unwrap_or_default(),
Entry::Path { .. } => false,
});
if denied {
false
} else {
self.allowed.iter().any(|entry| match entry.as_ref() {
Entry::Url(url_pattern) => url_pattern
.test(UrlPatternMatchInput::Url(url.clone()))
.unwrap_or_default(),
Entry::Path { .. } => false,
})
}
}
pub fn is_path_allowed(&self, path: &str) -> crate::Result<bool> {
let fs_scope = tauri::fs::Scope::new(
self.manager,
&tauri::utils::config::FsScope::Scope {
allow: self
.allowed
.iter()
.filter_map(|e| match e.as_ref() {
Entry::Path(path) => path.clone(),
_ => None,
})
.collect(),
deny: self
.denied
.iter()
.filter_map(|e| match e.as_ref() {
Entry::Path(path) => path.clone(),
_ => None,
})
.collect(),
require_literal_leading_dot: None,
},
)?;
Ok(fs_scope.is_allowed(path))
}
}

@ -0,0 +1,11 @@
use std::path::PathBuf;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
#[allow(unused)]
pub(crate) enum EntryRaw {
Url { url: String },
Path { path: PathBuf },
}

@ -11,8 +11,9 @@ use tauri::{
Manager, Runtime, State, Window, Manager, Runtime, State, Window,
}; };
#[allow(deprecated)]
use crate::open::Program;
use crate::{ use crate::{
open::Program,
process::{CommandEvent, TerminatedPayload}, process::{CommandEvent, TerminatedPayload},
scope::ExecuteArgs, scope::ExecuteArgs,
Shell, Shell,
@ -302,6 +303,7 @@ pub fn kill<R: Runtime>(
Ok(()) Ok(())
} }
#[allow(deprecated)]
#[tauri::command] #[tauri::command]
pub async fn open<R: Runtime>( pub async fn open<R: Runtime>(
_window: Window<R>, _window: Window<R>,

@ -29,6 +29,7 @@ mod commands;
mod config; mod config;
mod error; mod error;
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
#[allow(deprecated)]
pub mod open; pub mod open;
pub mod process; pub mod process;
mod scope; mod scope;
@ -74,6 +75,7 @@ impl<R: Runtime> Shell<R> {
/// See [`crate::open::open`] for how it handles security-related measures. /// See [`crate::open::open`] for how it handles security-related measures.
#[cfg(desktop)] #[cfg(desktop)]
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
#[allow(deprecated)]
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> { pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
open::open(&self.open_scope, path.into(), with).map_err(Into::into) open::open(&self.open_scope, path.into(), with).map_err(Into::into)
} }

@ -4,6 +4,7 @@
use std::sync::Arc; use std::sync::Arc;
#[allow(deprecated)]
use crate::open::Program; use crate::open::Program;
use crate::process::Command; use crate::process::Command;
@ -201,6 +202,7 @@ impl OpenScope {
/// ///
/// The path is validated against the `plugins > shell > open` validation regex, which /// The path is validated against the `plugins > shell > open` validation regex, which
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. /// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
#[allow(deprecated)]
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> { pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
// ensure we pass validation if the configuration has one // ensure we pass validation if the configuration has one
if let Some(regex) = &self.open { if let Some(regex) = &self.open {

Loading…
Cancel
Save