Merge remote-tracking branch 'origin/main' into desktop-keychain
# Conflicts: # src-tauri/src/lib.rs
This commit is contained in:
+197
-40
@@ -14,6 +14,8 @@ use tauri_plugin_opener::OpenerExt;
|
||||
pub mod gpu_workarounds;
|
||||
mod native;
|
||||
mod secure_session;
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", test))]
|
||||
mod webview_permissions;
|
||||
|
||||
/// Bring the main window to the foreground from the tray / a hidden /
|
||||
/// minimized state. Shared by the tray, single-instance, and deep-link paths.
|
||||
@@ -190,6 +192,47 @@ mod update_retry {
|
||||
}
|
||||
}
|
||||
|
||||
/// How this copy of the app gets updated.
|
||||
///
|
||||
/// Tauri's updater can replace the Windows install and a Linux AppImage, but
|
||||
/// not a copy installed by a package manager: it tries to write next to the
|
||||
/// binary in /usr/bin and fails with "Permission denied (os error 13)"
|
||||
/// (reported on CachyOS). Those installs update through their package
|
||||
/// manager instead; the web UI shows the right command.
|
||||
pub(crate) fn install_kind(linux: bool, appimage: bool, os_release: &str) -> &'static str {
|
||||
if !linux || appimage {
|
||||
return "in-app";
|
||||
}
|
||||
let field = |key: &str| {
|
||||
os_release
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix(key).and_then(|v| v.strip_prefix('=')))
|
||||
.map(|v| v.trim().trim_matches('"').to_ascii_lowercase())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let ids = format!("{} {}", field("ID"), field("ID_LIKE"));
|
||||
let has = |name: &str| ids.split_whitespace().any(|w| w == name);
|
||||
if has("arch") {
|
||||
"pacman"
|
||||
} else if has("debian") || has("ubuntu") {
|
||||
"deb"
|
||||
} else {
|
||||
"manual"
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_install_kind() -> &'static str {
|
||||
let linux = cfg!(target_os = "linux");
|
||||
let appimage = std::env::var_os("APPIMAGE").is_some();
|
||||
let os_release = if linux {
|
||||
std::fs::read_to_string("/etc/os-release").unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
install_kind(linux, appimage, &os_release)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_for_update(app: tauri::AppHandle) -> Result<UpdateInfo, String> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -212,6 +255,15 @@ async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
|
||||
{
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// A package-manager install can't be replaced in place (see
|
||||
// install_kind); refuse before downloading anything.
|
||||
let kind = update_install_kind();
|
||||
if kind != "in-app" {
|
||||
return Err(format!(
|
||||
"install: package-managed ({kind}): update Lotus Chat with your package manager"
|
||||
));
|
||||
}
|
||||
|
||||
let emit = |detail: serde_json::Value| {
|
||||
native::emit_to_web(&app, "lotus-update-progress", &detail.to_string());
|
||||
};
|
||||
@@ -940,6 +992,7 @@ pub fn run() {
|
||||
send_notification,
|
||||
check_for_update,
|
||||
install_update,
|
||||
update_install_kind,
|
||||
native::power::set_call_active,
|
||||
native::jumplist::set_jump_list,
|
||||
native::thumbbar::set_thumbbar,
|
||||
@@ -958,7 +1011,17 @@ pub fn run() {
|
||||
secure_session::secure_session_get,
|
||||
secure_session::secure_session_clear,
|
||||
])
|
||||
.plugin(tauri_plugin_localhost::Builder::new(port).build())
|
||||
// Bound to 127.0.0.1 explicitly (cinny #43). The app is still loaded as
|
||||
// http://localhost:{port} (its storage lives under that origin, and the
|
||||
// engines try 127.0.0.1 for `localhost`); the bundled call page is
|
||||
// loaded as http://127.0.0.1:{port}, a separate origin on the same
|
||||
// server. Binding the name `localhost` could pick ::1 only (Windows
|
||||
// lists it first), and then the call page wouldn't load.
|
||||
.plugin(
|
||||
tauri_plugin_localhost::Builder::new(port)
|
||||
.host("127.0.0.1")
|
||||
.build(),
|
||||
)
|
||||
.plugin(
|
||||
// DECORATIONS is excluded: the custom-chrome toggle (set_custom_chrome)
|
||||
// owns the decorated flag. Letting window-state restore a saved
|
||||
@@ -1226,41 +1289,76 @@ pub fn run() {
|
||||
let _ = window_vibrancy::apply_mica(&window, Some(true));
|
||||
}
|
||||
|
||||
// Auto-grant camera, microphone, and notification permissions in WebView2.
|
||||
#[cfg(target_os = "windows")]
|
||||
window.with_webview(|webview| {
|
||||
use webview2_com::{
|
||||
Microsoft::Web::WebView2::Win32::{
|
||||
COREWEBVIEW2_PERMISSION_KIND,
|
||||
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
|
||||
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
|
||||
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
|
||||
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
|
||||
},
|
||||
PermissionRequestedEventHandler,
|
||||
};
|
||||
// cinny-desktop #22: the app's own page gets the microphone, camera
|
||||
// and notifications without a prompt; other origins (room widgets,
|
||||
// link-preview embeds) are refused them. See webview_permissions.
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
let app_origins = webview_permissions::AppOrigins::new(
|
||||
port,
|
||||
app.config().build.dev_url.as_ref(),
|
||||
);
|
||||
|
||||
let controller = webview.controller();
|
||||
if let Ok(core) = unsafe { controller.CoreWebView2() } {
|
||||
let handler = PermissionRequestedEventHandler::create(Box::new(
|
||||
|_sender, args| {
|
||||
if let Some(args) = args {
|
||||
let mut kind = COREWEBVIEW2_PERMISSION_KIND(0);
|
||||
unsafe { args.PermissionKind(&mut kind) }?;
|
||||
if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|
||||
|| kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|
||||
|| kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS
|
||||
{
|
||||
unsafe {
|
||||
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
|
||||
}?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
#[cfg(target_os = "windows")]
|
||||
window.with_webview({
|
||||
let app_origins = app_origins.clone();
|
||||
move |webview| {
|
||||
use webview2_com::{
|
||||
Microsoft::Web::WebView2::Win32::{
|
||||
COREWEBVIEW2_PERMISSION_KIND,
|
||||
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
|
||||
COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION,
|
||||
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
|
||||
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
|
||||
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
|
||||
COREWEBVIEW2_PERMISSION_STATE_DENY,
|
||||
},
|
||||
));
|
||||
let mut token = Default::default();
|
||||
let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
|
||||
PermissionRequestedEventHandler,
|
||||
};
|
||||
use webview_permissions::{decide, Decision, Kind, WINDOWS_GRANTS};
|
||||
|
||||
let controller = webview.controller();
|
||||
if let Ok(core) = unsafe { controller.CoreWebView2() } {
|
||||
let handler = PermissionRequestedEventHandler::create(Box::new(
|
||||
move |_sender, args| {
|
||||
if let Some(args) = args {
|
||||
let mut raw = COREWEBVIEW2_PERMISSION_KIND(0);
|
||||
unsafe { args.PermissionKind(&mut raw) }?;
|
||||
let kind = if raw == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|
||||
|| raw == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|
||||
{
|
||||
Kind::Media
|
||||
} else if raw == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS {
|
||||
Kind::Notifications
|
||||
} else if raw == COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION {
|
||||
Kind::Geolocation
|
||||
} else {
|
||||
Kind::Other
|
||||
};
|
||||
// The origin of the frame that asked.
|
||||
let mut uri = windows::core::PWSTR::null();
|
||||
unsafe { args.Uri(&mut uri) }?;
|
||||
let uri = webview2_com::take_pwstr(uri);
|
||||
match decide(kind, &uri, &app_origins, WINDOWS_GRANTS) {
|
||||
Decision::Allow => unsafe {
|
||||
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
|
||||
}?,
|
||||
Decision::Deny => {
|
||||
eprintln!(
|
||||
"webview: denied {kind:?} permission to {uri}"
|
||||
);
|
||||
unsafe {
|
||||
args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)
|
||||
}?
|
||||
}
|
||||
Decision::Default => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
));
|
||||
let mut token = Default::default();
|
||||
let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -1268,19 +1366,48 @@ pub fn run() {
|
||||
// default (unlike WebView2/WKWebView), which leaves
|
||||
// `navigator.mediaDevices` undefined and makes Element Call
|
||||
// report "browser does not support WebRTC". Turn them on and
|
||||
// auto-grant the resulting camera/mic permission prompt, mirroring
|
||||
// the WebView2 handling above.
|
||||
// answer the permission requests, mirroring the WebView2 handling
|
||||
// above. WebKitGTK doesn't say which frame asked, so the origin
|
||||
// checked is the page in the window (see webview_permissions).
|
||||
#[cfg(target_os = "linux")]
|
||||
window.with_webview(|webview| {
|
||||
use webkit2gtk::{PermissionRequestExt, SettingsExt, WebViewExt};
|
||||
window.with_webview(move |webview| {
|
||||
use webkit2gtk::glib::prelude::ObjectExt;
|
||||
use webkit2gtk::{
|
||||
DeviceInfoPermissionRequest, GeolocationPermissionRequest,
|
||||
NotificationPermissionRequest, PermissionRequestExt, SettingsExt,
|
||||
UserMediaPermissionRequest, WebViewExt,
|
||||
};
|
||||
use webview_permissions::{decide, Decision, Kind, LINUX_GRANTS};
|
||||
|
||||
let wv = webview.inner();
|
||||
if let Some(settings) = WebViewExt::settings(&wv) {
|
||||
settings.set_enable_media_stream(true);
|
||||
settings.set_enable_webrtc(true);
|
||||
}
|
||||
wv.connect_permission_request(|_webview, request| {
|
||||
request.allow();
|
||||
wv.connect_permission_request(move |wv, request| {
|
||||
let kind = if request.is::<UserMediaPermissionRequest>() {
|
||||
Kind::Media
|
||||
} else if request.is::<DeviceInfoPermissionRequest>() {
|
||||
Kind::DeviceInfo
|
||||
} else if request.is::<NotificationPermissionRequest>() {
|
||||
Kind::Notifications
|
||||
} else if request.is::<GeolocationPermissionRequest>() {
|
||||
Kind::Geolocation
|
||||
} else {
|
||||
Kind::Other
|
||||
};
|
||||
let uri = wv.uri().map(|u| u.to_string()).unwrap_or_default();
|
||||
match decide(kind, &uri, &app_origins, LINUX_GRANTS) {
|
||||
Decision::Allow => request.allow(),
|
||||
// No prompt of our own: anything not granted is denied.
|
||||
Decision::Deny | Decision::Default => {
|
||||
eprintln!(
|
||||
"webview: denied {} to {uri}",
|
||||
request.type_().name()
|
||||
);
|
||||
request.deny();
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
})?;
|
||||
@@ -1338,6 +1465,36 @@ mod webview2_args_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod install_kind_tests {
|
||||
use super::install_kind;
|
||||
|
||||
#[test]
|
||||
fn package_installs_update_through_their_package_manager() {
|
||||
let cachy = "NAME=\"CachyOS Linux\"\nID=cachyos\nID_LIKE=arch\n";
|
||||
let arch = "NAME=\"Arch Linux\"\nID=arch\n";
|
||||
let ubuntu = "NAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n";
|
||||
let debian = "ID=debian\n";
|
||||
let mint = "ID=linuxmint\nID_LIKE=\"ubuntu debian\"\n";
|
||||
let fedora = "ID=fedora\n";
|
||||
assert_eq!(install_kind(true, false, cachy), "pacman");
|
||||
assert_eq!(install_kind(true, false, arch), "pacman");
|
||||
assert_eq!(install_kind(true, false, ubuntu), "deb");
|
||||
assert_eq!(install_kind(true, false, debian), "deb");
|
||||
assert_eq!(install_kind(true, false, mint), "deb");
|
||||
assert_eq!(install_kind(true, false, fedora), "manual");
|
||||
assert_eq!(install_kind(true, false, ""), "manual");
|
||||
// ID_LIKE mentioning arch only as part of a longer word doesn't count.
|
||||
assert_eq!(install_kind(true, false, "ID=x\nID_LIKE=archlike\n"), "manual");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appimage_and_windows_update_in_app() {
|
||||
assert_eq!(install_kind(true, true, "ID=cachyos\nID_LIKE=arch\n"), "in-app");
|
||||
assert_eq!(install_kind(false, false, ""), "in-app");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tray_tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user