phemios_core/
external_links.rs1use std::process::{Command, Stdio};
3use std::time::{Duration, Instant};
4pub fn browser_url(value: &str) -> Result<&str, String> {
6 if value.len() > 4096
7 || value.chars().any(|c| c.is_control() || c.is_whitespace())
8 || value.contains('\\')
9 {
10 return Err("Lien externe invalide.".into());
11 }
12 let address = value.split('#').next().ok_or("Lien externe invalide.")?;
13 let uri: ureq::http::Uri = address.parse().map_err(|_| "Lien externe invalide.")?;
14 let https = uri
15 .scheme_str()
16 .is_some_and(|s| s.eq_ignore_ascii_case("https"));
17 let http = uri
18 .scheme_str()
19 .is_some_and(|s| s.eq_ignore_ascii_case("http"));
20 let parsed = url::Url::parse(value).map_err(|_| "Lien externe invalide.")?;
21 let canonical = parsed
22 .as_str()
23 .split('#')
24 .next()
25 .ok_or("Lien externe invalide.")?;
26 let local_domain = parsed.domain().is_some_and(|host| {
27 let host = host.strip_suffix('.').unwrap_or(host);
28 host == "localhost" || host.ends_with(".localhost") || host.ends_with(".local")
29 });
30 if !(https || http)
31 || uri.authority().is_none_or(|a| a.as_str().contains('@'))
32 || uri.host().is_none_or(str::is_empty)
33 || !parsed.username().is_empty()
34 || parsed.password().is_some()
35 || !matches!(
36 (https, uri.port_u16()),
37 (_, None) | (true, Some(443)) | (false, Some(80))
38 )
39 || (http && (local_domain || crate::podcast::feed_address(canonical).is_err()))
40 {
41 return Err("Seuls les liens HTTP/HTTPS sans identifiants peuvent être ouverts.".into());
42 }
43 Ok(value)
44}
45fn browser_command(url: &str) -> Result<Command, String> {
46 #[cfg(target_os = "macos")]
47 {
48 let mut command = Command::new("/usr/bin/open");
49 command.arg("--").arg(url);
50 Ok(command)
51 }
52 #[cfg(target_os = "linux")]
53 {
54 let mut command = Command::new("/usr/bin/xdg-open");
55 command.arg(url);
56 Ok(command)
57 }
58 #[cfg(target_os = "windows")]
59 {
60 let system = std::env::var_os("SystemRoot").ok_or("Navigateur système indisponible.")?;
61 let mut command =
62 Command::new(std::path::PathBuf::from(system).join("System32/rundll32.exe"));
63 command.arg("url.dll,FileProtocolHandler").arg(url);
64 Ok(command)
65 }
66 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
67 {
68 let _ = url;
69 Err("Navigateur système indisponible.".into())
70 }
71}
72pub fn open_browser(url: &str) -> Result<(), String> {
75 let mut command = browser_command(browser_url(url)?)?;
76 let mut child = command
77 .stdin(Stdio::null())
78 .stdout(Stdio::null())
79 .stderr(Stdio::null())
80 .spawn()
81 .map_err(|_| "Impossible d’ouvrir le navigateur système.")?;
82 let deadline = Instant::now() + Duration::from_millis(500);
83 loop {
84 if let Some(status) = child
85 .try_wait()
86 .map_err(|_| "Impossible d’ouvrir le navigateur système.")?
87 {
88 return if status.success() {
89 Ok(())
90 } else {
91 Err("Impossible d’ouvrir le navigateur système.".into())
92 };
93 }
94 if Instant::now() >= deadline {
95 std::thread::spawn(move || {
96 let _ = child.wait();
97 });
98 return Ok(());
99 }
100 std::thread::sleep(Duration::from_millis(10));
101 }
102}
103#[cfg(test)]
104#[path = "external_links_tests.rs"]
105mod tests;