refactor: better cookie import experience

This commit is contained in:
zhom
2026-08-26 13:49:54 +04:00
parent 82271d8c3b
commit 7671b655cc
25 changed files with 5409 additions and 1439 deletions
+4
View File
@@ -148,6 +148,10 @@ windows = { version = "0.62", features = [
"Win32_Storage_FileSystem",
"Win32_System_Registry",
"Win32_UI_Shell",
# SendMessageTimeoutW, for the association-change broadcast in
# default_browser.rs. Going through the crate rather than a hand-written
# `extern "system"` block is what keeps `lpdwResult` typed as DWORD_PTR.
"Win32_UI_WindowsAndMessaging",
# CryptUnprotectData, for unwrapping the source browser's os_crypt key from
# `Local State` during profile import.
"Win32_Security_Cryptography",
+10 -4
View File
@@ -4253,7 +4253,7 @@ async fn import_profiles_api(
(status = 400, description = "Invalid cookie file or unsupported browser"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Profile not found"),
(status = 409, description = "Browser is currently running"),
(status = 409, description = "Browser is running, the profile is password-protected, or a remote session owns it"),
(status = 500, description = "Internal server error")
),
security(
@@ -4300,10 +4300,16 @@ async fn import_profile_cookies(
}))
}
Err(e) => {
let msg = e.to_lowercase();
if msg.contains("running") {
// The importer speaks in `{"code":…}` strings now; match those, and keep
// the substring checks for the messages that are still plain text.
if e.contains("COOKIE_IMPORT_BROWSER_RUNNING")
|| e.contains("COOKIE_IMPORT_PROFILE_PROTECTED")
|| e.contains("COOKIE_IMPORT_REMOTE_SESSION")
{
Err(StatusCode::CONFLICT)
} else if msg.contains("no valid cookies") || msg.contains("unsupported browser") {
} else if e.contains("COOKIE_IMPORT_NO_COOKIES")
|| e.to_lowercase().contains("unsupported browser")
{
Err(StatusCode::BAD_REQUEST)
} else {
Err(StatusCode::INTERNAL_SERVER_ERROR)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+31 -25
View File
@@ -358,38 +358,44 @@ mod windows {
Ok(())
}
/// Tell the shell that the association it has cached is stale.
///
/// `SHChangeNotify` is the documented announcement for an association change,
/// and the `WM_SETTINGCHANGE` broadcast is what the shell's own settings UI
/// sends alongside it, so both go out.
///
/// This used to hand-declare `SendMessageTimeoutA` with `lpdwResult` typed as
/// `*mut u32` and pass it a `u32`. The real parameter is `PDWORD_PTR`, eight
/// bytes on x64, so every call wrote four bytes past a stack slot. The result
/// was a corrupted stack at the exact moment a user set Donut as their default
/// browser, and the process died with nothing in the log. Go through the
/// `windows` crate instead, which types the out-parameter correctly and cannot
/// drift from the real ABI.
fn notify_system_of_changes() {
// Use Windows API to notify the system of association changes
// This helps refresh the system's understanding of the changes
use windows::core::w;
use windows::Win32::Foundation::{LPARAM, WPARAM};
use windows::Win32::UI::Shell::{SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_IDLIST};
use windows::Win32::UI::WindowsAndMessaging::{
SendMessageTimeoutW, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE,
};
unsafe {
use std::ffi::c_void;
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None);
const HWND_BROADCAST: *mut c_void = 0xffff as *mut c_void;
const WM_SETTINGCHANGE: u32 = 0x001A;
const SMTO_ABORTIFHUNG: u32 = 0x0002;
extern "system" {
fn SendMessageTimeoutA(
hWnd: *mut c_void,
Msg: u32,
wParam: usize,
lParam: isize,
fuFlags: u32,
uTimeout: u32,
lpdwResult: *mut u32,
) -> isize;
}
let mut result: u32 = 0;
SendMessageTimeoutA(
// The broadcast is best-effort: a hung top-level window elsewhere on the
// desktop must not hold up the click that triggered this, hence the
// timeout and SMTO_ABORTIFHUNG. `WM_SETTINGCHANGE`'s lParam string is
// marshalled cross-process by the window manager, and this one is
// 'static, so it stays valid for the whole call.
let mut result: usize = 0;
SendMessageTimeoutW(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0,
c"Software\\Classes".as_ptr() as isize,
WPARAM(0),
LPARAM(w!("Software\\Classes").as_ptr() as isize),
SMTO_ABORTIFHUNG,
1000,
&mut result,
Some(&mut result),
);
}
}
+49 -19
View File
@@ -105,6 +105,7 @@ mod cloud_errors;
mod commercial_license;
mod cookie_bot;
mod cookie_manager;
mod cookie_paste;
pub mod events;
mod mcp_integrations;
mod mcp_server;
@@ -476,29 +477,57 @@ async fn copy_profile_cookies(
Ok(results)
}
/// Push a profile's freshly written cookies to the cloud, if it syncs at all.
fn queue_profile_cookie_sync(profile_id: &str) {
let Some(scheduler) = crate::sync::get_global_scheduler() else {
return;
};
let Ok(profiles) = profile::manager::ProfileManager::instance().list_profiles() else {
return;
};
let syncs = profiles
.iter()
.any(|p| p.id.to_string() == profile_id && p.is_sync_enabled());
if !syncs {
return;
}
let pid = profile_id.to_string();
tauri::async_runtime::spawn(async move {
scheduler.queue_profile_sync(pid).await;
});
}
#[tauri::command]
async fn import_cookies_from_file(
async fn analyze_pasted_cookies(
app_handle: tauri::AppHandle,
profile_id: String,
content: String,
) -> Result<cookie_manager::CookieImportResult, String> {
let result =
cookie_manager::CookieManager::import_cookies(&app_handle, &profile_id, &content).await?;
site: Option<String>,
) -> Result<cookie_manager::CookiePasteAnalysis, String> {
cookie_manager::CookieManager::analyze_paste(&app_handle, &profile_id, &content, site.as_deref())
.await
}
// Trigger sync for the profile if sync is enabled
if let Some(scheduler) = crate::sync::get_global_scheduler() {
let profile_manager = profile::manager::ProfileManager::instance();
if let Ok(profiles) = profile_manager.list_profiles() {
if let Some(profile) = profiles.iter().find(|p| p.id.to_string() == profile_id) {
if profile.is_sync_enabled() {
let pid = profile_id.clone();
tauri::async_runtime::spawn(async move {
scheduler.queue_profile_sync(pid).await;
});
}
}
}
}
#[tauri::command]
async fn import_pasted_cookies(
app_handle: tauri::AppHandle,
profile_id: String,
content: String,
site: Option<String>,
mode: cookie_manager::CookieWriteMode,
include_expired: bool,
) -> Result<cookie_manager::CookiePasteImportResult, String> {
let result = cookie_manager::CookieManager::import_paste(
&app_handle,
&profile_id,
&content,
site.as_deref(),
mode,
include_expired,
)
.await?;
queue_profile_cookie_sync(&profile_id);
Ok(result)
}
@@ -2821,7 +2850,8 @@ pub fn run_with_builder(
read_profile_cookies,
get_profile_cookie_stats,
copy_profile_cookies,
import_cookies_from_file,
analyze_pasted_cookies,
import_pasted_cookies,
export_profile_cookies,
check_wayfern_terms_accepted,
check_wayfern_downloaded,