Skip to content

Commit

Permalink
feat: extend scopes with user selected paths, closes #3591 (#3595)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Mar 3, 2022
1 parent 64e0054 commit b744cd2
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changes/fs-absolute-paths.md
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Allow absolute paths on the filesystem APIs as long as it does not include parent directory components.
5 changes: 5 additions & 0 deletions .changes/fs-scope-runtime.md
@@ -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).
40 changes: 34 additions & 6 deletions core/tauri/src/endpoints/dialog.rs
Expand Up @@ -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};

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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")]
Expand Down Expand Up @@ -198,6 +225,7 @@ mod tests {
directory: bool::arbitrary(g),
default_path: Option::arbitrary(g),
title: Option::arbitrary(g),
recursive: bool::arbitrary(g),
}
}
}
Expand Down
7 changes: 1 addition & 6 deletions core/tauri/src/endpoints/file_system.rs
Expand Up @@ -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))
Expand Down
14 changes: 12 additions & 2 deletions core/tauri/src/manager.rs
Expand Up @@ -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"))]
Expand Down Expand Up @@ -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!(),
};
Expand Down
56 changes: 45 additions & 11 deletions core/tauri/src/scope/fs.rs
Expand Up @@ -5,6 +5,7 @@
use std::{
fmt,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};

use glob::Pattern;
Expand All @@ -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 {
Expand All @@ -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>>(),
Expand All @@ -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(
Expand All @@ -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.
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions core/tauri/src/scope/mod.rs
Expand Up @@ -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,
Expand All @@ -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);
}
}
21 changes: 20 additions & 1 deletion tooling/api/src/dialog.ts
Expand Up @@ -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. */
Expand All @@ -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)
*/
Expand All @@ -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> {
Expand Down
20 changes: 10 additions & 10 deletions tooling/api/src/window.ts
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down

0 comments on commit b744cd2

Please sign in to comment.