diff --git a/crates/archivr-core/src/capture.rs b/crates/archivr-core/src/capture.rs index bedc501..b02fe59 100644 --- a/crates/archivr-core/src/capture.rs +++ b/crates/archivr-core/src/capture.rs @@ -85,6 +85,8 @@ pub struct CaptureConfig { pub cookie_ext_enabled: Option, /// Apply Mozilla Readability to distil the page to article content before archiving. pub reader_mode: bool, + /// Override for modal-closer browser-script behavior during WebPage captures. + pub modal_closer_enabled: Option, } /// Resolves which cookies apply to `url` by evaluating all rules in ordinal order. @@ -1036,7 +1038,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.cookie_ext_enabled, config.reader_mode) { + match downloader::singlefile::save(locator, store_path, ×tamp, &cookies, config.ublock_enabled, config.cookie_ext_enabled, config.reader_mode, config.modal_closer_enabled) { Ok(result) => { let file_extension = ".html".to_string(); let temp_html = store_path diff --git a/crates/archivr-core/src/database.rs b/crates/archivr-core/src/database.rs index e4c8730..159972f 100644 --- a/crates/archivr-core/src/database.rs +++ b/crates/archivr-core/src/database.rs @@ -142,6 +142,8 @@ pub struct InstanceSettings { pub ublock_enabled: bool, /// Global default for cookie-consent banner dismissal via extension during WebPage captures. pub cookie_ext_enabled: bool, + /// Global default for modal-closer browser-script behavior during WebPage captures. + pub modal_closer_enabled: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -468,7 +470,8 @@ pub fn initialize_auth_schema(conn: &Connection) -> Result<()> { 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)), - cookie_ext_enabled INTEGER NOT NULL DEFAULT 1 CHECK (cookie_ext_enabled IN (0, 1)) + cookie_ext_enabled INTEGER NOT NULL DEFAULT 1 CHECK (cookie_ext_enabled IN (0, 1)), + modal_closer_enabled INTEGER NOT NULL DEFAULT 1 CHECK (modal_closer_enabled IN (0, 1)) ); INSERT OR IGNORE INTO instance_settings @@ -518,6 +521,11 @@ pub fn initialize_auth_schema(conn: &Connection) -> Result<()> { "ALTER TABLE instance_settings ADD COLUMN cookie_ext_enabled INTEGER NOT NULL DEFAULT 1", [], ); + // Add modal_closer_enabled column to instance_settings if not present (idempotent migration) + let _ = conn.execute( + "ALTER TABLE instance_settings ADD COLUMN modal_closer_enabled INTEGER NOT NULL DEFAULT 1", + [], + ); Ok(()) } @@ -745,7 +753,8 @@ pub fn get_instance_settings(conn: &Connection) -> Result { "SELECT public_index_enabled, public_entry_content_enabled, public_archive_submission_enabled, default_entry_visibility, COALESCE(ublock_enabled, 1), - COALESCE(cookie_ext_enabled, 1) + COALESCE(cookie_ext_enabled, 1), + COALESCE(modal_closer_enabled, 1) FROM instance_settings WHERE id = 1", [], |row| { @@ -756,6 +765,7 @@ pub fn get_instance_settings(conn: &Connection) -> Result { default_entry_visibility: row.get::<_, i64>(3)? as u32, ublock_enabled: row.get::<_, i64>(4)? != 0, cookie_ext_enabled: row.get::<_, i64>(5)? != 0, + modal_closer_enabled: row.get::<_, i64>(6)? != 0, }) }, ) @@ -770,7 +780,8 @@ pub fn update_instance_settings(conn: &Connection, settings: &InstanceSettings) public_archive_submission_enabled = ?3, default_entry_visibility = ?4, ublock_enabled = ?5, - cookie_ext_enabled = ?6 + cookie_ext_enabled = ?6, + modal_closer_enabled = ?7 WHERE id = 1", params![ settings.public_index_enabled as i64, @@ -779,6 +790,7 @@ pub fn update_instance_settings(conn: &Connection, settings: &InstanceSettings) settings.default_entry_visibility as i64, settings.ublock_enabled as i64, settings.cookie_ext_enabled as i64, + settings.modal_closer_enabled as i64, ], )?; Ok(()) diff --git a/crates/archivr-core/src/downloader/singlefile.rs b/crates/archivr-core/src/downloader/singlefile.rs index dabc667..78c80b7 100644 --- a/crates/archivr-core/src/downloader/singlefile.rs +++ b/crates/archivr-core/src/downloader/singlefile.rs @@ -94,6 +94,112 @@ const READER_MODE_SCRIPT: &str = concat!( "# ); +// The modal-closer browser scripts below (MODAL_CLOSER_DIALOG_OVERRIDES and +// MODAL_CLOSER_POLLING_SETUP) incorporate logic ported from the modalcloser +// plugin in the abx-plugins project: +// https://github.com/ArchiveBox/abx-plugins/tree/main/abx_plugins/plugins/modalcloser +// +// MIT License +// Copyright (c) 2024 Nick Sweeting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// Injected at the top of the strip-scripts user-script when modal-closer is enabled. +/// Bridges to the main JS world via an inline ` + diff --git a/frontend/src/api.js b/frontend/src/api.js index a3dc365..03e5704 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -122,11 +122,12 @@ export async function fetchTags(archiveId) { export async function submitCapture(archiveId, locator, quality = null, extensions = null) { const payload = { locator } if (quality && quality !== 'best') payload.quality = quality - // extensions: { ublock_enabled?: bool, reader_mode?: bool } — per-capture overrides + // extensions: { ublock_enabled?: bool, reader_mode?: bool, cookie_ext_enabled?: bool, modal_closer_enabled?: bool } 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 + if (typeof extensions.modal_closer_enabled === 'boolean') payload.modal_closer_enabled = extensions.modal_closer_enabled } const res = await fetch(`/api/archives/${archiveId}/captures`, { method: "POST", diff --git a/frontend/src/components/CaptureDialog.jsx b/frontend/src/components/CaptureDialog.jsx index 4b1677a..c4b326d 100644 --- a/frontend/src/components/CaptureDialog.jsx +++ b/frontend/src/components/CaptureDialog.jsx @@ -122,6 +122,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on const [globalSettings, setGlobalSettings] = useState(null) // Cookie consent: session-level only, initialized from server default const [cookieExtEnabled, setCookieExtEnabled] = useState(true) + const [modalCloserEnabled, setModalCloserEnabled] = useState(true) // Load global settings from server once on mount useEffect(() => { @@ -129,6 +130,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on .then(s => { setGlobalSettings(s) setCookieExtEnabled(s.cookie_ext_enabled ?? true) + setModalCloserEnabled(s.modal_closer_enabled ?? true) }) .catch(() => setGlobalSettings({})) }, []) @@ -299,7 +301,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, cookie_ext_enabled: cookieExtEnabled } + const extensions = { ublock_enabled: ublockEnabled, reader_mode: readerMode, cookie_ext_enabled: cookieExtEnabled, modal_closer_enabled: modalCloserEnabled } 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 @@ -499,6 +501,22 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on + )} diff --git a/frontend/src/components/SettingsView.jsx b/frontend/src/components/SettingsView.jsx index 38ce545..a60622f 100644 --- a/frontend/src/components/SettingsView.jsx +++ b/frontend/src/components/SettingsView.jsx @@ -683,12 +683,27 @@ function ExtensionsTab() { } } + async function toggleModalCloser(val) { + setSaving(true) + setMsg(null) + try { + await updateInstanceSettings({ modal_closer_enabled: val }) + setSettings(s => ({ ...s, modal_closer_enabled: val })) + setMsg({ ok: true, text: 'Saved.' }) + } catch (e) { + setMsg({ ok: false, text: e.message }) + } finally { + setSaving(false) + } + } + if (loading) return
Loading\u2026
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 + const modalCloserEnabled = settings?.modal_closer_enabled ?? true return (
@@ -756,6 +771,30 @@ function ExtensionsTab() {
+ +
+
+
+ Modal & Dialog Closer + + Auto-dismiss cookie banners, consent overlays, and other modal dialogs + before a WebPage capture is taken. Implemented as an injected browser + script; no external extension required. + +
+ +
+
{msg &&
{msg.text}
}