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:
parent
a6ca24b846
commit
52effb60f6
10 changed files with 260 additions and 70 deletions
|
|
@ -40,6 +40,8 @@ pub struct CaptureResult {
|
|||
/// `true` when uBlock was requested but the extension path was not found.
|
||||
/// The capture succeeded without ad-blocking; the UI should warn the user.
|
||||
pub ublock_skipped: bool,
|
||||
/// `true` when cookie-consent extension was requested but the path was not found.
|
||||
pub cookie_ext_skipped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -79,6 +81,8 @@ pub struct CaptureConfig {
|
|||
pub cookie_rules: Vec<database::CookieRule>,
|
||||
/// Override for uBlock Origin Lite during WebPage captures.
|
||||
pub ublock_enabled: Option<bool>,
|
||||
/// Override for cookie-consent extension during WebPage captures.
|
||||
pub cookie_ext_enabled: Option<bool>,
|
||||
/// Apply Mozilla Readability to distil the page to article content before archiving.
|
||||
pub reader_mode: bool,
|
||||
}
|
||||
|
|
@ -999,6 +1003,7 @@ pub fn perform_capture(
|
|||
run_uid: run.run_uid.clone(),
|
||||
status: "completed".to_string(),
|
||||
ublock_skipped: false,
|
||||
cookie_ext_skipped: false,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -1014,7 +1019,7 @@ pub fn perform_capture(
|
|||
|
||||
// Source: web page — archive as a self-contained HTML snapshot via single-file-cli
|
||||
if source == Source::WebPage {
|
||||
match downloader::singlefile::save(locator, store_path, ×tamp, &cookies, config.ublock_enabled, config.reader_mode) {
|
||||
match downloader::singlefile::save(locator, store_path, ×tamp, &cookies, config.ublock_enabled, config.cookie_ext_enabled, config.reader_mode) {
|
||||
Ok(result) => {
|
||||
let file_extension = ".html".to_string();
|
||||
let temp_html = store_path
|
||||
|
|
@ -1142,6 +1147,7 @@ pub fn perform_capture(
|
|||
run_uid: run.run_uid.clone(),
|
||||
status: "completed".to_string(),
|
||||
ublock_skipped: result.ublock_skipped,
|
||||
cookie_ext_skipped: result.cookie_ext_skipped,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -1197,6 +1203,7 @@ pub fn perform_capture(
|
|||
run_uid: run.run_uid.clone(),
|
||||
status: "completed".to_string(),
|
||||
ublock_skipped: false,
|
||||
cookie_ext_skipped: false,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -1353,6 +1360,7 @@ pub fn perform_capture(
|
|||
run_uid: run.run_uid.clone(),
|
||||
status: "completed".to_string(),
|
||||
ublock_skipped: false,
|
||||
cookie_ext_skipped: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ pub struct InstanceSettings {
|
|||
/// Global default for ad-blocking via uBlock Origin Lite during WebPage captures.
|
||||
/// Per-capture requests can override this.
|
||||
pub ublock_enabled: bool,
|
||||
/// Global default for cookie-consent banner dismissal via extension during WebPage captures.
|
||||
pub cookie_ext_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -198,7 +200,8 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> {
|
|||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)),
|
||||
public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)),
|
||||
public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1))
|
||||
public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)),
|
||||
cookie_ext_enabled INTEGER NOT NULL DEFAULT 1 CHECK (cookie_ext_enabled IN (0, 1))
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO instance_settings (
|
||||
|
|
@ -464,7 +467,8 @@ pub fn initialize_auth_schema(conn: &Connection) -> Result<()> {
|
|||
public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)),
|
||||
public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)),
|
||||
default_entry_visibility INTEGER NOT NULL DEFAULT 2,
|
||||
ublock_enabled INTEGER NOT NULL DEFAULT 1 CHECK (ublock_enabled IN (0, 1))
|
||||
ublock_enabled INTEGER NOT NULL DEFAULT 1 CHECK (ublock_enabled IN (0, 1)),
|
||||
cookie_ext_enabled INTEGER NOT NULL DEFAULT 1 CHECK (cookie_ext_enabled IN (0, 1))
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO instance_settings
|
||||
|
|
@ -509,6 +513,11 @@ pub fn initialize_auth_schema(conn: &Connection) -> Result<()> {
|
|||
"ALTER TABLE instance_settings ADD COLUMN ublock_enabled INTEGER NOT NULL DEFAULT 1",
|
||||
[],
|
||||
);
|
||||
// Add cookie_ext_enabled column to instance_settings if not present (idempotent migration)
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE instance_settings ADD COLUMN cookie_ext_enabled INTEGER NOT NULL DEFAULT 1",
|
||||
[],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -735,7 +744,8 @@ pub fn get_instance_settings(conn: &Connection) -> Result<InstanceSettings> {
|
|||
conn.query_row(
|
||||
"SELECT public_index_enabled, public_entry_content_enabled,
|
||||
public_archive_submission_enabled, default_entry_visibility,
|
||||
COALESCE(ublock_enabled, 1)
|
||||
COALESCE(ublock_enabled, 1),
|
||||
COALESCE(cookie_ext_enabled, 1)
|
||||
FROM instance_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
|
|
@ -745,6 +755,7 @@ pub fn get_instance_settings(conn: &Connection) -> Result<InstanceSettings> {
|
|||
open_registration_enabled: row.get::<_, i64>(2)? != 0,
|
||||
default_entry_visibility: row.get::<_, i64>(3)? as u32,
|
||||
ublock_enabled: row.get::<_, i64>(4)? != 0,
|
||||
cookie_ext_enabled: row.get::<_, i64>(5)? != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
@ -758,7 +769,8 @@ pub fn update_instance_settings(conn: &Connection, settings: &InstanceSettings)
|
|||
public_entry_content_enabled = ?2,
|
||||
public_archive_submission_enabled = ?3,
|
||||
default_entry_visibility = ?4,
|
||||
ublock_enabled = ?5
|
||||
ublock_enabled = ?5,
|
||||
cookie_ext_enabled = ?6
|
||||
WHERE id = 1",
|
||||
params![
|
||||
settings.public_index_enabled as i64,
|
||||
|
|
@ -766,6 +778,7 @@ pub fn update_instance_settings(conn: &Connection, settings: &InstanceSettings)
|
|||
settings.open_registration_enabled as i64,
|
||||
settings.default_entry_visibility as i64,
|
||||
settings.ublock_enabled as i64,
|
||||
settings.cookie_ext_enabled as i64,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -108,6 +108,9 @@ pub struct SaveResult {
|
|||
/// `true` when `ARCHIVR_UBLOCK=true` (the default) but the extension path
|
||||
/// was missing or invalid. The capture succeeded but ran without ad-blocking.
|
||||
pub ublock_skipped: bool,
|
||||
/// `true` when `ARCHIVR_COOKIE_CONSENT=true` (the default) but the extension path
|
||||
/// was missing or invalid. The capture succeeded but ran without cookie-consent blocking.
|
||||
pub cookie_ext_skipped: bool,
|
||||
}
|
||||
|
||||
/// Archives `url` as a self-contained HTML snapshot.
|
||||
|
|
@ -124,12 +127,14 @@ pub fn save(
|
|||
timestamp: &str,
|
||||
cookies: &HashMap<String, String>,
|
||||
ublock_enabled_override: Option<bool>,
|
||||
cookie_ext_enabled: Option<bool>,
|
||||
reader_mode: bool,
|
||||
) -> Result<SaveResult> {
|
||||
let single_file =
|
||||
env::var("ARCHIVR_SINGLE_FILE").unwrap_or_else(|_| "single-file".to_string());
|
||||
let chrome = env::var("ARCHIVR_CHROME").unwrap_or_else(|_| "chromium".to_string());
|
||||
let (ublock_ext, ublock_skipped) = resolve_ublock_config(ublock_enabled_override);
|
||||
let (cookie_ext, cookie_ext_skipped) = resolve_cookie_ext_config(cookie_ext_enabled);
|
||||
let mut result = save_with(
|
||||
url,
|
||||
store_path,
|
||||
|
|
@ -138,9 +143,11 @@ pub fn save(
|
|||
&chrome,
|
||||
cookies,
|
||||
ublock_ext.as_deref(),
|
||||
cookie_ext.as_deref(),
|
||||
reader_mode,
|
||||
)?;
|
||||
result.ublock_skipped = ublock_skipped;
|
||||
result.cookie_ext_skipped = cookie_ext_skipped;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +190,44 @@ fn resolve_ublock_config(enabled_override: Option<bool>) -> (Option<PathBuf>, bo
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolves cookie-consent extension configuration from env vars, optionally overridden by the caller.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `(Some(path), false)` — cookie-consent ext is enabled and the extension dir is valid.
|
||||
/// - `(None, true)` — cookie-consent ext is enabled but the extension dir is missing/invalid
|
||||
/// (warns to stderr; the capture proceeds without cookie-consent blocking).
|
||||
/// - `(None, false)` — cookie-consent ext is disabled (`ARCHIVR_COOKIE_CONSENT=false` or overridden).
|
||||
fn resolve_cookie_ext_config(enabled_override: Option<bool>) -> (Option<PathBuf>, bool) {
|
||||
let want_cookie_ext = enabled_override.unwrap_or_else(|| {
|
||||
let env_val = env::var("ARCHIVR_COOKIE_CONSENT").unwrap_or_else(|_| "true".to_string());
|
||||
!env_val.eq_ignore_ascii_case("false") && env_val != "0"
|
||||
});
|
||||
if !want_cookie_ext {
|
||||
return (None, false);
|
||||
}
|
||||
match env::var("ARCHIVR_COOKIE_EXT").ok().filter(|s| !s.is_empty()) {
|
||||
None => {
|
||||
eprintln!(
|
||||
"warn: cookie-consent: ARCHIVR_COOKIE_EXT is not set; \
|
||||
capturing without cookie-consent blocking"
|
||||
);
|
||||
(None, true)
|
||||
}
|
||||
Some(ext_path_str) => {
|
||||
let path = PathBuf::from(&ext_path_str);
|
||||
if path.is_dir() {
|
||||
(Some(path), false)
|
||||
} else {
|
||||
eprintln!(
|
||||
"warn: cookie-consent: ARCHIVR_COOKIE_EXT={ext_path_str:?} is not a directory; \
|
||||
capturing without cookie-consent blocking"
|
||||
);
|
||||
(None, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner implementation. Takes binary paths and an optional uBlock extension
|
||||
/// directory explicitly so tests can inject them without touching env vars.
|
||||
///
|
||||
|
|
@ -208,6 +253,7 @@ fn save_with(
|
|||
chrome: &str,
|
||||
cookies: &HashMap<String, String>,
|
||||
ublock_ext: Option<&Path>,
|
||||
cookie_ext: Option<&Path>,
|
||||
reader_mode: bool,
|
||||
) -> Result<SaveResult> {
|
||||
let temp_dir = store_path.join("temp").join(timestamp);
|
||||
|
|
@ -252,12 +298,21 @@ fn save_with(
|
|||
format!("--user-data-dir={}", chrome_data_dir.display()),
|
||||
"--window-size=1920,1080".to_string(),
|
||||
];
|
||||
if let Some(ext_path) = ublock_ext {
|
||||
// --headless=new is required for --load-extension to work.
|
||||
// It overrides single-file's own --headless default via the prefix-strip logic.
|
||||
// Build comma-separated extension list for Chrome flags.
|
||||
// --headless=new is required for --load-extension to work.
|
||||
let ext_paths: Vec<PathBuf> = [ublock_ext, cookie_ext]
|
||||
.iter()
|
||||
.filter_map(|p| p.map(|p| p.to_path_buf()))
|
||||
.collect();
|
||||
if !ext_paths.is_empty() {
|
||||
let joined = ext_paths
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
chrome_flags.push("--headless=new".to_string());
|
||||
chrome_flags.push(format!("--load-extension={}", ext_path.display()));
|
||||
chrome_flags.push(format!("--disable-extensions-except={}", ext_path.display()));
|
||||
chrome_flags.push(format!("--load-extension={joined}"));
|
||||
chrome_flags.push(format!("--disable-extensions-except={joined}"));
|
||||
}
|
||||
// Operator extras (e.g. --no-sandbox in Docker).
|
||||
let extra_chrome_args: Vec<String> = env::var("ARCHIVR_CHROME_ARGS")
|
||||
|
|
@ -350,7 +405,8 @@ fn save_with(
|
|||
title,
|
||||
favicon_hash,
|
||||
favicon_ext,
|
||||
ublock_skipped: false, // overwritten by save() from resolve_ublock_config()
|
||||
ublock_skipped: false, // overwritten by save() from resolve_ublock_config()
|
||||
cookie_ext_skipped: false, // overwritten by save() from resolve_cookie_ext_config()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -538,6 +594,7 @@ mod tests {
|
|||
"chromium",
|
||||
&HashMap::new(),
|
||||
None, // no ublock ext
|
||||
None, // no cookie ext
|
||||
false, // reader mode off
|
||||
);
|
||||
let err = result.unwrap_err();
|
||||
|
|
@ -548,6 +605,29 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_with_both_extensions_uses_comma_joined_flags() {
|
||||
use std::path::Path;
|
||||
// We can't run single-file here, but we can exercise the flag-building
|
||||
// logic by checking the path list construction directly.
|
||||
let ublock = Path::new("/tmp/ublock");
|
||||
let cookie = Path::new("/tmp/cookie");
|
||||
let ext_paths: Vec<std::path::PathBuf> = [Some(ublock), Some(cookie)]
|
||||
.iter()
|
||||
.filter_map(|p| p.map(|p| p.to_path_buf()))
|
||||
.collect();
|
||||
let joined = ext_paths
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
assert_eq!(joined, "/tmp/ublock,/tmp/cookie");
|
||||
let load_flag = format!("--load-extension={joined}");
|
||||
let except_flag = format!("--disable-extensions-except={joined}");
|
||||
assert_eq!(load_flag, "--load-extension=/tmp/ublock,/tmp/cookie");
|
||||
assert_eq!(except_flag, "--disable-extensions-except=/tmp/ublock,/tmp/cookie");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_ublock_config_disabled_when_false() {
|
||||
// Can't mutate env vars safely in parallel tests; test the logic directly
|
||||
|
|
|
|||
|
|
@ -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(¬es_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
40
crates/archivr-server/static/assets/index-DNXMjfPb.js
Normal file
40
crates/archivr-server/static/assets/index-DNXMjfPb.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export async function submitCapture(archiveId, locator, quality = null, extensio
|
|||
if (extensions) {
|
||||
if (typeof extensions.ublock_enabled === 'boolean') payload.ublock_enabled = extensions.ublock_enabled
|
||||
if (typeof extensions.reader_mode === 'boolean') payload.reader_mode = extensions.reader_mode
|
||||
if (typeof extensions.cookie_ext_enabled === 'boolean') payload.cookie_ext_enabled = extensions.cookie_ext_enabled
|
||||
}
|
||||
const res = await fetch(`/api/archives/${archiveId}/captures`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -116,18 +116,24 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
|
|||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
// null = use server default; true/false = per-session override
|
||||
const [ublockOverride, setUblockOverride] = useState(null)
|
||||
// Server-side global default (loaded on mount, null until loaded)
|
||||
const [globalUblock, setGlobalUblock] = useState(null)
|
||||
// Server-side global settings (loaded on mount, null until loaded)
|
||||
const [globalSettings, setGlobalSettings] = useState(null)
|
||||
// Cookie consent: session-level only, initialized from server default
|
||||
const [cookieExtEnabled, setCookieExtEnabled] = useState(true)
|
||||
|
||||
// Load global uBlock setting from server once on mount
|
||||
// Load global settings from server once on mount
|
||||
useEffect(() => {
|
||||
getInstanceSettings()
|
||||
.then(s => setGlobalUblock(s.ublock_enabled ?? true))
|
||||
.catch(() => setGlobalUblock(true)) // safe default
|
||||
.then(s => {
|
||||
setGlobalSettings(s)
|
||||
setCookieExtEnabled(s.cookie_ext_enabled ?? true)
|
||||
})
|
||||
.catch(() => setGlobalSettings({}))
|
||||
}, [])
|
||||
|
||||
// Effective uBlock for this session
|
||||
const ublockEnabled = ublockOverride !== null ? ublockOverride : (globalUblock ?? true)
|
||||
const ublockEnabled = ublockOverride !== null ? ublockOverride : (globalSettings?.ublock_enabled ?? true)
|
||||
|
||||
// Reader mode: off by default, per-session only
|
||||
const [readerMode, setReaderMode] = useState(false)
|
||||
|
||||
|
|
@ -242,7 +248,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
|
|||
const qual = item.quality || 'best'
|
||||
setItems(prev => prev.map(it => it.id === item.id ? { ...it, status: 'submitting', error: null } : it))
|
||||
try {
|
||||
const extensions = { ublock_enabled: ublockEnabled, reader_mode: readerMode }
|
||||
const extensions = { ublock_enabled: ublockEnabled, reader_mode: readerMode, cookie_ext_enabled: cookieExtEnabled }
|
||||
const job = await submitCapture(aid, loc, qual, extensions)
|
||||
setItems(prev => prev.map(it =>
|
||||
it.id === item.id ? { ...it, status: 'running', jobUid: job.job_uid, archiveId: aid } : it
|
||||
|
|
@ -397,6 +403,25 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
|
|||
<span className="ext-toggle-knob" />
|
||||
</button>
|
||||
</label>
|
||||
<label className="capture-ext-row" style={{ marginTop: 8 }}>
|
||||
<span className="capture-ext-label">
|
||||
<span className="capture-ext-name">Block cookie banners</span>
|
||||
<span className="capture-ext-desc">Dismiss cookie consent banners during this capture</span>
|
||||
{!globalSettings?.cookie_ext_available && (
|
||||
<span className="capture-ext-hint">Not configured — set <code>ARCHIVR_COOKIE_EXT</code></span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={cookieExtEnabled}
|
||||
className={`ext-toggle ext-toggle--sm${cookieExtEnabled ? ' ext-toggle--on' : ''}`}
|
||||
onClick={() => setCookieExtEnabled(v => !v)}
|
||||
aria-label="Toggle cookie banner blocking for this capture"
|
||||
>
|
||||
<span className="ext-toggle-knob" />
|
||||
</button>
|
||||
</label>
|
||||
<label className="capture-ext-row" style={{ marginTop: 8 }}>
|
||||
<span className="capture-ext-label">
|
||||
<span className="capture-ext-name">Reader mode</span>
|
||||
|
|
|
|||
|
|
@ -669,10 +669,26 @@ function ExtensionsTab() {
|
|||
}
|
||||
}
|
||||
|
||||
async function toggleCookieExt(val) {
|
||||
setSaving(true)
|
||||
setMsg(null)
|
||||
try {
|
||||
await updateInstanceSettings({ cookie_ext_enabled: val })
|
||||
setSettings(s => ({ ...s, cookie_ext_enabled: val }))
|
||||
setMsg({ ok: true, text: 'Saved.' })
|
||||
} catch (e) {
|
||||
setMsg({ ok: false, text: e.message })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="muted">Loading\u2026</div>
|
||||
|
||||
const extAvailable = settings?.ublock_ext_available ?? false
|
||||
const extEnabled = settings?.ublock_enabled ?? true
|
||||
const cookieExtAvailable = settings?.cookie_ext_available ?? false
|
||||
const cookieExtEnabled = settings?.cookie_ext_enabled ?? true
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 560 }}>
|
||||
|
|
@ -712,6 +728,34 @@ function ExtensionsTab() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ext-card">
|
||||
<div className="ext-card-header">
|
||||
<div className="ext-card-info">
|
||||
<span className="ext-card-name">I Still Don’t Care About Cookies</span>
|
||||
<span className="ext-card-desc">
|
||||
Dismiss cookie consent banners during archiving.
|
||||
</span>
|
||||
{!cookieExtAvailable && (
|
||||
<span className="ext-card-hint">
|
||||
Not configured — set <code>ARCHIVR_COOKIE_EXT</code> to the
|
||||
unpacked extension directory to enable.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={cookieExtEnabled}
|
||||
className={`ext-toggle${cookieExtEnabled ? ' ext-toggle--on' : ''}`}
|
||||
onClick={() => toggleCookieExt(!cookieExtEnabled)}
|
||||
disabled={saving}
|
||||
aria-label="Toggle I Still Don't Care About Cookies"
|
||||
>
|
||||
<span className="ext-toggle-knob" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{msg && <div className={`form-msg form-msg--${msg.ok ? 'ok' : 'err'}`}>{msg.text}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue