From 768e3ecbcfe86c6a9f5071748d5db021b83327b7 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 14 Nov 2024 15:36:25 +0200 Subject: [PATCH] remove `Program` enum --- plugins/opener/build.rs | 71 ++++++++++++++-- plugins/opener/guest-js/init.ts | 2 - plugins/opener/src/commands.rs | 14 +-- plugins/opener/src/error.rs | 8 +- plugins/opener/src/init-iife.js | 2 +- plugins/opener/src/lib.rs | 48 ++++++++--- plugins/opener/src/open.rs | 137 +++--------------------------- plugins/opener/src/scope.rs | 94 ++++++++++++-------- plugins/opener/src/scope_entry.rs | 26 +++++- 9 files changed, 208 insertions(+), 194 deletions(-) diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 546805a6..fbad4d3a 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -8,6 +8,25 @@ use std::path::PathBuf; #[allow(dead_code)] mod scope; +/// Opener scope application. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +enum Application { + /// Open in default application. + Default, + /// If true, allow open with any application. + Enable(bool), + /// Allow specific application to open with. + App(String), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + /// Opener scope entry. #[derive(schemars::JsonSchema)] #[serde(untagged)] @@ -26,6 +45,9 @@ enum OpenerScopeEntry { /// /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" url: String, + /// An application to open this url with, for example: firefox. + #[serde(default)] + app: Application, }, Path { /// A path that can be opened by the webview when using the Opener APIs. @@ -36,18 +58,55 @@ enum OpenerScopeEntry { /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. path: PathBuf, + /// An application to open this path with, for example: xdg-open. + #[serde(default)] + app: Application, }, } // 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 (scope::EntryRaw::Url { + url: String::new(), + app: scope::Application::Enable(true), + }) { + scope::EntryRaw::Url { url, app } => OpenerScopeEntry::Url { + url, + app: match app { + scope::Application::Enable(p) => Application::Enable(p), + scope::Application::App(p) => Application::App(p), + scope::Application::Default => Application::Default, + }, + }, + scope::EntryRaw::Path { path, app } => OpenerScopeEntry::Path { + path, + app: match app { + scope::Application::Enable(p) => Application::Enable(p), + scope::Application::App(p) => Application::App(p), + scope::Application::Default => Application::Default, + }, + }, }; - match (OpenerScopeEntry::Url { url: String::new() }) { - OpenerScopeEntry::Url { url } => scope::EntryRaw::Url { url }, - OpenerScopeEntry::Path { path } => scope::EntryRaw::Path { path }, + match (OpenerScopeEntry::Url { + url: String::new(), + app: Application::Enable(true), + }) { + OpenerScopeEntry::Url { url, app } => scope::EntryRaw::Url { + url, + app: match app { + Application::Enable(p) => scope::Application::Enable(p), + Application::App(p) => scope::Application::App(p), + Application::Default => scope::Application::Default, + }, + }, + OpenerScopeEntry::Path { path, app } => scope::EntryRaw::Path { + path, + app: match app { + Application::Enable(p) => scope::Application::Enable(p), + Application::App(p) => scope::Application::App(p), + Application::Default => scope::Application::Default, + }, + }, }; } diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index 9017c5ef..6cc20dcd 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -6,8 +6,6 @@ import { invoke } from '@tauri-apps/api/core' // open links with the API window.addEventListener('click', function (evt) { - console.log(evt.button) - // return early if if ( // event was prevented diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index aa38a5c3..1b46141b 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -9,7 +9,7 @@ use tauri::{ AppHandle, Runtime, }; -use crate::{open::Program, scope::Scope, Error}; +use crate::{scope::Scope, Error}; #[tauri::command] pub async fn open_url( @@ -17,7 +17,7 @@ pub async fn open_url( command_scope: CommandScope, global_scope: GlobalScope, url: String, - with: Option, + with: Option, ) -> crate::Result<()> { let scope = Scope::new( &app, @@ -33,10 +33,10 @@ pub async fn open_url( .collect(), ); - if scope.is_url_allowed(&url) { + if scope.is_url_allowed(&url, with.as_deref()) { crate::open_url(url, with) } else { - Err(Error::ForbiddenUrl(url)) + Err(Error::ForbiddenUrl { url, with }) } } @@ -46,7 +46,7 @@ pub async fn open_path( command_scope: CommandScope, global_scope: GlobalScope, path: String, - with: Option, + with: Option, ) -> crate::Result<()> { let scope = Scope::new( &app, @@ -62,10 +62,10 @@ pub async fn open_path( .collect(), ); - if scope.is_path_allowed(Path::new(&path))? { + if scope.is_path_allowed(Path::new(&path), with.as_deref())? { crate::open_path(path, with) } else { - Err(Error::ForbiddenPath(path)) + Err(Error::ForbiddenPath { path, with }) } } diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index f1603d64..231e4866 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -20,10 +20,10 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - #[error("Not allowed to open forbidden path: {0}")] - ForbiddenPath(String), - #[error("Not allowed to open forbidden url: {0}")] - ForbiddenUrl(String), + #[error("Not allowed to open path {0}{}", .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + ForbiddenPath { path: String, with: Option }, + #[error("Not allowed to open url {0}{}", .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + ForbiddenUrl { url: String, with: Option }, #[error("API not supported on the current platform")] UnsupportedPlatform, #[error(transparent)] diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index 34fb3ea9..51f6f068 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(console.log(e.button),e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 85a4f163..8342f1f9 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -26,7 +26,7 @@ mod scope_entry; pub use error::Error; type Result = std::result::Result; -pub use open::{open_path, open_url, Program}; +pub use open::{open_path, open_url}; pub use reveal_item_in_dir::reveal_item_in_dir; pub struct Opener { @@ -37,36 +37,60 @@ pub struct Opener { } impl Opener { - /// Open a url with a default or specific browser opening program. + /// Open a url with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(desktop)] - pub fn open_url(&self, url: impl Into, with: Option) -> Result<()> { - open::open(url.into(), with).map_err(Into::into) + pub fn open_url(&self, url: impl Into, with: Option>) -> Result<()> { + crate::open::open(url.into(), with.map(Into::into)).map_err(Into::into) } - /// Open a url with a default or specific browser opening program. + /// Open a url with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(mobile)] - pub fn open_url(&self, url: impl Into, _with: Option) -> Result<()> { + pub fn open_url(&self, url: impl Into, _with: Option>) -> Result<()> { self.mobile_plugin_handle .run_mobile_plugin("open", url.into()) .map_err(Into::into) } - /// Open a (url) path with a default or specific browser opening program. + /// Open a path with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(desktop)] - pub fn open_path(&self, path: impl Into, with: Option) -> Result<()> { - open::open(path.into(), with).map_err(Into::into) + pub fn open_path( + &self, + path: impl Into, + with: Option>, + ) -> Result<()> { + crate::open::open(path.into(), with.map(Into::into)).map_err(Into::into) } - /// Open a (url) path with a default or specific browser opening program. + /// Open a path with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(mobile)] - pub fn open_path(&self, path: impl Into, _with: Option) -> Result<()> { + pub fn open_path( + &self, + path: impl Into, + _with: Option>, + ) -> Result<()> { self.mobile_plugin_handle .run_mobile_plugin("open", path.into()) .map_err(Into::into) } pub fn reveal_item_in_dir>(&self, p: P) -> Result<()> { - reveal_item_in_dir::reveal_item_in_dir(p) + crate::reveal_item_in_dir::reveal_item_in_dir(p) } } diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs index 03cf9584..bbd836e3 100644 --- a/plugins/opener/src/open.rs +++ b/plugins/opener/src/open.rs @@ -4,128 +4,11 @@ //! Types and functions related to shell. -use serde::{Deserialize, Deserializer}; +use std::{ffi::OsStr, path::Path}; -use std::{ffi::OsStr, fmt::Display, path::Path, str::FromStr}; - -/// Program to use on the [`open()`] call. -#[derive(Debug)] -pub enum Program { - /// Use the `open` program. - Open, - /// Use the `start` program. - Start, - /// Use the `xdg-open` program. - XdgOpen, - /// Use the `gio` program. - Gio, - /// Use the `gnome-open` program. - GnomeOpen, - /// Use the `kde-open` program. - KdeOpen, - /// Use the `wslview` program. - WslView, - /// Use the `Firefox` program. - Firefox, - /// Use the `Google Chrome` program. - Chrome, - /// Use the `Chromium` program. - Chromium, - /// Use the `Safari` 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; - - fn from_str(s: &str) -> Result { - let p = match s.to_lowercase().as_str() { - "open" => Self::Open, - "start" => Self::Start, - "xdg-open" => Self::XdgOpen, - "gio" => Self::Gio, - "gnome-open" => Self::GnomeOpen, - "kde-open" => Self::KdeOpen, - "wslview" => Self::WslView, - "firefox" => Self::Firefox, - "chrome" | "google chrome" => Self::Chrome, - "chromium" => Self::Chromium, - "safari" => Self::Safari, - _ => return Err(crate::Error::UnknownProgramName(s.to_string())), - }; - Ok(p) - } -} - -impl<'de> Deserialize<'de> for Program { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Program::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string())) - } -} - -impl Program { - pub(crate) fn name(self) -> &'static str { - 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", - - #[cfg(target_os = "macos")] - Self::Firefox => "Firefox", - #[cfg(not(target_os = "macos"))] - Self::Firefox => "firefox", - - #[cfg(target_os = "macos")] - Self::Chrome => "Google Chrome", - #[cfg(not(target_os = "macos"))] - Self::Chrome => "google-chrome", - - #[cfg(target_os = "macos")] - Self::Chromium => "Chromium", - #[cfg(not(target_os = "macos"))] - Self::Chromium => "chromium", - - #[cfg(target_os = "macos")] - Self::Safari => "Safari", - #[cfg(not(target_os = "macos"))] - Self::Safari => "safari", - } - } -} - -pub(crate) fn open>(path: P, with: Option) -> crate::Result<()> { - match with.map(Program::name) { - Some(program) => ::open::with_detached(path, program), +pub(crate) fn open, S: AsRef>(path: P, with: Option) -> crate::Result<()> { + match with { + Some(program) => ::open::with_detached(path, program.as_ref()), None => ::open::that_detached(path), } .map_err(Into::into) @@ -133,6 +16,10 @@ pub(crate) fn open>(path: P, with: Option) -> crate::Re /// Opens URL with the program specified in `with`, or system default if `None`. /// +/// ## Platform-specific: +/// +/// - **Android / iOS**: Always opens using default program. +/// /// # Examples /// /// ```rust,no_run @@ -143,13 +30,17 @@ pub(crate) fn open>(path: P, with: Option) -> crate::Re /// Ok(()) /// }); /// ``` -pub fn open_url>(url: P, with: Option) -> crate::Result<()> { +pub fn open_url, S: AsRef>(url: P, with: Option) -> crate::Result<()> { let url = url.as_ref(); open(url, with) } /// Opens path with the program specified in `with`, or system default if `None`. /// +/// ## Platform-specific: +/// +/// - **Android / iOS**: Always opens using default program. +/// /// # Examples /// /// ```rust,no_run @@ -160,7 +51,7 @@ pub fn open_url>(url: P, with: Option) -> crate::Result<( /// Ok(()) /// }); /// ``` -pub fn open_path>(path: P, with: Option) -> crate::Result<()> { +pub fn open_path, S: AsRef>(path: P, with: Option) -> crate::Result<()> { let path = path.as_ref(); open(path, with) } diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index 3fda73cd..22c9787e 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -12,35 +12,44 @@ use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; use crate::{scope_entry::EntryRaw, Error}; +pub use crate::scope_entry::Application; + #[derive(Debug)] pub enum Entry { - Url(glob::Pattern), - Path(Option), + Url { + url: glob::Pattern, + app: Application, + }, + Path { + path: Option, + app: Application, + }, } impl ScopeObject for Entry { type Error = Error; fn deserialize( - app: &AppHandle, + app_handle: &AppHandle, raw: Value, ) -> std::result::Result { serde_json::from_value(raw.into()) .and_then(|raw| { let entry = match raw { - EntryRaw::Url { url } => Entry::Url( - glob::Pattern::new(&url) + EntryRaw::Url { url, app } => Entry::Url { + url: glob::Pattern::new(&url) .map_err(|e| serde::de::Error::custom(e.to_string()))?, - ), - EntryRaw::Path { path } => { - let path = match app.path().parse(path) { + app, + }, + EntryRaw::Path { path, app } => { + let path = match app_handle.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) + Entry::Path { path, app } } }; @@ -50,6 +59,39 @@ impl ScopeObject for Entry { } } +impl Application { + fn matches(&self, a: Option<&str>) -> bool { + match self { + Self::Default => a.is_none(), + Self::Enable(enable) => *enable, + Self::App(program) => Some(program.as_str()) == a, + } + } +} + +impl Entry { + fn path(&self) -> Option { + match self { + Self::Url { .. } => None, + Self::Path { path, .. } => path.clone(), + } + } + + fn matches_url(&self, u: &str, a: Option<&str>) -> bool { + match self { + Self::Url { url, app } => url.matches(u) && app.matches(a), + Self::Path { .. } => false, + } + } + + fn matches_path_program(&self, a: Option<&str>) -> bool { + match self { + Self::Url { .. } => false, + Self::Path { app, .. } => app.matches(a), + } + } +} + #[derive(Debug)] pub struct Scope<'a, R: Runtime, M: Manager> { allowed: Vec<&'a Arc>, @@ -72,45 +114,25 @@ impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { } } - pub fn is_url_allowed(&self, url: &str) -> bool { - let denied = self.denied.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url), - Entry::Path { .. } => false, - }); + pub fn is_url_allowed(&self, url: &str, with: Option<&str>) -> bool { + let denied = self.denied.iter().any(|e| e.matches_url(url, with)); if denied { false } else { - self.allowed.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url), - Entry::Path { .. } => false, - }) + self.allowed.iter().any(|e| e.matches_url(url, with)) } } - pub fn is_path_allowed(&self, path: &Path) -> crate::Result { + pub fn is_path_allowed(&self, path: &Path, with: Option<&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(), + allow: self.allowed.iter().filter_map(|e| e.path()).collect(), + deny: self.denied.iter().filter_map(|e| e.path()).collect(), require_literal_leading_dot: None, }, )?; - Ok(fs_scope.is_allowed(path)) + Ok(fs_scope.is_allowed(path) && self.allowed.iter().any(|e| e.matches_path_program(with))) } } diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs index 20484887..c7c6b1d3 100644 --- a/plugins/opener/src/scope_entry.rs +++ b/plugins/opener/src/scope_entry.rs @@ -6,10 +6,30 @@ use std::path::PathBuf; use serde::Deserialize; +#[derive(Deserialize, Debug)] +pub enum Application { + Default, + Enable(bool), + App(String), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + #[derive(Deserialize)] #[serde(untagged, rename_all = "camelCase")] -#[allow(unused)] pub(crate) enum EntryRaw { - Url { url: String }, - Path { path: PathBuf }, + Url { + url: String, + #[serde(default)] + app: Application, + }, + Path { + path: PathBuf, + #[serde(default)] + app: Application, + }, }