Implement passive consent evidence workflow with extension-driven capture
Dieser Commit ist enthalten in:
+445
-79
@@ -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_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
||||||
const AUTO_GVL_CHECK_STORAGE_KEY = "vendorgetAutoGvlUpdateStatus";
|
const AUTO_GVL_CHECK_STORAGE_KEY = "vendorgetAutoGvlUpdateStatus";
|
||||||
const CONSENT_CAPTURE_SESSION_TIMEOUT_MS = 10 * 60 * 1000;
|
const CONSENT_CAPTURE_SESSION_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
|
const CONSENT_CAPTURE_BADGE_BLINK_MS = 700;
|
||||||
|
|
||||||
let isAutoGvlCheckRunning = false;
|
let isAutoGvlCheckRunning = false;
|
||||||
let lastAutoGvlCheckStartedAt = null;
|
let lastAutoGvlCheckStartedAt = null;
|
||||||
let latestGvlUpdateStatus = null;
|
let latestGvlUpdateStatus = null;
|
||||||
const consentCaptureSessions = new Map();
|
const consentCaptureSessions = new Map();
|
||||||
|
const consentCaptureBadgeTimers = new Map();
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message, sender) =>
|
browser.runtime.onMessage.addListener((message, sender) =>
|
||||||
handleVendorGetMessage(message, sender)
|
handleVendorGetMessage(message, sender)
|
||||||
@@ -30,7 +32,7 @@ browser.tabs.onRemoved.addListener((tabId) => {
|
|||||||
|
|
||||||
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||||
if (changeInfo.url) {
|
if (changeInfo.url) {
|
||||||
cleanupConsentCaptureSessionsForTab(tabId, "page_changed");
|
cleanupConsentCaptureSessionsForTab(tabId, "page_changed", changeInfo.url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,6 +135,22 @@ async function handleVendorGetMessage(message, sender) {
|
|||||||
return handleGetLatestConsentStateMessage();
|
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") {
|
if (message.type === "get_consent_evidence_chain") {
|
||||||
return handleGetConsentEvidenceChainMessage(message);
|
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) {
|
async function handleGetConsentEvidenceChainMessage(message) {
|
||||||
const stateFingerprint = normalizeStateFingerprint(
|
const stateFingerprint = normalizeStateFingerprint(
|
||||||
message?.payload?.stateFingerprint
|
message?.payload?.stateFingerprint
|
||||||
@@ -2142,19 +2332,94 @@ function buildConsentEventDiagnostics(rawCapture, extraDiagnostics) {
|
|||||||
|
|
||||||
function startConsentCaptureSessionTimeout(session) {
|
function startConsentCaptureSessionTimeout(session) {
|
||||||
session.timeoutId = setTimeout(() => {
|
session.timeoutId = setTimeout(() => {
|
||||||
cleanupIncompleteConsentCaptureSession(session, "timeout");
|
timeoutConsentCaptureSession(session);
|
||||||
}, CONSENT_CAPTURE_SESSION_TIMEOUT_MS);
|
}, 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) {
|
async function handlePreConsentTcfEvent(rawCapture, sender) {
|
||||||
if (isEvidenceWriteSuspended()) {
|
if (isEvidenceWriteSuspended()) {
|
||||||
console.info("VG-Observe pre-consent capture skipped: maintenance mode");
|
console.info("VG-Observe pre-consent capture skipped: maintenance mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventStatus = rawCapture?.eventStatus ?? null;
|
if (!isOpenPreConsentCapture(rawCapture)) {
|
||||||
|
|
||||||
if (eventStatus !== "cmpuishown" && eventStatus !== "tcloaded") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2164,13 +2429,17 @@ async function handlePreConsentTcfEvent(rawCapture, sender) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = consentCaptureSessions.get(sessionKey);
|
if (hasCompletedConsentCaptureSessionForTab(sender?.tab?.id ?? null)) {
|
||||||
|
|
||||||
if (session?.status === "declined" || session?.status === "completed") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session?.status === "active") {
|
let session = consentCaptureSessions.get(sessionKey);
|
||||||
|
|
||||||
|
if (session?.status === "aborted" || session?.status === "completed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session?.status === "recording") {
|
||||||
await persistProviderAnnouncementEvent({
|
await persistProviderAnnouncementEvent({
|
||||||
captureSessionId: session.captureSessionId,
|
captureSessionId: session.captureSessionId,
|
||||||
rawCapture,
|
rawCapture,
|
||||||
@@ -2179,84 +2448,34 @@ async function handlePreConsentTcfEvent(rawCapture, sender) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
startAttentionConsentCaptureSession(rawCapture, sender);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveConsentCaptureSessionId(sender) {
|
function getActiveConsentCaptureSessionId(sender) {
|
||||||
const sessionKey = getConsentCaptureSessionKey(sender);
|
const sessionKey = getConsentCaptureSessionKey(sender);
|
||||||
const session = sessionKey ? consentCaptureSessions.get(sessionKey) : null;
|
const session = sessionKey ? consentCaptureSessions.get(sessionKey) : null;
|
||||||
|
|
||||||
if (!session || session.status !== "active") {
|
if (!session || session.status !== "recording") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.captureSessionId ?? 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) {
|
function completeConsentCaptureSession(sender) {
|
||||||
const sessionKey = getConsentCaptureSessionKey(sender);
|
const sessionKey = getConsentCaptureSessionKey(sender);
|
||||||
let session = sessionKey ? consentCaptureSessions.get(sessionKey) : null;
|
let session = sessionKey ? consentCaptureSessions.get(sessionKey) : null;
|
||||||
@@ -2282,30 +2501,57 @@ function completeConsentCaptureSession(sender) {
|
|||||||
session.status = "completed";
|
session.status = "completed";
|
||||||
session.completedAt = new Date().toISOString();
|
session.completedAt = new Date().toISOString();
|
||||||
session.bufferedPreConsentEvents = [];
|
session.bufferedPreConsentEvents = [];
|
||||||
session.promptInProgress = false;
|
|
||||||
|
|
||||||
if (session.timeoutId) {
|
if (session.timeoutId) {
|
||||||
clearTimeout(session.timeoutId);
|
clearTimeout(session.timeoutId);
|
||||||
session.timeoutId = null;
|
session.timeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetConsentCaptureBadge(session.tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupConsentCaptureSessionsForTab(tabId, reason) {
|
function cleanupConsentCaptureSessionsForTab(tabId, reason, nextUrl) {
|
||||||
consentCaptureSessions.forEach((session) => {
|
consentCaptureSessions.forEach((session) => {
|
||||||
if (session.tabId === tabId) {
|
if (session.tabId === tabId) {
|
||||||
|
if (
|
||||||
|
reason === "page_changed" &&
|
||||||
|
session.status === "completed" &&
|
||||||
|
isSameConsentCaptureUrl(session.page?.url, nextUrl)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cleanupIncompleteConsentCaptureSession(session, reason);
|
cleanupIncompleteConsentCaptureSession(session, reason);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSameConsentCaptureUrl(left, right) {
|
||||||
|
if (!left || !right) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(left) === String(right);
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupIncompleteConsentCaptureSession(session, reason) {
|
function cleanupIncompleteConsentCaptureSession(session, reason) {
|
||||||
|
const wasCompleted = session.status === "completed";
|
||||||
|
|
||||||
if (session.timeoutId) {
|
if (session.timeoutId) {
|
||||||
clearTimeout(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;
|
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) {
|
function getLatestGvlSnapshotByVendorListVersion(db) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||||
|
|||||||
@@ -1,9 +1,87 @@
|
|||||||
console.log("VendorGet content listener loaded:", window.location.href);
|
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) => {
|
window.addEventListener("VendorGetFromPage", async (event) => {
|
||||||
|
|
||||||
console.log("VendorGet message from page:", event.detail);
|
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({
|
await browser.runtime.sendMessage({
|
||||||
type: "vendorget_capture",
|
type: "vendorget_capture",
|
||||||
payload: event.detail
|
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) {
|
window.__tcfapi("ping", 2, function (pingData, pingSuccess) {
|
||||||
|
|
||||||
console.log("VendorGet __tcfapi ping:", {
|
console.log("VendorGet __tcfapi ping:", {
|
||||||
|
|||||||
@@ -123,6 +123,42 @@ h1 {
|
|||||||
display: none;
|
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 {
|
.evidence-counts {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
@@ -29,6 +29,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="evidence-retention" aria-label="Kurzstatus">
|
||||||
<h2>Workspace-Kurzstatus</h2>
|
<h2>Workspace-Kurzstatus</h2>
|
||||||
<div id="maintenance-warning" class="maintenance-warning" hidden>
|
<div id="maintenance-warning" class="maintenance-warning" hidden>
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ const requestMonitoringStatus = document.getElementById(
|
|||||||
);
|
);
|
||||||
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
|
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
|
||||||
const consentCaptureStatus = document.getElementById("consent-capture-status");
|
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 maintenanceWarning = document.getElementById("maintenance-warning");
|
||||||
const evidenceLockedCount = document.getElementById("evidence-locked-count");
|
const evidenceLockedCount = document.getElementById("evidence-locked-count");
|
||||||
const evidenceDashboardButton = document.getElementById(
|
const evidenceDashboardButton = document.getElementById(
|
||||||
@@ -47,6 +60,7 @@ const evidenceStoreCountCells = {
|
|||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
await renderSettings();
|
await renderSettings();
|
||||||
|
await renderPreConsentCaptureStatus();
|
||||||
await renderEvidenceMaintenanceStatus();
|
await renderEvidenceMaintenanceStatus();
|
||||||
await renderEvidenceRetentionStatus();
|
await renderEvidenceRetentionStatus();
|
||||||
|
|
||||||
@@ -57,7 +71,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
"consentCaptureEnabled",
|
"consentCaptureEnabled",
|
||||||
consentCaptureToggle.checked
|
consentCaptureToggle.checked
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (consentCaptureToggle.checked) {
|
||||||
|
await probePreConsentCaptureForActiveTab();
|
||||||
|
}
|
||||||
|
|
||||||
await renderSettings();
|
await renderSettings();
|
||||||
|
await renderPreConsentCaptureStatus();
|
||||||
|
|
||||||
consentCaptureToggle.disabled = false;
|
consentCaptureToggle.disabled = false;
|
||||||
});
|
});
|
||||||
@@ -84,6 +104,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
evidencePurgeUnlockedButton.addEventListener("click", openPurgeConfirmModal);
|
evidencePurgeUnlockedButton.addEventListener("click", openPurgeConfirmModal);
|
||||||
evidencePurgeCancelButton.addEventListener("click", closePurgeConfirmModal);
|
evidencePurgeCancelButton.addEventListener("click", closePurgeConfirmModal);
|
||||||
evidencePurgeConfirmButton.addEventListener("click", purgeUnlockedEvidence);
|
evidencePurgeConfirmButton.addEventListener("click", purgeUnlockedEvidence);
|
||||||
|
preConsentCaptureStart.addEventListener("click", startPreConsentCapture);
|
||||||
|
preConsentCaptureDecline.addEventListener("click", declinePreConsentCapture);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderSettings() {
|
async function renderSettings() {
|
||||||
@@ -158,6 +180,137 @@ function renderEvidenceRetentionMessage(message) {
|
|||||||
evidenceRetentionStatus.textContent = 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() {
|
function openPurgeConfirmModal() {
|
||||||
evidencePurgeConfirmModal.hidden = false;
|
evidencePurgeConfirmModal.hidden = false;
|
||||||
evidencePurgeCancelButton.focus();
|
evidencePurgeCancelButton.focus();
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren