parent
38369c832d
commit
42ab44cd5f
@ -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__})}
|
||||||
|
@ -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://*:*"
|
@ -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"]
|
@ -1,4 +1,6 @@
|
|||||||
"$schema" = "schemas/schema.json"
|
"$schema" = "schemas/schema.json"
|
||||||
|
|
||||||
[default]
|
[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"]
|
||||||
|
@ -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]
|
#[tauri::command]
|
||||||
pub async fn open<R: Runtime>(
|
pub async fn open<R: Runtime>(
|
||||||
_app: AppHandle<R>,
|
app: AppHandle<R>,
|
||||||
opener: State<'_, Opener<R>>,
|
command_scope: CommandScope<crate::scope::Entry>,
|
||||||
|
global_scope: GlobalScope<crate::scope::Entry>,
|
||||||
path: String,
|
path: String,
|
||||||
with: Option<Program>,
|
with: Option<Program>,
|
||||||
) -> crate::Result<()> {
|
) -> 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]
|
#[tauri::command]
|
||||||
pub async fn reveal_in_dir() {}
|
pub async fn reveal_item_in_dir() {}
|
||||||
|
@ -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<Regex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<PathBuf>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_url_pattern(
|
||||||
|
s: &str,
|
||||||
|
) -> std::result::Result<urlpattern::UrlPattern, urlpattern::quirks::Error> {
|
||||||
|
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(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<R: Runtime>(
|
||||||
|
app: &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 } => 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<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_allowed(&self, path_or_url: &str) -> crate::Result<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() {
|
||||||
|
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<bool> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -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 },
|
||||||
|
}
|
Loading…
Reference in new issue