diff --git a/.changes/shell-remove-open.md b/.changes/shell-remove-open.md new file mode 100644 index 000000000..863369ddc --- /dev/null +++ b/.changes/shell-remove-open.md @@ -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`), 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. diff --git a/Cargo.lock b/Cargo.lock index 6ce09a32b..67bfd8fc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8020,7 +8020,6 @@ version = "3.0.0-alpha.1" dependencies = [ "encoding_rs", "log", - "open", "os_pipe", "regex", "schemars 1.2.2", diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index 97adb1514..ff81cc970 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -48,7 +48,6 @@ } ] }, - "shell:default", "shell:allow-kill", "shell:allow-stdin-write", "process:allow-exit", diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index be4918de3..56dccd6bb 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -72,9 +72,6 @@ } } }, - "shell": { - "open": true - }, "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK", "dangerousInsecureTransportProtocol": true, diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml index f6f2b1bc5..82d03ab2e 100644 --- a/plugins/shell/Cargo.toml +++ b/plugins/shell/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tauri-plugin-shell" 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 } authors = { workspace = true } license = { workspace = true } @@ -21,8 +21,8 @@ exclude = [ windows = { level = "full", notes = "" } linux = { level = "full", notes = "" } macos = { level = "full", notes = "" } -android = { level = "partial", notes = "Only allows to open URLs via `open`" } -ios = { level = "partial", notes = "Only allows to open URLs via `open`" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } @@ -38,6 +38,5 @@ log = { workspace = true } thiserror = { workspace = true } shared_child = "1" regex = "1" -open = { version = "5", features = ["shellexecute-on-windows"] } encoding_rs = "0.8" os_pipe = "1" diff --git a/plugins/shell/README.md b/plugins/shell/README.md index 79cef0543..f67392ae7 100644 --- a/plugins/shell/README.md +++ b/plugins/shell/README.md @@ -1,14 +1,16 @@ ![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 | | -------- | --------- | | Linux | ✓ | | Windows | ✓ | | macOS | ✓ | -| Android | ✓ | -| iOS | ✓ | +| Android | x | +| iOS | x | ## Install diff --git a/plugins/shell/android/.gitignore b/plugins/shell/android/.gitignore deleted file mode 100644 index c0f21ec2f..000000000 --- a/plugins/shell/android/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build -/.tauri diff --git a/plugins/shell/android/build.gradle.kts b/plugins/shell/android/build.gradle.kts deleted file mode 100644 index 3f3ca0ea2..000000000 --- a/plugins/shell/android/build.gradle.kts +++ /dev/null @@ -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")) -} diff --git a/plugins/shell/android/consumer-rules.pro b/plugins/shell/android/consumer-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/plugins/shell/android/consumer-rules.pro +++ /dev/null @@ -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 \ No newline at end of file diff --git a/plugins/shell/android/settings.gradle b/plugins/shell/android/settings.gradle deleted file mode 100644 index 14a752e43..000000000 --- a/plugins/shell/android/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -include ':tauri-android' -project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/shell/android/src/main/AndroidManifest.xml b/plugins/shell/android/src/main/AndroidManifest.xml deleted file mode 100644 index 9a40236b9..000000000 --- a/plugins/shell/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/shell/android/src/main/java/ShellPlugin.kt b/plugins/shell/android/src/main/java/ShellPlugin.kt deleted file mode 100644 index 2268bc266..000000000 --- a/plugins/shell/android/src/main/java/ShellPlugin.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/plugins/shell/api-iife.js b/plugins/shell/api-iife.js index d14bde7c1..6904940f8 100644 --- a/plugins/shell/api-iife.js +++ b/plugins/shell/api-iife.js @@ -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__})} diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs index cb0404f07..e842e2a7f 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -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() { tauri_plugin::Builder::new(COMMANDS) @@ -172,21 +172,5 @@ fn main() { schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07()) .into_root_schema_for::(), ) - .android_path("android") - .ios_path("ios") .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}"); - } } diff --git a/plugins/shell/guest-js/index.ts b/plugins/shell/guest-js/index.ts index 081d54c13..bc29f170b 100644 --- a/plugins/shell/guest-js/index.ts +++ b/plugins/shell/guest-js/index.ts @@ -4,18 +4,14 @@ /** * 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 * * 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 * * The plugin permissions object has a `scope` field that defines an array of CLIs that can be used. @@ -572,40 +568,7 @@ type CommandEvent = | Event<'Terminated', TerminatedPayload> | Event<'Error', string> -/** - * 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 { - await invoke('plugin:shell|open', { - path, - with: openWith - }) -} - -export { Command, Child, EventEmitter, open } +export { Command, Child, EventEmitter } export type { IOPayload, CommandEvents, diff --git a/plugins/shell/ios/Package.resolved b/plugins/shell/ios/Package.resolved deleted file mode 100644 index 5f998e0e6..000000000 --- a/plugins/shell/ios/Package.resolved +++ /dev/null @@ -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 -} diff --git a/plugins/shell/ios/Package.swift b/plugins/shell/ios/Package.swift deleted file mode 100644 index c7b2a7aae..000000000 --- a/plugins/shell/ios/Package.swift +++ /dev/null @@ -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") - ] -) diff --git a/plugins/shell/ios/Sources/ShellPlugin.swift b/plugins/shell/ios/Sources/ShellPlugin.swift deleted file mode 100644 index 0fcb7dac1..000000000 --- a/plugins/shell/ios/Sources/ShellPlugin.swift +++ /dev/null @@ -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() -} diff --git a/plugins/shell/permissions/autogenerated/reference.md b/plugins/shell/permissions/autogenerated/reference.md index 87086a147..05ae23c3c 100644 --- a/plugins/shell/permissions/autogenerated/reference.md +++ b/plugins/shell/permissions/autogenerated/reference.md @@ -5,13 +5,8 @@ shell functionality is exposed by default. #### Granted Permissions -It allows to use the `open` functionality with a reasonable -scope pre-configured. It will allow opening `http(s)://`, -`tel:` and `mailto:` links. - -#### This default permission set includes the following: - -- `allow-open` +Nothing is granted by default. Spawning processes must be explicitly +allowed with a scope, see `allow-execute` and `allow-spawn`. ## Permission Table @@ -77,32 +72,6 @@ Denies the kill command without any pre-configured scope. -`shell:allow-open` - - - - -Enables the open command without any pre-configured scope. - - - - - - - -`shell:deny-open` - - - - -Denies the open command without any pre-configured scope. - - - - - - - `shell:allow-spawn` diff --git a/plugins/shell/permissions/default.toml b/plugins/shell/permissions/default.toml index dba2ea204..8cd8861d3 100644 --- a/plugins/shell/permissions/default.toml +++ b/plugins/shell/permissions/default.toml @@ -7,9 +7,8 @@ shell functionality is exposed by default. #### Granted Permissions -It allows to use the `open` functionality with a reasonable -scope pre-configured. It will allow opening `http(s)://`, -`tel:` and `mailto:` links. +Nothing is granted by default. Spawning processes must be explicitly +allowed with a scope, see `allow-execute` and `allow-spawn`. """ -permissions = ["allow-open"] +permissions = [] diff --git a/plugins/shell/src/commands.rs b/plugins/shell/src/commands.rs index 0facce719..db15ab56f 100644 --- a/plugins/shell/src/commands.rs +++ b/plugins/shell/src/commands.rs @@ -11,8 +11,6 @@ use tauri::{ Manager, Runtime, State, Window, }; -#[allow(deprecated)] -use crate::open::Program; use crate::{ process::{CommandEvent, TerminatedPayload}, scope::ExecuteArgs, @@ -307,14 +305,3 @@ pub fn kill( } Ok(()) } - -#[allow(deprecated)] -#[tauri::command] -pub async fn open( - _window: Window, - shell: State<'_, Shell>, - path: String, - with: Option, -) -> crate::Result<()> { - crate::open::open(Some(&shell.open_scope), path, with) -} diff --git a/plugins/shell/src/config.rs b/plugins/shell/src/config.rs deleted file mode 100644 index 1523c0805..000000000 --- a/plugins/shell/src/config.rs +++ /dev/null @@ -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), -} diff --git a/plugins/shell/src/error.rs b/plugins/shell/src/error.rs index 652421b8f..33fb135b2 100644 --- a/plugins/shell/src/error.rs +++ b/plugins/shell/src/error.rs @@ -8,15 +8,10 @@ use serde::{Serialize, Serializer}; #[derive(Debug, thiserror::Error)] pub enum Error { - #[cfg(mobile)] - #[error(transparent)] - PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), #[error(transparent)] Io(#[from] std::io::Error), #[error("current executable path has no parent")] CurrentExeHasNoParent, - #[error("unknown program {0}")] - UnknownProgramName(String), #[error(transparent)] Scope(#[from] crate::scope::Error), /// Sidecar not allowed by the configuration. diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 726406ae9..da8c25d4d 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 // 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( 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 regex::Regex; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Manager, RunEvent, Runtime, }; mod commands; -mod config; mod error; -#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] -#[allow(deprecated)] -pub mod open; pub mod process; mod scope; mod scope_entry; @@ -36,21 +33,11 @@ mod scope_entry; pub use error::Error; type Result = std::result::Result; -#[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>>; pub struct Shell { #[allow(dead_code)] app: AppHandle, - #[cfg(mobile)] - mobile_plugin_handle: PluginHandle, - open_scope: scope::OpenScope, children: ChildStore, } @@ -67,27 +54,6 @@ impl Shell { pub fn sidecar(&self, program: impl AsRef) -> Result { 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, with: Option) -> 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, _with: Option) -> Result<()> { - self.mobile_plugin_handle - .run_mobile_plugin("open", path.into()) - .map_err(Into::into) - } } pub trait ShellExt { @@ -100,32 +66,19 @@ impl> ShellExt for T { } } -pub fn init() -> TauriPlugin> { - Builder::>::new("shell") +pub fn init() -> TauriPlugin { + Builder::new("shell") .initialization_script(include_str!("init-iife.js").to_string()) .invoke_handler(tauri::generate_handler![ commands::execute, commands::spawn, commands::stdin_write, commands::kill, - commands::open ]) - .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)?; - + .setup(|app, _api| { app.manage(Shell { app: app.clone(), children: Default::default(), - open_scope: open_scope(&config.open), - - #[cfg(mobile)] - mobile_plugin_handle: handle, }); Ok(()) }) @@ -143,23 +96,3 @@ pub fn init() -> TauriPlugin> { }) .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(®ex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}")); - Some(validator) - } - }; - - scope::OpenScope { - open: shell_scope_open, - } -} diff --git a/plugins/shell/src/open.rs b/plugins/shell/src/open.rs deleted file mode 100644 index 6958a8324..000000000 --- a/plugins/shell/src/open.rs +++ /dev/null @@ -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 { - 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(deserializer: D) -> Result - 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>( - scope: Option<&OpenScope>, - path: P, - with: Option, -) -> 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) - } -} diff --git a/plugins/shell/src/scope.rs b/plugins/shell/src/scope.rs index 35fdeaffb..fce83cfeb 100644 --- a/plugins/shell/src/scope.rs +++ b/plugins/shell/src/scope.rs @@ -4,8 +4,6 @@ use std::sync::Arc; -#[allow(deprecated)] -use crate::open::Program; use crate::process::Command; 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, -} - /// Scope for shell process spawning. #[derive(Clone)] pub struct ShellScope<'a> { @@ -198,39 +189,6 @@ pub enum 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) -> 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<'_> { /// Validates argument inputs and creates a Tauri sidecar [`Command`]. pub fn prepare_sidecar(