refactor(shell)!: remove deprecated open API (#3604)

* refactor(shell)!: remove deprecated open API

`open` has been deprecated since 2.1.0 in favor of tauri-plugin-opener.
This removes:

- the `open` command and its `allow-open`/`deny-open` permissions
  (`shell:default` now grants nothing)
- the `plugins > shell > open` configuration; `init()` returns
  `TauriPlugin<R>` again
- `Shell::open`, the `tauri_plugin_shell::open` module and
  `Error::UnknownProgramName`
- the `open` JavaScript function
- the Android and iOS plugins, since `open` was their only command

* chore(examples): drop the now empty shell:default permission set

---------

Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
This commit is contained in:
Lucas Fernandes Nogueira
2026-09-22 06:14:49 -03:00
committed by GitHub
co-authored by Lucas Nogueira
parent 6a12f7c80a
commit 152ff5bb6d
26 changed files with 31 additions and 590 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"shell": major
"shell-js": major
---
**Breaking:** Removed the `open` API, which had been deprecated since v2.1.0 in favor of `tauri-plugin-opener`. This removes the `open` command and its `allow-open`/`deny-open` permissions, the `plugins > shell > open` configuration (`tauri_plugin_shell::init()` now returns `TauriPlugin<R>`), the `Shell::open` method and the `tauri_plugin_shell::open` module, the `Error::UnknownProgramName` variant, and the `open` JavaScript function. The `shell:default` permission set now grants nothing. Since `open` was the only mobile functionality, the Android and iOS plugins were removed as well.
Generated
-1
View File
@@ -8020,7 +8020,6 @@ version = "3.0.0-alpha.1"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
"open",
"os_pipe", "os_pipe",
"regex", "regex",
"schemars 1.2.2", "schemars 1.2.2",
@@ -48,7 +48,6 @@
} }
] ]
}, },
"shell:default",
"shell:allow-kill", "shell:allow-kill",
"shell:allow-stdin-write", "shell:allow-stdin-write",
"process:allow-exit", "process:allow-exit",
-3
View File
@@ -72,9 +72,6 @@
} }
} }
}, },
"shell": {
"open": true
},
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"dangerousInsecureTransportProtocol": true, "dangerousInsecureTransportProtocol": true,
+3 -4
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "3.0.0-alpha.1" version = "3.0.0-alpha.1"
description = "Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application." description = "Access the system shell. Allows you to spawn child processes."
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
@@ -21,8 +21,8 @@ exclude = [
windows = { level = "full", notes = "" } windows = { level = "full", notes = "" }
linux = { level = "full", notes = "" } linux = { level = "full", notes = "" }
macos = { level = "full", notes = "" } macos = { level = "full", notes = "" }
android = { level = "partial", notes = "Only allows to open URLs via `open`" } android = { level = "none", notes = "" }
ios = { level = "partial", notes = "Only allows to open URLs via `open`" } ios = { level = "none", notes = "" }
[build-dependencies] [build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] } tauri-plugin = { workspace = true, features = ["build"] }
@@ -38,6 +38,5 @@ log = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
shared_child = "1" shared_child = "1"
regex = "1" regex = "1"
open = { version = "5", features = ["shellexecute-on-windows"] }
encoding_rs = "0.8" encoding_rs = "0.8"
os_pipe = "1" os_pipe = "1"
+5 -3
View File
@@ -1,14 +1,16 @@
![plugin-shell](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/shell/banner.png) ![plugin-shell](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/shell/banner.png)
Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. Access the system shell. Allows you to spawn child processes.
To open files and URLs with their default application, use [tauri-plugin-opener](../opener).
| Platform | Supported | | Platform | Supported |
| -------- | --------- | | -------- | --------- |
| Linux | ✓ | | Linux | ✓ |
| Windows | ✓ | | Windows | ✓ |
| macOS | ✓ | | macOS | ✓ |
| Android | | | Android | x |
| iOS | | | iOS | x |
## Install ## Install
-2
View File
@@ -1,2 +0,0 @@
/build
/.tauri
-30
View File
@@ -1,30 +0,0 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "app.tauri.shell"
compileSdk = 36
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
implementation(project(":tauri-android"))
}
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-2
View File
@@ -1,2 +0,0 @@
include ':tauri-android'
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -1,30 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.shell
import android.app.Activity
import android.content.Intent
import android.net.Uri
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.Plugin
import java.io.File
@TauriPlugin
class ShellPlugin(private val activity: Activity) : Plugin(activity) {
@Command
fun open(invoke: Invoke) {
try {
val url = invoke.parseArgs(String::class.java)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.applicationContext?.startActivity(intent)
invoke.resolve()
} catch (ex: Exception) {
invoke.reject(ex.message)
}
}
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_SHELL__=function(e){"use strict";function t(e,t,s,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?i:"a"===s?i.call(e):i?i.value:t.get(e)}function s(e,t,s,i,n){if("function"==typeof t||!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var i,n,r,o;"function"==typeof SuppressedError&&SuppressedError;const a="__TAURI_TO_IPC_KEY__";class h{constructor(e){i.set(this,void 0),n.set(this,0),r.set(this,[]),o.set(this,void 0),s(this,i,e||(()=>{})),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}(e=>{const a=e.index;if("end"in e)return void(a==t(this,n,"f")?this.cleanupCallback():s(this,o,a));const h=e.message;if(a==t(this,n,"f")){for(t(this,i,"f").call(this,h),s(this,n,t(this,n,"f")+1);t(this,n,"f")in t(this,r,"f");){const e=t(this,r,"f")[t(this,n,"f")];t(this,i,"f").call(this,e),delete t(this,r,"f")[t(this,n,"f")],s(this,n,t(this,n,"f")+1)}t(this,n,"f")===t(this,o,"f")&&this.cleanupCallback()}else t(this,r,"f")[a]=h})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){s(this,i,e)}get onmessage(){return t(this,i,"f")}[(i=new WeakMap,n=new WeakMap,r=new WeakMap,o=new WeakMap,a)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[a]()}}async function c(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}class l{constructor(){this.eventListeners=Object.create(null)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}on(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}once(e,t){const s=i=>{this.removeListener(e,s),t(i)};return this.addListener(e,s)}off(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter(e=>e!==t)),this}removeAllListeners(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}emit(e,t){if(e in this.eventListeners){const s=this.eventListeners[e];for(const e of s)e(t);return!0}return!1}listenerCount(e){return e in this.eventListeners?this.eventListeners[e].length:0}prependListener(e,t){return e in this.eventListeners?this.eventListeners[e].unshift(t):this.eventListeners[e]=[t],this}prependOnceListener(e,t){const s=i=>{this.removeListener(e,s),t(i)};return this.prependListener(e,s)}}class u{constructor(e){this.pid=e}async write(e){await c("plugin:shell|stdin_write",{pid:this.pid,buffer:e})}async kill(){await c("plugin:shell|kill",{cmd:"killChild",pid:this.pid})}}class p extends l{constructor(e,t=[],s){super(),this.stdout=new l,this.stderr=new l,this.program=e,this.args="string"==typeof t?[t]:t,this.options=s??{}}static create(e,t=[],s){return new p(e,t,s)}static sidecar(e,t=[],s){const i=new p(e,t,s);return i.options.sidecar=!0,i}async spawn(){const e=this.program,t=this.args,s=this.options;"object"==typeof t&&Object.freeze(t);const i=new h;return i.onmessage=e=>{switch(e.event){case"Error":this.emit("error",e.payload);break;case"Terminated":this.emit("close",e.payload);break;case"Stdout":this.stdout.emit("data",e.payload);break;case"Stderr":this.stderr.emit("data",e.payload)}},await c("plugin:shell|spawn",{program:e,args:t,options:s,onEvent:i}).then(e=>new u(e))}async execute(){const e=this.program,t=this.args,s=this.options;return"object"==typeof t&&Object.freeze(t),await c("plugin:shell|execute",{program:e,args:t,options:s})}}return e.Child=u,e.Command=p,e.EventEmitter=l,e.open=async function(e,t){await c("plugin:shell|open",{path:e,with:t})},e}({});Object.defineProperty(window.__TAURI__,"shell",{value:__TAURI_PLUGIN_SHELL__})} if("__TAURI__"in window){var __TAURI_PLUGIN_SHELL__=function(e){"use strict";function t(e,t,s,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?i:"a"===s?i.call(e):i?i.value:t.get(e)}function s(e,t,s,i,n){if("function"==typeof t||!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var i,n,r,o;"function"==typeof SuppressedError&&SuppressedError;const a="__TAURI_TO_IPC_KEY__";class h{constructor(e){i.set(this,void 0),n.set(this,0),r.set(this,[]),o.set(this,void 0),s(this,i,e||(()=>{})),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}(e=>{const a=e.index;if("end"in e)return void(a==t(this,n,"f")?this.cleanupCallback():s(this,o,a));const h=e.message;if(a==t(this,n,"f")){for(t(this,i,"f").call(this,h),s(this,n,t(this,n,"f")+1);t(this,n,"f")in t(this,r,"f");){const e=t(this,r,"f")[t(this,n,"f")];t(this,i,"f").call(this,e),delete t(this,r,"f")[t(this,n,"f")],s(this,n,t(this,n,"f")+1)}t(this,n,"f")===t(this,o,"f")&&this.cleanupCallback()}else t(this,r,"f")[a]=h})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){s(this,i,e)}get onmessage(){return t(this,i,"f")}[(i=new WeakMap,n=new WeakMap,r=new WeakMap,o=new WeakMap,a)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[a]()}}async function c(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}class l{constructor(){this.eventListeners=Object.create(null)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}on(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}once(e,t){const s=i=>{this.removeListener(e,s),t(i)};return this.addListener(e,s)}off(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter(e=>e!==t)),this}removeAllListeners(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}emit(e,t){if(e in this.eventListeners){const s=this.eventListeners[e];for(const e of s)e(t);return!0}return!1}listenerCount(e){return e in this.eventListeners?this.eventListeners[e].length:0}prependListener(e,t){return e in this.eventListeners?this.eventListeners[e].unshift(t):this.eventListeners[e]=[t],this}prependOnceListener(e,t){const s=i=>{this.removeListener(e,s),t(i)};return this.prependListener(e,s)}}class u{constructor(e){this.pid=e}async write(e){await c("plugin:shell|stdin_write",{pid:this.pid,buffer:e})}async kill(){await c("plugin:shell|kill",{cmd:"killChild",pid:this.pid})}}class d extends l{constructor(e,t=[],s){super(),this.stdout=new l,this.stderr=new l,this.program=e,this.args="string"==typeof t?[t]:t,this.options=s??{}}static create(e,t=[],s){return new d(e,t,s)}static sidecar(e,t=[],s){const i=new d(e,t,s);return i.options.sidecar=!0,i}async spawn(){const e=this.program,t=this.args,s=this.options;"object"==typeof t&&Object.freeze(t);const i=new h;return i.onmessage=e=>{switch(e.event){case"Error":this.emit("error",e.payload);break;case"Terminated":this.emit("close",e.payload);break;case"Stdout":this.stdout.emit("data",e.payload);break;case"Stderr":this.stderr.emit("data",e.payload)}},await c("plugin:shell|spawn",{program:e,args:t,options:s,onEvent:i}).then(e=>new u(e))}async execute(){const e=this.program,t=this.args,s=this.options;return"object"==typeof t&&Object.freeze(t),await c("plugin:shell|execute",{program:e,args:t,options:s})}}return e.Child=u,e.Command=d,e.EventEmitter=l,e}({});Object.defineProperty(window.__TAURI__,"shell",{value:__TAURI_PLUGIN_SHELL__})}
+1 -17
View File
@@ -163,7 +163,7 @@ fn _f() {
}; };
} }
const COMMANDS: &[&str] = &["execute", "spawn", "stdin_write", "kill", "open"]; const COMMANDS: &[&str] = &["execute", "spawn", "stdin_write", "kill"];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)
@@ -172,21 +172,5 @@ fn main() {
schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07()) schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
.into_root_schema_for::<ShellScopeEntry>(), .into_root_schema_for::<ShellScopeEntry>(),
) )
.android_path("android")
.ios_path("ios")
.build(); .build();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let mobile = target_os == "ios" || target_os == "android";
alias("desktop", !mobile);
alias("mobile", mobile);
}
// creates a cfg alias if `has_feature` is true.
// `alias` must be a snake case string.
fn alias(alias: &str, has_feature: bool) {
println!("cargo:rustc-check-cfg=cfg({alias})");
if has_feature {
println!("cargo:rustc-cfg={alias}");
}
} }
+4 -41
View File
@@ -4,18 +4,14 @@
/** /**
* Access the system shell. * Access the system shell.
* Allows you to spawn child processes and manage files and URLs using their default application. * Allows you to spawn child processes.
*
* To open files and URLs with their default application, use `@tauri-apps/plugin-opener`.
* *
* ## Security * ## Security
* *
* This API has a scope configuration that forces you to restrict the programs and arguments that can be used. * This API has a scope configuration that forces you to restrict the programs and arguments that can be used.
* *
* ### Restricting access to the {@link open | `open`} API
*
* On the configuration object, `open: true` means that the {@link open} API can be used with any URL,
* as the argument is validated with the `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+` regex.
* You can change that regex by changing the boolean value to a string, e.g. `open: ^https://github.com/`.
*
* ### Restricting access to the {@link Command | `Command`} APIs * ### Restricting access to the {@link Command | `Command`} APIs
* *
* The plugin permissions object has a `scope` field that defines an array of CLIs that can be used. * The plugin permissions object has a `scope` field that defines an array of CLIs that can be used.
@@ -572,40 +568,7 @@ type CommandEvent<O extends IOPayload> =
| Event<'Terminated', TerminatedPayload> | Event<'Terminated', TerminatedPayload>
| Event<'Error', string> | Event<'Error', string>
/** export { Command, Child, EventEmitter }
* Opens a path or URL with the system's default app,
* 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
* ```typescript
* import { open } from '@tauri-apps/plugin-shell';
* // opens the given URL on the default browser:
* await open('https://github.com/tauri-apps/tauri');
* // opens the given URL using `firefox`:
* await open('https://github.com/tauri-apps/tauri', 'firefox');
* // opens a file using the default program:
* await open('/path/to/file');
* ```
*
* @param path The path or URL to open.
* This value is matched against the string regex defined on `tauri.conf.json > plugins > shell > open`,
* which defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
* @param openWith The app to open the file or URL with.
* Defaults to the system default application for the specified path type.
*
* @since 2.0.0
*/
async function open(path: string, openWith?: string): Promise<void> {
await invoke('plugin:shell|open', {
path,
with: openWith
})
}
export { Command, Child, EventEmitter, open }
export type { export type {
IOPayload, IOPayload,
CommandEvents, CommandEvents,
-16
View File
@@ -1,16 +0,0 @@
{
"object": {
"pins": [
{
"package": "SwiftRs",
"repositoryURL": "https://github.com/Brendonovich/swift-rs",
"state": {
"branch": null,
"revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e",
"version": "1.0.6"
}
}
]
},
"version": 1
}
-34
View File
@@ -1,34 +0,0 @@
// swift-tools-version:5.3
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import PackageDescription
let package = Package(
name: "tauri-plugin-shell",
platforms: [
.macOS(.v10_13),
.iOS(.v13),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "tauri-plugin-shell",
type: .static,
targets: ["tauri-plugin-shell"])
],
dependencies: [
.package(name: "Tauri", path: "../.tauri/tauri-api")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "tauri-plugin-shell",
dependencies: [
.byName(name: "Tauri")
],
path: "Sources")
]
)
@@ -1,34 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
import SwiftRs
import Tauri
import UIKit
import WebKit
class ShellPlugin: Plugin {
@objc public func open(_ invoke: Invoke) throws {
do {
let urlString = try invoke.parseArgs(String.self)
if let url = URL(string: urlString) {
if #available(iOS 10, *) {
UIApplication.shared.open(url, options: [:])
} else {
UIApplication.shared.openURL(url)
}
}
invoke.resolve()
} catch {
invoke.reject(error.localizedDescription)
}
}
}
@_cdecl("init_plugin_shell")
func initPlugin() -> Plugin {
return ShellPlugin()
}
@@ -5,13 +5,8 @@ shell functionality is exposed by default.
#### Granted Permissions #### Granted Permissions
It allows to use the `open` functionality with a reasonable Nothing is granted by default. Spawning processes must be explicitly
scope pre-configured. It will allow opening `http(s)://`, allowed with a scope, see `allow-execute` and `allow-spawn`.
`tel:` and `mailto:` links.
#### This default permission set includes the following:
- `allow-open`
## Permission Table ## Permission Table
@@ -77,32 +72,6 @@ Denies the kill command without any pre-configured scope.
<tr> <tr>
<td> <td>
`shell:allow-open`
</td>
<td>
Enables the open command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`shell:deny-open`
</td>
<td>
Denies the open command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`shell:allow-spawn` `shell:allow-spawn`
</td> </td>
+3 -4
View File
@@ -7,9 +7,8 @@ shell functionality is exposed by default.
#### Granted Permissions #### Granted Permissions
It allows to use the `open` functionality with a reasonable Nothing is granted by default. Spawning processes must be explicitly
scope pre-configured. It will allow opening `http(s)://`, allowed with a scope, see `allow-execute` and `allow-spawn`.
`tel:` and `mailto:` links.
""" """
permissions = ["allow-open"] permissions = []
-13
View File
@@ -11,8 +11,6 @@ use tauri::{
Manager, Runtime, State, Window, Manager, Runtime, State, Window,
}; };
#[allow(deprecated)]
use crate::open::Program;
use crate::{ use crate::{
process::{CommandEvent, TerminatedPayload}, process::{CommandEvent, TerminatedPayload},
scope::ExecuteArgs, scope::ExecuteArgs,
@@ -307,14 +305,3 @@ pub fn kill<R: Runtime>(
} }
Ok(()) Ok(())
} }
#[allow(deprecated)]
#[tauri::command]
pub async fn open<R: Runtime>(
_window: Window<R>,
shell: State<'_, Shell<R>>,
path: String,
with: Option<Program>,
) -> crate::Result<()> {
crate::open::open(Some(&shell.open_scope), path, with)
}
-39
View File
@@ -1,39 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
/// 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: ShellAllowlistOpen,
}
/// Defines the `shell > open` api scope.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
#[derive(Default)]
pub enum ShellAllowlistOpen {
/// Shell open API allowlist is not defined by the user.
/// In this case we add the default validation regex (same as [`Self::Flag(true)`]).
#[default]
Unset,
/// If the shell open API should be enabled.
///
/// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used.
Flag(bool),
/// Enable the shell 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),
}
-5
View File
@@ -8,15 +8,10 @@ use serde::{Serialize, Serializer};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("current executable path has no parent")] #[error("current executable path has no parent")]
CurrentExeHasNoParent, CurrentExeHasNoParent,
#[error("unknown program {0}")]
UnknownProgramName(String),
#[error(transparent)] #[error(transparent)]
Scope(#[from] crate::scope::Error), Scope(#[from] crate::scope::Error),
/// Sidecar not allowed by the configuration. /// Sidecar not allowed by the configuration.
+6 -73
View File
@@ -2,7 +2,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. //! Access the system shell. Allows you to spawn child processes.
//!
//! To open files and URLs with their default application, use `tauri-plugin-opener`.
#![doc( #![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
@@ -17,18 +19,13 @@ use std::{
}; };
use process::{Command, CommandChild}; use process::{Command, CommandChild};
use regex::Regex;
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
AppHandle, Manager, RunEvent, Runtime, AppHandle, Manager, RunEvent, Runtime,
}; };
mod commands; mod commands;
mod config;
mod error; mod error;
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
#[allow(deprecated)]
pub mod open;
pub mod process; pub mod process;
mod scope; mod scope;
mod scope_entry; mod scope_entry;
@@ -36,21 +33,11 @@ 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>;
#[cfg(mobile)]
use tauri::plugin::PluginHandle;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.shell";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_shell);
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>; type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
pub struct Shell<R: Runtime> { pub struct Shell<R: Runtime> {
#[allow(dead_code)] #[allow(dead_code)]
app: AppHandle<R>, app: AppHandle<R>,
#[cfg(mobile)]
mobile_plugin_handle: PluginHandle<R>,
open_scope: scope::OpenScope,
children: ChildStore, children: ChildStore,
} }
@@ -67,27 +54,6 @@ impl<R: Runtime> Shell<R> {
pub fn sidecar(&self, program: impl AsRef<Path>) -> Result<Command> { pub fn sidecar(&self, program: impl AsRef<Path>) -> Result<Command> {
Command::new_sidecar(program) Command::new_sidecar(program)
} }
/// Open a (url) path with a default or specific browser opening program.
///
/// See [`crate::open::open`] for how it handles security-related measures.
#[cfg(desktop)]
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
#[allow(deprecated)]
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
open::open(None, path.into(), with)
}
/// Open a (url) path with a default or specific browser opening program.
///
/// See [`crate::open::open`] for how it handles security-related measures.
#[cfg(mobile)]
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
self.mobile_plugin_handle
.run_mobile_plugin("open", path.into())
.map_err(Into::into)
}
} }
pub trait ShellExt<R: Runtime> { pub trait ShellExt<R: Runtime> {
@@ -100,32 +66,19 @@ impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
} }
} }
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::<R, Option<config::Config>>::new("shell") Builder::new("shell")
.initialization_script(include_str!("init-iife.js").to_string()) .initialization_script(include_str!("init-iife.js").to_string())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::execute, commands::execute,
commands::spawn, commands::spawn,
commands::stdin_write, commands::stdin_write,
commands::kill, commands::kill,
commands::open
]) ])
.setup(|app, api| { .setup(|app, _api| {
let default_config = config::Config::default();
let config = api.config().as_ref().unwrap_or(&default_config);
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_shell)?;
app.manage(Shell { app.manage(Shell {
app: app.clone(), app: app.clone(),
children: Default::default(), children: Default::default(),
open_scope: open_scope(&config.open),
#[cfg(mobile)]
mobile_plugin_handle: handle,
}); });
Ok(()) Ok(())
}) })
@@ -143,23 +96,3 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
}) })
.build() .build()
} }
fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
let shell_scope_open = match open {
config::ShellAllowlistOpen::Flag(false) => None,
// we want to add a basic regex validation even if the config is not set
config::ShellAllowlistOpen::Unset | config::ShellAllowlistOpen::Flag(true) => {
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
}
config::ShellAllowlistOpen::Validate(validator) => {
let regex = format!("^{validator}$");
let validator =
Regex::new(&regex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
Some(validator)
}
};
scope::OpenScope {
open: shell_scope_open,
}
}
-138
View File
@@ -1,138 +0,0 @@
// 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 serde::{Deserialize, Deserializer};
use crate::scope::OpenScope;
use std::str::FromStr;
/// Program to use on the [`open()`] call.
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
pub enum Program {
/// Use the `open` program.
Open,
/// Use the `start` program.
Start,
/// Use the `xdg-open` program.
XdgOpen,
/// Use the `gio` program.
Gio,
/// Use the `gnome-open` program.
GnomeOpen,
/// Use the `kde-open` program.
KdeOpen,
/// Use the `wslview` program.
WslView,
/// Use the `Firefox` program.
Firefox,
/// Use the `Google Chrome` program.
Chrome,
/// Use the `Chromium` program.
Chromium,
/// Use the `Safari` program.
Safari,
}
impl FromStr for Program {
type Err = super::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let p = match s.to_lowercase().as_str() {
"open" => Self::Open,
"start" => Self::Start,
"xdg-open" => Self::XdgOpen,
"gio" => Self::Gio,
"gnome-open" => Self::GnomeOpen,
"kde-open" => Self::KdeOpen,
"wslview" => Self::WslView,
"firefox" => Self::Firefox,
"chrome" | "google chrome" => Self::Chrome,
"chromium" => Self::Chromium,
"safari" => Self::Safari,
_ => return Err(crate::Error::UnknownProgramName(s.to_string())),
};
Ok(p)
}
}
impl<'de> Deserialize<'de> for Program {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Program::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string()))
}
}
impl Program {
pub(crate) fn name(self) -> &'static str {
match self {
Self::Open => "open",
Self::Start => "start",
Self::XdgOpen => "xdg-open",
Self::Gio => "gio",
Self::GnomeOpen => "gnome-open",
Self::KdeOpen => "kde-open",
Self::WslView => "wslview",
#[cfg(target_os = "macos")]
Self::Firefox => "Firefox",
#[cfg(not(target_os = "macos"))]
Self::Firefox => "firefox",
#[cfg(target_os = "macos")]
Self::Chrome => "Google Chrome",
#[cfg(not(target_os = "macos"))]
Self::Chrome => "google-chrome",
#[cfg(target_os = "macos")]
Self::Chromium => "Chromium",
#[cfg(not(target_os = "macos"))]
Self::Chromium => "chromium",
#[cfg(target_os = "macos")]
Self::Safari => "Safari",
#[cfg(not(target_os = "macos"))]
Self::Safari => "safari",
}
}
}
/// Opens path or URL with the program specified in `with`, or system default if `None`.
///
/// The path will be matched against the shell open validation regex, defaulting to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
/// A custom validation regex may be supplied in the config in `plugins > shell > scope > open`.
///
/// # Examples
///
/// ```rust,no_run
/// use tauri_plugin_shell::ShellExt;
/// tauri::Builder::default()
/// .setup(|app| {
/// // open the given URL on the system default browser
/// app.shell().open("https://github.com/tauri-apps/tauri", None)?;
/// Ok(())
/// });
/// ```
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
pub fn open<P: AsRef<str>>(
scope: Option<&OpenScope>,
path: P,
with: Option<Program>,
) -> crate::Result<()> {
// validate scope if we have any (JS calls)
if let Some(scope) = scope {
scope.open(path.as_ref(), with).map_err(Into::into)
} else {
// when running directly from Rust code we don't need to validate the path
match with.map(Program::name) {
Some(program) => ::open::with_detached(path.as_ref(), program),
None => ::open::that_detached(path.as_ref()),
}
.map_err(Into::into)
}
}
-42
View File
@@ -4,8 +4,6 @@
use std::sync::Arc; use std::sync::Arc;
#[allow(deprecated)]
use crate::open::Program;
use crate::process::Command; use crate::process::Command;
use regex::Regex; use regex::Regex;
@@ -139,13 +137,6 @@ impl ScopeAllowedArg {
} }
} }
/// Scope for the open command
pub struct OpenScope {
/// The validation regex that `shell > open` paths must match against.
/// When set to `None`, no values are accepted.
pub open: Option<Regex>,
}
/// Scope for shell process spawning. /// Scope for shell process spawning.
#[derive(Clone)] #[derive(Clone)]
pub struct ShellScope<'a> { pub struct ShellScope<'a> {
@@ -198,39 +189,6 @@ pub enum Error {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
impl OpenScope {
/// Open a path in the default (or specified) browser.
///
/// The path is validated against the `plugins > shell > open` validation regex, which
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
#[allow(deprecated)]
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
// ensure we pass validation if the configuration has one
if let Some(regex) = &self.open {
if !regex.is_match(path) {
return Err(Error::Validation {
index: 0,
validation: regex.as_str().into(),
});
}
} else {
log::warn!("open() command called but the plugin configuration denies calls from JavaScript; set `tauri.conf.json > plugins > shell > open` to true or a validation regex string");
return Err(Error::Validation {
index: 0,
validation: "tauri^".to_string(), // purposefully impossible regex
});
}
// 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)
}
}
impl ShellScope<'_> { impl ShellScope<'_> {
/// Validates argument inputs and creates a Tauri sidecar [`Command`]. /// Validates argument inputs and creates a Tauri sidecar [`Command`].
pub fn prepare_sidecar( pub fn prepare_sidecar(