feat: extend scopes with user selected paths, closes #3591 (#3595)

This commit is contained in:
Lucas Fernandes Nogueira 2022-03-03 15:41:58 -03:00 committed by GitHub
parent 64e0054299
commit b744cd2758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 149 additions and 36 deletions

View File

@ -0,0 +1,5 @@
---
"tauri": patch
---
Allow absolute paths on the filesystem APIs as long as it does not include parent directory components.

View File

@ -0,0 +1,5 @@
---
"tauri": patch
---
Extend the allowed patterns for the filesystem and asset protocol when the user selects a path (dialog open and save commands and file drop on the window).

View File

@ -3,9 +3,9 @@
// SPDX-License-Identifier: MIT
use super::{InvokeContext, InvokeResponse};
#[cfg(any(dialog_open, dialog_save))]
use crate::api::dialog::blocking::FileDialogBuilder;
use crate::Runtime;
#[cfg(any(dialog_open, dialog_save))]
use crate::{api::dialog::blocking::FileDialogBuilder, Manager, Scopes};
use serde::Deserialize;
use tauri_macros::{module_command_handler, CommandModule};
@ -36,6 +36,10 @@ pub struct OpenDialogOptions {
pub directory: bool,
/// The initial path of the dialog.
pub default_path: Option<PathBuf>,
/// If [`Self::directory`] is true, indicates that it will be read recursively later.
/// Defines whether subdirectories will be allowed on the scope or not.
#[serde(default)]
pub recursive: bool,
}
/// The options for the save dialog API.
@ -97,12 +101,28 @@ impl Cmd {
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
}
let scopes = context.window.state::<Scopes>();
let res = if options.directory {
dialog_builder.pick_folder().into()
let folder = dialog_builder.pick_folder();
if let Some(path) = &folder {
scopes.allow_directory(path, options.recursive);
}
folder.into()
} else if options.multiple {
dialog_builder.pick_files().into()
let files = dialog_builder.pick_files();
if let Some(files) = &files {
for file in files {
scopes.allow_file(file);
}
}
files.into()
} else {
dialog_builder.pick_file().into()
let file = dialog_builder.pick_file();
if let Some(file) = &file {
scopes.allow_file(file);
}
file.into()
};
Ok(res)
@ -127,7 +147,14 @@ impl Cmd {
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
}
Ok(dialog_builder.save_file())
let scopes = context.window.state::<Scopes>();
let path = dialog_builder.save_file();
if let Some(p) = &path {
scopes.allow_file(p);
}
Ok(path)
}
#[module_command_handler(dialog_message, "dialog > message")]
@ -198,6 +225,7 @@ mod tests {
directory: bool::arbitrary(g),
default_path: Option::arbitrary(g),
title: Option::arbitrary(g),
recursive: bool::arbitrary(g),
}
}
}

View File

@ -41,12 +41,7 @@ impl<'de> Deserialize<'de> for SafePathBuf {
D: Deserializer<'de>,
{
let path = std::path::PathBuf::deserialize(deserializer)?;
if path.components().any(|x| {
matches!(
x,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)
}) {
if path.components().any(|x| matches!(x, Component::ParentDir)) {
Err(DeError::custom("cannot traverse directory"))
} else {
Ok(SafePathBuf(path))

View File

@ -47,7 +47,7 @@ use crate::{
config::{AppUrl, Config, WindowUrl},
PackageInfo,
},
Context, Invoke, Pattern, StateManager, Window,
Context, Invoke, Manager, Pattern, Scopes, StateManager, Window,
};
#[cfg(any(target_os = "linux", target_os = "windows"))]
@ -828,7 +828,17 @@ impl<R: Runtime> WindowManager<R> {
let window = Window::new(manager.clone(), window, app_handle.clone());
let _ = match event {
FileDropEvent::Hovered(paths) => window.emit_and_trigger("tauri://file-drop-hover", paths),
FileDropEvent::Dropped(paths) => window.emit_and_trigger("tauri://file-drop", paths),
FileDropEvent::Dropped(paths) => {
let scopes = window.state::<Scopes>();
for path in &paths {
if path.is_file() {
scopes.allow_file(path);
} else {
scopes.allow_directory(path, false);
}
}
window.emit_and_trigger("tauri://file-drop", paths)
}
FileDropEvent::Cancelled => window.emit_and_trigger("tauri://file-drop-cancelled", ()),
_ => unimplemented!(),
};

View File

@ -5,6 +5,7 @@
use std::{
fmt,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use glob::Pattern;
@ -18,7 +19,7 @@ use crate::api::path::parse as parse_path;
/// Scope for filesystem access.
#[derive(Clone)]
pub struct Scope {
allow_patterns: Vec<Pattern>,
allow_patterns: Arc<Mutex<Vec<Pattern>>>,
}
impl fmt::Debug for Scope {
@ -28,6 +29,8 @@ impl fmt::Debug for Scope {
"allow_patterns",
&self
.allow_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
@ -36,6 +39,16 @@ impl fmt::Debug for Scope {
}
}
fn push_pattern<P: AsRef<Path>>(list: &mut Vec<Pattern>, pattern: P) {
let pattern: PathBuf = pattern.as_ref().components().collect();
list.push(Pattern::new(&pattern.to_string_lossy()).expect("invalid glob pattern"));
#[cfg(windows)]
{
list
.push(Pattern::new(&format!("\\\\?\\{}", pattern.display())).expect("invalid glob pattern"));
}
}
impl Scope {
/// Creates a new scope from a `FsAllowlistScope` configuration.
pub fn for_fs_api(
@ -47,17 +60,33 @@ impl Scope {
let mut allow_patterns = Vec::new();
for path in &scope.0 {
if let Ok(path) = parse_path(config, package_info, env, path) {
let path: PathBuf = path.components().collect();
allow_patterns.push(Pattern::new(&path.to_string_lossy()).expect("invalid glob pattern"));
#[cfg(windows)]
{
allow_patterns.push(
Pattern::new(&format!("\\\\?\\{}", path.display())).expect("invalid glob pattern"),
);
}
push_pattern(&mut allow_patterns, path);
}
}
Self { allow_patterns }
Self {
allow_patterns: Arc::new(Mutex::new(allow_patterns)),
}
}
/// Extend the allowed patterns with the given directory.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read
/// the directory and all of its files and subdirectories.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
let path = path.as_ref().to_path_buf();
let mut list = self.allow_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, &path);
// allow its files and subdirectories to be read
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
}
/// Extend the allowed patterns with the given file path.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
push_pattern(&mut self.allow_patterns.lock().unwrap(), path);
}
/// Determines if the given path is allowed on this scope.
@ -71,7 +100,12 @@ impl Scope {
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
let allowed = self.allow_patterns.iter().any(|p| p.matches_path(&path));
let allowed = self
.allow_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path(&path));
allowed
} else {
false

View File

@ -15,6 +15,7 @@ pub use shell::{
ScopeAllowedCommand as ShellScopeAllowedCommand, ScopeConfig as ShellScopeConfig,
ScopeError as ShellScopeError,
};
use std::path::Path;
pub(crate) struct Scopes {
pub fs: FsScope,
@ -25,3 +26,19 @@ pub(crate) struct Scopes {
#[cfg(shell_scope)]
pub shell: ShellScope,
}
impl Scopes {
#[allow(dead_code)]
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) {
self.fs.allow_directory(path, recursive);
#[cfg(protocol_asset)]
self.asset_protocol.allow_directory(path, recursive);
}
#[allow(dead_code)]
pub(crate) fn allow_file(&self, path: &Path) {
self.fs.allow_file(path);
#[cfg(protocol_asset)]
self.asset_protocol.allow_file(path);
}
}

View File

@ -53,6 +53,11 @@ interface OpenDialogOptions {
multiple?: boolean
/** Whether the dialog is a directory selection or not. */
directory?: boolean
/**
* If `directory` is true, indicates that it will be read recursively later.
* Defines whether subdirectories will be allowed on the scope or not.
*/
recursive?: boolean
}
/** Options for the save dialog. */
@ -70,7 +75,14 @@ interface SaveDialogOptions {
}
/**
* Open a file/directory selection dialog
* Open a file/directory selection dialog.
*
* The selected paths are added to the filesystem and asset protocol allowlist scopes.
* When security is more important than the easy of use of this API,
* prefer writing a dedicated command instead.
*
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
*
* @returns A promise resolving to the selected path(s)
*/
@ -93,6 +105,13 @@ async function open(
/**
* Open a file/directory save dialog.
*
* The selected path is added to the filesystem and asset protocol allowlist scopes.
* When security is more important than the easy of use of this API,
* prefer writing a dedicated command instead.
*
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
*
* @returns A promise resolving to the selected path.
*/
async function save(options: SaveDialogOptions = {}): Promise<string> {

View File

@ -879,12 +879,12 @@ class WindowManager extends WebviewWindowHandle {
type: 'setMinSize',
payload: size
? {
type: size.type,
data: {
width: size.width,
height: size.height
type: size.type,
data: {
width: size.width,
height: size.height
}
}
}
: null
}
}
@ -921,12 +921,12 @@ class WindowManager extends WebviewWindowHandle {
type: 'setMaxSize',
payload: size
? {
type: size.type,
data: {
width: size.width,
height: size.height
type: size.type,
data: {
width: size.width,
height: size.height
}
}
}
: null
}
}