Implement passive consent evidence workflow with extension-driven capture
Dieser Commit ist enthalten in:
+443
-77
@@ -9,11 +9,13 @@ const GVL_AUTO_UPDATE_SOURCE = "extension_startup";
|
||||
const AUTO_GVL_CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
||||
const AUTO_GVL_CHECK_STORAGE_KEY = "vendorgetAutoGvlUpdateStatus";
|
||||
const CONSENT_CAPTURE_SESSION_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const CONSENT_CAPTURE_BADGE_BLINK_MS = 700;
|
||||
|
||||
let isAutoGvlCheckRunning = false;
|
||||
let lastAutoGvlCheckStartedAt = null;
|
||||
let latestGvlUpdateStatus = null;
|
||||
const consentCaptureSessions = new Map();
|
||||
const consentCaptureBadgeTimers = new Map();
|
||||
|
||||
browser.runtime.onMessage.addListener((message, sender) =>
|
||||
handleVendorGetMessage(message, sender)
|
||||
@@ -30,7 +32,7 @@ browser.tabs.onRemoved.addListener((tabId) => {
|
||||
|
||||
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||
if (changeInfo.url) {
|
||||
cleanupConsentCaptureSessionsForTab(tabId, "page_changed");
|
||||
cleanupConsentCaptureSessionsForTab(tabId, "page_changed", changeInfo.url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,6 +135,22 @@ async function handleVendorGetMessage(message, sender) {
|
||||
return handleGetLatestConsentStateMessage();
|
||||
}
|
||||
|
||||
if (message.type === "get_pre_consent_capture_status") {
|
||||
return handleGetPreConsentCaptureStatusMessage(message);
|
||||
}
|
||||
|
||||
if (message.type === "start_pre_consent_capture") {
|
||||
return handleStartPreConsentCaptureMessage(message);
|
||||
}
|
||||
|
||||
if (message.type === "decline_pre_consent_capture") {
|
||||
return handleDeclinePreConsentCaptureMessage(message);
|
||||
}
|
||||
|
||||
if (message.type === "probe_pre_consent_capture_for_tab") {
|
||||
return handleProbePreConsentCaptureForTabMessage(message);
|
||||
}
|
||||
|
||||
if (message.type === "get_consent_evidence_chain") {
|
||||
return handleGetConsentEvidenceChainMessage(message);
|
||||
}
|
||||
@@ -1003,6 +1021,178 @@ async function handleGetLatestConsentStateMessage() {
|
||||
};
|
||||
}
|
||||
|
||||
async function handleGetPreConsentCaptureStatusMessage(message) {
|
||||
const tabId = normalizeTabId(message?.payload?.tabId);
|
||||
const session = getOpenConsentCaptureSessionForTab(tabId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
capture: session ? buildPreConsentCaptureStatus(session) : null
|
||||
};
|
||||
}
|
||||
|
||||
async function handleStartPreConsentCaptureMessage(message) {
|
||||
const tabId = normalizeTabId(message?.payload?.tabId);
|
||||
const session = getOpenConsentCaptureSessionForTab(tabId);
|
||||
|
||||
if (!session || session.status !== "attention") {
|
||||
return {
|
||||
success: false,
|
||||
error: "pre_consent_capture_not_available"
|
||||
};
|
||||
}
|
||||
|
||||
session.status = "recording";
|
||||
session.captureSessionId = createCaptureSessionId();
|
||||
updateConsentCaptureBadge(session);
|
||||
|
||||
const bufferedEvents = session.bufferedPreConsentEvents;
|
||||
session.bufferedPreConsentEvents = [];
|
||||
|
||||
for (const bufferedEvent of bufferedEvents) {
|
||||
await persistProviderAnnouncementEvent({
|
||||
captureSessionId: session.captureSessionId,
|
||||
rawCapture: bufferedEvent.rawCapture,
|
||||
sender: bufferedEvent.sender
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
capture: buildPreConsentCaptureStatus(session)
|
||||
};
|
||||
}
|
||||
|
||||
async function handleDeclinePreConsentCaptureMessage(message) {
|
||||
const tabId = normalizeTabId(message?.payload?.tabId);
|
||||
const session = getOpenConsentCaptureSessionForTab(tabId);
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
error: "pre_consent_capture_not_available"
|
||||
};
|
||||
}
|
||||
|
||||
declineConsentCaptureSession(session);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
capture: null
|
||||
};
|
||||
}
|
||||
|
||||
async function handleProbePreConsentCaptureForTabMessage(message) {
|
||||
const tabId = normalizeTabId(message?.payload?.tabId);
|
||||
|
||||
if (tabId === null || !(await isConsentCaptureEnabled())) {
|
||||
return {
|
||||
success: true,
|
||||
capture: null
|
||||
};
|
||||
}
|
||||
|
||||
if (hasCompletedConsentCaptureSessionForTab(tabId)) {
|
||||
return {
|
||||
success: true,
|
||||
capture: null
|
||||
};
|
||||
}
|
||||
|
||||
const existingSession = getOpenConsentCaptureSessionForTab(tabId);
|
||||
|
||||
if (existingSession) {
|
||||
return {
|
||||
success: true,
|
||||
capture: buildPreConsentCaptureStatus(existingSession)
|
||||
};
|
||||
}
|
||||
|
||||
let tab = null;
|
||||
|
||||
try {
|
||||
tab = await browser.tabs.get(tabId);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: true,
|
||||
capture: null
|
||||
};
|
||||
}
|
||||
|
||||
let probe = null;
|
||||
|
||||
try {
|
||||
probe = await browser.tabs.sendMessage(tabId, {
|
||||
type: "probe_tcf_state"
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
success: true,
|
||||
capture: null
|
||||
};
|
||||
}
|
||||
|
||||
const rawCapture = probe?.capture ?? null;
|
||||
|
||||
if (!probe?.success || !isProbeOpenPreConsentCapture(rawCapture, probe.source)) {
|
||||
return {
|
||||
success: true,
|
||||
capture: null
|
||||
};
|
||||
}
|
||||
|
||||
const sender = {
|
||||
tab,
|
||||
frameId: 0,
|
||||
url: tab?.url ?? null
|
||||
};
|
||||
|
||||
const session = startAttentionConsentCaptureSession(rawCapture, sender);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
capture: session ? buildPreConsentCaptureStatus(session) : null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTabId(value) {
|
||||
const tabId = Number(value);
|
||||
|
||||
return Number.isInteger(tabId) ? tabId : null;
|
||||
}
|
||||
|
||||
function getOpenConsentCaptureSessionForTab(tabId) {
|
||||
if (tabId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const session of consentCaptureSessions.values()) {
|
||||
if (
|
||||
session.tabId === tabId &&
|
||||
(session.status === "attention" || session.status === "recording")
|
||||
) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildPreConsentCaptureStatus(session) {
|
||||
return {
|
||||
captureSessionId: session.captureSessionId ?? null,
|
||||
status: session.status,
|
||||
tabId: session.tabId,
|
||||
frameId: session.frameId,
|
||||
eventCount: session.bufferedPreConsentEvents?.length ?? 0,
|
||||
firstEventStatus:
|
||||
session.bufferedPreConsentEvents?.[0]?.rawCapture?.eventStatus ?? null,
|
||||
page: session.page ?? null,
|
||||
detectedAt: session.detectedAt ?? null,
|
||||
updatedAt: session.updatedAt ?? null
|
||||
};
|
||||
}
|
||||
|
||||
async function handleGetConsentEvidenceChainMessage(message) {
|
||||
const stateFingerprint = normalizeStateFingerprint(
|
||||
message?.payload?.stateFingerprint
|
||||
@@ -2142,19 +2332,94 @@ function buildConsentEventDiagnostics(rawCapture, extraDiagnostics) {
|
||||
|
||||
function startConsentCaptureSessionTimeout(session) {
|
||||
session.timeoutId = setTimeout(() => {
|
||||
cleanupIncompleteConsentCaptureSession(session, "timeout");
|
||||
timeoutConsentCaptureSession(session);
|
||||
}, CONSENT_CAPTURE_SESSION_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function isOpenPreConsentCapture(rawCapture) {
|
||||
const eventStatus = rawCapture?.eventStatus ?? null;
|
||||
|
||||
return eventStatus === "cmpuishown" || eventStatus === "tcloaded";
|
||||
}
|
||||
|
||||
function isProbeOpenPreConsentCapture(rawCapture, source) {
|
||||
if (source === "content_memory") {
|
||||
return isOpenPreConsentCapture(rawCapture);
|
||||
}
|
||||
|
||||
const eventStatus = rawCapture?.eventStatus ?? null;
|
||||
|
||||
if (eventStatus === "cmpuishown") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventStatus !== "tcloaded") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !rawCapture?.tcString && !rawCapture?.addtlConsent;
|
||||
}
|
||||
|
||||
function startAttentionConsentCaptureSession(rawCapture, sender) {
|
||||
if (!isOpenPreConsentCapture(rawCapture)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionKey = getConsentCaptureSessionKey(sender);
|
||||
|
||||
if (!sessionKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasCompletedConsentCaptureSessionForTab(sender?.tab?.id ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let session = consentCaptureSessions.get(sessionKey);
|
||||
|
||||
if (session?.status === "aborted" || session?.status === "completed") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session?.status === "recording") {
|
||||
return session;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = {
|
||||
key: sessionKey,
|
||||
tabId: sender?.tab?.id ?? null,
|
||||
frameId: sender?.frameId ?? null,
|
||||
status: "attention",
|
||||
captureSessionId: null,
|
||||
bufferedPreConsentEvents: [],
|
||||
timeoutId: null,
|
||||
page: buildConsentEventPageContext(rawCapture, sender),
|
||||
detectedAt: new Date().toISOString(),
|
||||
updatedAt: null
|
||||
};
|
||||
|
||||
consentCaptureSessions.set(sessionKey, session);
|
||||
startConsentCaptureSessionTimeout(session);
|
||||
}
|
||||
|
||||
session.updatedAt = new Date().toISOString();
|
||||
session.bufferedPreConsentEvents.push({
|
||||
rawCapture,
|
||||
sender
|
||||
});
|
||||
updateConsentCaptureBadge(session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
async function handlePreConsentTcfEvent(rawCapture, sender) {
|
||||
if (isEvidenceWriteSuspended()) {
|
||||
console.info("VG-Observe pre-consent capture skipped: maintenance mode");
|
||||
return;
|
||||
}
|
||||
|
||||
const eventStatus = rawCapture?.eventStatus ?? null;
|
||||
|
||||
if (eventStatus !== "cmpuishown" && eventStatus !== "tcloaded") {
|
||||
if (!isOpenPreConsentCapture(rawCapture)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2164,13 +2429,17 @@ async function handlePreConsentTcfEvent(rawCapture, sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasCompletedConsentCaptureSessionForTab(sender?.tab?.id ?? null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let session = consentCaptureSessions.get(sessionKey);
|
||||
|
||||
if (session?.status === "declined" || session?.status === "completed") {
|
||||
if (session?.status === "aborted" || session?.status === "completed") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session?.status === "active") {
|
||||
if (session?.status === "recording") {
|
||||
await persistProviderAnnouncementEvent({
|
||||
captureSessionId: session.captureSessionId,
|
||||
rawCapture,
|
||||
@@ -2179,84 +2448,34 @@ async function handlePreConsentTcfEvent(rawCapture, sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = {
|
||||
key: sessionKey,
|
||||
tabId: sender?.tab?.id ?? null,
|
||||
frameId: sender?.frameId ?? null,
|
||||
status: "prompting",
|
||||
captureSessionId: null,
|
||||
bufferedPreConsentEvents: [],
|
||||
timeoutId: null
|
||||
};
|
||||
|
||||
consentCaptureSessions.set(sessionKey, session);
|
||||
startConsentCaptureSessionTimeout(session);
|
||||
}
|
||||
|
||||
session.bufferedPreConsentEvents.push({
|
||||
rawCapture,
|
||||
sender
|
||||
});
|
||||
|
||||
if (session.promptInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.promptInProgress = true;
|
||||
|
||||
const accepted = await askUserForConsentCapture(sender);
|
||||
|
||||
if (!consentCaptureSessions.has(sessionKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.promptInProgress = false;
|
||||
|
||||
if (!accepted) {
|
||||
session.status = "declined";
|
||||
session.bufferedPreConsentEvents = [];
|
||||
return;
|
||||
}
|
||||
|
||||
session.status = "active";
|
||||
session.captureSessionId = createCaptureSessionId();
|
||||
|
||||
const bufferedEvents = session.bufferedPreConsentEvents;
|
||||
session.bufferedPreConsentEvents = [];
|
||||
|
||||
for (const bufferedEvent of bufferedEvents) {
|
||||
await persistProviderAnnouncementEvent({
|
||||
captureSessionId: session.captureSessionId,
|
||||
rawCapture: bufferedEvent.rawCapture,
|
||||
sender: bufferedEvent.sender
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function askUserForConsentCapture(sender) {
|
||||
console.info(
|
||||
"VG-Observe pre-consent provider announcement capture inactive: extension-owned consent prompt is not implemented",
|
||||
{
|
||||
tabId: sender?.tab?.id ?? null,
|
||||
frameId: sender?.frameId ?? null
|
||||
}
|
||||
);
|
||||
|
||||
return false;
|
||||
startAttentionConsentCaptureSession(rawCapture, sender);
|
||||
}
|
||||
|
||||
function getActiveConsentCaptureSessionId(sender) {
|
||||
const sessionKey = getConsentCaptureSessionKey(sender);
|
||||
const session = sessionKey ? consentCaptureSessions.get(sessionKey) : null;
|
||||
|
||||
if (!session || session.status !== "active") {
|
||||
if (!session || session.status !== "recording") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.captureSessionId ?? null;
|
||||
}
|
||||
|
||||
function hasCompletedConsentCaptureSessionForTab(tabId) {
|
||||
if (tabId === null || tabId === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const session of consentCaptureSessions.values()) {
|
||||
if (session.tabId === tabId && session.status === "completed") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function completeConsentCaptureSession(sender) {
|
||||
const sessionKey = getConsentCaptureSessionKey(sender);
|
||||
let session = sessionKey ? consentCaptureSessions.get(sessionKey) : null;
|
||||
@@ -2282,30 +2501,57 @@ function completeConsentCaptureSession(sender) {
|
||||
session.status = "completed";
|
||||
session.completedAt = new Date().toISOString();
|
||||
session.bufferedPreConsentEvents = [];
|
||||
session.promptInProgress = false;
|
||||
|
||||
if (session.timeoutId) {
|
||||
clearTimeout(session.timeoutId);
|
||||
session.timeoutId = null;
|
||||
}
|
||||
|
||||
resetConsentCaptureBadge(session.tabId);
|
||||
}
|
||||
|
||||
function cleanupConsentCaptureSessionsForTab(tabId, reason) {
|
||||
function cleanupConsentCaptureSessionsForTab(tabId, reason, nextUrl) {
|
||||
consentCaptureSessions.forEach((session) => {
|
||||
if (session.tabId === tabId) {
|
||||
if (
|
||||
reason === "page_changed" &&
|
||||
session.status === "completed" &&
|
||||
isSameConsentCaptureUrl(session.page?.url, nextUrl)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupIncompleteConsentCaptureSession(session, reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isSameConsentCaptureUrl(left, right) {
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return String(left) === String(right);
|
||||
}
|
||||
|
||||
function cleanupIncompleteConsentCaptureSession(session, reason) {
|
||||
const wasCompleted = session.status === "completed";
|
||||
|
||||
if (session.timeoutId) {
|
||||
clearTimeout(session.timeoutId);
|
||||
}
|
||||
|
||||
consentCaptureSessions.delete(session.key);
|
||||
if (!wasCompleted) {
|
||||
session.status = "aborted";
|
||||
session.abortedAt = new Date().toISOString();
|
||||
session.abortReason = reason;
|
||||
session.bufferedPreConsentEvents = [];
|
||||
}
|
||||
|
||||
if (!session.captureSessionId || session.status === "completed") {
|
||||
consentCaptureSessions.delete(session.key);
|
||||
resetConsentCaptureBadge(session.tabId);
|
||||
|
||||
if (!session.captureSessionId || wasCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2317,6 +2563,126 @@ function cleanupIncompleteConsentCaptureSession(session, reason) {
|
||||
});
|
||||
}
|
||||
|
||||
function declineConsentCaptureSession(session) {
|
||||
if (session.timeoutId) {
|
||||
clearTimeout(session.timeoutId);
|
||||
session.timeoutId = null;
|
||||
}
|
||||
|
||||
session.status = "aborted";
|
||||
session.abortedAt = new Date().toISOString();
|
||||
session.abortReason = "user_declined";
|
||||
session.bufferedPreConsentEvents = [];
|
||||
resetConsentCaptureBadge(session.tabId);
|
||||
}
|
||||
|
||||
function timeoutConsentCaptureSession(session) {
|
||||
if (!consentCaptureSessions.has(session.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.status === "completed") {
|
||||
return;
|
||||
}
|
||||
|
||||
session.timeoutId = null;
|
||||
session.status = "aborted";
|
||||
session.abortedAt = new Date().toISOString();
|
||||
session.abortReason = "timeout";
|
||||
session.bufferedPreConsentEvents = [];
|
||||
resetConsentCaptureBadge(session.tabId);
|
||||
|
||||
if (!session.captureSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteProviderAnnouncementEventsForSession(
|
||||
session.captureSessionId,
|
||||
"timeout"
|
||||
).catch((error) => {
|
||||
console.warn("VG-Observe provider announcement cleanup failed", error);
|
||||
});
|
||||
}
|
||||
|
||||
function getActionApi() {
|
||||
return browser.action ?? browser.browserAction ?? null;
|
||||
}
|
||||
|
||||
function updateConsentCaptureBadge(session) {
|
||||
const actionApi = getActionApi();
|
||||
const tabId = session?.tabId;
|
||||
|
||||
if (!actionApi || tabId === null || tabId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const badgeText = session.status === "recording" ? "REC" : "!";
|
||||
const badgeColor = session.status === "recording" ? "#2563eb" : "#dc2626";
|
||||
|
||||
stopConsentCaptureBadgeBlink(tabId);
|
||||
|
||||
let visible = true;
|
||||
setActionBadgeBackgroundColor(actionApi, { tabId, color: badgeColor });
|
||||
setActionBadgeText(actionApi, { tabId, text: badgeText });
|
||||
|
||||
const timerId = setInterval(() => {
|
||||
visible = !visible;
|
||||
setActionBadgeText(actionApi, {
|
||||
tabId,
|
||||
text: visible ? badgeText : ""
|
||||
});
|
||||
}, CONSENT_CAPTURE_BADGE_BLINK_MS);
|
||||
|
||||
consentCaptureBadgeTimers.set(tabId, timerId);
|
||||
}
|
||||
|
||||
function resetConsentCaptureBadge(tabId) {
|
||||
const actionApi = getActionApi();
|
||||
|
||||
stopConsentCaptureBadgeBlink(tabId);
|
||||
|
||||
if (!actionApi || tabId === null || tabId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionBadgeText(actionApi, { tabId, text: "" });
|
||||
}
|
||||
|
||||
function stopConsentCaptureBadgeBlink(tabId) {
|
||||
const timerId = consentCaptureBadgeTimers.get(tabId);
|
||||
|
||||
if (!timerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(timerId);
|
||||
consentCaptureBadgeTimers.delete(tabId);
|
||||
}
|
||||
|
||||
function setActionBadgeText(actionApi, details) {
|
||||
try {
|
||||
const result = actionApi.setBadgeText(details);
|
||||
|
||||
if (result?.catch) {
|
||||
result.catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
// Badge updates are best-effort extension UI only.
|
||||
}
|
||||
}
|
||||
|
||||
function setActionBadgeBackgroundColor(actionApi, details) {
|
||||
try {
|
||||
const result = actionApi.setBadgeBackgroundColor(details);
|
||||
|
||||
if (result?.catch) {
|
||||
result.catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
// Badge updates are best-effort extension UI only.
|
||||
}
|
||||
}
|
||||
|
||||
function getLatestGvlSnapshotByVendorListVersion(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||
|
||||
@@ -1,9 +1,87 @@
|
||||
console.log("VendorGet content listener loaded:", window.location.href);
|
||||
|
||||
let latestObservedPreConsentCapture = null;
|
||||
let latestObservedConsentCompleted = false;
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message?.type !== "probe_tcf_state") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return probeTcfState();
|
||||
});
|
||||
|
||||
function probeTcfState() {
|
||||
if (latestObservedConsentCompleted) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
capture: null,
|
||||
source: "content_completed"
|
||||
});
|
||||
}
|
||||
|
||||
if (latestObservedPreConsentCapture && !latestObservedConsentCompleted) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
capture: latestObservedPreConsentCapture,
|
||||
source: "content_memory"
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const requestId = [
|
||||
"tcf-probe",
|
||||
Date.now().toString(36),
|
||||
Math.random().toString(36).slice(2)
|
||||
].join("-");
|
||||
const timeoutId = setTimeout(() => {
|
||||
window.removeEventListener("VendorGetTcfProbeResponse", handleResponse);
|
||||
resolve({
|
||||
success: false,
|
||||
capture: null,
|
||||
error: "tcf_probe_timeout"
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
function handleResponse(event) {
|
||||
if (event?.detail?.requestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("VendorGetTcfProbeResponse", handleResponse);
|
||||
resolve({
|
||||
success: event.detail.success === true,
|
||||
capture: event.detail.capture ?? null,
|
||||
source: "tcf_get_tc_data"
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("VendorGetTcfProbeResponse", handleResponse);
|
||||
window.dispatchEvent(new CustomEvent("VendorGetTcfProbeRequest", {
|
||||
detail: {
|
||||
requestId
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("VendorGetFromPage", async (event) => {
|
||||
|
||||
console.log("VendorGet message from page:", event.detail);
|
||||
|
||||
if (
|
||||
event.detail?.eventName === "tcf_pre_consent_event" &&
|
||||
!latestObservedConsentCompleted
|
||||
) {
|
||||
latestObservedPreConsentCapture = event.detail.payload ?? null;
|
||||
}
|
||||
|
||||
if (event.detail?.eventName === "consent_capture") {
|
||||
latestObservedConsentCompleted = true;
|
||||
latestObservedPreConsentCapture = null;
|
||||
}
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: "vendorget_capture",
|
||||
payload: event.detail
|
||||
|
||||
@@ -115,6 +115,20 @@
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("VendorGetTcfProbeRequest", function (event) {
|
||||
const requestId = event?.detail?.requestId ?? null;
|
||||
|
||||
window.__tcfapi("getTCData", 2, function (tcData, success) {
|
||||
window.dispatchEvent(new CustomEvent("VendorGetTcfProbeResponse", {
|
||||
detail: {
|
||||
requestId: requestId,
|
||||
success: success,
|
||||
capture: success && tcData ? buildTcfEventCapture(tcData) : null
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
window.__tcfapi("ping", 2, function (pingData, pingSuccess) {
|
||||
|
||||
console.log("VendorGet __tcfapi ping:", {
|
||||
|
||||
@@ -123,6 +123,42 @@ h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pre-consent-capture {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
border: 1px solid #6366f1;
|
||||
border-radius: 6px;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.pre-consent-capture[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pre-consent-capture h2 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pre-consent-capture p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.pre-consent-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pre-consent-actions button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.evidence-counts {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
@@ -29,6 +29,31 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="pre-consent-capture"
|
||||
class="pre-consent-capture"
|
||||
aria-label="Pre-Consent-Erfassung"
|
||||
hidden
|
||||
>
|
||||
<h2>Consent-Vorgang erfassen?</h2>
|
||||
<p id="pre-consent-capture-summary">
|
||||
CMP-Vorgang erkannt.
|
||||
</p>
|
||||
<div class="pre-consent-actions">
|
||||
<button id="pre-consent-capture-decline" type="button">
|
||||
Nein
|
||||
</button>
|
||||
<button id="pre-consent-capture-start" type="button">
|
||||
Ja
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="pre-consent-capture-status"
|
||||
class="retention-status"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section class="evidence-retention" aria-label="Kurzstatus">
|
||||
<h2>Workspace-Kurzstatus</h2>
|
||||
<div id="maintenance-warning" class="maintenance-warning" hidden>
|
||||
|
||||
@@ -8,6 +8,19 @@ const requestMonitoringStatus = document.getElementById(
|
||||
);
|
||||
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
|
||||
const consentCaptureStatus = document.getElementById("consent-capture-status");
|
||||
const preConsentCapture = document.getElementById("pre-consent-capture");
|
||||
const preConsentCaptureSummary = document.getElementById(
|
||||
"pre-consent-capture-summary"
|
||||
);
|
||||
const preConsentCaptureStart = document.getElementById(
|
||||
"pre-consent-capture-start"
|
||||
);
|
||||
const preConsentCaptureDecline = document.getElementById(
|
||||
"pre-consent-capture-decline"
|
||||
);
|
||||
const preConsentCaptureStatus = document.getElementById(
|
||||
"pre-consent-capture-status"
|
||||
);
|
||||
const maintenanceWarning = document.getElementById("maintenance-warning");
|
||||
const evidenceLockedCount = document.getElementById("evidence-locked-count");
|
||||
const evidenceDashboardButton = document.getElementById(
|
||||
@@ -47,6 +60,7 @@ const evidenceStoreCountCells = {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await renderSettings();
|
||||
await renderPreConsentCaptureStatus();
|
||||
await renderEvidenceMaintenanceStatus();
|
||||
await renderEvidenceRetentionStatus();
|
||||
|
||||
@@ -57,7 +71,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
"consentCaptureEnabled",
|
||||
consentCaptureToggle.checked
|
||||
);
|
||||
|
||||
if (consentCaptureToggle.checked) {
|
||||
await probePreConsentCaptureForActiveTab();
|
||||
}
|
||||
|
||||
await renderSettings();
|
||||
await renderPreConsentCaptureStatus();
|
||||
|
||||
consentCaptureToggle.disabled = false;
|
||||
});
|
||||
@@ -84,6 +104,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
evidencePurgeUnlockedButton.addEventListener("click", openPurgeConfirmModal);
|
||||
evidencePurgeCancelButton.addEventListener("click", closePurgeConfirmModal);
|
||||
evidencePurgeConfirmButton.addEventListener("click", purgeUnlockedEvidence);
|
||||
preConsentCaptureStart.addEventListener("click", startPreConsentCapture);
|
||||
preConsentCaptureDecline.addEventListener("click", declinePreConsentCapture);
|
||||
});
|
||||
|
||||
async function renderSettings() {
|
||||
@@ -158,6 +180,137 @@ function renderEvidenceRetentionMessage(message) {
|
||||
evidenceRetentionStatus.textContent = message;
|
||||
}
|
||||
|
||||
async function renderPreConsentCaptureStatus() {
|
||||
try {
|
||||
const activeTab = await getActiveTab();
|
||||
|
||||
if (!activeTab?.id) {
|
||||
renderNoPreConsentCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await browser.runtime.sendMessage({
|
||||
type: "get_pre_consent_capture_status",
|
||||
payload: {
|
||||
tabId: activeTab.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error ?? "get_pre_consent_capture_status_failed");
|
||||
}
|
||||
|
||||
renderPreConsentCapture(result.capture);
|
||||
} catch (error) {
|
||||
renderNoPreConsentCapture();
|
||||
console.warn("VendorGet-IV pre-consent status failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function probePreConsentCaptureForActiveTab() {
|
||||
try {
|
||||
const activeTab = await getActiveTab();
|
||||
|
||||
if (!activeTab?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: "probe_pre_consent_capture_for_tab",
|
||||
payload: {
|
||||
tabId: activeTab.id
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("VendorGet-IV pre-consent probe failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreConsentCapture(capture) {
|
||||
if (!capture) {
|
||||
renderNoPreConsentCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
preConsentCapture.hidden = false;
|
||||
preConsentCaptureStart.hidden = capture.status !== "attention";
|
||||
preConsentCaptureDecline.hidden = capture.status !== "attention";
|
||||
|
||||
if (capture.status === "recording") {
|
||||
preConsentCaptureSummary.textContent = "Pre-Consent-Erfassung läuft.";
|
||||
preConsentCaptureStatus.textContent =
|
||||
"Provider-Announcement wurde gesichert; warte auf useractioncomplete.";
|
||||
return;
|
||||
}
|
||||
|
||||
preConsentCaptureSummary.textContent = buildPreConsentCaptureSummary(capture);
|
||||
preConsentCaptureStatus.textContent = "";
|
||||
}
|
||||
|
||||
function renderNoPreConsentCapture() {
|
||||
preConsentCapture.hidden = true;
|
||||
preConsentCaptureSummary.textContent = "";
|
||||
preConsentCaptureStatus.textContent = "";
|
||||
preConsentCaptureStart.disabled = false;
|
||||
preConsentCaptureDecline.disabled = false;
|
||||
}
|
||||
|
||||
function buildPreConsentCaptureSummary(capture) {
|
||||
const eventLabel = capture.firstEventStatus ?? "TCF-Event";
|
||||
const eventCount = Number(capture.eventCount ?? 0);
|
||||
|
||||
return `${eventLabel} erkannt, ${eventCount} Pre-Consent-Event(s) gepuffert.`;
|
||||
}
|
||||
|
||||
async function startPreConsentCapture() {
|
||||
await answerPreConsentCapture("start_pre_consent_capture");
|
||||
}
|
||||
|
||||
async function declinePreConsentCapture() {
|
||||
await answerPreConsentCapture("decline_pre_consent_capture");
|
||||
}
|
||||
|
||||
async function answerPreConsentCapture(messageType) {
|
||||
preConsentCaptureStart.disabled = true;
|
||||
preConsentCaptureDecline.disabled = true;
|
||||
|
||||
try {
|
||||
const activeTab = await getActiveTab();
|
||||
|
||||
if (!activeTab?.id) {
|
||||
throw new Error("active_tab_unavailable");
|
||||
}
|
||||
|
||||
const result = await browser.runtime.sendMessage({
|
||||
type: messageType,
|
||||
payload: {
|
||||
tabId: activeTab.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error ?? messageType);
|
||||
}
|
||||
|
||||
renderPreConsentCapture(result.capture);
|
||||
} catch (error) {
|
||||
preConsentCaptureStatus.textContent = "Aktion konnte nicht ausgeführt werden";
|
||||
console.warn("VendorGet-IV pre-consent action failed", error);
|
||||
} finally {
|
||||
preConsentCaptureStart.disabled = false;
|
||||
preConsentCaptureDecline.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveTab() {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
});
|
||||
|
||||
return tabs[0] ?? null;
|
||||
}
|
||||
|
||||
function openPurgeConfirmModal() {
|
||||
evidencePurgeConfirmModal.hidden = false;
|
||||
evidencePurgeCancelButton.focus();
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren