diff --git a/Cargo.lock b/Cargo.lock index f0451beb..8b5b67c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6574,7 +6574,6 @@ dependencies = [ "tauri", "tauri-plugin", "thiserror 2.0.3", - "url", "windows 0.58.0", "zbus", ] diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index e1f4154a..81a22036 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -34,7 +34,6 @@ serde_json = { workspace = true } tauri = { workspace = true } thiserror = { workspace = true } open = { version = "5", features = ["shellexecute-on-windows"] } -url = { workspace = true } glob = { workspace = true } [target."cfg(windows)".dependencies] diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 345837b2..30415a61 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.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},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.openPath=async function(n,_){await e("plugin:opener|open_path",{path:n,with:_})},n.openUrl=async function(n,_){await e("plugin:opener|open_url",{url:n,with:_})},n.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 42926233..546805a6 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -51,7 +51,7 @@ fn _f() { }; } -const COMMANDS: &[&str] = &["open", "reveal_item_in_dir"]; +const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir"]; fn main() { tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 9484f5bd..37601757 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -34,31 +34,57 @@ export type Program = | 'wslview' /** - * Opens a path or URL with the system's default app, - * or the one specified with `openWith`. - * - * The `openWith` value must be one of `firefox`, `google chrome`, `chromium` `safari`, - * `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. + * Opens a url with the system's default app, or the one specified with {@linkcode openWith}. * * @example * ```typescript - * import { open } from '@tauri-apps/plugin-opener'; + * import { openUrl } from '@tauri-apps/plugin-opener'; + * * // opens the given URL on the default browser: - * await open('https://github.com/tauri-apps/tauri'); + * await openUrl('https://github.com/tauri-apps/tauri'); * // opens the given URL using `firefox`: - * await open('https://github.com/tauri-apps/tauri', 'firefox'); + * await openUrl('https://github.com/tauri-apps/tauri', 'firefox'); + * ``` + * + * @param url The URL to open. + * @param openWith The app to open the URL with. + * Must be one of `firefox`, `google chrome`, `chromium` `safari`, `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. + * If not specified, defaults to the system default application for the specified url type. + * + * @since 2.0.0 + */ +export async function openUrl(url: string, openWith?: Program): Promise { + await invoke('plugin:opener|open_url', { + url, + with: openWith + }) +} + +/** + * Opens a path with the system's default app, or the one specified with {@linkcode openWith}. + * + * @example + * ```typescript + * import { openPath } from '@tauri-apps/plugin-opener'; + * * // opens a file using the default program: - * await open('/path/to/file'); + * await openPath('/path/to/file'); + * // opens a file using `start` command on Windows. + * await openPath('C:/path/to/file', 'start'); * ``` * - * @param path The path or URL to open. - * @param openWith The app to open the file or URL with. - * Defaults to the system default application for the specified path type. + * @param path The path to open. + * @param openWith The app to open the path with. + * Must be one of `firefox`, `google chrome`, `chromium` `safari`, `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. + * If not specified, defaults to the system default application for the specified path type. * * @since 2.0.0 */ -export async function open(path: string, openWith?: Program): Promise { - await invoke('plugin:opener|open', { +export async function openPath( + path: string, + openWith?: Program +): Promise { + await invoke('plugin:opener|open_path', { path, with: openWith }) diff --git a/plugins/opener/permissions/autogenerated/commands/open_path.toml b/plugins/opener/permissions/autogenerated/commands/open_path.toml new file mode 100644 index 00000000..ae67b939 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open_path.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open-path" +description = "Enables the open_path command without any pre-configured scope." +commands.allow = ["open_path"] + +[[permission]] +identifier = "deny-open-path" +description = "Denies the open_path command without any pre-configured scope." +commands.deny = ["open_path"] diff --git a/plugins/opener/permissions/autogenerated/commands/open_url.toml b/plugins/opener/permissions/autogenerated/commands/open_url.toml new file mode 100644 index 00000000..f1e694b1 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open_url.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open-url" +description = "Enables the open_url command without any pre-configured scope." +commands.allow = ["open_url"] + +[[permission]] +identifier = "deny-open-url" +description = "Denies the open_url command without any pre-configured scope." +commands.deny = ["open_url"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index b49b335b..bfefeb36 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -3,7 +3,7 @@ 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-url` - `allow-reveal-item-in-dir` - `allow-default-urls` @@ -58,6 +58,58 @@ Denies the open command without any pre-configured scope. +`opener:allow-open-path` + + + + +Enables the open_path command without any pre-configured scope. + + + + + + + +`opener:deny-open-path` + + + + +Denies the open_path command without any pre-configured scope. + + + + + + + +`opener:allow-open-url` + + + + +Enables the open_url command without any pre-configured scope. + + + + + + + +`opener:deny-open-url` + + + + +Denies the open_url command without any pre-configured scope. + + + + + + + `opener:allow-reveal-item-in-dir` diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index f9e3184d..300832c8 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -3,4 +3,8 @@ [default] 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"] +permissions = [ + "allow-open-url", + "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 22d9d40d..48b90b0b 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -309,6 +309,26 @@ "type": "string", "const": "deny-open" }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "allow-open-path" + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "deny-open-path" + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "allow-open-url" + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "deny-open-url" + }, { "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", "type": "string", diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 0190ccdb..09d41b10 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tauri::{ ipc::{CommandScope, GlobalScope}, @@ -12,7 +12,7 @@ use tauri::{ use crate::{open::Program, scope::Scope, Error}; #[tauri::command] -pub async fn open( +pub async fn open_url( app: AppHandle, command_scope: CommandScope, global_scope: GlobalScope, @@ -33,10 +33,39 @@ pub async fn open( .collect(), ); - if scope.is_allowed(&path)? { - crate::open(path, with) + if scope.is_url_allowed(&path) { + crate::open_url(path, with) } else { - Err(Error::NotAllowed(path)) + Err(Error::ForbiddenUrl(path)) + } +} + +#[tauri::command] +pub async fn open_path( + app: AppHandle, + command_scope: CommandScope, + global_scope: GlobalScope, + path: String, + with: Option, +) -> crate::Result<()> { + 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_path_allowed(Path::new(&path))? { + crate::open_path(path, with) + } else { + Err(Error::ForbiddenPath(path)) } } diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 42de2927..f1603d64 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -20,8 +20,10 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - #[error("Not allowed to open forbidden path or url: {0}")] - NotAllowed(String), + #[error("Not allowed to open forbidden path: {0}")] + ForbiddenPath(String), + #[error("Not allowed to open forbidden url: {0}")] + ForbiddenUrl(String), #[error("API not supported on the current platform")] UnsupportedPlatform, #[error(transparent)] diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 1ad71c47..85a4f163 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, Program}; +pub use open::{open_path, open_url, Program}; pub use reveal_item_in_dir::reveal_item_in_dir; pub struct Opener { @@ -37,15 +37,29 @@ pub struct Opener { } impl Opener { + /// Open a url with a default or specific browser opening program. + #[cfg(desktop)] + pub fn open_url(&self, url: impl Into, with: Option) -> Result<()> { + open::open(url.into(), with).map_err(Into::into) + } + + /// Open a url with a default or specific browser opening program. + #[cfg(mobile)] + 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. #[cfg(desktop)] - pub fn open(&self, path: impl Into, with: Option) -> Result<()> { + pub fn open_path(&self, path: impl Into, with: Option) -> Result<()> { open::open(path.into(), with).map_err(Into::into) } /// Open a (url) path with a default or specific browser opening program. #[cfg(mobile)] - pub fn open(&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) @@ -85,7 +99,8 @@ pub fn init() -> TauriPlugin { Ok(()) }) .invoke_handler(tauri::generate_handler![ - commands::open, + commands::open_url, + commands::open_path, commands::reveal_item_in_dir ]) .build() diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs index b472cb34..03cf9584 100644 --- a/plugins/opener/src/open.rs +++ b/plugins/opener/src/open.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer}; -use std::{fmt::Display, str::FromStr}; +use std::{ffi::OsStr, fmt::Display, path::Path, str::FromStr}; /// Program to use on the [`open()`] call. #[derive(Debug)] @@ -123,40 +123,44 @@ impl Program { } } -/// Opens path or URL with the program specified in `with`, or system default if `None`. +pub(crate) fn open>(path: P, with: Option) -> crate::Result<()> { + match with.map(Program::name) { + Some(program) => ::open::with_detached(path, program), + None => ::open::that_detached(path), + } + .map_err(Into::into) +} + +/// Opens URL with the program specified in `with`, or system default if `None`. +/// +/// # Examples /// -/// The path will be matched against the shell open validation regex, defaulting to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. -/// A custom validation regex may be supplied in the config in `plugins > shell > scope > open`. +/// ```rust,no_run +/// tauri::Builder::default() +/// .setup(|app| { +/// // open the given URL on the system default browser +/// tauri_plugin_opener::open_url("https://github.com/tauri-apps/tauri", None)?; +/// Ok(()) +/// }); +/// ``` +pub fn open_url>(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`. /// /// # Examples /// /// ```rust,no_run -/// use tauri_plugin_shell::ShellExt; /// tauri::Builder::default() /// .setup(|app| { /// // open the given URL on the system default browser -/// app.shell().open("https://github.com/tauri-apps/tauri", None)?; +/// tauri_plugin_opener::open_path("/path/to/file", None)?; /// Ok(()) /// }); /// ``` -pub fn open>(path: P, with: Option) -> crate::Result<()> { +pub fn open_path>(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(), - // }); - // } - // } - - // 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`. - match with.map(Program::name) { - Some(program) => ::open::with_detached(path, program), - None => ::open::that_detached(path), - } - .map_err(Into::into) + open(path, with) } diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index b5199303..3fda73cd 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{marker::PhantomData, path::PathBuf, sync::Arc}; +use std::{ + marker::PhantomData, + path::{Path, PathBuf}, + sync::Arc, +}; use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; -use url::Url; - use crate::{scope_entry::EntryRaw, Error}; #[derive(Debug)] @@ -70,30 +72,22 @@ impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { } } - 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 { + 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.as_str()), + Entry::Url(url_pattern) => url_pattern.matches(url), Entry::Path { .. } => false, }); if denied { false } else { self.allowed.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url.as_str()), + Entry::Url(url_pattern) => url_pattern.matches(url), Entry::Path { .. } => false, }) } } - pub fn is_path_allowed(&self, path: &str) -> crate::Result { + pub fn is_path_allowed(&self, path: &Path) -> crate::Result { let fs_scope = tauri::fs::Scope::new( self.manager, &tauri::utils::config::FsScope::Scope {