support login data
This commit is contained in:
@@ -19,6 +19,7 @@ pub struct BrowserView {
|
||||
pub profiles: Vec<ProfileSummary>,
|
||||
pub extensions: Vec<ExtensionSummary>,
|
||||
pub bookmarks: Vec<BookmarkSummary>,
|
||||
pub password_sites: Vec<PasswordSiteSummary>,
|
||||
pub stats: BrowserStats,
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ pub struct BrowserStats {
|
||||
pub profile_count: usize,
|
||||
pub extension_count: usize,
|
||||
pub bookmark_count: usize,
|
||||
pub password_site_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -64,6 +66,15 @@ pub struct BookmarkSummary {
|
||||
pub profiles: Vec<BookmarkAssociatedProfileSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordSiteSummary {
|
||||
pub url: String,
|
||||
pub domain: String,
|
||||
pub profile_ids: Vec<String>,
|
||||
pub profiles: Vec<AssociatedProfileSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssociatedProfileSummary {
|
||||
@@ -165,3 +176,10 @@ pub struct TempBookmark {
|
||||
pub profile_ids: BTreeSet<String>,
|
||||
pub profiles: BTreeMap<String, BookmarkAssociatedProfileSummary>,
|
||||
}
|
||||
|
||||
pub struct TempPasswordSite {
|
||||
pub url: String,
|
||||
pub domain: String,
|
||||
pub profile_ids: BTreeSet<String>,
|
||||
pub profiles: BTreeMap<String, AssociatedProfileSummary>,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use serde_json::Value;
|
||||
use tauri::AppHandle;
|
||||
|
||||
@@ -10,10 +11,12 @@ use crate::{
|
||||
config_store,
|
||||
models::{
|
||||
AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary,
|
||||
BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, ProfileSummary,
|
||||
ScanResponse, TempBookmark, TempExtension,
|
||||
BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, PasswordSiteSummary,
|
||||
ProfileSummary, ScanResponse, TempBookmark, TempExtension, TempPasswordSite,
|
||||
},
|
||||
utils::{
|
||||
copy_sqlite_database_to_temp, first_non_empty, load_image_as_data_url, read_json_file,
|
||||
},
|
||||
utils::{first_non_empty, load_image_as_data_url, read_json_file},
|
||||
};
|
||||
|
||||
pub fn scan_browsers(app: &AppHandle) -> Result<ScanResponse, String> {
|
||||
@@ -43,6 +46,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
||||
let mut profiles = Vec::new();
|
||||
let mut extensions = BTreeMap::<String, TempExtension>::new();
|
||||
let mut bookmarks = BTreeMap::<String, TempBookmark>::new();
|
||||
let mut password_sites = BTreeMap::<String, TempPasswordSite>::new();
|
||||
|
||||
for profile_id in profile_ids {
|
||||
let profile_path = root.join(&profile_id);
|
||||
@@ -51,14 +55,11 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
||||
}
|
||||
|
||||
let profile_info = profile_cache.and_then(|cache| cache.get(&profile_id));
|
||||
let profile_summary = build_profile_summary(
|
||||
&root,
|
||||
&profile_path,
|
||||
&profile_id,
|
||||
profile_info,
|
||||
);
|
||||
let profile_summary =
|
||||
build_profile_summary(&root, &profile_path, &profile_id, profile_info);
|
||||
scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions);
|
||||
scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks);
|
||||
scan_password_sites_for_profile(&profile_path, &profile_summary, &mut password_sites);
|
||||
profiles.push(profile_summary);
|
||||
}
|
||||
|
||||
@@ -83,6 +84,15 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
||||
profiles: entry.profiles.into_values().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let password_sites = password_sites
|
||||
.into_values()
|
||||
.map(|entry| PasswordSiteSummary {
|
||||
url: entry.url,
|
||||
domain: entry.domain,
|
||||
profile_ids: entry.profile_ids.into_iter().collect(),
|
||||
profiles: entry.profiles.into_values().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(BrowserView {
|
||||
browser_id: config.id,
|
||||
@@ -94,10 +104,12 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
||||
profile_count: profiles.len(),
|
||||
extension_count: extensions.len(),
|
||||
bookmark_count: bookmarks.len(),
|
||||
password_site_count: password_sites.len(),
|
||||
},
|
||||
profiles,
|
||||
extensions: sort_extensions(extensions),
|
||||
bookmarks: sort_bookmarks(bookmarks),
|
||||
password_sites: sort_password_sites(password_sites),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -291,7 +303,10 @@ fn resolve_extension_install_dir(
|
||||
} else if candidate.is_absolute() {
|
||||
(candidate, ExtensionInstallSource::ExternalAbsolute)
|
||||
} else {
|
||||
(PathBuf::from(raw_path), ExtensionInstallSource::ExternalAbsolute)
|
||||
(
|
||||
PathBuf::from(raw_path),
|
||||
ExtensionInstallSource::ExternalAbsolute,
|
||||
)
|
||||
};
|
||||
|
||||
resolved.is_dir().then_some((resolved, source))
|
||||
@@ -451,10 +466,8 @@ fn collect_bookmarks(
|
||||
} else {
|
||||
ancestors.join(" > ")
|
||||
};
|
||||
entry
|
||||
.profiles
|
||||
.entry(profile.id.clone())
|
||||
.or_insert_with(|| BookmarkAssociatedProfileSummary {
|
||||
entry.profiles.entry(profile.id.clone()).or_insert_with(|| {
|
||||
BookmarkAssociatedProfileSummary {
|
||||
id: profile.id.clone(),
|
||||
name: profile.name.clone(),
|
||||
avatar_data_url: profile.avatar_data_url.clone(),
|
||||
@@ -463,7 +476,8 @@ fn collect_bookmarks(
|
||||
default_avatar_stroke_color: profile.default_avatar_stroke_color,
|
||||
avatar_label: profile.avatar_label.clone(),
|
||||
bookmark_path,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Some("folder") => {
|
||||
if let Some(children) = node.get("children").and_then(Value::as_array) {
|
||||
@@ -526,3 +540,103 @@ fn sort_bookmarks(mut bookmarks: Vec<BookmarkSummary>) -> Vec<BookmarkSummary> {
|
||||
});
|
||||
bookmarks
|
||||
}
|
||||
|
||||
fn scan_password_sites_for_profile(
|
||||
profile_path: &Path,
|
||||
profile: &ProfileSummary,
|
||||
password_sites: &mut BTreeMap<String, TempPasswordSite>,
|
||||
) {
|
||||
let login_data_path = profile_path.join("Login Data");
|
||||
if !login_data_path.is_file() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(temp_copy) = copy_sqlite_database_to_temp(&login_data_path) else {
|
||||
return;
|
||||
};
|
||||
let Ok(connection) = Connection::open_with_flags(
|
||||
temp_copy.path(),
|
||||
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(mut statement) = connection
|
||||
.prepare("SELECT origin_url, signon_realm FROM logins WHERE blacklisted_by_user = 0")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok(rows) = statement.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, Option<String>>(0)?,
|
||||
row.get::<_, Option<String>>(1)?,
|
||||
))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for row in rows.flatten() {
|
||||
let Some(url) = normalize_login_site(row.0.as_deref(), row.1.as_deref()) else {
|
||||
continue;
|
||||
};
|
||||
let domain = domain_from_url(&url).unwrap_or_else(|| url.clone());
|
||||
|
||||
let entry = password_sites
|
||||
.entry(url.clone())
|
||||
.or_insert_with(|| TempPasswordSite {
|
||||
url: url.clone(),
|
||||
domain: domain.clone(),
|
||||
profile_ids: BTreeSet::new(),
|
||||
profiles: BTreeMap::new(),
|
||||
});
|
||||
|
||||
if entry.domain == entry.url && domain != entry.url {
|
||||
entry.domain = domain.clone();
|
||||
}
|
||||
entry.profile_ids.insert(profile.id.clone());
|
||||
entry
|
||||
.profiles
|
||||
.entry(profile.id.clone())
|
||||
.or_insert_with(|| AssociatedProfileSummary {
|
||||
id: profile.id.clone(),
|
||||
name: profile.name.clone(),
|
||||
avatar_data_url: profile.avatar_data_url.clone(),
|
||||
avatar_icon: profile.avatar_icon.clone(),
|
||||
default_avatar_fill_color: profile.default_avatar_fill_color,
|
||||
default_avatar_stroke_color: profile.default_avatar_stroke_color,
|
||||
avatar_label: profile.avatar_label.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_login_site(origin_url: Option<&str>, signon_realm: Option<&str>) -> Option<String> {
|
||||
let candidate = [signon_realm, origin_url]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(str::trim)
|
||||
.find(|value| {
|
||||
!value.is_empty() && (value.starts_with("http://") || value.starts_with("https://"))
|
||||
})?;
|
||||
|
||||
Some(candidate.to_string())
|
||||
}
|
||||
|
||||
fn domain_from_url(url: &str) -> Option<String> {
|
||||
let (_, remainder) = url.split_once("://")?;
|
||||
let host = remainder.split('/').next()?.trim();
|
||||
if host.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(host.to_string())
|
||||
}
|
||||
|
||||
fn sort_password_sites(mut password_sites: Vec<PasswordSiteSummary>) -> Vec<PasswordSiteSummary> {
|
||||
password_sites.sort_by(|left, right| {
|
||||
left.domain
|
||||
.to_lowercase()
|
||||
.cmp(&right.domain.to_lowercase())
|
||||
.then_with(|| left.url.cmp(&right.url))
|
||||
});
|
||||
password_sites
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
@@ -46,3 +48,48 @@ pub fn first_non_empty<'a>(values: impl IntoIterator<Item = Option<&'a str>>) ->
|
||||
.flatten()
|
||||
.find(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
pub struct TempSqliteCopy {
|
||||
path: PathBuf,
|
||||
directory: PathBuf,
|
||||
}
|
||||
|
||||
impl TempSqliteCopy {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempSqliteCopy {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.directory);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_sqlite_database_to_temp(path: &Path) -> Option<TempSqliteCopy> {
|
||||
let file_name = path.file_name()?.to_str()?;
|
||||
let unique_id = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()?
|
||||
.as_nanos();
|
||||
let directory =
|
||||
env::temp_dir().join(format!("chrom-tool-scan-{}-{}", process::id(), unique_id));
|
||||
|
||||
fs::create_dir_all(&directory).ok()?;
|
||||
|
||||
let main_target = directory.join(file_name);
|
||||
fs::copy(path, &main_target).ok()?;
|
||||
|
||||
for suffix in ["-wal", "-shm"] {
|
||||
let source = PathBuf::from(format!("{}{}", path.display(), suffix));
|
||||
if source.is_file() {
|
||||
let target = directory.join(format!("{file_name}{suffix}"));
|
||||
let _ = fs::copy(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
Some(TempSqliteCopy {
|
||||
path: main_target,
|
||||
directory,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user