From 38369c832d9f52e7c712ec61dbf6823475f1c430 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 05:11:38 +0200 Subject: [PATCH 01/31] [WIP] create tauri-plugin-opener --- .changes/config.json | 10 + .changes/opener-initial.md | 6 + .github/workflows/check-generated-files.yml | 4 + .github/workflows/lint-rust.yml | 3 + .github/workflows/test-rust.yml | 4 + Cargo.lock | 14 + README.md | 1 + examples/api/package.json | 1 + examples/api/src-tauri/Cargo.toml | 1 + examples/api/src-tauri/capabilities/base.json | 1 + examples/api/src-tauri/src/lib.rs | 1 + examples/api/src/App.svelte | 7 +- examples/api/src/views/Opener.svelte | 47 +++ package.json | 3 +- plugins/opener/Cargo.toml | 36 ++ plugins/opener/LICENSE.spdx | 20 ++ plugins/opener/LICENSE_APACHE-2.0 | 177 ++++++++++ plugins/opener/LICENSE_MIT | 21 ++ plugins/opener/README.md | 100 ++++++ plugins/opener/SECURITY.md | 23 ++ plugins/opener/android/.gitignore | 2 + plugins/opener/android/build.gradle.kts | 39 +++ plugins/opener/android/proguard-rules.pro | 21 ++ plugins/opener/android/settings.gradle | 2 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/OpenerPlugin.kt | 30 ++ plugins/opener/api-iife.js | 1 + plugins/opener/build.rs | 27 ++ plugins/opener/guest-js/index.ts | 57 +++ plugins/opener/guest-js/init.ts | 40 +++ plugins/opener/ios/Package.resolved | 16 + plugins/opener/ios/Package.swift | 34 ++ plugins/opener/ios/Sources/OpenerPlugin.swift | 34 ++ plugins/opener/package.json | 29 ++ .../autogenerated/commands/open.toml | 13 + .../autogenerated/commands/reveal_in_dir.toml | 13 + .../permissions/autogenerated/reference.md | 68 ++++ plugins/opener/permissions/default.toml | 4 + .../opener/permissions/schemas/schema.json | 324 ++++++++++++++++++ plugins/opener/rollup.config.js | 22 ++ plugins/opener/src/commands.rs | 16 + plugins/opener/src/config.rs | 62 ++++ plugins/opener/src/error.rs | 34 ++ plugins/opener/src/init-iife.js | 1 + plugins/opener/src/lib.rs | 91 +++++ plugins/opener/src/open.rs | 141 ++++++++ plugins/opener/tsconfig.json | 4 + plugins/shell/build.rs | 1 + plugins/shell/src/lib.rs | 3 + plugins/shell/src/open.rs | 2 + plugins/updater/build.rs | 1 + pnpm-lock.yaml | 26 +- 52 files changed, 1630 insertions(+), 11 deletions(-) create mode 100644 .changes/opener-initial.md create mode 100644 examples/api/src/views/Opener.svelte create mode 100644 plugins/opener/Cargo.toml create mode 100644 plugins/opener/LICENSE.spdx create mode 100644 plugins/opener/LICENSE_APACHE-2.0 create mode 100644 plugins/opener/LICENSE_MIT create mode 100644 plugins/opener/README.md create mode 100644 plugins/opener/SECURITY.md create mode 100644 plugins/opener/android/.gitignore create mode 100644 plugins/opener/android/build.gradle.kts create mode 100644 plugins/opener/android/proguard-rules.pro create mode 100644 plugins/opener/android/settings.gradle create mode 100644 plugins/opener/android/src/main/AndroidManifest.xml create mode 100644 plugins/opener/android/src/main/java/OpenerPlugin.kt create mode 100644 plugins/opener/api-iife.js create mode 100644 plugins/opener/build.rs create mode 100644 plugins/opener/guest-js/index.ts create mode 100644 plugins/opener/guest-js/init.ts create mode 100644 plugins/opener/ios/Package.resolved create mode 100644 plugins/opener/ios/Package.swift create mode 100644 plugins/opener/ios/Sources/OpenerPlugin.swift create mode 100644 plugins/opener/package.json create mode 100644 plugins/opener/permissions/autogenerated/commands/open.toml create mode 100644 plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml create mode 100644 plugins/opener/permissions/autogenerated/reference.md create mode 100644 plugins/opener/permissions/default.toml create mode 100644 plugins/opener/permissions/schemas/schema.json create mode 100644 plugins/opener/rollup.config.js create mode 100644 plugins/opener/src/commands.rs create mode 100644 plugins/opener/src/config.rs create mode 100644 plugins/opener/src/error.rs create mode 100644 plugins/opener/src/init-iife.js create mode 100644 plugins/opener/src/lib.rs create mode 100644 plugins/opener/src/open.rs create mode 100644 plugins/opener/tsconfig.json diff --git a/.changes/config.json b/.changes/config.json index 5f5fe841d..ec0fd7486 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -62,6 +62,7 @@ "dialog", "fs", "global-shortcut", + "opener", "http", "nfc", "notification", @@ -87,6 +88,7 @@ "dialog-js", "fs-js", "global-shortcut-js", + "opener-js", "http-js", "nfc-js", "notification-js", @@ -186,6 +188,14 @@ "path": "./plugins/global-shortcut", "manager": "javascript" }, + "opener": { + "path": "./plugins/opener", + "manager": "rust" + }, + "opener-js": { + "path": "./plugins/opener", + "manager": "javascript" + }, "haptics": { "path": "./plugins/haptics", "manager": "rust" diff --git a/.changes/opener-initial.md b/.changes/opener-initial.md new file mode 100644 index 000000000..29141fe52 --- /dev/null +++ b/.changes/opener-initial.md @@ -0,0 +1,6 @@ +--- +"opener": "patch" +"opener-js": "patch" +--- + +Initial Release diff --git a/.github/workflows/check-generated-files.yml b/.github/workflows/check-generated-files.yml index 96bbdd05a..d2c2c8745 100644 --- a/.github/workflows/check-generated-files.yml +++ b/.github/workflows/check-generated-files.yml @@ -53,6 +53,10 @@ jobs: - .github/workflows/check-generated-files.yml - plugins/global-shortcut/guest-js/** - plugins/global-shortcut/src/api-iife.js + opener: + - .github/workflows/check-generated-files.yml + - plugins/opener/guest-js/** + - plugins/opener/src/api-iife.js haptics: - .github/workflows/check-generated-files.yml - plugins/haptics/guest-js/** diff --git a/.github/workflows/lint-rust.yml b/.github/workflows/lint-rust.yml index d9e0c50cb..db922ef1a 100644 --- a/.github/workflows/lint-rust.yml +++ b/.github/workflows/lint-rust.yml @@ -66,6 +66,9 @@ jobs: tauri-plugin-global-shortcut: - .github/workflows/lint-rust.yml - plugins/global-shortcut/** + tauri-plugin-opener: + - .github/workflows/lint-rust.yml + - plugins/opener/** tauri-plugin-haptics: - .github/workflows/lint-rust.yml - plugins/haptics/** diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 3793160d2..595bfb763 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -77,6 +77,10 @@ jobs: - .github/workflows/test-rust.yml - Cargo.toml - plugins/global-shortcut/** + tauri-plugin-opener: + - .github/workflows/test-rust.yml + - Cargo.toml + - plugins/opener/** tauri-plugin-haptics: - .github/workflows/test-rust.yml - Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 8dc5b710e..d94e7d169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-nfc", "tauri-plugin-notification", + "tauri-plugin-opener", "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-shell", @@ -6579,6 +6580,19 @@ dependencies = [ "windows-version", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.0.0" +dependencies = [ + "open", + "regex", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror", +] + [[package]] name = "tauri-plugin-os" version = "2.0.1" diff --git a/README.md b/README.md index fe4f90f2b..f9fa736af 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This repo and all plugins require a Rust version of at least **1.77.2** | [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ | | [nfc](plugins/nfc) | Read and write NFC tags on Android and iOS. | ? | ? | ? | ✅ | ✅ | | [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [opener](plugins/opener) | Open files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? | | [os](plugins/os) | Read information about the operating system. | ✅ | ✅ | ✅ | ✅ | ✅ | | [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? | | [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ❌ | ❌ | diff --git a/examples/api/package.json b/examples/api/package.json index 693588b30..185b9d37a 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -18,6 +18,7 @@ "@tauri-apps/plugin-fs": "2.0.2", "@tauri-apps/plugin-geolocation": "2.0.0", "@tauri-apps/plugin-global-shortcut": "2.0.0", + "@tauri-apps/plugin-opener": "2.0.0", "@tauri-apps/plugin-haptics": "2.0.0", "@tauri-apps/plugin-http": "2.0.1", "@tauri-apps/plugin-nfc": "2.0.0", diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 6bf016149..414f69a46 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -33,6 +33,7 @@ tauri-plugin-notification = { path = "../../../plugins/notification", version = ] } tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.1" } tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.1" } +tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.0.0" } tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.2" } tauri-plugin-store = { path = "../../../plugins/store", version = "2.1.0" } diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index 68a22389d..c039c8e0a 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -16,6 +16,7 @@ }, "core:default", "fs:default", + "opener:default", "core:window:allow-minimize", "core:window:allow-maximize", "core:window:allow-unmaximize", diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 701f67318..dc1337995 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -36,6 +36,7 @@ pub fn run() { .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_store::Builder::default().build()) .setup(move |app| { diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index c5fcf55e0..aa0c49071 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -1,6 +1,5 @@ + +
+
+ + + with + + + +
+
diff --git a/package.json b/package.json index f1192e958..cf6c46b8a 100644 --- a/package.json +++ b/package.json @@ -31,5 +31,6 @@ }, "engines": { "pnpm": "^9.0.0" - } + }, + "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b" } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml new file mode 100644 index 000000000..92aa4efad --- /dev/null +++ b/plugins/opener/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "tauri-plugin-opener" +version = "2.0.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-opener" + +[package.metadata.docs.rs] +rustc-args = ["--cfg", "docsrs"] +rustdoc-args = ["--cfg", "docsrs"] + +# Platforms supported by the plugin +# Support levels are "full", "partial", "none", "unknown" +# Details of the support level are left to plugin maintainer +[package.metadata.platforms] +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`" } + + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +thiserror = { workspace = true } +regex = "1" +open = { version = "5", features = ["shellexecute-on-windows"] } diff --git a/plugins/opener/LICENSE.spdx b/plugins/opener/LICENSE.spdx new file mode 100644 index 000000000..cdd0df5ad --- /dev/null +++ b/plugins/opener/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/opener/LICENSE_APACHE-2.0 b/plugins/opener/LICENSE_APACHE-2.0 new file mode 100644 index 000000000..4947287f7 --- /dev/null +++ b/plugins/opener/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/plugins/opener/LICENSE_MIT b/plugins/opener/LICENSE_MIT new file mode 100644 index 000000000..4d7547256 --- /dev/null +++ b/plugins/opener/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/opener/README.md b/plugins/opener/README.md new file mode 100644 index 000000000..17cc5b63f --- /dev/null +++ b/plugins/opener/README.md @@ -0,0 +1,100 @@ +![opener](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/opener/banner.png) + + + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ? | +| iOS | ? | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-opener = "2.0.0" +# alternatively with Git: +tauri-plugin-opener = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use. + + + +```sh +pnpm add @tauri-apps/plugin-opener +# or +npm add @tauri-apps/plugin-opener +# or +yarn add @tauri-apps/plugin-opener + +# alternatively with Git: +pnpm add https://github.com/tauri-apps/tauri-plugin-opener#v2 +# or +npm add https://github.com/tauri-apps/tauri-plugin-opener#v2 +# or +yarn add https://github.com/tauri-apps/tauri-plugin-opener#v2 +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/main.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript + +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/opener/SECURITY.md b/plugins/opener/SECURITY.md new file mode 100644 index 000000000..4f09bbacd --- /dev/null +++ b/plugins/opener/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/opener/android/.gitignore b/plugins/opener/android/.gitignore new file mode 100644 index 000000000..c0f21ec2f --- /dev/null +++ b/plugins/opener/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/opener/android/build.gradle.kts b/plugins/opener/android/build.gradle.kts new file mode 100644 index 000000000..9dcce212e --- /dev/null +++ b/plugins/opener/android/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.opener" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-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/opener/android/proguard-rules.pro b/plugins/opener/android/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/plugins/opener/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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/opener/android/settings.gradle b/plugins/opener/android/settings.gradle new file mode 100644 index 000000000..14a752e43 --- /dev/null +++ b/plugins/opener/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/opener/android/src/main/AndroidManifest.xml b/plugins/opener/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/plugins/opener/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/opener/android/src/main/java/OpenerPlugin.kt b/plugins/opener/android/src/main/java/OpenerPlugin.kt new file mode 100644 index 000000000..0beeb9775 --- /dev/null +++ b/plugins/opener/android/src/main/java/OpenerPlugin.kt @@ -0,0 +1,30 @@ +// 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 OpenerPlugin(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/opener/api-iife.js b/plugins/opener/api-iife.js new file mode 100644 index 000000000..078521237 --- /dev/null +++ b/plugins/opener/api-iife.js @@ -0,0 +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__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs new file mode 100644 index 000000000..6bf98b264 --- /dev/null +++ b/plugins/opener/build.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["open", "reveal_in_dir"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .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/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts new file mode 100644 index 000000000..82c047388 --- /dev/null +++ b/plugins/opener/guest-js/index.ts @@ -0,0 +1,57 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Open files and URLs using their default application. + * + * ## Security + * + * This API has a scope configuration that forces you to restrict the files and urls to be opened. + * + * ### 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/`. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * 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-opener'; + * // 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 > opener > 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 + */ +export async function open(path: string, openWith?: string): Promise { + await invoke('plugin:opener|open', { + path, + with: openWith + }) +} +export async function revealInDir() { + return invoke('plugin:opener|reveal_in_dir') +} diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts new file mode 100644 index 000000000..ef7f45542 --- /dev/null +++ b/plugins/opener/guest-js/init.ts @@ -0,0 +1,40 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { invoke } from '@tauri-apps/api/core' + +// open links with the API +function openLinks(): void { + document.querySelector('body')?.addEventListener('click', function (e) { + let target: HTMLElement | null = e.target as HTMLElement + while (target) { + if (target.matches('a')) { + const t = target as HTMLAnchorElement + if ( + t.href !== '' && + ['http://', 'https://', 'mailto:', 'tel:'].some((v) => + t.href.startsWith(v) + ) && + t.target === '_blank' + ) { + void invoke('plugin:opener|open', { + path: t.href + }) + e.preventDefault() + } + break + } + target = target.parentElement + } + }) +} + +if ( + document.readyState === 'complete' || + document.readyState === 'interactive' +) { + openLinks() +} else { + window.addEventListener('DOMContentLoaded', openLinks, true) +} diff --git a/plugins/opener/ios/Package.resolved b/plugins/opener/ios/Package.resolved new file mode 100644 index 000000000..5f998e0e6 --- /dev/null +++ b/plugins/opener/ios/Package.resolved @@ -0,0 +1,16 @@ +{ + "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/opener/ios/Package.swift b/plugins/opener/ios/Package.swift new file mode 100644 index 000000000..8d06936e6 --- /dev/null +++ b/plugins/opener/ios/Package.swift @@ -0,0 +1,34 @@ +// 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-opener", + 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-opener", + type: .static, + targets: ["tauri-plugin-opener"]) + ], + 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-opener", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/opener/ios/Sources/OpenerPlugin.swift b/plugins/opener/ios/Sources/OpenerPlugin.swift new file mode 100644 index 000000000..39bc5725c --- /dev/null +++ b/plugins/opener/ios/Sources/OpenerPlugin.swift @@ -0,0 +1,34 @@ +// 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 OpenerPlugin: 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 OpenerPlugin() +} diff --git a/plugins/opener/package.json b/plugins/opener/package.json new file mode 100644 index 000000000..a1f31f995 --- /dev/null +++ b/plugins/opener/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-opener", + "version": "2.0.0", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } +} diff --git a/plugins/opener/permissions/autogenerated/commands/open.toml b/plugins/opener/permissions/autogenerated/commands/open.toml new file mode 100644 index 000000000..4ea6dff16 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open" +description = "Enables the open command without any pre-configured scope." +commands.allow = ["open"] + +[[permission]] +identifier = "deny-open" +description = "Denies the open command without any pre-configured scope." +commands.deny = ["open"] diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml new file mode 100644 index 000000000..a5c79836b --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-reveal-in-dir" +description = "Enables the reveal_in_dir command without any pre-configured scope." +commands.allow = ["reveal_in_dir"] + +[[permission]] +identifier = "deny-reveal-in-dir" +description = "Denies the reveal_in_dir command without any pre-configured scope." +commands.deny = ["reveal_in_dir"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md new file mode 100644 index 000000000..a03c50e6b --- /dev/null +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -0,0 +1,68 @@ +## Default Permission + + + +- `allow-open` +- `allow-reveal-in-dir` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`opener:allow-open` + + + +Enables the open command without any pre-configured scope. + +
+ +`opener:deny-open` + + + +Denies the open command without any pre-configured scope. + +
+ +`opener:allow-reveal-in-dir` + + + +Enables the reveal_in_dir command without any pre-configured scope. + +
+ +`opener:deny-reveal-in-dir` + + + +Denies the reveal_in_dir command without any pre-configured scope. + +
diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml new file mode 100644 index 000000000..7286f06ce --- /dev/null +++ b/plugins/opener/permissions/default.toml @@ -0,0 +1,4 @@ +"$schema" = "schemas/schema.json" + +[default] +permissions = ["allow-open", "allow-reveal-in-dir"] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json new file mode 100644 index 000000000..8078fbbc4 --- /dev/null +++ b/plugins/opener/permissions/schemas/schema.json @@ -0,0 +1,324 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "allow-open" + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "deny-open" + }, + { + "description": "Enables the reveal_in_dir command without any pre-configured scope.", + "type": "string", + "const": "allow-reveal-in-dir" + }, + { + "description": "Denies the reveal_in_dir command without any pre-configured scope.", + "type": "string", + "const": "deny-reveal-in-dir" + }, + { + "type": "string", + "const": "default" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/opener/rollup.config.js b/plugins/opener/rollup.config.js new file mode 100644 index 000000000..a7dbd4f69 --- /dev/null +++ b/plugins/opener/rollup.config.js @@ -0,0 +1,22 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import terser from '@rollup/plugin-terser' + +export default createConfig({ + additionalConfigs: { + input: 'guest-js/init.ts', + output: { + file: 'src/init-iife.js', + format: 'iife' + }, + plugins: [typescript(), terser(), nodeResolve()], + onwarn: (warning) => { + throw Object.assign(new Error(), warning) + } + } +}) diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs new file mode 100644 index 000000000..6a6ba118a --- /dev/null +++ b/plugins/opener/src/commands.rs @@ -0,0 +1,16 @@ +use tauri::{AppHandle, Runtime, State}; + +use crate::{open::Program, Opener}; + +#[tauri::command] +pub async fn open( + _app: AppHandle, + opener: State<'_, Opener>, + path: String, + with: Option, +) -> crate::Result<()> { + opener.open(path, with) +} + +#[tauri::command] +pub async fn reveal_in_dir() {} diff --git a/plugins/opener/src/config.rs b/plugins/opener/src/config.rs new file mode 100644 index 000000000..9ac28014a --- /dev/null +++ b/plugins/opener/src/config.rs @@ -0,0 +1,62 @@ +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, +} + +/// 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) + } +} diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs new file mode 100644 index 000000000..833e7a6fe --- /dev/null +++ b/plugins/opener/src/error.rs @@ -0,0 +1,34 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +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("unknown program {0}")] + UnknownProgramName(String), + /// At least one argument did not pass input validation. + #[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] + Validation { + /// Index of the variable. + index: usize, + + /// Regex that the variable value failed to match. + validation: String, + }, +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js new file mode 100644 index 000000000..47e1df39b --- /dev/null +++ b/plugins/opener/src/init-iife.js @@ -0,0 +1 @@ +!function(){"use strict";async function e(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function t(){document.querySelector("body")?.addEventListener("click",(function(t){let n=t.target;for(;n;){if(n.matches("a")){const r=n;""!==r.href&&["http://","https://","mailto:","tel:"].some((e=>r.href.startsWith(e)))&&"_blank"===r.target&&(e("plugin:opener|open",{path:r.href}),t.preventDefault());break}n=n.parentElement}}))}"function"==typeof SuppressedError&&SuppressedError,"complete"===document.readyState||"interactive"===document.readyState?t():window.addEventListener("DOMContentLoaded",t,!0)}(); diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs new file mode 100644 index 000000000..8b75dbaf5 --- /dev/null +++ b/plugins/opener/src/lib.rs @@ -0,0 +1,91 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use config::OpenScope; +use tauri::{ + plugin::{Builder, TauriPlugin}, + AppHandle, Manager, Runtime, +}; + +#[cfg(mobile)] +use tauri::plugin::PluginHandle; +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.opener"; +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_opener); + +mod commands; +mod config; +mod error; +mod open; + +pub use error::Error; +type Result = std::result::Result; + +pub struct Opener { + #[allow(dead_code)] + app: AppHandle, + #[cfg(mobile)] + mobile_plugin_handle: PluginHandle, + open_scope: OpenScope, +} + +impl Opener { + /// 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)] + pub fn open(&self, path: impl Into, with: Option) -> Result<()> { + open::open(&self.open_scope, path.into(), with).map_err(Into::into) + } + + /// 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)] + pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { + self.mobile_plugin_handle + .run_mobile_plugin("open", path.into()) + .map_err(Into::into) + } +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs. +pub trait OpenerExt { + fn opener(&self) -> &Opener; +} + +impl> crate::OpenerExt for T { + fn opener(&self) -> &Opener { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin> { + Builder::>::new("opener") + .js_init_script(include_str!("init-iife.js").to_string()) + .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, "OpenerPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_opener)?; + + app.manage(Opener { + app: app.clone(), + open_scope: config.open_scope(), + #[cfg(mobile)] + mobile_plugin_handle: handle, + }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::open, + commands::reveal_in_dir + ]) + .build() +} diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs new file mode 100644 index 000000000..33ddb0e3f --- /dev/null +++ b/plugins/opener/src/open.rs @@ -0,0 +1,141 @@ +// 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 std::str::FromStr; + +use crate::{config::OpenScope, Error}; + +/// Program to use on the [`open()`] call. +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(()) +/// }); +/// ``` +pub fn open>(scope: &OpenScope, path: P, with: Option) -> crate::Result<()> { + let path = path.as_ref(); + + // ensure we pass validation if the configuration has one + if let Some(regex) = &scope.open { + if !regex.is_match(path) { + return Err(Error::Validation { + index: 0, + validation: regex.as_str().into(), + }); + } + } + + // 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) +} diff --git a/plugins/opener/tsconfig.json b/plugins/opener/tsconfig.json new file mode 100644 index 000000000..5098169aa --- /dev/null +++ b/plugins/opener/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["guest-js/*.ts"] +} diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs index a4a74a990..4e19ccd84 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -182,6 +182,7 @@ fn main() { // 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/src/lib.rs b/plugins/shell/src/lib.rs index 1ec90e2e1..9066da933 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -28,6 +28,7 @@ use tauri::{ mod commands; mod config; mod error; +#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] pub mod open; pub mod process; mod scope; @@ -72,6 +73,7 @@ impl Shell { /// /// See [`crate::open::open`] for how it handles security-related measures. #[cfg(desktop)] + #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] pub fn open(&self, path: impl Into, with: Option) -> Result<()> { open::open(&self.open_scope, path.into(), with).map_err(Into::into) } @@ -80,6 +82,7 @@ impl Shell { /// /// 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()) diff --git a/plugins/shell/src/open.rs b/plugins/shell/src/open.rs index 8366b45b3..fe2247040 100644 --- a/plugins/shell/src/open.rs +++ b/plugins/shell/src/open.rs @@ -10,6 +10,7 @@ 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, @@ -117,6 +118,7 @@ impl Program { /// Ok(()) /// }); /// ``` +#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] pub fn open>(scope: &OpenScope, path: P, with: Option) -> crate::Result<()> { scope.open(path.as_ref(), with).map_err(Into::into) } diff --git a/plugins/updater/build.rs b/plugins/updater/build.rs index 7c7774b90..30f70b984 100644 --- a/plugins/updater/build.rs +++ b/plugins/updater/build.rs @@ -18,6 +18,7 @@ fn main() { // 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/pnpm-lock.yaml b/pnpm-lock.yaml index 2c7a6604c..f9ddcdcd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@tauri-apps/plugin-notification': specifier: 2.0.0 version: link:../../plugins/notification + '@tauri-apps/plugin-opener': + specifier: 2.0.0 + version: link:../../plugins/opener '@tauri-apps/plugin-os': specifier: 2.0.0 version: link:../../plugins/os @@ -249,6 +252,12 @@ importers: specifier: ^2.0.0 version: 2.0.3 + plugins/opener: + dependencies: + '@tauri-apps/api': + specifier: ^2.0.0 + version: 2.0.3 + plugins/os: dependencies: '@tauri-apps/api': @@ -2415,14 +2424,13 @@ snapshots: picocolors: 1.1.0 sisteransi: 1.0.5 - '@covector/apply@0.10.0(mocha@10.7.3)': + '@covector/apply@0.10.0': dependencies: '@covector/files': 0.8.0 effection: 2.0.8(mocha@10.7.3) semver: 7.6.3 transitivePeerDependencies: - encoding - - mocha '@covector/assemble@0.12.0(mocha@10.7.3)': dependencies: @@ -2440,7 +2448,7 @@ snapshots: - mocha - supports-color - '@covector/changelog@0.12.0(mocha@10.7.3)': + '@covector/changelog@0.12.0': dependencies: '@covector/files': 0.8.0 effection: 2.0.8(mocha@10.7.3) @@ -2450,12 +2458,11 @@ snapshots: unified: 9.2.2 transitivePeerDependencies: - encoding - - mocha - supports-color '@covector/command@0.8.0(mocha@10.7.3)': dependencies: - '@effection/process': 2.1.4(mocha@10.7.3) + '@effection/process': 2.1.4 effection: 2.0.8(mocha@10.7.3) transitivePeerDependencies: - encoding @@ -2506,8 +2513,10 @@ snapshots: dependencies: effection: 2.0.8(mocha@10.7.3) mocha: 10.7.3 + transitivePeerDependencies: + - encoding - '@effection/process@2.1.4(mocha@10.7.3)': + '@effection/process@2.1.4': dependencies: cross-spawn: 7.0.3 ctrlc-windows: 2.1.0 @@ -2515,7 +2524,6 @@ snapshots: shellwords: 0.1.1 transitivePeerDependencies: - encoding - - mocha '@effection/stream@2.0.6': dependencies: @@ -3385,9 +3393,9 @@ snapshots: covector@0.12.3(mocha@10.7.3): dependencies: '@clack/prompts': 0.7.0 - '@covector/apply': 0.10.0(mocha@10.7.3) + '@covector/apply': 0.10.0 '@covector/assemble': 0.12.0(mocha@10.7.3) - '@covector/changelog': 0.12.0(mocha@10.7.3) + '@covector/changelog': 0.12.0 '@covector/command': 0.8.0(mocha@10.7.3) '@covector/files': 0.8.0 effection: 2.0.8(mocha@10.7.3) From 42ab44cd5f9b365ff27cfb087a8e68d915c5b767 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 21:09:55 +0200 Subject: [PATCH 02/31] migrate to using scope --- Cargo.lock | 3 + Cargo.toml | 2 + examples/api/src-tauri/capabilities/base.json | 4 +- examples/api/src/views/Opener.svelte | 9 +- plugins/fs/Cargo.toml | 2 +- plugins/fs/build.rs | 17 +- plugins/http/Cargo.toml | 4 +- plugins/opener/Cargo.toml | 10 +- plugins/opener/api-iife.js | 2 +- plugins/opener/build.rs | 55 ++++++- plugins/opener/guest-js/index.ts | 2 +- .../permissions/allow-default-urls.toml | 17 ++ .../autogenerated/commands/reveal_in_dir.toml | 12 +- .../commands/reveal_item_in_dir.toml | 13 ++ .../permissions/autogenerated/reference.md | 53 ++++++- plugins/opener/permissions/default.toml | 4 +- .../opener/permissions/schemas/schema.json | 24 ++- plugins/opener/src/commands.rs | 34 +++- plugins/opener/src/config.rs | 62 -------- plugins/opener/src/error.rs | 15 +- plugins/opener/src/lib.rs | 23 +-- plugins/opener/src/open.rs | 47 ++++-- plugins/opener/src/scope.rs | 146 ++++++++++++++++++ plugins/opener/src/scope_entry.rs | 11 ++ plugins/shell/src/commands.rs | 4 +- plugins/shell/src/lib.rs | 2 + plugins/shell/src/scope.rs | 2 + 27 files changed, 439 insertions(+), 140 deletions(-) create mode 100644 plugins/opener/permissions/allow-default-urls.toml create mode 100644 plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml delete mode 100644 plugins/opener/src/config.rs create mode 100644 plugins/opener/src/scope.rs create mode 100644 plugins/opener/src/scope_entry.rs diff --git a/Cargo.lock b/Cargo.lock index d94e7d169..dcbc2f306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6586,11 +6586,14 @@ version = "2.0.0" dependencies = [ "open", "regex", + "schemars", "serde", "serde_json", "tauri", "tauri-plugin", "thiserror", + "url", + "urlpattern", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a17d33eaf..d011d0ab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ url = "2" schemars = "0.8" dunce = "1" specta = "=2.0.0-rc.20" +glob = "0.3" +urlpattern = "0.3" #tauri-specta = "=2.0.0-rc.11" [workspace.package] diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index c039c8e0a..ae93aeefb 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -16,7 +16,6 @@ }, "core:default", "fs:default", - "opener:default", "core:window:allow-minimize", "core:window:allow-maximize", "core:window:allow-unmaximize", @@ -81,6 +80,7 @@ ], "deny": ["$APPDATA/db/*.stronghold"] }, - "store:default" + "store:default", + "opener:default" ] } diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte index cb5a27735..3a2afaab5 100644 --- a/examples/api/src/views/Opener.svelte +++ b/examples/api/src/views/Opener.svelte @@ -27,7 +27,10 @@
-
+
- -
+ +
diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml index 5d9a7efb6..7546baef1 100644 --- a/plugins/fs/Cargo.toml +++ b/plugins/fs/Cargo.toml @@ -34,7 +34,7 @@ thiserror = { workspace = true } url = { workspace = true } anyhow = "1" uuid = { version = "1", features = ["v4"] } -glob = "0.3" +glob = { workspace = true } # TODO: Remove `serialization-compat-6` in v3 notify = { version = "7", optional = true, features = [ "serde", diff --git a/plugins/fs/build.rs b/plugins/fs/build.rs index cb9d00daf..935e0a81b 100644 --- a/plugins/fs/build.rs +++ b/plugins/fs/build.rs @@ -16,10 +16,23 @@ mod scope; #[serde(untagged)] #[allow(unused)] enum FsScopeEntry { - /// FS scope path. + /// A path that can be accessed by the webview when using the fs APIs. + /// FS scope path pattern. + /// + /// The pattern can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. Value(PathBuf), Object { - /// FS scope path. + /// A path that can be accessed by the webview when using the fs APIs. + /// + /// The pattern can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. path: PathBuf, }, } diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index 8c1801a38..7b008c14f 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -25,7 +25,7 @@ tauri-plugin = { workspace = true, features = ["build"] } schemars = { workspace = true } serde = { workspace = true } url = { workspace = true } -urlpattern = "0.3" +urlpattern = { workspace = true } regex = "1" [dependencies] @@ -35,7 +35,7 @@ tauri = { workspace = true } thiserror = { workspace = true } tokio = { version = "1", features = ["sync", "macros"] } tauri-plugin-fs = { path = "../fs", version = "2.0.3" } -urlpattern = "0.3" +urlpattern = { workspace = true } regex = "1" http = "1" reqwest = { version = "0.12", default-features = false } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 92aa4efad..f450e15de 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -24,13 +24,17 @@ ios = { level = "partial", notes = "Only allows to open URLs via `open`" } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +schemars = { workspace = true } +serde = { workspace = true } +urlpattern = { workspace = true } +regex = "1" [dependencies] serde = { workspace = true } serde_json = { workspace = true } tauri = { workspace = true } thiserror = { workspace = true } -regex = "1" open = { version = "5", features = ["shellexecute-on-windows"] } +urlpattern = { workspace = true } +regex = "1" +url = { workspace = true } diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 078521237..5b37b44e4 100644 --- a/plugins/opener/api-iife.js +++ b/plugins/opener/api-iife.js @@ -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__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 6bf98b264..6bccbdd94 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -2,13 +2,66 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -const COMMANDS: &[&str] = &["open", "reveal_in_dir"]; +use std::path::PathBuf; + +#[path = "src/scope_entry.rs"] +#[allow(dead_code)] +mod scope; + +/// Opener scope entry. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +enum OpenerScopeEntry { + Url { + /// A URL that can be opened by the webview when using the Opener APIs. + /// Wildcards can be used following the URL pattern standard. + /// + /// See [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information. + /// + /// Examples: + /// + /// - "https://*" : allows all HTTPS origin on port 443 + /// + /// - "https://*:*" : allows all HTTPS origin on any port + /// + /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path + /// + /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" + url: String, + }, + Path { + /// A path that can be opened by the webview when using the Opener APIs. + /// + /// The pattern can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. + path: PathBuf, + }, +} + +// Ensure `OpenerScopeEntry` and `scope::EntryRaw` is kept in sync +fn _f() { + match (scope::EntryRaw::Url { url: String::new() }) { + scope::EntryRaw::Url { url } => OpenerScopeEntry::Url { url }, + scope::EntryRaw::Path { path } => OpenerScopeEntry::Path { path }, + }; + match (OpenerScopeEntry::Url { url: String::new() }) { + OpenerScopeEntry::Url { url } => scope::EntryRaw::Url { url }, + OpenerScopeEntry::Path { path } => scope::EntryRaw::Path { path }, + }; +} + +const COMMANDS: &[&str] = &["open", "reveal_item_in_dir"]; fn main() { tauri_plugin::Builder::new(COMMANDS) .global_api_script_path("./api-iife.js") .android_path("android") .ios_path("ios") + .global_scope_schema(schemars::schema_for!(OpenerScopeEntry)) .build(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 82c047388..0435fcc45 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -53,5 +53,5 @@ export async function open(path: string, openWith?: string): Promise { }) } export async function revealInDir() { - return invoke('plugin:opener|reveal_in_dir') + return invoke('plugin:opener|reveal_item_in_dir') } diff --git a/plugins/opener/permissions/allow-default-urls.toml b/plugins/opener/permissions/allow-default-urls.toml new file mode 100644 index 000000000..cad02770b --- /dev/null +++ b/plugins/opener/permissions/allow-default-urls.toml @@ -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://*:*" diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml index a5c79836b..e669620ff 100644 --- a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml +++ b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml @@ -3,11 +3,11 @@ "$schema" = "../../schemas/schema.json" [[permission]] -identifier = "allow-reveal-in-dir" -description = "Enables the reveal_in_dir command without any pre-configured scope." -commands.allow = ["reveal_in_dir"] +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-in-dir" -description = "Denies the reveal_in_dir command without any pre-configured scope." -commands.deny = ["reveal_in_dir"] +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"] diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml new file mode 100644 index 000000000..e669620ff --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml @@ -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"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index a03c50e6b..0a795ef7c 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -1,9 +1,11 @@ ## Default Permission - +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 - `allow-open` -- `allow-reveal-in-dir` +- `allow-reveal-item-in-dir` +- `default-urls` ## Permission Table @@ -43,12 +45,51 @@ Denies the open command without any pre-configured scope. -`opener:allow-reveal-in-dir` +`opener:allow-reveal-item-in-dir` + + + + +Enables the reveal_item_in_dir command without any pre-configured scope. + + + + + + + +`opener:deny-reveal-item-in-dir` + + + + +Denies the reveal_item_in_dir command without any pre-configured scope. + + + + + + + +`opener:allow-reveal-item-in-dir` + + + + +Enables the reveal_item_in_dir command without any pre-configured scope. + + + + + + + +`opener:deny-reveal-item-in-dir` -Enables the reveal_in_dir command without any pre-configured scope. +Denies the reveal_item_in_dir command without any pre-configured scope. @@ -56,12 +97,12 @@ Enables the reveal_in_dir command without any pre-configured scope. -`opener:deny-reveal-in-dir` +`opener:default-urls` -Denies the reveal_in_dir command without any pre-configured scope. +This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application. diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index 7286f06ce..f9e3184d2 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -1,4 +1,6 @@ "$schema" = "schemas/schema.json" [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"] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 8078fbbc4..94891e8fe 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -305,16 +305,32 @@ "const": "deny-open" }, { - "description": "Enables the reveal_in_dir command without any pre-configured scope.", + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", "type": "string", - "const": "allow-reveal-in-dir" + "const": "allow-reveal-item-in-dir" }, { - "description": "Denies the reveal_in_dir command without any pre-configured scope.", + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", "type": "string", - "const": "deny-reveal-in-dir" + "const": "deny-reveal-item-in-dir" }, { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "allow-reveal-item-in-dir" + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "deny-reveal-item-in-dir" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "default-urls" + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer", "type": "string", "const": "default" } diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 6a6ba118a..b3eec1037 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -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] pub async fn open( - _app: AppHandle, - opener: State<'_, Opener>, + app: AppHandle, + command_scope: CommandScope, + global_scope: GlobalScope, path: String, with: Option, ) -> 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] -pub async fn reveal_in_dir() {} +pub async fn reveal_item_in_dir() {} diff --git a/plugins/opener/src/config.rs b/plugins/opener/src/config.rs deleted file mode 100644 index 9ac28014a..000000000 --- a/plugins/opener/src/config.rs +++ /dev/null @@ -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, -} - -/// 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) - } -} diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 833e7a6fe..943305d62 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -10,18 +10,15 @@ pub enum Error { #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] Io(#[from] std::io::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - /// At least one argument did not pass input validation. - #[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] - Validation { - /// Index of the variable. - index: usize, - - /// Regex that the variable value failed to match. - validation: String, - }, + #[error("Not allowed to open {0}")] + NotAllowed(String), } impl Serialize for Error { diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 8b75dbaf5..94d06851c 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use config::OpenScope; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Manager, Runtime, @@ -16,9 +15,10 @@ const PLUGIN_IDENTIFIER: &str = "app.tauri.opener"; tauri::ios_plugin_binding!(init_plugin_opener); mod commands; -mod config; mod error; mod open; +mod scope; +mod scope_entry; pub use error::Error; type Result = std::result::Result; @@ -28,21 +28,16 @@ pub struct Opener { app: AppHandle, #[cfg(mobile)] mobile_plugin_handle: PluginHandle, - open_scope: OpenScope, } impl Opener { /// 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)] pub fn open(&self, path: impl Into, with: Option) -> Result<()> { - open::open(&self.open_scope, path.into(), with).map_err(Into::into) + open::open(path.into(), with).map_err(Into::into) } /// 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)] pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { self.mobile_plugin_handle @@ -63,13 +58,10 @@ impl> crate::OpenerExt for T { } /// Initializes the plugin. -pub fn init() -> TauriPlugin> { - Builder::>::new("opener") +pub fn init() -> TauriPlugin { + Builder::new("opener") .js_init_script(include_str!("init-iife.js").to_string()) - .setup(|app, api| { - let default_config = config::Config::default(); - let config = api.config().as_ref().unwrap_or(&default_config); - + .setup(|app, _api| { #[cfg(target_os = "android")] let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; #[cfg(target_os = "ios")] @@ -77,7 +69,6 @@ pub fn init() -> TauriPlugin> { app.manage(Opener { app: app.clone(), - open_scope: config.open_scope(), #[cfg(mobile)] mobile_plugin_handle: handle, }); @@ -85,7 +76,7 @@ pub fn init() -> TauriPlugin> { }) .invoke_handler(tauri::generate_handler![ commands::open, - commands::reveal_in_dir + commands::reveal_item_in_dir ]) .build() } diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs index 33ddb0e3f..b472cb34c 100644 --- a/plugins/opener/src/open.rs +++ b/plugins/opener/src/open.rs @@ -6,11 +6,10 @@ use serde::{Deserialize, Deserializer}; -use std::str::FromStr; - -use crate::{config::OpenScope, Error}; +use std::{fmt::Display, str::FromStr}; /// Program to use on the [`open()`] call. +#[derive(Debug)] pub enum Program { /// Use the `open` program. Open, @@ -36,6 +35,28 @@ pub enum Program { Safari, } +impl Display for Program { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + 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", + Self::Firefox => "firefox", + Self::Chrome => "chrome", + Self::Chromium => "chromium", + Self::Safari => "safari", + } + ) + } +} + impl FromStr for Program { type Err = super::Error; @@ -118,18 +139,18 @@ impl Program { /// Ok(()) /// }); /// ``` -pub fn open>(scope: &OpenScope, path: P, with: Option) -> crate::Result<()> { +pub fn open>(path: P, with: Option) -> crate::Result<()> { let path = path.as_ref(); - // ensure we pass validation if the configuration has one - if let Some(regex) = &scope.open { - if !regex.is_match(path) { - return Err(Error::Validation { - index: 0, - validation: regex.as_str().into(), - }); - } - } + // // ensure we pass validation if the configuration has one + // if let Some(regex) = &scope.open { + // if !regex.is_match(path) { + // return Err(Error::Validation { + // index: 0, + // validation: regex.as_str().into(), + // }); + // } + // } // 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`. diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs new file mode 100644 index 000000000..b26b910cc --- /dev/null +++ b/plugins/opener/src/scope.rs @@ -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), +} + +fn parse_url_pattern( + s: &str, +) -> std::result::Result { + let mut init = urlpattern::UrlPatternInit::parse_constructor_string::(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( + app: &AppHandle, + raw: Value, + ) -> std::result::Result { + 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> { + allowed: Vec<&'a Arc>, + denied: Vec<&'a Arc>, + manager: &'a M, + _marker: PhantomData, +} + +impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { + pub(crate) fn new( + manager: &'a M, + allowed: Vec<&'a Arc>, + denied: Vec<&'a Arc>, + ) -> Self { + Self { + manager, + allowed, + denied, + _marker: PhantomData, + } + } + + pub fn is_allowed(&self, path_or_url: &str) -> crate::Result { + 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 { + 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)) + } +} diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs new file mode 100644 index 000000000..105e2d591 --- /dev/null +++ b/plugins/opener/src/scope_entry.rs @@ -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 }, +} diff --git a/plugins/shell/src/commands.rs b/plugins/shell/src/commands.rs index 3345bb3a6..5bee6b92e 100644 --- a/plugins/shell/src/commands.rs +++ b/plugins/shell/src/commands.rs @@ -11,8 +11,9 @@ use tauri::{ Manager, Runtime, State, Window, }; +#[allow(deprecated)] +use crate::open::Program; use crate::{ - open::Program, process::{CommandEvent, TerminatedPayload}, scope::ExecuteArgs, Shell, @@ -302,6 +303,7 @@ pub fn kill( Ok(()) } +#[allow(deprecated)] #[tauri::command] pub async fn open( _window: Window, diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 9066da933..4e8b245f6 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -29,6 +29,7 @@ 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; @@ -74,6 +75,7 @@ impl Shell { /// 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(&self.open_scope, path.into(), with).map_err(Into::into) } diff --git a/plugins/shell/src/scope.rs b/plugins/shell/src/scope.rs index 8178ab101..9e76931aa 100644 --- a/plugins/shell/src/scope.rs +++ b/plugins/shell/src/scope.rs @@ -4,6 +4,7 @@ use std::sync::Arc; +#[allow(deprecated)] use crate::open::Program; use crate::process::Command; @@ -201,6 +202,7 @@ impl OpenScope { /// /// 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 { From b9ced5422b087483b62f9a17aca39fdebdaf4267 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 21:11:19 +0200 Subject: [PATCH 03/31] clippy --- .../permissions/autogenerated/reference.md | 28 +++++++++---------- .../opener/permissions/schemas/schema.json | 10 +++---- plugins/opener/src/scope.rs | 16 ++++++----- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index 0a795ef7c..b49b335b4 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -5,7 +5,7 @@ as well as reveal file in directories using default file explorer - `allow-open` - `allow-reveal-item-in-dir` -- `default-urls` +- `allow-default-urls` ## Permission Table @@ -16,6 +16,19 @@ as well as reveal file in directories using default file explorer + + + +`opener:allow-default-urls` + + + + +This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application. + + + + @@ -91,19 +104,6 @@ Enables the reveal_item_in_dir command without any pre-configured scope. Denies the reveal_item_in_dir command without any pre-configured scope. - - - - - - -`opener:default-urls` - - - - -This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application. - diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 94891e8fe..22d9d40dd 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -294,6 +294,11 @@ "PermissionKind": { "type": "string", "oneOf": [ + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "allow-default-urls" + }, { "description": "Enables the open command without any pre-configured scope.", "type": "string", @@ -324,11 +329,6 @@ "type": "string", "const": "deny-reveal-item-in-dir" }, - { - "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", - "type": "string", - "const": "default-urls" - }, { "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer", "type": "string", diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index b26b910cc..be4d0c861 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -9,7 +9,7 @@ use crate::{scope_entry::EntryRaw, Error}; #[derive(Debug)] pub enum Entry { - Url(urlpattern::UrlPattern), + Url(Box), Path(Option), } @@ -44,12 +44,14 @@ impl ScopeObject for Entry { 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::Url { url } => { + let url_pattern = parse_url_pattern(&url).map_err(|e| { + serde::de::Error::custom(format!( + "`{url}` is not a valid URL pattern: {e}" + )) + })?; + Entry::Url(Box::new(url_pattern)) + } EntryRaw::Path { path } => { let path = match app.path().parse(path) { Ok(path) => Some(path), From 6766bfdbd43bad2137acd418edf1e30afa89e9e6 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 21:19:00 +0200 Subject: [PATCH 04/31] add show_item_in_dir (windows impl) --- Cargo.lock | 22 +++++ plugins/opener/Cargo.toml | 13 +++ plugins/opener/src/error.rs | 11 +++ plugins/opener/src/lib.rs | 10 +++ plugins/opener/src/show_item_in_dir.rs | 120 +++++++++++++++++++++++++ 5 files changed, 176 insertions(+) create mode 100644 plugins/opener/src/show_item_in_dir.rs diff --git a/Cargo.lock b/Cargo.lock index dcbc2f306..2240faaf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6584,6 +6584,7 @@ dependencies = [ name = "tauri-plugin-opener" version = "2.0.0" dependencies = [ + "dunce", "open", "regex", "schemars", @@ -6594,6 +6595,7 @@ dependencies = [ "thiserror", "url", "urlpattern", + "windows 0.54.0", ] [[package]] @@ -7942,6 +7944,16 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.56.0" @@ -7971,6 +7983,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.56.0" diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index f450e15de..bb3ecfab7 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -38,3 +38,16 @@ open = { version = "5", features = ["shellexecute-on-windows"] } urlpattern = { workspace = true } regex = "1" url = { workspace = true } + +[target."cfg(windows)".dependencies] +dunce = { workspace = true } + +[target."cfg(windows)".dependencies.windows] +version = "0.54" +features = [ + "Win32_Foundation", + "Win32_UI_Shell_Common", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Com", + "Win32_System_Registry", +] diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 943305d62..50676df6d 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use std::path::PathBuf; + use serde::{Serialize, Serializer}; #[derive(Debug, thiserror::Error)] @@ -19,6 +21,15 @@ pub enum Error { UnknownProgramName(String), #[error("Not allowed to open {0}")] NotAllowed(String), + /// API not supported on the current platform + #[error("API not supported on the current platform")] + UnsupportedPlatform, + #[error(transparent)] + #[cfg(windows)] + Win32Error(#[from] windows::core::Error), + /// Path doesn't have a parent. + #[error("Path doesn't have a parent: {0}")] + NoParent(PathBuf), } impl Serialize for Error { diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 94d06851c..5a40ec7e6 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use std::path::Path; + use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Manager, Runtime, @@ -19,10 +21,14 @@ mod error; mod open; mod scope; mod scope_entry; +mod show_item_in_dir; pub use error::Error; type Result = std::result::Result; +pub use open::{open, Program}; +pub use show_item_in_dir::show_item_in_dir; + pub struct Opener { #[allow(dead_code)] app: AppHandle, @@ -44,6 +50,10 @@ impl Opener { .run_mobile_plugin("open", path.into()) .map_err(Into::into) } + + pub fn show_item_in_dir>(&self, p: P) -> Result<()> { + show_item_in_dir::show_item_in_dir(p) + } } /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs. diff --git a/plugins/opener/src/show_item_in_dir.rs b/plugins/opener/src/show_item_in_dir.rs new file mode 100644 index 000000000..96719120e --- /dev/null +++ b/plugins/opener/src/show_item_in_dir.rs @@ -0,0 +1,120 @@ +use std::path::Path; +use std::path::PathBuf; + +/// Show +/// +/// ## Platform-specific: +/// +/// - **Android / iOS:** Unsupported. +pub fn show_item_in_dir>(p: P) -> crate::Result<()> { + let p = p.as_ref().canonicalize()?; + + #[cfg(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + return imp::show_item_in_dir(p); + + #[cfg(not(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + Err(crate::Error::UnsupportedPlatform) +} + +#[cfg(windows)] +mod imp { + use super::*; + + use windows::{ + core::{w, HSTRING, PCWSTR}, + Win32::{ + Foundation::ERROR_FILE_NOT_FOUND, + System::Com::CoInitialize, + UI::{ + Shell::{ + ILCreateFromPathW, ILFree, SHOpenFolderAndSelectItems, ShellExecuteExW, + SHELLEXECUTEINFOW, + }, + WindowsAndMessaging::SW_SHOWNORMAL, + }, + }, + }; + + pub fn show_item_in_dir(p: PathBuf) -> crate::Result<()> { + let file = dunce::simplified(&p); + + let _ = unsafe { CoInitialize(None) }; + + let dir = file + .parent() + .ok_or_else(|| crate::Error::NoParent(file.to_path_buf()))?; + + let dir = HSTRING::from(dir); + let dir_item = unsafe { ILCreateFromPathW(PCWSTR::from_raw(dir.as_ptr())) }; + + let file_h = HSTRING::from(file); + let file_item = unsafe { ILCreateFromPathW(PCWSTR::from_raw(file_h.as_ptr())) }; + + unsafe { + if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 0) { + if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + let is_dir = std::fs::metadata(file).map(|f| f.is_dir()).unwrap_or(false); + let mut info = SHELLEXECUTEINFOW { + cbSize: std::mem::size_of::() as _, + nShow: SW_SHOWNORMAL.0, + lpVerb: if is_dir { + w!("explore") + } else { + PCWSTR::null() + }, + lpClass: if is_dir { w!("folder") } else { PCWSTR::null() }, + lpFile: PCWSTR(file_h.as_ptr()), + ..std::mem::zeroed() + }; + + ShellExecuteExW(&mut info)?; + } + } + } + + unsafe { + ILFree(Some(dir_item)); + ILFree(Some(file_item)); + } + + Ok(()) + } +} + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod imp { + use super::*; + + pub fn show_item_in_dir(p: PathBuf) -> crate::Result<()> { + Ok(()) + } +} + +#[cfg(target_os = "macos")] +mod imp { + use super::*; + + pub fn show_item_in_dir(p: PathBuf) -> crate::Result<()> {} +} From 67c8f4293695e909bf396117e6e6ee6bc784c8bb Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 21:54:22 +0200 Subject: [PATCH 05/31] reveal_item_in_dir (linux impl) --- Cargo.lock | 1 + Cargo.toml | 1 + plugins/opener/Cargo.toml | 3 + plugins/opener/src/error.rs | 14 +++- plugins/opener/src/lib.rs | 8 +-- ...w_item_in_dir.rs => reveal_item_in_dir.rs} | 70 ++++++++++++++++--- plugins/single-instance/Cargo.toml | 2 +- 7 files changed, 83 insertions(+), 16 deletions(-) rename plugins/opener/src/{show_item_in_dir.rs => reveal_item_in_dir.rs} (56%) diff --git a/Cargo.lock b/Cargo.lock index 2240faaf7..ef81c04cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6596,6 +6596,7 @@ dependencies = [ "url", "urlpattern", "windows 0.54.0", + "zbus", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d011d0ab8..4b075df41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ dunce = "1" specta = "=2.0.0-rc.20" glob = "0.3" urlpattern = "0.3" +zbus = "4" #tauri-specta = "=2.0.0-rc.11" [workspace.package] diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index bb3ecfab7..0045fb954 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -51,3 +51,6 @@ features = [ "Win32_System_Com", "Win32_System_Registry", ] + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] +zbus = { workspace = true } diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 50676df6d..2f9dff4bc 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use serde::{Serialize, Serializer}; #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum Error { #[cfg(mobile)] #[error(transparent)] @@ -21,15 +22,24 @@ pub enum Error { UnknownProgramName(String), #[error("Not allowed to open {0}")] NotAllowed(String), - /// API not supported on the current platform #[error("API not supported on the current platform")] UnsupportedPlatform, #[error(transparent)] #[cfg(windows)] Win32Error(#[from] windows::core::Error), - /// Path doesn't have a parent. #[error("Path doesn't have a parent: {0}")] NoParent(PathBuf), + #[error("Failed to convert path to file:// url")] + FailedToConvertPathToFileUrl, + #[error(transparent)] + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + Zbus(#[from] zbus::Error), } impl Serialize for Error { diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 5a40ec7e6..489368749 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -19,15 +19,15 @@ tauri::ios_plugin_binding!(init_plugin_opener); mod commands; mod error; mod open; +mod reveal_item_in_dir; mod scope; mod scope_entry; -mod show_item_in_dir; pub use error::Error; type Result = std::result::Result; pub use open::{open, Program}; -pub use show_item_in_dir::show_item_in_dir; +pub use reveal_item_in_dir::reveal_item_in_dir; pub struct Opener { #[allow(dead_code)] @@ -51,8 +51,8 @@ impl Opener { .map_err(Into::into) } - pub fn show_item_in_dir>(&self, p: P) -> Result<()> { - show_item_in_dir::show_item_in_dir(p) + pub fn reveal_item_in_dir>(&self, p: P) -> Result<()> { + reveal_item_in_dir::reveal_item_in_dir(p) } } diff --git a/plugins/opener/src/show_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs similarity index 56% rename from plugins/opener/src/show_item_in_dir.rs rename to plugins/opener/src/reveal_item_in_dir.rs index 96719120e..5b3cc5bcc 100644 --- a/plugins/opener/src/show_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -1,13 +1,12 @@ use std::path::Path; -use std::path::PathBuf; /// Show /// /// ## Platform-specific: /// /// - **Android / iOS:** Unsupported. -pub fn show_item_in_dir>(p: P) -> crate::Result<()> { - let p = p.as_ref().canonicalize()?; +pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { + let path = path.as_ref().canonicalize()?; #[cfg(any( windows, @@ -18,7 +17,7 @@ pub fn show_item_in_dir>(p: P) -> crate::Result<()> { target_os = "netbsd", target_os = "openbsd" ))] - return imp::show_item_in_dir(p); + return imp::reveal_item_in_dir(&path); #[cfg(not(any( windows, @@ -51,8 +50,8 @@ mod imp { }, }; - pub fn show_item_in_dir(p: PathBuf) -> crate::Result<()> { - let file = dunce::simplified(&p); + pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> { + let file = dunce::simplified(path); let _ = unsafe { CoInitialize(None) }; @@ -105,10 +104,63 @@ mod imp { target_os = "openbsd" ))] mod imp { + + use std::collections::HashMap; + use super::*; - pub fn show_item_in_dir(p: PathBuf) -> crate::Result<()> { - Ok(()) + pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> { + let connection = zbus::blocking::Connection::session()?; + + reveal_with_filemanager1(path, &connection) + .or_else(|_| reveal_with_open_uri_portal(path, &connection)) + } + + fn reveal_with_filemanager1( + path: &Path, + connection: &zbus::blocking::Connection, + ) -> crate::Result<()> { + let uri = url::Url::from_file_path(path) + .map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?; + + #[zbus::proxy( + interface = "org.freedesktop.FileManager1", + default_service = "org.freedesktop.FileManager1", + default_path = "/org/freedesktop/FileManager1" + )] + trait FileManager1 { + async fn ShowItems(&self, name: Vec<&str>, arg2: &str) -> crate::Result<()>; + } + + let proxy = FileManager1ProxyBlocking::new(connection)?; + + proxy.ShowItems(vec![uri.as_str()], "") + } + + fn reveal_with_open_uri_portal( + path: &Path, + connection: &zbus::blocking::Connection, + ) -> crate::Result<()> { + let uri = url::Url::from_file_path(path) + .map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?; + + #[zbus::proxy( + interface = "org.freedesktop.portal.Desktop", + default_service = "org.freedesktop.portal.OpenURI", + default_path = "/org/freedesktop/portal/desktop" + )] + trait PortalDesktop { + async fn OpenDirectory( + &self, + arg1: &str, + name: &str, + arg3: HashMap<&str, &str>, + ) -> crate::Result<()>; + } + + let proxy = PortalDesktopProxyBlocking::new(connection)?; + + proxy.OpenDirectory("", uri.as_str(), HashMap::new()) } } @@ -116,5 +168,5 @@ mod imp { mod imp { use super::*; - pub fn show_item_in_dir(p: PathBuf) -> crate::Result<()> {} + pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {} } diff --git a/plugins/single-instance/Cargo.toml b/plugins/single-instance/Cargo.toml index 666c0d047..c59048391 100644 --- a/plugins/single-instance/Cargo.toml +++ b/plugins/single-instance/Cargo.toml @@ -42,7 +42,7 @@ features = [ ] [target."cfg(target_os = \"linux\")".dependencies] -zbus = "4" +zbus = { workspace = true } [features] semver = ["dep:semver"] From 73959f5d9a3b11d1484cc2017c200f010a205e6e Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 22:06:51 +0200 Subject: [PATCH 06/31] macos impl --- Cargo.lock | 2 ++ plugins/opener/Cargo.toml | 8 ++++++++ plugins/opener/src/reveal_item_in_dir.rs | 13 ++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ef81c04cb..ad48616da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6585,6 +6585,8 @@ name = "tauri-plugin-opener" version = "2.0.0" dependencies = [ "dunce", + "objc2-app-kit", + "objc2-foundation", "open", "regex", "schemars", diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 0045fb954..6c6f09193 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -54,3 +54,11 @@ features = [ [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] zbus = { workspace = true } + +[target."cfg(target_os = \"macos\")".dependencies.objc2-app-kit] +version = "0.2" +features = ["NSWorkspace"] + +[target."cfg(target_os = \"macos\")".dependencies.objc2-foundation] +version = "0.2" +features = ["NSURL", "NSArray", "NSString"] diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 5b3cc5bcc..89ce1492a 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -167,6 +167,17 @@ mod imp { #[cfg(target_os = "macos")] mod imp { use super::*; + use objc2_app_kit::NSWorkspace; + use objc2_foundation::{NSArray, NSString, NSURL}; + pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> { + unsafe { + let path = path.to_string_lossy(); + let path = NSString::from_str(&path); + let urls = vec![NSURL::fileURLWithPath(&path)]; + let urls = NSArray::from_vec(urls); - pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {} + let workspace = NSWorkspace::new(); + workspace.activateFileViewerSelectingURLs(&urls); + } + } } From fe2fdee60bdc3e90a26c455e492e2f167bac25de Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 22:08:57 +0200 Subject: [PATCH 07/31] 1.0.0 & major in change file --- .changes/opener-initial.md | 4 ++-- Cargo.lock | 2 +- examples/api/package.json | 2 +- examples/api/src-tauri/Cargo.toml | 2 +- package.json | 3 +-- plugins/opener/Cargo.toml | 2 +- plugins/opener/package.json | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.changes/opener-initial.md b/.changes/opener-initial.md index 29141fe52..151ea9c15 100644 --- a/.changes/opener-initial.md +++ b/.changes/opener-initial.md @@ -1,6 +1,6 @@ --- -"opener": "patch" -"opener-js": "patch" +"opener": "major" +"opener-js": "major" --- Initial Release diff --git a/Cargo.lock b/Cargo.lock index ad48616da..61484c128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6582,7 +6582,7 @@ dependencies = [ [[package]] name = "tauri-plugin-opener" -version = "2.0.0" +version = "1.0.0" dependencies = [ "dunce", "objc2-app-kit", diff --git a/examples/api/package.json b/examples/api/package.json index 185b9d37a..cf43229d9 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -18,7 +18,7 @@ "@tauri-apps/plugin-fs": "2.0.2", "@tauri-apps/plugin-geolocation": "2.0.0", "@tauri-apps/plugin-global-shortcut": "2.0.0", - "@tauri-apps/plugin-opener": "2.0.0", + "@tauri-apps/plugin-opener": "1.0.0", "@tauri-apps/plugin-haptics": "2.0.0", "@tauri-apps/plugin-http": "2.0.1", "@tauri-apps/plugin-nfc": "2.0.0", diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 414f69a46..90d278760 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -33,7 +33,7 @@ tauri-plugin-notification = { path = "../../../plugins/notification", version = ] } tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.1" } tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.1" } -tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.0.0" } +tauri-plugin-opener = { path = "../../../plugins/opener", version = "1.0.0" } tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.2" } tauri-plugin-store = { path = "../../../plugins/store", version = "2.1.0" } diff --git a/package.json b/package.json index cf6c46b8a..f1192e958 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,5 @@ }, "engines": { "pnpm": "^9.0.0" - }, - "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b" + } } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 6c6f09193..b162c8300 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-opener" -version = "2.0.0" +version = "1.0.0" edition = { workspace = true } authors = { workspace = true } license = { workspace = true } diff --git a/plugins/opener/package.json b/plugins/opener/package.json index a1f31f995..b3490ad1a 100644 --- a/plugins/opener/package.json +++ b/plugins/opener/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-opener", - "version": "2.0.0", + "version": "1.0.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" From 8555563919e97e1ad9711f3eefc474a8f66d0a9e Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 22:12:01 +0200 Subject: [PATCH 08/31] lock file and licenese headers --- plugins/opener/src/commands.rs | 4 ++++ plugins/opener/src/reveal_item_in_dir.rs | 4 ++++ plugins/opener/src/scope.rs | 4 ++++ plugins/opener/src/scope_entry.rs | 4 ++++ pnpm-lock.yaml | 2 +- 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index b3eec1037..0cd584518 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + use tauri::{ ipc::{CommandScope, GlobalScope}, AppHandle, Runtime, diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 89ce1492a..ca4b2ac94 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + use std::path::Path; /// Show diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index be4d0c861..ae3254162 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + use std::{marker::PhantomData, path::PathBuf, sync::Arc}; use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs index 105e2d591..20484887e 100644 --- a/plugins/opener/src/scope_entry.rs +++ b/plugins/opener/src/scope_entry.rs @@ -1,3 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + use std::path::PathBuf; use serde::Deserialize; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9ddcdcd8..62bb99327 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,7 +97,7 @@ importers: specifier: 2.0.0 version: link:../../plugins/notification '@tauri-apps/plugin-opener': - specifier: 2.0.0 + specifier: 1.0.0 version: link:../../plugins/opener '@tauri-apps/plugin-os': specifier: 2.0.0 From 5399e543e96ce5e556bef5e035e07534b1aff89f Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 7 Nov 2024 22:19:05 +0200 Subject: [PATCH 09/31] implement revealItemInDir in JS --- plugins/opener/Cargo.toml | 10 +++++----- plugins/opener/api-iife.js | 2 +- plugins/opener/guest-js/index.ts | 24 ++++++++++++++++++++---- plugins/opener/src/commands.rs | 8 ++++++-- plugins/opener/src/reveal_item_in_dir.rs | 2 +- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index b162c8300..e5a3fcf7e 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -45,11 +45,11 @@ dunce = { workspace = true } [target."cfg(windows)".dependencies.windows] version = "0.54" features = [ - "Win32_Foundation", - "Win32_UI_Shell_Common", - "Win32_UI_WindowsAndMessaging", - "Win32_System_Com", - "Win32_System_Registry", + "Win32_Foundation", + "Win32_UI_Shell_Common", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Com", + "Win32_System_Registry", ] [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 5b37b44e4..345837b28 100644 --- a/plugins/opener/api-iife.js +++ b/plugins/opener/api-iife.js @@ -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_item_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.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})} diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 0435fcc45..d586572fe 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -39,8 +39,6 @@ import { invoke } from '@tauri-apps/api/core' * ``` * * @param path The path or URL to open. - * This value is matched against the string regex defined on `tauri.conf.json > plugins > opener > 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. * @@ -52,6 +50,24 @@ export async function open(path: string, openWith?: string): Promise { with: openWith }) } -export async function revealInDir() { - return invoke('plugin:opener|reveal_item_in_dir') + +/** + * Reveal a path the system's default explorer. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported. + * + * @example + * ```typescript + * import { revealItemInDir } from '@tauri-apps/plugin-opener'; + * await revealItemInDir('/path/to/file'); + * ``` + * + * @param path The path to reveal. + * + * @since 2.0.0 + */ +export async function revealItemInDir(path: string) { + return invoke('plugin:opener|reveal_item_in_dir', { path }) } diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 0cd584518..0190ccdb4 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use std::path::PathBuf; + use tauri::{ ipc::{CommandScope, GlobalScope}, AppHandle, Runtime, @@ -32,11 +34,13 @@ pub async fn open( ); if scope.is_allowed(&path)? { - crate::open::open(path, with) + crate::open(path, with) } else { Err(Error::NotAllowed(path)) } } #[tauri::command] -pub async fn reveal_item_in_dir() {} +pub async fn reveal_item_in_dir(path: PathBuf) -> crate::Result<()> { + crate::reveal_item_in_dir(path) +} diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index ca4b2ac94..70f6cc75f 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -4,7 +4,7 @@ use std::path::Path; -/// Show +/// Reveal a path the system's default explorer. /// /// ## Platform-specific: /// From 51581012f96c4ba3d2518c4aa1e76f89427b256f Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 02:32:58 +0200 Subject: [PATCH 10/31] remove regex and urlpattern deps --- Cargo.lock | 5 +-- Cargo.toml | 1 - plugins/http/Cargo.toml | 4 +- plugins/opener/Cargo.toml | 6 +-- plugins/opener/build.rs | 7 +-- plugins/opener/package.json | 1 + .../permissions/allow-default-urls.toml | 4 +- plugins/opener/src/lib.rs | 4 +- plugins/opener/src/reveal_item_in_dir.rs | 2 + plugins/opener/src/scope.rs | 44 +++---------------- 10 files changed, 22 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bf093898..4cac7181c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6564,18 +6564,17 @@ name = "tauri-plugin-opener" version = "1.0.0" dependencies = [ "dunce", + "glob", "objc2-app-kit", "objc2-foundation", "open", - "regex", "schemars", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror", + "thiserror 2.0.3", "url", - "urlpattern", "windows 0.54.0", "zbus", ] diff --git a/Cargo.toml b/Cargo.toml index a4593f0fa..f1fee0435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ schemars = "0.8" dunce = "1" specta = "=2.0.0-rc.20" glob = "0.3" -urlpattern = "0.3" zbus = "4" #tauri-specta = "=2.0.0-rc.11" diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index 7b008c14f..8c1801a38 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -25,7 +25,7 @@ tauri-plugin = { workspace = true, features = ["build"] } schemars = { workspace = true } serde = { workspace = true } url = { workspace = true } -urlpattern = { workspace = true } +urlpattern = "0.3" regex = "1" [dependencies] @@ -35,7 +35,7 @@ tauri = { workspace = true } thiserror = { workspace = true } tokio = { version = "1", features = ["sync", "macros"] } tauri-plugin-fs = { path = "../fs", version = "2.0.3" } -urlpattern = { workspace = true } +urlpattern = "0.3" regex = "1" http = "1" reqwest = { version = "0.12", default-features = false } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index e5a3fcf7e..bd471081d 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "tauri-plugin-opener" version = "1.0.0" +description = "Open files and URLs using their default application." edition = { workspace = true } authors = { workspace = true } license = { workspace = true } @@ -26,8 +27,6 @@ ios = { level = "partial", notes = "Only allows to open URLs via `open`" } tauri-plugin = { workspace = true, features = ["build"] } schemars = { workspace = true } serde = { workspace = true } -urlpattern = { workspace = true } -regex = "1" [dependencies] serde = { workspace = true } @@ -35,9 +34,8 @@ serde_json = { workspace = true } tauri = { workspace = true } thiserror = { workspace = true } open = { version = "5", features = ["shellexecute-on-windows"] } -urlpattern = { workspace = true } -regex = "1" url = { workspace = true } +glob = { workspace = true } [target."cfg(windows)".dependencies] dunce = { workspace = true } diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 6bccbdd94..429262330 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -15,15 +15,12 @@ mod scope; enum OpenerScopeEntry { Url { /// A URL that can be opened by the webview when using the Opener APIs. - /// Wildcards can be used following the URL pattern standard. /// - /// See [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information. + /// Wildcards can be used following the UNIX glob pattern. /// /// Examples: /// - /// - "https://*" : allows all HTTPS origin on port 443 - /// - /// - "https://*:*" : allows all HTTPS origin on any port + /// - "https://*" : allows all HTTPS origin /// /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path /// diff --git a/plugins/opener/package.json b/plugins/opener/package.json index b3490ad1a..c3dc314ef 100644 --- a/plugins/opener/package.json +++ b/plugins/opener/package.json @@ -1,6 +1,7 @@ { "name": "@tauri-apps/plugin-opener", "version": "1.0.0", + "description": "Open files and URLs using their default application.", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/opener/permissions/allow-default-urls.toml b/plugins/opener/permissions/allow-default-urls.toml index cad02770b..93495b897 100644 --- a/plugins/opener/permissions/allow-default-urls.toml +++ b/plugins/opener/permissions/allow-default-urls.toml @@ -11,7 +11,7 @@ url = "mailto:*" url = "tel:*" [[permission.scope.allow]] -url = "http://*:*" +url = "http://*" [[permission.scope.allow]] -url = "https://*:*" +url = "https://*" diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 489368749..1ad71c473 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -73,9 +73,9 @@ pub fn init() -> TauriPlugin { .js_init_script(include_str!("init-iife.js").to_string()) .setup(|app, _api| { #[cfg(target_os = "android")] - let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; + let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; #[cfg(target_os = "ios")] - let handle = api.register_ios_plugin(init_plugin_opener)?; + let handle = _api.register_ios_plugin(init_plugin_opener)?; app.manage(Opener { app: app.clone(), diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 70f6cc75f..fbd91e290 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -183,5 +183,7 @@ mod imp { let workspace = NSWorkspace::new(); workspace.activateFileViewerSelectingURLs(&urls); } + + Ok(()) } } diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index ae3254162..b51993036 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -7,37 +7,15 @@ 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(Box), + Url(glob::Pattern), Path(Option), } -fn parse_url_pattern( - s: &str, -) -> std::result::Result { - let mut init = urlpattern::UrlPatternInit::parse_constructor_string::(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; @@ -48,14 +26,10 @@ impl ScopeObject for Entry { serde_json::from_value(raw.into()) .and_then(|raw| { let entry = match raw { - EntryRaw::Url { url } => { - let url_pattern = parse_url_pattern(&url).map_err(|e| { - serde::de::Error::custom(format!( - "`{url}` is not a valid URL pattern: {e}" - )) - })?; - Entry::Url(Box::new(url_pattern)) - } + EntryRaw::Url { url } => Entry::Url( + glob::Pattern::new(&url) + .map_err(|e| serde::de::Error::custom(e.to_string()))?, + ), EntryRaw::Path { path } => { let path = match app.path().parse(path) { Ok(path) => Some(path), @@ -106,18 +80,14 @@ impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { 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::Url(url_pattern) => url_pattern.matches(url.as_str()), 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::Url(url_pattern) => url_pattern.matches(url.as_str()), Entry::Path { .. } => false, }) } From a24915eb2cf8da9ed1e3286e4508909f47a2912b Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 02:38:42 +0200 Subject: [PATCH 11/31] error msg --- plugins/opener/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 2f9dff4bc..42de29273 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -20,7 +20,7 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - #[error("Not allowed to open {0}")] + #[error("Not allowed to open forbidden path or url: {0}")] NotAllowed(String), #[error("API not supported on the current platform")] UnsupportedPlatform, From 7f7f3e4633a3673f7a7a7c20ffeefe63370d11b4 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 02:42:17 +0200 Subject: [PATCH 12/31] lock file --- pnpm-lock.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f424404f..ad6a07640 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,7 +256,7 @@ importers: dependencies: '@tauri-apps/api': specifier: ^2.0.0 - version: 2.0.3 + version: 2.1.1 plugins/os: dependencies: @@ -2424,13 +2424,14 @@ snapshots: picocolors: 1.1.0 sisteransi: 1.0.5 - '@covector/apply@0.10.0': + '@covector/apply@0.10.0(mocha@10.7.3)': dependencies: '@covector/files': 0.8.0 effection: 2.0.8(mocha@10.7.3) semver: 7.6.3 transitivePeerDependencies: - encoding + - mocha '@covector/assemble@0.12.0': dependencies: From eb88ad5a62a1ec01678a449c7382a62ae52038d5 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 04:27:09 +0200 Subject: [PATCH 13/31] try to fix CI --- plugins/opener/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index bd471081d..d904443ba 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -60,3 +60,6 @@ features = ["NSWorkspace"] [target."cfg(target_os = \"macos\")".dependencies.objc2-foundation] version = "0.2" features = ["NSURL", "NSArray", "NSString"] + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } \ No newline at end of file From f3a5f88702b3881559381f0b1a21056377f2cab5 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 04:32:58 +0200 Subject: [PATCH 14/31] fmt --- plugins/opener/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index d904443ba..16c75b12d 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -62,4 +62,4 @@ version = "0.2" features = ["NSURL", "NSArray", "NSString"] [target.'cfg(target_os = "ios")'.dependencies] -tauri = { workspace = true, features = ["wry"] } \ No newline at end of file +tauri = { workspace = true, features = ["wry"] } From f5da7859024b883dc3f9d5b563eac3ad5fc52702 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 16:01:06 +0200 Subject: [PATCH 15/31] update window crate --- Cargo.lock | 22 +--------------------- plugins/opener/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cac7181c..f0451beb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6575,7 +6575,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.3", "url", - "windows 0.54.0", + "windows 0.58.0", "zbus", ] @@ -7946,16 +7946,6 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.56.0" @@ -7985,16 +7975,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.56.0" diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 16c75b12d..e1f4154aa 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -41,7 +41,7 @@ glob = { workspace = true } dunce = { workspace = true } [target."cfg(windows)".dependencies.windows] -version = "0.54" +version = "0.58" features = [ "Win32_Foundation", "Win32_UI_Shell_Common", From adf4d52445a63e59c636cfc1de3e7bc4ec32c4a3 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 16:01:26 +0200 Subject: [PATCH 16/31] strong type openWith paramter --- plugins/opener/guest-js/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index d586572fe..9484f5bde 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -20,6 +20,19 @@ import { invoke } from '@tauri-apps/api/core' +export type Program = + | 'firefox' + | 'google chrome' + | 'chromium' + | 'safari' + | 'open' + | 'start' + | 'xdg-open' + | 'gio' + | 'gnome-open' + | 'kde-open' + | 'wslview' + /** * Opens a path or URL with the system's default app, * or the one specified with `openWith`. @@ -44,7 +57,7 @@ import { invoke } from '@tauri-apps/api/core' * * @since 2.0.0 */ -export async function open(path: string, openWith?: string): Promise { +export async function open(path: string, openWith?: Program): Promise { await invoke('plugin:opener|open', { path, with: openWith From bcc6c092f265813afba6df1d580de86374069d85 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 17:04:49 +0200 Subject: [PATCH 17/31] split into openUrl and openPath --- Cargo.lock | 1 - plugins/opener/Cargo.toml | 1 - plugins/opener/api-iife.js | 2 +- plugins/opener/build.rs | 2 +- plugins/opener/guest-js/index.ts | 54 ++++++++++++++----- .../autogenerated/commands/open_path.toml | 13 +++++ .../autogenerated/commands/open_url.toml | 13 +++++ .../permissions/autogenerated/reference.md | 54 ++++++++++++++++++- plugins/opener/permissions/default.toml | 6 ++- .../opener/permissions/schemas/schema.json | 20 +++++++ plugins/opener/src/commands.rs | 39 ++++++++++++-- plugins/opener/src/error.rs | 6 ++- plugins/opener/src/lib.rs | 23 ++++++-- plugins/opener/src/open.rs | 54 ++++++++++--------- plugins/opener/src/scope.rs | 24 ++++----- 15 files changed, 241 insertions(+), 71 deletions(-) create mode 100644 plugins/opener/permissions/autogenerated/commands/open_path.toml create mode 100644 plugins/opener/permissions/autogenerated/commands/open_url.toml diff --git a/Cargo.lock b/Cargo.lock index f0451beb7..8b5b67c2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6574,7 +6574,6 @@ dependencies = [ "tauri", "tauri-plugin", "thiserror 2.0.3", - "url", "windows 0.58.0", "zbus", ] diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index e1f4154aa..81a220360 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -34,7 +34,6 @@ serde_json = { workspace = true } tauri = { workspace = true } thiserror = { workspace = true } open = { version = "5", features = ["shellexecute-on-windows"] } -url = { workspace = true } glob = { workspace = true } [target."cfg(windows)".dependencies] diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 345837b28..30415a61e 100644 --- a/plugins/opener/api-iife.js +++ b/plugins/opener/api-iife.js @@ -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.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},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.openPath=async function(n,_){await e("plugin:opener|open_path",{path:n,with:_})},n.openUrl=async function(n,_){await e("plugin:opener|open_url",{url:n,with:_})},n.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 429262330..546805a6f 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -51,7 +51,7 @@ fn _f() { }; } -const COMMANDS: &[&str] = &["open", "reveal_item_in_dir"]; +const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir"]; fn main() { tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 9484f5bde..376017577 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -34,31 +34,57 @@ export type Program = | 'wslview' /** - * 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`. + * Opens a url with the system's default app, or the one specified with {@linkcode openWith}. * * @example * ```typescript - * import { open } from '@tauri-apps/plugin-opener'; + * import { openUrl } from '@tauri-apps/plugin-opener'; + * * // opens the given URL on the default browser: - * await open('https://github.com/tauri-apps/tauri'); + * await openUrl('https://github.com/tauri-apps/tauri'); * // opens the given URL using `firefox`: - * await open('https://github.com/tauri-apps/tauri', 'firefox'); + * await openUrl('https://github.com/tauri-apps/tauri', 'firefox'); + * ``` + * + * @param url The URL to open. + * @param openWith The app to open the URL with. + * Must be one of `firefox`, `google chrome`, `chromium` `safari`, `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. + * If not specified, defaults to the system default application for the specified url type. + * + * @since 2.0.0 + */ +export async function openUrl(url: string, openWith?: Program): Promise { + await invoke('plugin:opener|open_url', { + url, + with: openWith + }) +} + +/** + * Opens a path with the system's default app, or the one specified with {@linkcode openWith}. + * + * @example + * ```typescript + * import { openPath } from '@tauri-apps/plugin-opener'; + * * // opens a file using the default program: - * await open('/path/to/file'); + * await openPath('/path/to/file'); + * // opens a file using `start` command on Windows. + * await openPath('C:/path/to/file', 'start'); * ``` * - * @param path The path or URL to open. - * @param openWith The app to open the file or URL with. - * Defaults to the system default application for the specified path type. + * @param path The path to open. + * @param openWith The app to open the path with. + * Must be one of `firefox`, `google chrome`, `chromium` `safari`, `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. + * If not specified, defaults to the system default application for the specified path type. * * @since 2.0.0 */ -export async function open(path: string, openWith?: Program): Promise { - await invoke('plugin:opener|open', { +export async function openPath( + path: string, + openWith?: Program +): Promise { + await invoke('plugin:opener|open_path', { path, with: openWith }) diff --git a/plugins/opener/permissions/autogenerated/commands/open_path.toml b/plugins/opener/permissions/autogenerated/commands/open_path.toml new file mode 100644 index 000000000..ae67b9394 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open_path.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open-path" +description = "Enables the open_path command without any pre-configured scope." +commands.allow = ["open_path"] + +[[permission]] +identifier = "deny-open-path" +description = "Denies the open_path command without any pre-configured scope." +commands.deny = ["open_path"] diff --git a/plugins/opener/permissions/autogenerated/commands/open_url.toml b/plugins/opener/permissions/autogenerated/commands/open_url.toml new file mode 100644 index 000000000..f1e694b1b --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open_url.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open-url" +description = "Enables the open_url command without any pre-configured scope." +commands.allow = ["open_url"] + +[[permission]] +identifier = "deny-open-url" +description = "Denies the open_url command without any pre-configured scope." +commands.deny = ["open_url"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index b49b335b4..bfefeb360 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -3,7 +3,7 @@ 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 -- `allow-open` +- `allow-open-url` - `allow-reveal-item-in-dir` - `allow-default-urls` @@ -58,6 +58,58 @@ Denies the open command without any pre-configured scope. +`opener:allow-open-path` + + + + +Enables the open_path command without any pre-configured scope. + + + + + + + +`opener:deny-open-path` + + + + +Denies the open_path command without any pre-configured scope. + + + + + + + +`opener:allow-open-url` + + + + +Enables the open_url command without any pre-configured scope. + + + + + + + +`opener:deny-open-url` + + + + +Denies the open_url command without any pre-configured scope. + + + + + + + `opener:allow-reveal-item-in-dir` diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index f9e3184d2..300832c85 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -3,4 +3,8 @@ [default] 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"] +permissions = [ + "allow-open-url", + "allow-reveal-item-in-dir", + "allow-default-urls", +] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 22d9d40dd..48b90b0b1 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -309,6 +309,26 @@ "type": "string", "const": "deny-open" }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "allow-open-path" + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "deny-open-path" + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "allow-open-url" + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "deny-open-url" + }, { "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", "type": "string", diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 0190ccdb4..09d41b108 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tauri::{ ipc::{CommandScope, GlobalScope}, @@ -12,7 +12,7 @@ use tauri::{ use crate::{open::Program, scope::Scope, Error}; #[tauri::command] -pub async fn open( +pub async fn open_url( app: AppHandle, command_scope: CommandScope, global_scope: GlobalScope, @@ -33,10 +33,39 @@ pub async fn open( .collect(), ); - if scope.is_allowed(&path)? { - crate::open(path, with) + if scope.is_url_allowed(&path) { + crate::open_url(path, with) } else { - Err(Error::NotAllowed(path)) + Err(Error::ForbiddenUrl(path)) + } +} + +#[tauri::command] +pub async fn open_path( + app: AppHandle, + command_scope: CommandScope, + global_scope: GlobalScope, + path: String, + with: Option, +) -> crate::Result<()> { + 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_path_allowed(Path::new(&path))? { + crate::open_path(path, with) + } else { + Err(Error::ForbiddenPath(path)) } } diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 42de29273..f1603d640 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -20,8 +20,10 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - #[error("Not allowed to open forbidden path or url: {0}")] - NotAllowed(String), + #[error("Not allowed to open forbidden path: {0}")] + ForbiddenPath(String), + #[error("Not allowed to open forbidden url: {0}")] + ForbiddenUrl(String), #[error("API not supported on the current platform")] UnsupportedPlatform, #[error(transparent)] diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 1ad71c473..85a4f163f 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -26,7 +26,7 @@ mod scope_entry; pub use error::Error; type Result = std::result::Result; -pub use open::{open, Program}; +pub use open::{open_path, open_url, Program}; pub use reveal_item_in_dir::reveal_item_in_dir; pub struct Opener { @@ -37,15 +37,29 @@ pub struct Opener { } impl Opener { + /// Open a url with a default or specific browser opening program. + #[cfg(desktop)] + pub fn open_url(&self, url: impl Into, with: Option) -> Result<()> { + open::open(url.into(), with).map_err(Into::into) + } + + /// Open a url with a default or specific browser opening program. + #[cfg(mobile)] + pub fn open_url(&self, url: impl Into, _with: Option) -> Result<()> { + self.mobile_plugin_handle + .run_mobile_plugin("open", url.into()) + .map_err(Into::into) + } + /// Open a (url) path with a default or specific browser opening program. #[cfg(desktop)] - pub fn open(&self, path: impl Into, with: Option) -> Result<()> { + pub fn open_path(&self, path: impl Into, with: Option) -> Result<()> { open::open(path.into(), with).map_err(Into::into) } /// Open a (url) path with a default or specific browser opening program. #[cfg(mobile)] - pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { + pub fn open_path(&self, path: impl Into, _with: Option) -> Result<()> { self.mobile_plugin_handle .run_mobile_plugin("open", path.into()) .map_err(Into::into) @@ -85,7 +99,8 @@ pub fn init() -> TauriPlugin { Ok(()) }) .invoke_handler(tauri::generate_handler![ - commands::open, + commands::open_url, + commands::open_path, commands::reveal_item_in_dir ]) .build() diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs index b472cb34c..03cf9584e 100644 --- a/plugins/opener/src/open.rs +++ b/plugins/opener/src/open.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer}; -use std::{fmt::Display, str::FromStr}; +use std::{ffi::OsStr, fmt::Display, path::Path, str::FromStr}; /// Program to use on the [`open()`] call. #[derive(Debug)] @@ -123,40 +123,44 @@ impl Program { } } -/// Opens path or URL with the program specified in `with`, or system default if `None`. +pub(crate) fn open>(path: P, with: Option) -> crate::Result<()> { + match with.map(Program::name) { + Some(program) => ::open::with_detached(path, program), + None => ::open::that_detached(path), + } + .map_err(Into::into) +} + +/// Opens URL with the program specified in `with`, or system default if `None`. +/// +/// # Examples /// -/// 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`. +/// ```rust,no_run +/// tauri::Builder::default() +/// .setup(|app| { +/// // open the given URL on the system default browser +/// tauri_plugin_opener::open_url("https://github.com/tauri-apps/tauri", None)?; +/// Ok(()) +/// }); +/// ``` +pub fn open_url>(url: P, with: Option) -> crate::Result<()> { + let url = url.as_ref(); + open(url, with) +} + +/// Opens path with the program specified in `with`, or system default if `None`. /// /// # 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)?; +/// tauri_plugin_opener::open_path("/path/to/file", None)?; /// Ok(()) /// }); /// ``` -pub fn open>(path: P, with: Option) -> crate::Result<()> { +pub fn open_path>(path: P, with: Option) -> crate::Result<()> { let path = path.as_ref(); - - // // ensure we pass validation if the configuration has one - // if let Some(regex) = &scope.open { - // if !regex.is_match(path) { - // return Err(Error::Validation { - // index: 0, - // validation: regex.as_str().into(), - // }); - // } - // } - - // 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) + open(path, with) } diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index b51993036..3fda73cd0 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{marker::PhantomData, path::PathBuf, sync::Arc}; +use std::{ + marker::PhantomData, + path::{Path, PathBuf}, + sync::Arc, +}; use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; -use url::Url; - use crate::{scope_entry::EntryRaw, Error}; #[derive(Debug)] @@ -70,30 +72,22 @@ impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { } } - pub fn is_allowed(&self, path_or_url: &str) -> crate::Result { - 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 { + pub fn is_url_allowed(&self, url: &str) -> bool { let denied = self.denied.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url.as_str()), + Entry::Url(url_pattern) => url_pattern.matches(url), Entry::Path { .. } => false, }); if denied { false } else { self.allowed.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url.as_str()), + Entry::Url(url_pattern) => url_pattern.matches(url), Entry::Path { .. } => false, }) } } - pub fn is_path_allowed(&self, path: &str) -> crate::Result { + pub fn is_path_allowed(&self, path: &Path) -> crate::Result { let fs_scope = tauri::fs::Scope::new( self.manager, &tauri::utils::config::FsScope::Scope { From e0264cca3c1e59dda591fafa4ba4b1e327bdbedc Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 17:45:56 +0200 Subject: [PATCH 18/31] refactor init script --- plugins/opener/guest-js/init.ts | 67 +++++++++++++++++---------------- plugins/opener/src/init-iife.js | 2 +- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index ef7f45542..b77c7452f 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -5,36 +5,39 @@ import { invoke } from '@tauri-apps/api/core' // open
links with the API -function openLinks(): void { - document.querySelector('body')?.addEventListener('click', function (e) { - let target: HTMLElement | null = e.target as HTMLElement - while (target) { - if (target.matches('a')) { - const t = target as HTMLAnchorElement - if ( - t.href !== '' && - ['http://', 'https://', 'mailto:', 'tel:'].some((v) => - t.href.startsWith(v) - ) && - t.target === '_blank' - ) { - void invoke('plugin:opener|open', { - path: t.href - }) - e.preventDefault() - } - break - } - target = target.parentElement - } +window.addEventListener('click', function (evt) { + if ( + evt.defaultPrevented || + evt.button !== 0 || + evt.metaKey || + evt.altKey || + evt.ctrlKey || + evt.shiftKey + ) + return + + const a = evt + .composedPath() + .find((el) => el instanceof Node && el.nodeName.toUpperCase() === 'A') as + | HTMLAnchorElement + | undefined + + // only open if supposed to be open in a new tab + if (!a || !a.href || a.target !== '_blank') return + + const url = new URL(a.href) + + if ( + // only open if not same origin + url.origin === window.location.origin || + // only open default protocols + ['http:', 'https:', 'mailto:', 'tel:'].every((p) => url.protocol !== p) + ) + return + + evt.preventDefault() + + void invoke('plugin:opener|open_url', { + path: url }) -} - -if ( - document.readyState === 'complete' || - document.readyState === 'interactive' -) { - openLinks() -} else { - window.addEventListener('DOMContentLoaded', openLinks, true) -} +}) diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index 47e1df39b..4703edc83 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";async function e(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function t(){document.querySelector("body")?.addEventListener("click",(function(t){let n=t.target;for(;n;){if(n.matches("a")){const r=n;""!==r.href&&["http://","https://","mailto:","tel:"].some((e=>r.href.startsWith(e)))&&"_blank"===r.target&&(e("plugin:opener|open",{path:r.href}),t.preventDefault());break}n=n.parentElement}}))}"function"==typeof SuppressedError&&SuppressedError,"complete"===document.readyState||"interactive"===document.readyState?t():window.addEventListener("DOMContentLoaded",t,!0)}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{path:n}))}))}(); From 95946392f53e93ce48ee234260b9bc7ab3c2a364 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 12 Nov 2024 18:09:10 +0200 Subject: [PATCH 19/31] fmt --- plugins/opener/permissions/default.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index 300832c85..846d6e51b 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -4,7 +4,7 @@ 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-url", - "allow-reveal-item-in-dir", - "allow-default-urls", + "allow-open-url", + "allow-reveal-item-in-dir", + "allow-default-urls", ] From 6d62ec79ec5c41c7d47006d675f934db122bedeb Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 13 Nov 2024 04:48:12 +0200 Subject: [PATCH 20/31] update example --- examples/api/src-tauri/capabilities/base.json | 6 +- examples/api/src/views/Opener.svelte | 63 ++++++++++++++++--- plugins/opener/src/commands.rs | 8 +-- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index ae93aeefb..92532e7d1 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -81,6 +81,10 @@ "deny": ["$APPDATA/db/*.stronghold"] }, "store:default", - "opener:default" + "opener:default", + { + "identifier": "opener:allow-open-path", + "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }] + } ] } diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte index 3a2afaab5..5cc16b20b 100644 --- a/examples/api/src/views/Opener.svelte +++ b/examples/api/src/views/Opener.svelte @@ -1,5 +1,5 @@ -
+
+
+ + + + + with + +
+
+ + with - {#each programs as p} {/each} +
- +
+ + +
diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 09d41b108..aa38a5c3a 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -16,7 +16,7 @@ pub async fn open_url( app: AppHandle, command_scope: CommandScope, global_scope: GlobalScope, - path: String, + url: String, with: Option, ) -> crate::Result<()> { let scope = Scope::new( @@ -33,10 +33,10 @@ pub async fn open_url( .collect(), ); - if scope.is_url_allowed(&path) { - crate::open_url(path, with) + if scope.is_url_allowed(&url) { + crate::open_url(url, with) } else { - Err(Error::ForbiddenUrl(path)) + Err(Error::ForbiddenUrl(url)) } } From ec35bf3625e215d03a53e93bc4e1c78245b5e874 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 13 Nov 2024 04:51:46 +0200 Subject: [PATCH 21/31] fix evt.ctrlKey and evt.shiftKey --- plugins/opener/guest-js/init.ts | 18 ++++++++---------- plugins/opener/src/init-iife.js | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index b77c7452f..d2070e8e2 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -6,14 +6,7 @@ import { invoke } from '@tauri-apps/api/core' // open
links with the API window.addEventListener('click', function (evt) { - if ( - evt.defaultPrevented || - evt.button !== 0 || - evt.metaKey || - evt.altKey || - evt.ctrlKey || - evt.shiftKey - ) + if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey) return const a = evt @@ -22,8 +15,13 @@ window.addEventListener('click', function (evt) { | HTMLAnchorElement | undefined - // only open if supposed to be open in a new tab - if (!a || !a.href || a.target !== '_blank') return + if ( + !a || + !a.href || + // only open if supposed to be open in a new tab + !(a.target === '_blank' || evt.ctrlKey || evt.shiftKey) + ) + return const url = new URL(a.href) diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index 4703edc83..ac75587c4 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{path:n}))}))}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{path:n}))}))}(); From 7cd8b23e5613b196b1f02732bf40a8e773178a03 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 13 Nov 2024 04:55:59 +0200 Subject: [PATCH 22/31] simplify and fix leak --- plugins/opener/src/reveal_item_in_dir.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index fbd91e290..098a7c5d2 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -64,10 +64,10 @@ mod imp { .ok_or_else(|| crate::Error::NoParent(file.to_path_buf()))?; let dir = HSTRING::from(dir); - let dir_item = unsafe { ILCreateFromPathW(PCWSTR::from_raw(dir.as_ptr())) }; + let dir_item = unsafe { ILCreateFromPathW(&dir) }; let file_h = HSTRING::from(file); - let file_item = unsafe { ILCreateFromPathW(PCWSTR::from_raw(file_h.as_ptr())) }; + let file_item = unsafe { ILCreateFromPathW(&file_h) }; unsafe { if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 0) { @@ -86,7 +86,10 @@ mod imp { ..std::mem::zeroed() }; - ShellExecuteExW(&mut info)?; + ShellExecuteExW(&mut info).inspect_err(|_| { + ILFree(Some(dir_item)); + ILFree(Some(file_item)); + })?; } } } From 5df1e4b3841a3428e66e6dcd7c4314842dd333d7 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 14 Nov 2024 13:54:06 +0200 Subject: [PATCH 23/31] remove lefover --- .../autogenerated/commands/open.toml | 13 ----- .../autogenerated/commands/reveal_in_dir.toml | 13 ----- .../permissions/autogenerated/reference.md | 52 ------------------- .../opener/permissions/schemas/schema.json | 20 ------- 4 files changed, 98 deletions(-) delete mode 100644 plugins/opener/permissions/autogenerated/commands/open.toml delete mode 100644 plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml diff --git a/plugins/opener/permissions/autogenerated/commands/open.toml b/plugins/opener/permissions/autogenerated/commands/open.toml deleted file mode 100644 index 4ea6dff16..000000000 --- a/plugins/opener/permissions/autogenerated/commands/open.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-open" -description = "Enables the open command without any pre-configured scope." -commands.allow = ["open"] - -[[permission]] -identifier = "deny-open" -description = "Denies the open command without any pre-configured scope." -commands.deny = ["open"] diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml deleted file mode 100644 index e669620ff..000000000 --- a/plugins/opener/permissions/autogenerated/commands/reveal_in_dir.toml +++ /dev/null @@ -1,13 +0,0 @@ -# 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"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index bfefeb360..66c232c6f 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -32,32 +32,6 @@ This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using thei -`opener:allow-open` - - - - -Enables the open command without any pre-configured scope. - - - - - - - -`opener:deny-open` - - - - -Denies the open command without any pre-configured scope. - - - - - - - `opener:allow-open-path` @@ -130,32 +104,6 @@ Enables the reveal_item_in_dir command without any pre-configured scope. Denies the reveal_item_in_dir command without any pre-configured scope. - - - - - - -`opener:allow-reveal-item-in-dir` - - - - -Enables the reveal_item_in_dir command without any pre-configured scope. - - - - - - - -`opener:deny-reveal-item-in-dir` - - - - -Denies the reveal_item_in_dir command without any pre-configured scope. - diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 48b90b0b1..b958ac636 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -299,16 +299,6 @@ "type": "string", "const": "allow-default-urls" }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "allow-open" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "deny-open" - }, { "description": "Enables the open_path command without any pre-configured scope.", "type": "string", @@ -339,16 +329,6 @@ "type": "string", "const": "deny-reveal-item-in-dir" }, - { - "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", - "type": "string", - "const": "allow-reveal-item-in-dir" - }, - { - "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", - "type": "string", - "const": "deny-reveal-item-in-dir" - }, { "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer", "type": "string", From b696024611375c787dcad9a8e6e03ac47e46d5a9 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 14 Nov 2024 14:24:02 +0200 Subject: [PATCH 24/31] fix example, fix linux, document --- Cargo.lock | 1 + examples/api/src/views/Opener.svelte | 2 +- plugins/opener/Cargo.toml | 1 + plugins/opener/guest-js/init.ts | 34 +++++++++++++++++++++++----- plugins/opener/src/init-iife.js | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b5b67c2b..f0451beb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6574,6 +6574,7 @@ dependencies = [ "tauri", "tauri-plugin", "thiserror 2.0.3", + "url", "windows 0.58.0", "zbus", ] diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte index 5cc16b20b..6ffaca887 100644 --- a/examples/api/src/views/Opener.svelte +++ b/examples/api/src/views/Opener.svelte @@ -32,7 +32,7 @@ function openPath() { opener - .openPath(path, pathProgram === 'Default' ? undefined : urlProgram) + .openPath(path, pathProgram === 'Default' ? undefined : pathProgram) .catch(onMessage) } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 81a220360..20eee7d21 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -51,6 +51,7 @@ features = [ [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] zbus = { workspace = true } +url = { workspace = true } [target."cfg(target_os = \"macos\")".dependencies.objc2-app-kit] version = "0.2" diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index d2070e8e2..9017c5ef9 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -6,7 +6,19 @@ import { invoke } from '@tauri-apps/api/core' // open links with the API window.addEventListener('click', function (evt) { - if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey) + console.log(evt.button) + + // return early if + if ( + // event was prevented + evt.defaultPrevented || + // or not a left click or middle click + evt.button !== 0 || + // or meta key pressed + evt.metaKey || + // or al key pressed + evt.altKey + ) return const a = evt @@ -15,20 +27,30 @@ window.addEventListener('click', function (evt) { | HTMLAnchorElement | undefined + // return early if if ( + // not tirggered from element !a || + // or doesn't have a href !a.href || - // only open if supposed to be open in a new tab - !(a.target === '_blank' || evt.ctrlKey || evt.shiftKey) + // or not supposed to be open in a new tab + !( + a.target === '_blank' || + // or ctrl key pressed + evt.ctrlKey || + // or shift key pressed + evt.shiftKey + ) ) return const url = new URL(a.href) + // return early if if ( - // only open if not same origin + // same origin (internal navigation) url.origin === window.location.origin || - // only open default protocols + // not default protocols ['http:', 'https:', 'mailto:', 'tel:'].every((p) => url.protocol !== p) ) return @@ -36,6 +58,6 @@ window.addEventListener('click', function (evt) { evt.preventDefault() void invoke('plugin:opener|open_url', { - path: url + url }) }) diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index ac75587c4..34fb3ea9d 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{path:n}))}))}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(console.log(e.button),e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); From 768e3ecbcfe86c6a9f5071748d5db021b83327b7 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 14 Nov 2024 15:36:25 +0200 Subject: [PATCH 25/31] remove `Program` enum --- plugins/opener/build.rs | 71 ++++++++++++++-- plugins/opener/guest-js/init.ts | 2 - plugins/opener/src/commands.rs | 14 +-- plugins/opener/src/error.rs | 8 +- plugins/opener/src/init-iife.js | 2 +- plugins/opener/src/lib.rs | 48 ++++++++--- plugins/opener/src/open.rs | 137 +++--------------------------- plugins/opener/src/scope.rs | 94 ++++++++++++-------- plugins/opener/src/scope_entry.rs | 26 +++++- 9 files changed, 208 insertions(+), 194 deletions(-) diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 546805a6f..fbad4d3ac 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -8,6 +8,25 @@ use std::path::PathBuf; #[allow(dead_code)] mod scope; +/// Opener scope application. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +enum Application { + /// Open in default application. + Default, + /// If true, allow open with any application. + Enable(bool), + /// Allow specific application to open with. + App(String), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + /// Opener scope entry. #[derive(schemars::JsonSchema)] #[serde(untagged)] @@ -26,6 +45,9 @@ enum OpenerScopeEntry { /// /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" url: String, + /// An application to open this url with, for example: firefox. + #[serde(default)] + app: Application, }, Path { /// A path that can be opened by the webview when using the Opener APIs. @@ -36,18 +58,55 @@ enum OpenerScopeEntry { /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. path: PathBuf, + /// An application to open this path with, for example: xdg-open. + #[serde(default)] + app: Application, }, } // Ensure `OpenerScopeEntry` and `scope::EntryRaw` is kept in sync fn _f() { - match (scope::EntryRaw::Url { url: String::new() }) { - scope::EntryRaw::Url { url } => OpenerScopeEntry::Url { url }, - scope::EntryRaw::Path { path } => OpenerScopeEntry::Path { path }, + match (scope::EntryRaw::Url { + url: String::new(), + app: scope::Application::Enable(true), + }) { + scope::EntryRaw::Url { url, app } => OpenerScopeEntry::Url { + url, + app: match app { + scope::Application::Enable(p) => Application::Enable(p), + scope::Application::App(p) => Application::App(p), + scope::Application::Default => Application::Default, + }, + }, + scope::EntryRaw::Path { path, app } => OpenerScopeEntry::Path { + path, + app: match app { + scope::Application::Enable(p) => Application::Enable(p), + scope::Application::App(p) => Application::App(p), + scope::Application::Default => Application::Default, + }, + }, }; - match (OpenerScopeEntry::Url { url: String::new() }) { - OpenerScopeEntry::Url { url } => scope::EntryRaw::Url { url }, - OpenerScopeEntry::Path { path } => scope::EntryRaw::Path { path }, + match (OpenerScopeEntry::Url { + url: String::new(), + app: Application::Enable(true), + }) { + OpenerScopeEntry::Url { url, app } => scope::EntryRaw::Url { + url, + app: match app { + Application::Enable(p) => scope::Application::Enable(p), + Application::App(p) => scope::Application::App(p), + Application::Default => scope::Application::Default, + }, + }, + OpenerScopeEntry::Path { path, app } => scope::EntryRaw::Path { + path, + app: match app { + Application::Enable(p) => scope::Application::Enable(p), + Application::App(p) => scope::Application::App(p), + Application::Default => scope::Application::Default, + }, + }, }; } diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index 9017c5ef9..6cc20dcde 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -6,8 +6,6 @@ import { invoke } from '@tauri-apps/api/core' // open links with the API window.addEventListener('click', function (evt) { - console.log(evt.button) - // return early if if ( // event was prevented diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index aa38a5c3a..1b46141b0 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -9,7 +9,7 @@ use tauri::{ AppHandle, Runtime, }; -use crate::{open::Program, scope::Scope, Error}; +use crate::{scope::Scope, Error}; #[tauri::command] pub async fn open_url( @@ -17,7 +17,7 @@ pub async fn open_url( command_scope: CommandScope, global_scope: GlobalScope, url: String, - with: Option, + with: Option, ) -> crate::Result<()> { let scope = Scope::new( &app, @@ -33,10 +33,10 @@ pub async fn open_url( .collect(), ); - if scope.is_url_allowed(&url) { + if scope.is_url_allowed(&url, with.as_deref()) { crate::open_url(url, with) } else { - Err(Error::ForbiddenUrl(url)) + Err(Error::ForbiddenUrl { url, with }) } } @@ -46,7 +46,7 @@ pub async fn open_path( command_scope: CommandScope, global_scope: GlobalScope, path: String, - with: Option, + with: Option, ) -> crate::Result<()> { let scope = Scope::new( &app, @@ -62,10 +62,10 @@ pub async fn open_path( .collect(), ); - if scope.is_path_allowed(Path::new(&path))? { + if scope.is_path_allowed(Path::new(&path), with.as_deref())? { crate::open_path(path, with) } else { - Err(Error::ForbiddenPath(path)) + Err(Error::ForbiddenPath { path, with }) } } diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index f1603d640..231e48664 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -20,10 +20,10 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - #[error("Not allowed to open forbidden path: {0}")] - ForbiddenPath(String), - #[error("Not allowed to open forbidden url: {0}")] - ForbiddenUrl(String), + #[error("Not allowed to open path {0}{}", .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + ForbiddenPath { path: String, with: Option }, + #[error("Not allowed to open url {0}{}", .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + ForbiddenUrl { url: String, with: Option }, #[error("API not supported on the current platform")] UnsupportedPlatform, #[error(transparent)] diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index 34fb3ea9d..51f6f0684 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(console.log(e.button),e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 85a4f163f..8342f1f91 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -26,7 +26,7 @@ mod scope_entry; pub use error::Error; type Result = std::result::Result; -pub use open::{open_path, open_url, Program}; +pub use open::{open_path, open_url}; pub use reveal_item_in_dir::reveal_item_in_dir; pub struct Opener { @@ -37,36 +37,60 @@ pub struct Opener { } impl Opener { - /// Open a url with a default or specific browser opening program. + /// Open a url with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(desktop)] - pub fn open_url(&self, url: impl Into, with: Option) -> Result<()> { - open::open(url.into(), with).map_err(Into::into) + pub fn open_url(&self, url: impl Into, with: Option>) -> Result<()> { + crate::open::open(url.into(), with.map(Into::into)).map_err(Into::into) } - /// Open a url with a default or specific browser opening program. + /// Open a url with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(mobile)] - pub fn open_url(&self, url: impl Into, _with: Option) -> Result<()> { + pub fn open_url(&self, url: impl Into, _with: Option>) -> Result<()> { self.mobile_plugin_handle .run_mobile_plugin("open", url.into()) .map_err(Into::into) } - /// Open a (url) path with a default or specific browser opening program. + /// Open a path with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(desktop)] - pub fn open_path(&self, path: impl Into, with: Option) -> Result<()> { - open::open(path.into(), with).map_err(Into::into) + pub fn open_path( + &self, + path: impl Into, + with: Option>, + ) -> Result<()> { + crate::open::open(path.into(), with.map(Into::into)).map_err(Into::into) } - /// Open a (url) path with a default or specific browser opening program. + /// Open a path with a default or specific program. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. #[cfg(mobile)] - pub fn open_path(&self, path: impl Into, _with: Option) -> Result<()> { + pub fn open_path( + &self, + path: impl Into, + _with: Option>, + ) -> Result<()> { self.mobile_plugin_handle .run_mobile_plugin("open", path.into()) .map_err(Into::into) } pub fn reveal_item_in_dir>(&self, p: P) -> Result<()> { - reveal_item_in_dir::reveal_item_in_dir(p) + crate::reveal_item_in_dir::reveal_item_in_dir(p) } } diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs index 03cf9584e..bbd836e37 100644 --- a/plugins/opener/src/open.rs +++ b/plugins/opener/src/open.rs @@ -4,128 +4,11 @@ //! Types and functions related to shell. -use serde::{Deserialize, Deserializer}; +use std::{ffi::OsStr, path::Path}; -use std::{ffi::OsStr, fmt::Display, path::Path, str::FromStr}; - -/// Program to use on the [`open()`] call. -#[derive(Debug)] -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 Display for Program { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - 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", - Self::Firefox => "firefox", - Self::Chrome => "chrome", - Self::Chromium => "chromium", - Self::Safari => "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", - } - } -} - -pub(crate) fn open>(path: P, with: Option) -> crate::Result<()> { - match with.map(Program::name) { - Some(program) => ::open::with_detached(path, program), +pub(crate) fn open, S: AsRef>(path: P, with: Option) -> crate::Result<()> { + match with { + Some(program) => ::open::with_detached(path, program.as_ref()), None => ::open::that_detached(path), } .map_err(Into::into) @@ -133,6 +16,10 @@ pub(crate) fn open>(path: P, with: Option) -> crate::Re /// Opens URL with the program specified in `with`, or system default if `None`. /// +/// ## Platform-specific: +/// +/// - **Android / iOS**: Always opens using default program. +/// /// # Examples /// /// ```rust,no_run @@ -143,13 +30,17 @@ pub(crate) fn open>(path: P, with: Option) -> crate::Re /// Ok(()) /// }); /// ``` -pub fn open_url>(url: P, with: Option) -> crate::Result<()> { +pub fn open_url, S: AsRef>(url: P, with: Option) -> crate::Result<()> { let url = url.as_ref(); open(url, with) } /// Opens path with the program specified in `with`, or system default if `None`. /// +/// ## Platform-specific: +/// +/// - **Android / iOS**: Always opens using default program. +/// /// # Examples /// /// ```rust,no_run @@ -160,7 +51,7 @@ pub fn open_url>(url: P, with: Option) -> crate::Result<( /// Ok(()) /// }); /// ``` -pub fn open_path>(path: P, with: Option) -> crate::Result<()> { +pub fn open_path, S: AsRef>(path: P, with: Option) -> crate::Result<()> { let path = path.as_ref(); open(path, with) } diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index 3fda73cd0..22c9787e0 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -12,35 +12,44 @@ use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; use crate::{scope_entry::EntryRaw, Error}; +pub use crate::scope_entry::Application; + #[derive(Debug)] pub enum Entry { - Url(glob::Pattern), - Path(Option), + Url { + url: glob::Pattern, + app: Application, + }, + Path { + path: Option, + app: Application, + }, } impl ScopeObject for Entry { type Error = Error; fn deserialize( - app: &AppHandle, + app_handle: &AppHandle, raw: Value, ) -> std::result::Result { serde_json::from_value(raw.into()) .and_then(|raw| { let entry = match raw { - EntryRaw::Url { url } => Entry::Url( - glob::Pattern::new(&url) + EntryRaw::Url { url, app } => Entry::Url { + url: glob::Pattern::new(&url) .map_err(|e| serde::de::Error::custom(e.to_string()))?, - ), - EntryRaw::Path { path } => { - let path = match app.path().parse(path) { + app, + }, + EntryRaw::Path { path, app } => { + let path = match app_handle.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) + Entry::Path { path, app } } }; @@ -50,6 +59,39 @@ impl ScopeObject for Entry { } } +impl Application { + fn matches(&self, a: Option<&str>) -> bool { + match self { + Self::Default => a.is_none(), + Self::Enable(enable) => *enable, + Self::App(program) => Some(program.as_str()) == a, + } + } +} + +impl Entry { + fn path(&self) -> Option { + match self { + Self::Url { .. } => None, + Self::Path { path, .. } => path.clone(), + } + } + + fn matches_url(&self, u: &str, a: Option<&str>) -> bool { + match self { + Self::Url { url, app } => url.matches(u) && app.matches(a), + Self::Path { .. } => false, + } + } + + fn matches_path_program(&self, a: Option<&str>) -> bool { + match self { + Self::Url { .. } => false, + Self::Path { app, .. } => app.matches(a), + } + } +} + #[derive(Debug)] pub struct Scope<'a, R: Runtime, M: Manager> { allowed: Vec<&'a Arc>, @@ -72,45 +114,25 @@ impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { } } - pub fn is_url_allowed(&self, url: &str) -> bool { - let denied = self.denied.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url), - Entry::Path { .. } => false, - }); + pub fn is_url_allowed(&self, url: &str, with: Option<&str>) -> bool { + let denied = self.denied.iter().any(|e| e.matches_url(url, with)); if denied { false } else { - self.allowed.iter().any(|entry| match entry.as_ref() { - Entry::Url(url_pattern) => url_pattern.matches(url), - Entry::Path { .. } => false, - }) + self.allowed.iter().any(|e| e.matches_url(url, with)) } } - pub fn is_path_allowed(&self, path: &Path) -> crate::Result { + pub fn is_path_allowed(&self, path: &Path, with: Option<&str>) -> crate::Result { 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(), + allow: self.allowed.iter().filter_map(|e| e.path()).collect(), + deny: self.denied.iter().filter_map(|e| e.path()).collect(), require_literal_leading_dot: None, }, )?; - Ok(fs_scope.is_allowed(path)) + Ok(fs_scope.is_allowed(path) && self.allowed.iter().any(|e| e.matches_path_program(with))) } } diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs index 20484887e..c7c6b1d3f 100644 --- a/plugins/opener/src/scope_entry.rs +++ b/plugins/opener/src/scope_entry.rs @@ -6,10 +6,30 @@ use std::path::PathBuf; use serde::Deserialize; +#[derive(Deserialize, Debug)] +pub enum Application { + Default, + Enable(bool), + App(String), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + #[derive(Deserialize)] #[serde(untagged, rename_all = "camelCase")] -#[allow(unused)] pub(crate) enum EntryRaw { - Url { url: String }, - Path { path: PathBuf }, + Url { + url: String, + #[serde(default)] + app: Application, + }, + Path { + path: PathBuf, + #[serde(default)] + app: Application, + }, } From 3b80f1eae29d5b7b98b6883ab22eb1d8b969f25f Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Thu, 14 Nov 2024 16:07:14 +0200 Subject: [PATCH 26/31] remove middle click comment --- plugins/opener/guest-js/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index 6cc20dcde..046db99c4 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -10,7 +10,7 @@ window.addEventListener('click', function (evt) { if ( // event was prevented evt.defaultPrevented || - // or not a left click or middle click + // or not a left click evt.button !== 0 || // or meta key pressed evt.metaKey || From 8ed396f821a64dc2f1750f05a0618a3a20fb39f4 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 14 Nov 2024 16:50:44 +0200 Subject: [PATCH 27/31] remove AppHandle, Program from ts --- examples/api/src/views/Opener.svelte | 39 +++------------------------- plugins/opener/guest-js/index.ts | 32 +++++------------------ plugins/opener/src/lib.rs | 11 +++++--- 3 files changed, 17 insertions(+), 65 deletions(-) diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte index 6ffaca887..83e88073e 100644 --- a/examples/api/src/views/Opener.svelte +++ b/examples/api/src/views/Opener.svelte @@ -3,20 +3,6 @@ export let onMessage - const programs = [ - 'firefox', - 'google chrome', - 'chromium', - 'safari', - 'open', - 'start', - 'xdg-open', - 'gio', - 'gnome-open', - 'kde-open', - 'wslview' - ] - let url = '' let path = '' let revealPath = '' @@ -25,14 +11,12 @@ let pathProgram = 'Default' function openUrl() { - opener - .openUrl(url, urlProgram === 'Default' ? undefined : urlProgram) - .catch(onMessage) + opener.openUrl(url, urlProgram ? urlProgram : undefined).catch(onMessage) } function openPath() { opener - .openPath(path, pathProgram === 'Default' ? undefined : pathProgram) + .openPath(path, pathProgram ? pathProgram : undefined) .catch(onMessage) } @@ -47,20 +31,13 @@ on:submit|preventDefault={openUrl} > - - with - +
- - with - +
- { +export async function openUrl(url: string, openWith?: string): Promise { await invoke('plugin:opener|open_url', { url, with: openWith @@ -69,21 +54,16 @@ export async function openUrl(url: string, openWith?: Program): Promise { * * // opens a file using the default program: * await openPath('/path/to/file'); - * // opens a file using `start` command on Windows. - * await openPath('C:/path/to/file', 'start'); + * // opens a file using `vlc` command on Windows. + * await openPath('C:/path/to/file', 'vlc'); * ``` * * @param path The path to open. - * @param openWith The app to open the path with. - * Must be one of `firefox`, `google chrome`, `chromium` `safari`, `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. - * If not specified, defaults to the system default application for the specified path type. + * @param openWith The app to open the path with. If not specified, defaults to the system default application for the specified path type. * * @since 2.0.0 */ -export async function openPath( - path: string, - openWith?: Program -): Promise { +export async function openPath(path: string, openWith?: string): Promise { await invoke('plugin:opener|open_path', { path, with: openWith diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 8342f1f91..064d50f58 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -6,7 +6,7 @@ use std::path::Path; use tauri::{ plugin::{Builder, TauriPlugin}, - AppHandle, Manager, Runtime, + Manager, Runtime, }; #[cfg(mobile)] @@ -30,8 +30,10 @@ pub use open::{open_path, open_url}; pub use reveal_item_in_dir::reveal_item_in_dir; pub struct Opener { - #[allow(dead_code)] - app: AppHandle, + // we use `fn() -> R` to slicence the unused generic error + // while keeping this struct `Send + Sync` without requiring `R` to be + #[cfg(not(mobile))] + _marker: std::marker::PhantomData R>, #[cfg(mobile)] mobile_plugin_handle: PluginHandle, } @@ -116,7 +118,8 @@ pub fn init() -> TauriPlugin { let handle = _api.register_ios_plugin(init_plugin_opener)?; app.manage(Opener { - app: app.clone(), + #[cfg(not(mobile))] + _marker: std::marker::PhantomData:: R>, #[cfg(mobile)] mobile_plugin_handle: handle, }); From 30dcce89ce9dd3b636ddda3e7bb690830b6c1e91 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 14 Nov 2024 17:53:10 +0200 Subject: [PATCH 28/31] fix permissions error --- examples/api/src/views/Opener.svelte | 10 ++++------ plugins/opener/src/error.rs | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte index 83e88073e..eca634ace 100644 --- a/examples/api/src/views/Opener.svelte +++ b/examples/api/src/views/Opener.svelte @@ -4,22 +4,20 @@ export let onMessage let url = '' - let path = '' - let revealPath = '' - - let urlProgram = 'Default' - let pathProgram = 'Default' - + let urlProgram = '' function openUrl() { opener.openUrl(url, urlProgram ? urlProgram : undefined).catch(onMessage) } + let path = '' + let pathProgram = '' function openPath() { opener .openPath(path, pathProgram ? pathProgram : undefined) .catch(onMessage) } + let revealPath = '' function revealItemInDir() { opener.revealItemInDir(revealPath).catch(onMessage) } diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 231e48664..157922fc6 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -20,9 +20,9 @@ pub enum Error { Json(#[from] serde_json::Error), #[error("unknown program {0}")] UnknownProgramName(String), - #[error("Not allowed to open path {0}{}", .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + #[error("Not allowed to open path {}{}", .path, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] ForbiddenPath { path: String, with: Option }, - #[error("Not allowed to open url {0}{}", .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + #[error("Not allowed to open url {}{}", .url, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] ForbiddenUrl { url: String, with: Option }, #[error("API not supported on the current platform")] UnsupportedPlatform, From ff17c5d084ce92a14f7d54f81effd9b5ea3fd8cf Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Thu, 14 Nov 2024 18:11:52 +0200 Subject: [PATCH 29/31] Update plugins/opener/src/scope_entry.rs Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com> --- plugins/opener/src/scope_entry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs index c7c6b1d3f..cf9004a2b 100644 --- a/plugins/opener/src/scope_entry.rs +++ b/plugins/opener/src/scope_entry.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use serde::Deserialize; #[derive(Deserialize, Debug)] +#[serde(untagged)] pub enum Application { Default, Enable(bool), From 3a54f2f82eeff52c6a78c2d9c239ee10576d79c3 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 19 Nov 2024 01:49:06 +0200 Subject: [PATCH 30/31] fix fallback --- plugins/opener/src/reveal_item_in_dir.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 098a7c5d2..6d7b9268d 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -71,18 +71,22 @@ mod imp { unsafe { if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 0) { + // from https://github.com/electron/electron/blob/10d967028af2e72382d16b7e2025d243b9e204ae/shell/common/platform_util_win.cc#L302 + // On some systems, the above call mysteriously fails with "file not + // found" even though the file is there. In these cases, ShellExecute() + // seems to work as a fallback (although it won't select the file). if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { - let is_dir = std::fs::metadata(file).map(|f| f.is_dir()).unwrap_or(false); + let is_dir = file.is_dir(); let mut info = SHELLEXECUTEINFOW { cbSize: std::mem::size_of::() as _, nShow: SW_SHOWNORMAL.0, + lpFile: PCWSTR(dir.as_ptr()), + lpClass: if is_dir { w!("folder") } else { PCWSTR::null() }, lpVerb: if is_dir { w!("explore") } else { PCWSTR::null() }, - lpClass: if is_dir { w!("folder") } else { PCWSTR::null() }, - lpFile: PCWSTR(file_h.as_ptr()), ..std::mem::zeroed() }; From 23143e93fe36dd33239ccd9110743d0998f1461c Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 19 Nov 2024 03:48:37 +0200 Subject: [PATCH 31/31] add option to configure global script behavior --- Cargo.lock | 2 +- plugins/opener/src/lib.rs | 87 +++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae8f966e1..fc2070547 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6714,7 +6714,7 @@ dependencies = [ "thiserror 2.0.3", "url", "windows 0.58.0", - "zbus", + "zbus 4.4.0", ] [[package]] diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 064d50f58..0b6c6007c 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -4,10 +4,7 @@ use std::path::Path; -use tauri::{ - plugin::{Builder, TauriPlugin}, - Manager, Runtime, -}; +use tauri::{plugin::TauriPlugin, Manager, Runtime}; #[cfg(mobile)] use tauri::plugin::PluginHandle; @@ -107,28 +104,66 @@ impl> crate::OpenerExt for T { } } +/// The opener plugin Builder. +pub struct Builder { + open_js_links_on_click: bool, +} + +impl Default for Builder { + fn default() -> Self { + Self { + open_js_links_on_click: true, + } + } +} + +impl Builder { + /// Create a new opener plugin Builder. + pub fn new() -> Self { + Self::default() + } + + /// Whether the plugin should inject a JS script to open URLs in default browser + /// when clicking on `` elements that has `_blank` target, or when pressing `Ctrl` or `Shift` while clicking it. + /// + /// Enabled by default for `http:`, `https:`, `mailto:`, `tel:` links. + pub fn open_js_links_on_click(mut self, open: bool) -> Self { + self.open_js_links_on_click = open; + self + } + + /// Build and Initializes the plugin. + pub fn build(self) -> TauriPlugin { + let mut builder = tauri::plugin::Builder::new("opener") + .setup(|app, _api| { + #[cfg(target_os = "android")] + let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; + #[cfg(target_os = "ios")] + let handle = _api.register_ios_plugin(init_plugin_opener)?; + + app.manage(Opener { + #[cfg(not(mobile))] + _marker: std::marker::PhantomData:: R>, + #[cfg(mobile)] + mobile_plugin_handle: handle, + }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::open_url, + commands::open_path, + commands::reveal_item_in_dir + ]); + + if self.open_js_links_on_click { + builder = builder.js_init_script(include_str!("init-iife.js").to_string()); + } + + builder.build() + } +} + /// Initializes the plugin. pub fn init() -> TauriPlugin { - Builder::new("opener") - .js_init_script(include_str!("init-iife.js").to_string()) - .setup(|app, _api| { - #[cfg(target_os = "android")] - let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; - #[cfg(target_os = "ios")] - let handle = _api.register_ios_plugin(init_plugin_opener)?; - - app.manage(Opener { - #[cfg(not(mobile))] - _marker: std::marker::PhantomData:: R>, - #[cfg(mobile)] - mobile_plugin_handle: handle, - }); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - commands::open_url, - commands::open_path, - commands::reveal_item_in_dir - ]) - .build() + Builder::default().build() }