split into openUrl and openPath

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

1
Cargo.lock generated

@ -6574,7 +6574,6 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.3", "thiserror 2.0.3",
"url",
"windows 0.58.0", "windows 0.58.0",
"zbus", "zbus",
] ]

@ -34,7 +34,6 @@ serde_json = { workspace = true }
tauri = { workspace = true } tauri = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
open = { version = "5", features = ["shellexecute-on-windows"] } open = { version = "5", features = ["shellexecute-on-windows"] }
url = { workspace = true }
glob = { workspace = true } glob = { workspace = true }
[target."cfg(windows)".dependencies] [target."cfg(windows)".dependencies]

@ -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__})}

@ -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() { fn main() {
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)

@ -34,31 +34,57 @@ export type Program =
| 'wslview' | 'wslview'
/** /**
* Opens a path or URL with the system's default app, * Opens a url with the system's default app, or the one specified with {@linkcode openWith}.
* 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`.
* *
* @example * @example
* ```typescript * ```typescript
* import { open } from '@tauri-apps/plugin-opener'; * import { openUrl } from '@tauri-apps/plugin-opener';
*
* // opens the given URL on the default browser: * // 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`: * // 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<void> {
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: * // 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 path The path to open.
* @param openWith The app to open the file or URL with. * @param openWith The app to open the path with.
* Defaults to the system default application for the specified path type. * 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 * @since 2.0.0
*/ */
export async function open(path: string, openWith?: Program): Promise<void> { export async function openPath(
await invoke('plugin:opener|open', { path: string,
openWith?: Program
): Promise<void> {
await invoke('plugin:opener|open_path', {
path, path,
with: openWith with: openWith
}) })

@ -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"]

@ -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"]

@ -3,7 +3,7 @@
This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application 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 as well as reveal file in directories using default file explorer
- `allow-open` - `allow-open-url`
- `allow-reveal-item-in-dir` - `allow-reveal-item-in-dir`
- `allow-default-urls` - `allow-default-urls`
@ -58,6 +58,58 @@ Denies the open command without any pre-configured scope.
<tr> <tr>
<td> <td>
`opener:allow-open-path`
</td>
<td>
Enables the open_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:deny-open-path`
</td>
<td>
Denies the open_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:allow-open-url`
</td>
<td>
Enables the open_url command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:deny-open-url`
</td>
<td>
Denies the open_url command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`opener:allow-reveal-item-in-dir` `opener:allow-reveal-item-in-dir`
</td> </td>

@ -3,4 +3,8 @@
[default] [default]
description = """This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application 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""" 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",
]

@ -309,6 +309,26 @@
"type": "string", "type": "string",
"const": "deny-open" "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.", "description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string", "type": "string",

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::path::PathBuf; use std::path::{Path, PathBuf};
use tauri::{ use tauri::{
ipc::{CommandScope, GlobalScope}, ipc::{CommandScope, GlobalScope},
@ -12,7 +12,7 @@ use tauri::{
use crate::{open::Program, scope::Scope, Error}; use crate::{open::Program, scope::Scope, Error};
#[tauri::command] #[tauri::command]
pub async fn open<R: Runtime>( pub async fn open_url<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
command_scope: CommandScope<crate::scope::Entry>, command_scope: CommandScope<crate::scope::Entry>,
global_scope: GlobalScope<crate::scope::Entry>, global_scope: GlobalScope<crate::scope::Entry>,
@ -33,10 +33,39 @@ pub async fn open<R: Runtime>(
.collect(), .collect(),
); );
if scope.is_allowed(&path)? { if scope.is_url_allowed(&path) {
crate::open(path, with) crate::open_url(path, with)
} else { } else {
Err(Error::NotAllowed(path)) Err(Error::ForbiddenUrl(path))
}
}
#[tauri::command]
pub async fn open_path<R: Runtime>(
app: AppHandle<R>,
command_scope: CommandScope<crate::scope::Entry>,
global_scope: GlobalScope<crate::scope::Entry>,
path: String,
with: Option<Program>,
) -> 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))
} }
} }

@ -20,8 +20,10 @@ pub enum Error {
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error("unknown program {0}")] #[error("unknown program {0}")]
UnknownProgramName(String), UnknownProgramName(String),
#[error("Not allowed to open forbidden path or url: {0}")] #[error("Not allowed to open forbidden path: {0}")]
NotAllowed(String), ForbiddenPath(String),
#[error("Not allowed to open forbidden url: {0}")]
ForbiddenUrl(String),
#[error("API not supported on the current platform")] #[error("API not supported on the current platform")]
UnsupportedPlatform, UnsupportedPlatform,
#[error(transparent)] #[error(transparent)]

@ -26,7 +26,7 @@ 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>;
pub use open::{open, Program}; pub use open::{open_path, open_url, Program};
pub use reveal_item_in_dir::reveal_item_in_dir; pub use reveal_item_in_dir::reveal_item_in_dir;
pub struct Opener<R: Runtime> { pub struct Opener<R: Runtime> {
@ -37,15 +37,29 @@ pub struct Opener<R: Runtime> {
} }
impl<R: Runtime> Opener<R> { impl<R: Runtime> Opener<R> {
/// Open a url with a default or specific browser opening program.
#[cfg(desktop)]
pub fn open_url(&self, url: impl Into<String>, with: Option<open::Program>) -> 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<String>, _with: Option<open::Program>) -> 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 (url) path with a default or specific browser opening program.
#[cfg(desktop)] #[cfg(desktop)]
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> { pub fn open_path(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
open::open(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.
#[cfg(mobile)] #[cfg(mobile)]
pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> { pub fn open_path(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
self.mobile_plugin_handle self.mobile_plugin_handle
.run_mobile_plugin("open", path.into()) .run_mobile_plugin("open", path.into())
.map_err(Into::into) .map_err(Into::into)
@ -85,7 +99,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::open, commands::open_url,
commands::open_path,
commands::reveal_item_in_dir commands::reveal_item_in_dir
]) ])
.build() .build()

@ -6,7 +6,7 @@
use serde::{Deserialize, Deserializer}; 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. /// Program to use on the [`open()`] call.
#[derive(Debug)] #[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<P: AsRef<OsStr>>(path: P, with: Option<Program>) -> 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+)).+`. /// ```rust,no_run
/// A custom validation regex may be supplied in the config in `plugins > shell > scope > open`. /// 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<P: AsRef<str>>(url: P, with: Option<Program>) -> crate::Result<()> {
let url = url.as_ref();
open(url, with)
}
/// Opens path with the program specified in `with`, or system default if `None`.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use tauri_plugin_shell::ShellExt;
/// tauri::Builder::default() /// tauri::Builder::default()
/// .setup(|app| { /// .setup(|app| {
/// // open the given URL on the system default browser /// // 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(()) /// Ok(())
/// }); /// });
/// ``` /// ```
pub fn open<P: AsRef<str>>(path: P, with: Option<Program>) -> crate::Result<()> { pub fn open_path<P: AsRef<Path>>(path: P, with: Option<Program>) -> crate::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
open(path, with)
// // 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)
} }

@ -2,12 +2,14 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // 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 tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime};
use url::Url;
use crate::{scope_entry::EntryRaw, Error}; use crate::{scope_entry::EntryRaw, Error};
#[derive(Debug)] #[derive(Debug)]
@ -70,30 +72,22 @@ impl<'a, R: Runtime, M: Manager<R>> Scope<'a, R, M> {
} }
} }
pub fn is_allowed(&self, path_or_url: &str) -> crate::Result<bool> { pub fn is_url_allowed(&self, url: &str) -> 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() { 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, Entry::Path { .. } => false,
}); });
if denied { if denied {
false false
} else { } else {
self.allowed.iter().any(|entry| match entry.as_ref() { 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, Entry::Path { .. } => false,
}) })
} }
} }
pub fn is_path_allowed(&self, path: &str) -> crate::Result<bool> { pub fn is_path_allowed(&self, path: &Path) -> crate::Result<bool> {
let fs_scope = tauri::fs::Scope::new( let fs_scope = tauri::fs::Scope::new(
self.manager, self.manager,
&tauri::utils::config::FsScope::Scope { &tauri::utils::config::FsScope::Scope {

Loading…
Cancel
Save