mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-07-30 17:48:50 +02:00
feat: add tauri-plugin-opener (#2019)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tauri::{
|
||||
ipc::{CommandScope, GlobalScope},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::{scope::Scope, Error};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_url<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
command_scope: CommandScope<crate::scope::Entry>,
|
||||
global_scope: GlobalScope<crate::scope::Entry>,
|
||||
url: String,
|
||||
with: Option<String>,
|
||||
) -> 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_url_allowed(&url, with.as_deref()) {
|
||||
crate::open_url(url, with)
|
||||
} else {
|
||||
Err(Error::ForbiddenUrl { url, with })
|
||||
}
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
) -> 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), with.as_deref())? {
|
||||
crate::open_path(path, with)
|
||||
} else {
|
||||
Err(Error::ForbiddenPath { path, with })
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reveal_item_in_dir(path: PathBuf) -> crate::Result<()> {
|
||||
crate::reveal_item_in_dir(path)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[cfg(mobile)]
|
||||
#[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),
|
||||
#[error("Not allowed to open path {}{}", .path, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())]
|
||||
ForbiddenPath { path: String, with: Option<String> },
|
||||
#[error("Not allowed to open url {}{}", .url, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())]
|
||||
ForbiddenUrl { url: String, with: Option<String> },
|
||||
#[error("API not supported on the current platform")]
|
||||
UnsupportedPlatform,
|
||||
#[error(transparent)]
|
||||
#[cfg(windows)]
|
||||
Win32Error(#[from] windows::core::Error),
|
||||
#[error("Path doesn't have a parent: {0}")]
|
||||
NoParent(PathBuf),
|
||||
#[error("Failed to convert path to file:// url")]
|
||||
FailedToConvertPathToFileUrl,
|
||||
#[error(transparent)]
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
Zbus(#[from] zbus::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
!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}))}))}();
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use tauri::{plugin::TauriPlugin, Manager, Runtime};
|
||||
|
||||
#[cfg(mobile)]
|
||||
use tauri::plugin::PluginHandle;
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.opener";
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_opener);
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod open;
|
||||
mod reveal_item_in_dir;
|
||||
mod scope;
|
||||
mod scope_entry;
|
||||
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub use open::{open_path, open_url};
|
||||
pub use reveal_item_in_dir::reveal_item_in_dir;
|
||||
|
||||
pub struct Opener<R: Runtime> {
|
||||
// we use `fn() -> R` to slicence the unused generic error
|
||||
// while keeping this struct `Send + Sync` without requiring `R` to be
|
||||
#[cfg(not(mobile))]
|
||||
_marker: std::marker::PhantomData<fn() -> R>,
|
||||
#[cfg(mobile)]
|
||||
mobile_plugin_handle: PluginHandle<R>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Opener<R> {
|
||||
/// 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<String>, with: Option<impl Into<String>>) -> Result<()> {
|
||||
crate::open::open(url.into(), with.map(Into::into)).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// 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<String>, _with: Option<impl Into<String>>) -> Result<()> {
|
||||
self.mobile_plugin_handle
|
||||
.run_mobile_plugin("open", url.into())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
with: Option<impl Into<String>>,
|
||||
) -> Result<()> {
|
||||
crate::open::open(path.into(), with.map(Into::into)).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
_with: Option<impl Into<String>>,
|
||||
) -> Result<()> {
|
||||
self.mobile_plugin_handle
|
||||
.run_mobile_plugin("open", path.into())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn reveal_item_in_dir<P: AsRef<Path>>(&self, p: P) -> Result<()> {
|
||||
crate::reveal_item_in_dir::reveal_item_in_dir(p)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs.
|
||||
pub trait OpenerExt<R: Runtime> {
|
||||
fn opener(&self) -> &Opener<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::OpenerExt<R> for T {
|
||||
fn opener(&self) -> &Opener<R> {
|
||||
self.state::<Opener<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// The opener plugin Builder.
|
||||
pub struct Builder {
|
||||
open_js_links_on_click: bool,
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open_js_links_on_click: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Create a new opener plugin Builder.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Whether the plugin should inject a JS script to open URLs in default browser
|
||||
/// when clicking on `<a>` elements that has `_blank` target, or when pressing `Ctrl` or `Shift` while clicking it.
|
||||
///
|
||||
/// Enabled by default for `http:`, `https:`, `mailto:`, `tel:` links.
|
||||
pub fn open_js_links_on_click(mut self, open: bool) -> Self {
|
||||
self.open_js_links_on_click = open;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build and Initializes the plugin.
|
||||
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
||||
let mut builder = tauri::plugin::Builder::new("opener")
|
||||
.setup(|app, _api| {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = _api.register_ios_plugin(init_plugin_opener)?;
|
||||
|
||||
app.manage(Opener {
|
||||
#[cfg(not(mobile))]
|
||||
_marker: std::marker::PhantomData::<fn() -> R>,
|
||||
#[cfg(mobile)]
|
||||
mobile_plugin_handle: handle,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::open_url,
|
||||
commands::open_path,
|
||||
commands::reveal_item_in_dir
|
||||
]);
|
||||
|
||||
if self.open_js_links_on_click {
|
||||
builder = builder.js_init_script(include_str!("init-iife.js").to_string());
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::default().build()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Types and functions related to shell.
|
||||
|
||||
use std::{ffi::OsStr, path::Path};
|
||||
|
||||
pub(crate) fn open<P: AsRef<OsStr>, S: AsRef<str>>(path: P, with: Option<S>) -> crate::Result<()> {
|
||||
match with {
|
||||
Some(program) => ::open::with_detached(path, program.as_ref()),
|
||||
None => ::open::that_detached(path),
|
||||
}
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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>, S: AsRef<str>>(url: P, with: Option<S>) -> 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
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// // open the given URL on the system default browser
|
||||
/// tauri_plugin_opener::open_path("/path/to/file", None)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn open_path<P: AsRef<Path>, S: AsRef<str>>(path: P, with: Option<S>) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
open(path, with)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Reveal a path the system's default explorer.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Android / iOS:** Unsupported.
|
||||
pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
let path = path.as_ref().canonicalize()?;
|
||||
|
||||
#[cfg(any(
|
||||
windows,
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
return imp::reveal_item_in_dir(&path);
|
||||
|
||||
#[cfg(not(any(
|
||||
windows,
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
)))]
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
use windows::{
|
||||
core::{w, HSTRING, PCWSTR},
|
||||
Win32::{
|
||||
Foundation::ERROR_FILE_NOT_FOUND,
|
||||
System::Com::CoInitialize,
|
||||
UI::{
|
||||
Shell::{
|
||||
ILCreateFromPathW, ILFree, SHOpenFolderAndSelectItems, ShellExecuteExW,
|
||||
SHELLEXECUTEINFOW,
|
||||
},
|
||||
WindowsAndMessaging::SW_SHOWNORMAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
|
||||
let file = dunce::simplified(path);
|
||||
|
||||
let _ = unsafe { CoInitialize(None) };
|
||||
|
||||
let dir = file
|
||||
.parent()
|
||||
.ok_or_else(|| crate::Error::NoParent(file.to_path_buf()))?;
|
||||
|
||||
let dir = HSTRING::from(dir);
|
||||
let dir_item = unsafe { ILCreateFromPathW(&dir) };
|
||||
|
||||
let file_h = HSTRING::from(file);
|
||||
let file_item = unsafe { ILCreateFromPathW(&file_h) };
|
||||
|
||||
unsafe {
|
||||
if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 0) {
|
||||
// from https://github.com/electron/electron/blob/10d967028af2e72382d16b7e2025d243b9e204ae/shell/common/platform_util_win.cc#L302
|
||||
// On some systems, the above call mysteriously fails with "file not
|
||||
// found" even though the file is there. In these cases, ShellExecute()
|
||||
// seems to work as a fallback (although it won't select the file).
|
||||
if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
|
||||
let is_dir = file.is_dir();
|
||||
let mut info = SHELLEXECUTEINFOW {
|
||||
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as _,
|
||||
nShow: SW_SHOWNORMAL.0,
|
||||
lpFile: PCWSTR(dir.as_ptr()),
|
||||
lpClass: if is_dir { w!("folder") } else { PCWSTR::null() },
|
||||
lpVerb: if is_dir {
|
||||
w!("explore")
|
||||
} else {
|
||||
PCWSTR::null()
|
||||
},
|
||||
..std::mem::zeroed()
|
||||
};
|
||||
|
||||
ShellExecuteExW(&mut info).inspect_err(|_| {
|
||||
ILFree(Some(dir_item));
|
||||
ILFree(Some(file_item));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ILFree(Some(dir_item));
|
||||
ILFree(Some(file_item));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
mod imp {
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
|
||||
let connection = zbus::blocking::Connection::session()?;
|
||||
|
||||
reveal_with_filemanager1(path, &connection)
|
||||
.or_else(|_| reveal_with_open_uri_portal(path, &connection))
|
||||
}
|
||||
|
||||
fn reveal_with_filemanager1(
|
||||
path: &Path,
|
||||
connection: &zbus::blocking::Connection,
|
||||
) -> crate::Result<()> {
|
||||
let uri = url::Url::from_file_path(path)
|
||||
.map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?;
|
||||
|
||||
#[zbus::proxy(
|
||||
interface = "org.freedesktop.FileManager1",
|
||||
default_service = "org.freedesktop.FileManager1",
|
||||
default_path = "/org/freedesktop/FileManager1"
|
||||
)]
|
||||
trait FileManager1 {
|
||||
async fn ShowItems(&self, name: Vec<&str>, arg2: &str) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
let proxy = FileManager1ProxyBlocking::new(connection)?;
|
||||
|
||||
proxy.ShowItems(vec![uri.as_str()], "")
|
||||
}
|
||||
|
||||
fn reveal_with_open_uri_portal(
|
||||
path: &Path,
|
||||
connection: &zbus::blocking::Connection,
|
||||
) -> crate::Result<()> {
|
||||
let uri = url::Url::from_file_path(path)
|
||||
.map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?;
|
||||
|
||||
#[zbus::proxy(
|
||||
interface = "org.freedesktop.portal.Desktop",
|
||||
default_service = "org.freedesktop.portal.OpenURI",
|
||||
default_path = "/org/freedesktop/portal/desktop"
|
||||
)]
|
||||
trait PortalDesktop {
|
||||
async fn OpenDirectory(
|
||||
&self,
|
||||
arg1: &str,
|
||||
name: &str,
|
||||
arg3: HashMap<&str, &str>,
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
let proxy = PortalDesktopProxyBlocking::new(connection)?;
|
||||
|
||||
proxy.OpenDirectory("", uri.as_str(), HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use super::*;
|
||||
use objc2_app_kit::NSWorkspace;
|
||||
use objc2_foundation::{NSArray, NSString, NSURL};
|
||||
pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
|
||||
unsafe {
|
||||
let path = path.to_string_lossy();
|
||||
let path = NSString::from_str(&path);
|
||||
let urls = vec![NSURL::fileURLWithPath(&path)];
|
||||
let urls = NSArray::from_vec(urls);
|
||||
|
||||
let workspace = NSWorkspace::new();
|
||||
workspace.activateFileViewerSelectingURLs(&urls);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
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 {
|
||||
url: glob::Pattern,
|
||||
app: Application,
|
||||
},
|
||||
Path {
|
||||
path: Option<PathBuf>,
|
||||
app: Application,
|
||||
},
|
||||
}
|
||||
|
||||
impl ScopeObject for Entry {
|
||||
type Error = Error;
|
||||
|
||||
fn deserialize<R: Runtime>(
|
||||
app_handle: &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, app } => Entry::Url {
|
||||
url: glob::Pattern::new(&url)
|
||||
.map_err(|e| serde::de::Error::custom(e.to_string()))?,
|
||||
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, app }
|
||||
}
|
||||
};
|
||||
|
||||
Ok(entry)
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
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<PathBuf> {
|
||||
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<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_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(|e| e.matches_url(url, with))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_path_allowed(&self, path: &Path, with: Option<&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| e.path()).collect(),
|
||||
deny: self.denied.iter().filter_map(|e| e.path()).collect(),
|
||||
require_literal_leading_dot: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(fs_scope.is_allowed(path) && self.allowed.iter().any(|e| e.matches_path_program(with)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum Application {
|
||||
Default,
|
||||
Enable(bool),
|
||||
App(String),
|
||||
}
|
||||
|
||||
impl Default for Application {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged, rename_all = "camelCase")]
|
||||
pub(crate) enum EntryRaw {
|
||||
Url {
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
app: Application,
|
||||
},
|
||||
Path {
|
||||
path: PathBuf,
|
||||
#[serde(default)]
|
||||
app: Application,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user