1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat: cookie consent extension support (ARCHIVR_COOKIE_EXT)

Mirrors the uBlock Origin Lite integration exactly:

Backend:
- singlefile.rs: resolve_cookie_ext_config() reads ARCHIVR_COOKIE_CONSENT
  (default true) + ARCHIVR_COOKIE_EXT path; extension paths comma-joined
  into --load-extension / --disable-extensions-except so uBlock and cookie
  ext can coexist; SaveResult.cookie_ext_skipped tracks miss
- database.rs: cookie_ext_enabled column on instance_settings (DEFAULT 1);
  idempotent ALTER TABLE migration; get/update wired through
- capture.rs: CaptureConfig.cookie_ext_enabled: Option<bool>; threaded to
  singlefile::save(); cookie_ext_skipped surfaced in CaptureResult
- routes.rs: CaptureBody + UpdateInstanceSettingsBody get cookie_ext_enabled;
  capture handler resolves effective value (body overrides global); notes_json
  only includes skipped fields that are true; GET instance-settings includes
  cookie_ext_available from env path check

Frontend:
- api.js: submitCapture forwards cookie_ext_enabled
- SettingsView.jsx: 'I Still Don't Care About Cookies' card in Extensions
  tab; always-active toggle (user can disable even when ext not installed);
  amber 'Not configured' hint + ARCHIVR_COOKIE_EXT guidance when unavailable
- CaptureDialog.jsx: 'Block cookie banners' toggle in Advanced options;
  always shown with amber hint when ext not configured; defaults from
  global setting

Operator setup: download + unzip the extension from GitHub releases, set
ARCHIVR_COOKIE_EXT=/path/to/unpacked/ext. No Node daemon needed.
This commit is contained in:
TheGeneralist 2026-07-08 11:25:37 +02:00
parent a6ca24b846
commit 52effb60f6
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
10 changed files with 260 additions and 70 deletions

View file

@ -654,6 +654,7 @@ struct CaptureBody {
ublock_enabled: Option<bool>,
/// Distil to article content via Readability before archiving. Absent = false.
reader_mode: Option<bool>,
cookie_ext_enabled: Option<bool>,
}
#[derive(Debug, serde::Deserialize)]
@ -733,26 +734,27 @@ async fn capture_handler(
let conn = database::open_or_initialize(&mounted.archive_path)?;
let job_uid = database::create_capture_job(&conn, &archive_id)?;
drop(conn);
// Load cookie rules and global uBlock setting from the auth DB.
let (cookie_rules, global_ublock) = {
// Load cookie rules and global uBlock / cookie-ext settings from the auth DB.
let (cookie_rules, global_ublock, global_cookie_ext) = {
match database::open_auth_db(&state.auth_db_path) {
Ok(conn) => {
let rules = database::list_cookie_rules(&conn).unwrap_or_default();
let ublock = database::get_instance_settings(&conn)
.map(|s| s.ublock_enabled)
.unwrap_or(true);
(rules, ublock)
let settings = database::get_instance_settings(&conn);
let ublock = settings.as_ref().map(|s| s.ublock_enabled).unwrap_or(true);
let cookie_ext = settings.map(|s| s.cookie_ext_enabled).unwrap_or(true);
(rules, ublock, cookie_ext)
}
Err(_) => (vec![], true),
Err(_) => (vec![], true, true),
}
};
// Per-capture body overrides global; if body doesn't specify, use the global setting.
// The resolved bool is then passed as Some(_) to singlefile, overriding the env var.
let effective_ublock = body.ublock_enabled.unwrap_or(global_ublock);
let effective_cookie_ext = body.cookie_ext_enabled.unwrap_or(global_cookie_ext);
let capture_config = capture::CaptureConfig {
cookie_rules,
ublock_enabled: Some(effective_ublock),
cookie_ext_enabled: Some(effective_cookie_ext),
reader_mode: body.reader_mode.unwrap_or(false),
};
@ -773,10 +775,19 @@ async fn capture_handler(
database::update_capture_job_status(&conn, &job_uid_bg, "running", None, None, None).ok();
match capture::perform_capture(&archive_paths, &locator, Some(&archive_id_bg), quality.as_deref(), &capture_config) {
Ok(result) => {
let notes = if result.ublock_skipped {
Some(r#"{"ublock_skipped":true}"#)
} else {
let mut notes_map = serde_json::Map::new();
if result.ublock_skipped {
notes_map.insert("ublock_skipped".into(), serde_json::Value::Bool(true));
}
if result.cookie_ext_skipped {
notes_map.insert("cookie_ext_skipped".into(), serde_json::Value::Bool(true));
}
let notes_str;
let notes: Option<&str> = if notes_map.is_empty() {
None
} else {
notes_str = serde_json::Value::Object(notes_map).to_string();
Some(&notes_str)
};
database::update_capture_job_status(
&conn,
@ -1043,9 +1054,15 @@ async fn get_instance_settings_handler(
.filter(|s| !s.is_empty())
.map(|p| std::path::Path::new(&p).is_dir())
.unwrap_or(false);
let cookie_ext_available = std::env::var("ARCHIVR_COOKIE_EXT")
.ok()
.filter(|s| !s.is_empty())
.map(|p| std::path::Path::new(&p).is_dir())
.unwrap_or(false);
let mut val = serde_json::to_value(&settings).unwrap_or_default();
if let Some(obj) = val.as_object_mut() {
obj.insert("ublock_ext_available".into(), serde_json::Value::Bool(ublock_ext_available));
obj.insert("cookie_ext_available".into(), serde_json::Value::Bool(cookie_ext_available));
}
Ok(Json(val))
}
@ -1063,6 +1080,7 @@ async fn update_instance_settings_handler(
if let Some(v) = body.open_registration_enabled { settings.open_registration_enabled = v; }
if let Some(v) = body.default_entry_visibility { settings.default_entry_visibility = v; }
if let Some(v) = body.ublock_enabled { settings.ublock_enabled = v; }
if let Some(v) = body.cookie_ext_enabled { settings.cookie_ext_enabled = v; }
database::update_instance_settings(&conn, &settings)?;
Ok(StatusCode::NO_CONTENT)
}
@ -1393,6 +1411,7 @@ struct UpdateInstanceSettingsBody {
open_registration_enabled: Option<bool>,
default_entry_visibility: Option<u32>,
ublock_enabled: Option<bool>,
cookie_ext_enabled: Option<bool>,
}
async fn admin_list_users(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title>
<script type="module" crossorigin src="/assets/index-Cxnyn8_x.js"></script>
<script type="module" crossorigin src="/assets/index-DNXMjfPb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C0BIEdow.css">
</head>
<body>