Skip to main content

Mountain/IPC/WindServiceHandlers/NativeHost/
OpenExternal.rs

1#![allow(unused_variables, dead_code, unused_imports)]
2
3//! Wire method: `native:openExternal`, `nativeHost:openExternal`.
4//! Opens an http/https URL in the platform default browser.
5
6use std::sync::Arc;
7
8use serde_json::Value;
9
10use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
11
12pub async fn Fn(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
13	// Accept both a plain URI string and the object shape
14	// `{ uri: "..." }` that some VS Code callers emit.
15	let url_str = match Arguments.first() {
16		Some(Value::String(S)) => S.as_str(),
17
18		Some(Value::Object(Obj)) => Obj.get("uri").or_else(|| Obj.get("url")).and_then(|V| V.as_str()).unwrap_or(""),
19
20		_ => return Ok(Value::Bool(false)),
21	};
22
23	if url_str.is_empty() {
24		return Ok(Value::Bool(false));
25	}
26
27	dev_log!("lifecycle", "openExternal: {}", url_str);
28
29	// Allowlist of safe protocols. Block `file://` (arbitrary filesystem
30	// access) and bare shell commands. Everything else that parses as a
31	// valid URI scheme is forwarded to the OS default handler.
32	let Scheme = url_str.splitn(2, ':').next().unwrap_or("").to_lowercase();
33
34	let AllowedSchemes = [
35		"http",
36		"https",
37		"mailto",
38		"ftp",
39		"vscode",
40		"fiddee",
41		"ssh",
42		"git",
43		"x-github-client",
44		"github-windows",
45		"slack",
46		"teams",
47		"zoommtg",
48		"tel",
49		"callto",
50	];
51
52	if Scheme == "file" || Scheme.is_empty() || !url_str.contains(':') {
53		dev_log!(
54			"lifecycle",
55			"warn: [OpenExternal] blocked scheme '{}' for uri '{}'",
56			Scheme,
57			url_str
58		);
59
60		return Ok(Value::Bool(false));
61	}
62
63	let IsKnownScheme = AllowedSchemes.contains(&Scheme.as_str());
64
65	if !IsKnownScheme {
66		dev_log!(
67			"lifecycle",
68			"[OpenExternal] unknown scheme '{}' - forwarding to OS anyway",
69			Scheme
70		);
71	}
72
73	#[cfg(target_os = "macos")]
74	{
75		use std::process::Command;
76
77		let result = Command::new("open")
78			.arg(url_str)
79			.output()
80			.map_err(|Error| format!("Failed to execute open command: {}", Error))?;
81
82		if !result.status.success() {
83			return Err(format!("Failed to open URL: {}", String::from_utf8_lossy(&result.stderr)));
84		}
85	}
86
87	#[cfg(target_os = "windows")]
88	{
89		use std::process::Command;
90
91		let result = Command::new("cmd")
92			.arg("/c")
93			.arg("start")
94			.arg(url_str)
95			.output()
96			.map_err(|Error| format!("Failed to execute start command: {}", Error))?;
97
98		if !result.status.success() {
99			return Err(format!("Failed to open URL: {}", String::from_utf8_lossy(&result.stderr)));
100		}
101	}
102
103	#[cfg(target_os = "linux")]
104	{
105		use std::process::Command;
106
107		let handlers = ["xdg-open", "gnome-open", "kde-open", "x-www-browser"];
108
109		let mut last_error = String::new();
110
111		for handler in handlers.iter() {
112			let result = Command::new(handler).arg(url_str).output();
113
114			match result {
115				Ok(output) if output.status.success() => {
116					dev_log!("lifecycle", "opened with {}", handler);
117
118					break;
119				},
120
121				Err(e) => {
122					last_error = e.to_string();
123
124					continue;
125				},
126
127				_ => continue,
128			}
129		}
130
131		if !last_error.is_empty() {
132			return Err(format!("Failed to open URL with any handler: {}", last_error));
133		}
134	}
135
136	dev_log!("lifecycle", "opened URL: {}", url_str);
137
138	Ok(Value::Bool(true))
139}