"use strict"; const documentedConsentEmpty = document.getElementById( "documented-consent-empty" ); const documentedConsentContent = document.getElementById( "documented-consent-content" ); const documentedConsentList = document.getElementById("documented-consent-list"); const consentDetailObservation = document.getElementById( "consent-detail-observation" ); const consentDetailBasics = document.getElementById("consent-detail-basics"); const consentDetailSummary = document.getElementById("consent-detail-summary"); const consentDetailPublisher = document.getElementById( "consent-detail-publisher" ); const consentDetailFieldPaths = document.getElementById( "consent-detail-field-paths" ); const consentDetailRawStrings = document.getElementById( "consent-detail-raw-strings" ); const consentDetailDiagnostics = document.getElementById( "consent-detail-diagnostics" ); const consentDetailJson = document.getElementById("consent-detail-json"); let documentedConsentStates = []; let selectedConsentStateFingerprint = null; const FIELD_EXPLANATIONS = { firstSeenAt: "Zeitpunkt, zu dem dieser Consent-Zustand erstmals lokal beobachtet wurde.", lastSeenAt: "Zeitpunkt, zu dem derselbe Consent-Zustand zuletzt wieder beobachtet wurde.", seenCount: "Wie oft VG-Observe diesen identischen Zustand erkannt hat.", "page.origin": "Webseite, auf der dieser Consent-Zustand beobachtet wurde.", "page.url": "Vollständige beobachtete Seitenadresse, soweit verfügbar.", stateFingerprint: "Technischer Wiedererkennungswert dieses Consent-Zustands.", "consent.tcString": "Technischer Consent-Nachweis. Für normale Nutzer meist nur als Rohbeleg relevant.", "consent.addtlConsent": "Zusätzlicher Google-Consent-String außerhalb des normalen TCF-Consent-Strings.", "gvl.vendorListVersion": "Vendorlisten-Version, die im beobachteten Consent-Kontext gemeldet wurde. Nicht automatisch die aktuellste IAB-Europe-Version.", "cmp.tcfPolicyVersion": "Version der TCF-Regelgrundlage, die im beobachteten Consent-Kontext gemeldet wurde.", "cmp.gdprApplies": "Angabe, ob das beobachtete Consent-System DSGVO-Anwendbarkeit gemeldet hat.", "observation.eventStatus": "Vom beobachteten TCF-Ereignis gemeldeter Status der Consent-Erfassung.", "observation.cmpStatus": "Vom beobachteten Consent-System gemeldeter Laufzeitstatus.", "purposes.consents": "Anzahl der Zwecke, für die Zustimmung gemeldet wurde.", "purposes.legitimateInterests": "Anzahl der Zwecke, für die berechtigtes Interesse gemeldet wurde.", "vendors.consents": "Anzahl der Firmen, für die Zustimmung gemeldet wurde.", "vendors.legitimateInterests": "Anzahl der Firmen, die sich laut beobachtetem Kontext auf berechtigtes Interesse stützen.", specialFeatureOptins: "Anzahl besonderer Funktionen, für die eine aktive Auswahl beobachtet wurde.", "vendors.disclosedVendors": "Anzahl der im beobachteten tcData-Kontext offengelegten Vendoren/Firmen.", "publisher.consents": "Anzahl der vom Webseitenbetreiber gemeldeten Publisher-Zustimmungen.", "publisher.legitimateInterests": "Anzahl der vom Webseitenbetreiber gemeldeten Publisher-Angaben zu berechtigtem Interesse.", "publisher.restrictions": "Technische Einschränkungen/Vorgaben des Webseitenbetreibers im TCF-Kontext. Für Nutzer nur eingeschränkt aussagekräftig." }; document.addEventListener("DOMContentLoaded", async () => { await renderDocumentedConsentStates(); }); async function renderDocumentedConsentStates() { try { const result = await browser.runtime.sendMessage({ type: "list_recent_consent_states" }); if (!result?.success) { throw new Error(result?.error ?? "list_recent_consent_states_failed"); } documentedConsentStates = result.consentStates ?? []; if (documentedConsentStates.length === 0) { selectedConsentStateFingerprint = null; renderNoDocumentedConsentStates(); return; } documentedConsentEmpty.hidden = true; documentedConsentContent.hidden = false; if (!findDocumentedConsentState(selectedConsentStateFingerprint)) { selectedConsentStateFingerprint = documentedConsentStates[0]?.stateFingerprint ?? null; } renderDocumentedConsentStateList(); renderSelectedConsentState(); } catch (error) { documentedConsentEmpty.hidden = false; documentedConsentContent.hidden = true; documentedConsentEmpty.textContent = "Dokumentierte Consent-Zustände konnten nicht geladen werden."; console.warn("VendorGet-IV documented consent states failed", error); } } function renderNoDocumentedConsentStates() { documentedConsentList.textContent = ""; clearConsentStateDetails(); documentedConsentEmpty.hidden = false; documentedConsentContent.hidden = true; documentedConsentEmpty.textContent = "Keine dokumentierten Consent-Zustände vorhanden."; } function renderDocumentedConsentStateList() { documentedConsentList.textContent = ""; for (const consentState of documentedConsentStates) { const row = document.createElement("tr"); const isSelected = consentState?.stateFingerprint === selectedConsentStateFingerprint; row.className = isSelected ? "is-selected" : ""; row.tabIndex = 0; row.setAttribute("role", "button"); row.setAttribute("aria-pressed", isSelected ? "true" : "false"); row.addEventListener("click", () => { selectDocumentedConsentState(consentState?.stateFingerprint ?? null); }); row.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectDocumentedConsentState(consentState?.stateFingerprint ?? null); } }); appendListCell(row, formatNullable(consentState?.lastSeenAt)); appendListCell(row, formatNullable(consentState?.firstSeenAt)); appendListCell(row, formatNullable(consentState?.seenCount), "numeric"); appendListCell(row, formatNullable(consentState?.page?.origin)); appendListCell(row, shortenLongString(consentState?.page?.url, 80), "url-cell"); appendListCell( row, String(countTruthyObjectValues(consentState?.purposes?.consents)), "numeric" ); appendListCell( row, String(countTruthyObjectValues(consentState?.vendors?.consents)), "numeric" ); appendListCell( row, String(countTruthyObjectValues(consentState?.vendors?.legitimateInterests)), "numeric" ); appendListCell(row, formatNullable(consentState?.gvl?.vendorListVersion)); appendListCell(row, shortenFingerprint(consentState?.stateFingerprint)); documentedConsentList.append(row); } } function appendListCell(row, value, className) { const cell = document.createElement("td"); if (className) { cell.className = className; } cell.textContent = value; row.append(cell); } function selectDocumentedConsentState(stateFingerprint) { selectedConsentStateFingerprint = stateFingerprint; renderDocumentedConsentStateList(); renderSelectedConsentState(); } function renderSelectedConsentState() { const consentState = findDocumentedConsentState(selectedConsentStateFingerprint); clearConsentStateDetails(); if (!consentState) { return; } renderDetailSection(consentDetailObservation, "Beobachtung", [ ["Erste Beobachtung", formatNullable(consentState?.firstSeenAt), "firstSeenAt"], ["Letzte Beobachtung", formatNullable(consentState?.lastSeenAt), "lastSeenAt"], ["Anzahl Beobachtungen", formatNullable(consentState?.seenCount), "seenCount"], ["Seite / Origin", formatNullable(consentState?.page?.origin), "page.origin"], ["URL", formatLongString(consentState?.page?.url), "page.url"], [ "State-Fingerprint", formatLongString(consentState?.stateFingerprint), "stateFingerprint" ] ]); renderDetailSection( consentDetailBasics, "Gemeldete Consent-Grunddaten", [ [ "Beobachteter TC-String / euconsent-v2", summarizeRawString(consentState?.consent?.tcString), "consent.tcString" ], [ "Beobachteter Google Additional Consent String", summarizeRawString(consentState?.consent?.addtlConsent), "consent.addtlConsent" ], [ "Vom Consent-System gemeldete Vendorlisten-Version", formatNullable(consentState?.gvl?.vendorListVersion), "gvl.vendorListVersion" ], [ "Vom Consent-System gemeldete TCF-Policy-Version", formatNullable(consentState?.cmp?.tcfPolicyVersion), "cmp.tcfPolicyVersion" ], [ "Vom Consent-System gemeldete DSGVO-Anwendbarkeit", formatBoolean(consentState?.cmp?.gdprApplies), "cmp.gdprApplies" ], [ "Beobachteter TCF Event Status", formatNullable(consentState?.observation?.eventStatus), "observation.eventStatus" ], [ "Beobachteter Consent-System-Status", formatNullable(consentState?.observation?.cmpStatus), "observation.cmpStatus" ] ] ); renderDetailSection( consentDetailSummary, "Zusammenfassung der Zwecke und Firmen", [ [ "Anzahl Purposes mit aktivem Consent", String(countTruthyObjectValues(consentState?.purposes?.consents)), "purposes.consents" ], [ "Anzahl Purposes mit aktivem Legitimate Interest", String(countTruthyObjectValues(consentState?.purposes?.legitimateInterests)), "purposes.legitimateInterests" ], [ "Anzahl Vendoren/Firmen mit aktivem Consent", String(countTruthyObjectValues(consentState?.vendors?.consents)), "vendors.consents" ], [ "Anzahl Vendoren/Firmen mit aktivem Legitimate Interest", String(countTruthyObjectValues(consentState?.vendors?.legitimateInterests)), "vendors.legitimateInterests" ], [ "Anzahl aktivierter Special Features", String(countTruthyObjectValues(consentState?.specialFeatureOptins)), "specialFeatureOptins" ], [ "Anzahl offengelegter Vendoren/Firmen laut beobachtetem tcData-Kontext", String(countTruthyObjectValues(consentState?.vendors?.disclosedVendors)), "vendors.disclosedVendors" ] ] ); renderDetailSection(consentDetailPublisher, "Publisher-Angaben", [ [ "Publisher Consents: Anzahl aktiv", String(countTruthyObjectValues(consentState?.publisher?.consents)), "publisher.consents" ], [ "Publisher Legitimate Interests: Anzahl aktiv", String(countTruthyObjectValues(consentState?.publisher?.legitimateInterests)), "publisher.legitimateInterests" ], [ "Publisher Restrictions: Anzahl Einträge", String(countObjectKeys(consentState?.publisher?.restrictions)), "publisher.restrictions" ] ]); consentDetailFieldPaths.textContent = JSON.stringify( buildConsentStateFieldPathOverview(), null, 2 ); consentDetailRawStrings.textContent = JSON.stringify( { tcString: consentState?.consent?.tcString ?? null, addtlConsent: consentState?.consent?.addtlConsent ?? null }, null, 2 ); consentDetailDiagnostics.textContent = JSON.stringify( { rawTcData: consentState?.rawTcData ?? null, diagnostics: consentState?.diagnostics ?? null }, null, 2 ); consentDetailJson.textContent = JSON.stringify(consentState, null, 2); } function renderDetailSection(container, title, rows) { const heading = document.createElement("h3"); const table = document.createElement("table"); const body = document.createElement("tbody"); heading.textContent = title; table.className = "inspector-table"; for (const [label, value, technicalField, helpText] of rows) { body.append(createInspectorRow(label, value, technicalField, helpText)); } table.append(body); container.append(heading, table); } function createInspectorRow(label, value, technicalField, helpText) { const row = document.createElement("tr"); const labelCell = document.createElement("th"); const valueCell = document.createElement("td"); const explanationCell = document.createElement("td"); const mainLabel = document.createElement("span"); const explanation = helpText ?? FIELD_EXPLANATIONS[technicalField] ?? "-"; labelCell.scope = "row"; mainLabel.textContent = label; labelCell.append(mainLabel); if (technicalField) { const technicalFieldElement = document.createElement("small"); technicalFieldElement.className = "technical-field"; technicalFieldElement.textContent = `Technisches Feld: ${technicalField}`; labelCell.append(technicalFieldElement); } if (helpText) { const helpTextElement = document.createElement("small"); helpTextElement.className = "inspector-help"; helpTextElement.textContent = helpText; labelCell.append(helpTextElement); } valueCell.textContent = value; valueCell.className = "inspector-value"; explanationCell.textContent = explanation; explanationCell.className = "inspector-explanation"; row.append(labelCell, valueCell, explanationCell); return row; } function clearConsentStateDetails() { consentDetailObservation.textContent = ""; consentDetailBasics.textContent = ""; consentDetailSummary.textContent = ""; consentDetailPublisher.textContent = ""; consentDetailFieldPaths.textContent = ""; consentDetailRawStrings.textContent = ""; consentDetailDiagnostics.textContent = ""; consentDetailJson.textContent = ""; } function findDocumentedConsentState(stateFingerprint) { if (!stateFingerprint) { return null; } return ( documentedConsentStates.find((consentState) => { return consentState?.stateFingerprint === stateFingerprint; }) ?? null ); } function buildConsentStateFieldPathOverview() { return { beobachtung: { "Erste Beobachtung": "firstSeenAt", "Letzte Beobachtung": "lastSeenAt", "Anzahl Beobachtungen": "seenCount", "Seite / Origin": "page.origin", URL: "page.url", "State-Fingerprint": "stateFingerprint" }, grunddaten: { "TC-String / euconsent-v2": "consent.tcString", "Google Additional Consent String": "consent.addtlConsent", Vendorliste: "gvl.vendorListVersion", "TCF-Policy-Version": "cmp.tcfPolicyVersion", "DSGVO-Anwendbarkeit": "cmp.gdprApplies", "TCF Event Status": "observation.eventStatus", "Consent-System-Status": "observation.cmpStatus" }, zusammenfassung: { "Purposes mit Consent": "purposes.consents", "Purposes mit Legitimate Interest": "purposes.legitimateInterests", "Vendoren/Firmen mit Consent": "vendors.consents", "Vendoren/Firmen mit Legitimate Interest": "vendors.legitimateInterests", "Special Features": "specialFeatureOptins", "Offengelegte Vendoren/Firmen": "vendors.disclosedVendors" }, publisher: { "Publisher Consents": "publisher.consents", "Publisher Legitimate Interests": "publisher.legitimateInterests", "Publisher Restrictions": "publisher.restrictions" } }; } function shortenFingerprint(value) { if (!value) { return "-"; } return shortenLongString(value, 16); } function shortenLongString(value, maxLength) { const formattedValue = formatNullable(value); if (formattedValue === "-" || formattedValue.length <= maxLength) { return formattedValue; } return `${formattedValue.slice(0, maxLength)}...`; } function summarizeRawString(value) { if (value === null || value === undefined || value === "") { return "nicht vorhanden"; } const stringValue = String(value); const prefix = stringValue.slice(0, 32); return `vorhanden, Länge ${stringValue.length}, Anfang: ${prefix}${ stringValue.length > prefix.length ? "..." : "" }`; } function countTruthyObjectValues(obj) { if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return 0; } return Object.values(obj).filter(Boolean).length; } function countObjectKeys(obj) { if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return 0; } return Object.keys(obj).length; } function formatNullable(value) { if (value === null || value === undefined || value === "") { return "-"; } return String(value); } function formatBoolean(value) { if (value === true) { return "true"; } if (value === false) { return "false"; } return "-"; } function formatLongString(value) { return formatNullable(value); }