diff --git a/Cargo.lock b/Cargo.lock index d94e7d16..dcbc2f30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6586,11 +6586,14 @@ version = "2.0.0" dependencies = [ "open", "regex", + "schemars", "serde", "serde_json", "tauri", "tauri-plugin", "thiserror", + "url", + "urlpattern", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a17d33ea..d011d0ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ url = "2" schemars = "0.8" dunce = "1" specta = "=2.0.0-rc.20" +glob = "0.3" +urlpattern = "0.3" #tauri-specta = "=2.0.0-rc.11" [workspace.package] diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index c039c8e0..ae93aeef 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -16,7 +16,6 @@ }, "core:default", "fs:default", - "opener:default", "core:window:allow-minimize", "core:window:allow-maximize", "core:window:allow-unmaximize", @@ -81,6 +80,7 @@ ], "deny": ["$APPDATA/db/*.stronghold"] }, - "store:default" + "store:default", + "opener:default" ] } diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte index cb5a2773..3a2afaab 100644 --- a/examples/api/src/views/Opener.svelte +++ b/examples/api/src/views/Opener.svelte @@ -27,7 +27,10 @@
-
+
- -
+ +
diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml index 5d9a7efb..7546baef 100644 --- a/plugins/fs/Cargo.toml +++ b/plugins/fs/Cargo.toml @@ -34,7 +34,7 @@ thiserror = { workspace = true } url = { workspace = true } anyhow = "1" uuid = { version = "1", features = ["v4"] } -glob = "0.3" +glob = { workspace = true } # TODO: Remove `serialization-compat-6` in v3 notify = { version = "7", optional = true, features = [ "serde", diff --git a/plugins/fs/build.rs b/plugins/fs/build.rs index cb9d00da..935e0a81 100644 --- a/plugins/fs/build.rs +++ b/plugins/fs/build.rs @@ -16,10 +16,23 @@ mod scope; #[serde(untagged)] #[allow(unused)] 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), 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, }, } diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index 8c1801a3..7b008c14 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -25,7 +25,7 @@ tauri-plugin = { workspace = true, features = ["build"] } schemars = { workspace = true } serde = { workspace = true } url = { workspace = true } -urlpattern = "0.3" +urlpattern = { workspace = true } regex = "1" [dependencies] @@ -35,7 +35,7 @@ tauri = { workspace = true } thiserror = { workspace = true } tokio = { version = "1", features = ["sync", "macros"] } tauri-plugin-fs = { path = "../fs", version = "2.0.3" } -urlpattern = "0.3" +urlpattern = { workspace = true } regex = "1" http = "1" reqwest = { version = "0.12", default-features = false } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 92aa4efa..f450e15d 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -24,13 +24,17 @@ ios = { level = "partial", notes = "Only allows to open URLs via `open`" } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +schemars = { workspace = true } +serde = { workspace = true } +urlpattern = { workspace = true } +regex = "1" [dependencies] serde = { workspace = true } serde_json = { workspace = true } tauri = { workspace = true } thiserror = { workspace = true } -regex = "1" open = { version = "5", features = ["shellexecute-on-windows"] } +urlpattern = { workspace = true } +regex = "1" +url = { workspace = true } diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 07852123..5b37b44e 100644 --- a/plugins/opener/api-iife.js +++ b/plugins/opener/api-iife.js @@ -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__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 6bf98b26..6bccbdd9 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -2,13 +2,66 @@ // SPDX-License-Identifier: Apache-2.0 // 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() { tauri_plugin::Builder::new(COMMANDS) .global_api_script_path("./api-iife.js") .android_path("android") .ios_path("ios") + .global_scope_schema(schemars::schema_for!(OpenerScopeEntry)) .build(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 82c04738..0435fcc4 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -53,5 +53,5 @@ export async function open(path: string, openWith?: string): Promise { }) } export async function revealInDir() { - return invoke('plugin:opener|reveal_in_dir') + return invoke('plugin:opener|reveal_item_in_dir') } diff --git a/plugins/opener/permissions/allow-default-urls.toml b/plugins/opener/permissions/allow-default-urls.toml new file mode 100644 index 00000000..cad02770 --- /dev/null +++ b/plugins/opener/permissions/allow-default-urls.toml @@ -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://*:*" diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml index a5c79836..e669620f 100644 --- a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml +++ b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml @@ -3,11 +3,11 @@ "$schema" = "../../schemas/schema.json" [[permission]] -identifier = "allow-reveal-in-dir" -description = "Enables the reveal_in_dir command without any pre-configured scope." -commands.allow = ["reveal_in_dir"] +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-in-dir" -description = "Denies the reveal_in_dir command without any pre-configured scope." -commands.deny = ["reveal_in_dir"] +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"] diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml new file mode 100644 index 00000000..e669620f --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml @@ -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"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index a03c50e6..0a795ef7 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -1,9 +1,11 @@ ## 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-reveal-in-dir` +- `allow-reveal-item-in-dir` +- `default-urls` ## Permission Table @@ -43,12 +45,51 @@ Denies the open command without any pre-configured scope. -`opener:allow-reveal-in-dir` +`opener:allow-reveal-item-in-dir` + + + + +Enables the reveal_item_in_dir command without any pre-configured scope. + + + + + + + +`opener:deny-reveal-item-in-dir` + + + + +Denies the reveal_item_in_dir command without any pre-configured scope. + + + + + + + +`opener:allow-reveal-item-in-dir` + + + + +Enables the reveal_item_in_dir command without any pre-configured scope. + + + + + + + +`opener:deny-reveal-item-in-dir` -Enables the reveal_in_dir command without any pre-configured scope. +Denies the reveal_item_in_dir command without any pre-configured scope. @@ -56,12 +97,12 @@ Enables the reveal_in_dir command without any pre-configured scope. -`opener:deny-reveal-in-dir` +`opener:default-urls` -Denies the reveal_in_dir command without any pre-configured scope. +This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application. diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index 7286f06c..f9e3184d 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -1,4 +1,6 @@ "$schema" = "schemas/schema.json" [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"] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 8078fbbc..94891e8f 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -305,16 +305,32 @@ "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", - "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", - "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", "const": "default" } diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 6a6ba118..b3eec103 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -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] pub async fn open( - _app: AppHandle, - opener: State<'_, Opener>, + app: AppHandle, + command_scope: CommandScope, + global_scope: GlobalScope, path: String, with: Option, ) -> 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] -pub async fn reveal_in_dir() {} +pub async fn reveal_item_in_dir() {} diff --git a/plugins/opener/src/config.rs b/plugins/opener/src/config.rs deleted file mode 100644 index 9ac28014..00000000 --- a/plugins/opener/src/config.rs +++ /dev/null @@ -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, -} - -/// 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(®ex).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) - } -} diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 833e7a6f..943305d6 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -10,18 +10,15 @@ pub enum Error { #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] Io(#[from] std::io::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - /// At least one argument did not pass input validation. - #[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] - Validation { - /// Index of the variable. - index: usize, - - /// Regex that the variable value failed to match. - validation: String, - }, + #[error("Not allowed to open {0}")] + NotAllowed(String), } impl Serialize for Error { diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 8b75dbaf..94d06851 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use config::OpenScope; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Manager, Runtime, @@ -16,9 +15,10 @@ const PLUGIN_IDENTIFIER: &str = "app.tauri.opener"; tauri::ios_plugin_binding!(init_plugin_opener); mod commands; -mod config; mod error; mod open; +mod scope; +mod scope_entry; pub use error::Error; type Result = std::result::Result; @@ -28,21 +28,16 @@ pub struct Opener { app: AppHandle, #[cfg(mobile)] mobile_plugin_handle: PluginHandle, - open_scope: OpenScope, } impl Opener { /// 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)] pub fn open(&self, path: impl Into, with: Option) -> 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. - /// - /// See [`crate::open::open`] for how it handles security-related measures. #[cfg(mobile)] pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { self.mobile_plugin_handle @@ -63,13 +58,10 @@ impl> crate::OpenerExt for T { } /// Initializes the plugin. -pub fn init() -> TauriPlugin> { - Builder::>::new("opener") +pub fn init() -> TauriPlugin { + Builder::new("opener") .js_init_script(include_str!("init-iife.js").to_string()) - .setup(|app, api| { - let default_config = config::Config::default(); - let config = api.config().as_ref().unwrap_or(&default_config); - + .setup(|app, _api| { #[cfg(target_os = "android")] let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; #[cfg(target_os = "ios")] @@ -77,7 +69,6 @@ pub fn init() -> TauriPlugin> { app.manage(Opener { app: app.clone(), - open_scope: config.open_scope(), #[cfg(mobile)] mobile_plugin_handle: handle, }); @@ -85,7 +76,7 @@ pub fn init() -> TauriPlugin> { }) .invoke_handler(tauri::generate_handler![ commands::open, - commands::reveal_in_dir + commands::reveal_item_in_dir ]) .build() } diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs index 33ddb0e3..b472cb34 100644 --- a/plugins/opener/src/open.rs +++ b/plugins/opener/src/open.rs @@ -6,11 +6,10 @@ use serde::{Deserialize, Deserializer}; -use std::str::FromStr; - -use crate::{config::OpenScope, Error}; +use std::{fmt::Display, str::FromStr}; /// Program to use on the [`open()`] call. +#[derive(Debug)] pub enum Program { /// Use the `open` program. Open, @@ -36,6 +35,28 @@ pub enum Program { 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 { type Err = super::Error; @@ -118,18 +139,18 @@ impl Program { /// Ok(()) /// }); /// ``` -pub fn open>(scope: &OpenScope, path: P, with: Option) -> crate::Result<()> { +pub fn open>(path: P, with: Option) -> crate::Result<()> { let path = path.as_ref(); - // ensure we pass validation if the configuration has one - if let Some(regex) = &scope.open { - if !regex.is_match(path) { - return Err(Error::Validation { - index: 0, - validation: regex.as_str().into(), - }); - } - } + // // ensure we pass validation if the configuration has one + // if let Some(regex) = &scope.open { + // if !regex.is_match(path) { + // return Err(Error::Validation { + // index: 0, + // validation: regex.as_str().into(), + // }); + // } + // } // 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`. diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs new file mode 100644 index 00000000..b26b910c --- /dev/null +++ b/plugins/opener/src/scope.rs @@ -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), +} + +fn parse_url_pattern( + s: &str, +) -> std::result::Result { + let mut init = urlpattern::UrlPatternInit::parse_constructor_string::(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( + app: &AppHandle, + raw: Value, + ) -> std::result::Result { + 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> { + allowed: Vec<&'a Arc>, + denied: Vec<&'a Arc>, + manager: &'a M, + _marker: PhantomData, +} + +impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { + pub(crate) fn new( + manager: &'a M, + allowed: Vec<&'a Arc>, + denied: Vec<&'a Arc>, + ) -> Self { + Self { + manager, + allowed, + denied, + _marker: PhantomData, + } + } + + pub fn is_allowed(&self, path_or_url: &str) -> crate::Result { + 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 { + 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)) + } +} diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs new file mode 100644 index 00000000..105e2d59 --- /dev/null +++ b/plugins/opener/src/scope_entry.rs @@ -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 }, +} diff --git a/plugins/shell/src/commands.rs b/plugins/shell/src/commands.rs index 3345bb3a..5bee6b92 100644 --- a/plugins/shell/src/commands.rs +++ b/plugins/shell/src/commands.rs @@ -11,8 +11,9 @@ use tauri::{ Manager, Runtime, State, Window, }; +#[allow(deprecated)] +use crate::open::Program; use crate::{ - open::Program, process::{CommandEvent, TerminatedPayload}, scope::ExecuteArgs, Shell, @@ -302,6 +303,7 @@ pub fn kill( Ok(()) } +#[allow(deprecated)] #[tauri::command] pub async fn open( _window: Window, diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 9066da93..4e8b245f 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -29,6 +29,7 @@ mod commands; mod config; mod error; #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] +#[allow(deprecated)] pub mod open; pub mod process; mod scope; @@ -74,6 +75,7 @@ impl Shell { /// See [`crate::open::open`] for how it handles security-related measures. #[cfg(desktop)] #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] + #[allow(deprecated)] pub fn open(&self, path: impl Into, with: Option) -> Result<()> { open::open(&self.open_scope, path.into(), with).map_err(Into::into) } diff --git a/plugins/shell/src/scope.rs b/plugins/shell/src/scope.rs index 8178ab10..9e76931a 100644 --- a/plugins/shell/src/scope.rs +++ b/plugins/shell/src/scope.rs @@ -4,6 +4,7 @@ use std::sync::Arc; +#[allow(deprecated)] use crate::open::Program; use crate::process::Command; @@ -201,6 +202,7 @@ impl OpenScope { /// /// The path is validated against the `plugins > shell > open` validation regex, which /// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. + #[allow(deprecated)] pub fn open(&self, path: &str, with: Option) -> Result<(), Error> { // ensure we pass validation if the configuration has one if let Some(regex) = &self.open {