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

feat: integrate Modal Closer as injected browser script (#27)

Port the modalcloser abx-plugin as a SingleFile --browser-script rather
than a spawned Puppeteer daemon. No external extension download required.

Core behavior (singlefile.rs):
- MODAL_CLOSER_DIALOG_OVERRIDES: main-world <script> bridge (best-effort;
  blocked by strict script-src CSP) that overrides window.alert/confirm/
  prompt/print, nulls window.onbeforeunload, traps its setter, and wraps
  window.addEventListener to no-op 'beforeunload' registrations.
- MODAL_CLOSER_POLLING_SETUP: defines _archivr_mc_run() which runs two
  passes on every tick (immediate first run, then setInterval every 500 ms
  matching MODALCLOSER_POLL_INTERVAL default):
    Pass 1 (best-effort, CSP-sensitive): main-world <script> bridge calls
    Bootstrap/jQuery/jQuery UI/SweetAlert teardown APIs.
    Pass 2 (always, CSP-immune): isolated-world DOM mutations — Escape-key
    dispatch (Radix/Headless UI/Angular Material), backdrop clicks, full
    CSS selector hiding for 40+ named consent/overlay vendors, body
    scroll-lock reset. Direct DOM mutations need no inline script execution.
- resolve_modal_closer_config(): reads ARCHIVR_MODAL_CLOSER env var
  (default true); no external resource required.
- modal_closer_enabled: Option<bool> in CaptureConfig so Default::default()
  yields None (follow env var) rather than false.

Schema (database.rs):
- modal_closer_enabled column in auth DB instance_settings (default 1).
- Idempotent ALTER TABLE migration for existing databases.
- get/update_instance_settings updated (SELECT col 6, UPDATE param ?7).

HTTP (routes.rs):
- modal_closer_enabled in CaptureBody and UpdateInstanceSettingsBody.
- Per-capture body overrides global setting which overrides env var.

Frontend:
- SettingsView ExtensionsTab: third ext-card for Modal & Dialog Closer.
- CaptureDialog: per-capture toggle in Advanced Options, state initialized
  from server default, included in submitCapture payload.
- api.js: submitCapture forwards modal_closer_enabled to capture body.

Behavior vs original modalcloser daemon:
- Functionally identical on non-strict-CSP pages (the large majority).
- Gap: <script> bridge is subject to page CSP; page.evaluate() is not.
  On strict-CSP pages framework teardown and dialog overrides are no-ops;
  CSS selector hiding and Escape dispatch (Pass 2) always work.
- Gap: alert/confirm/prompt overridden to instant no-op vs CDP timed
  dialog.accept() with 1250 ms delay; irrelevant for archival in practice.
This commit is contained in:
TheGeneralist 2026-07-14 13:47:47 +02:00 committed by GitHub
parent b0d02034ea
commit 278dc928df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 294 additions and 63 deletions

View file

@ -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
<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">Close modals and dialogs</span>
<span className="capture-ext-desc">Auto-dismiss cookie banners and overlays during this capture</span>
</span>
<button
type="button"
role="switch"
aria-checked={modalCloserEnabled}
className={`ext-toggle ext-toggle--sm${modalCloserEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => setModalCloserEnabled(v => !v)}
aria-label="Toggle modal closer for this capture"
>
<span className="ext-toggle-knob" />
</button>
</label>
</div>
)}
</div>