diff --git a/report-window/email-content.tsx b/report-window/email-content.tsx index e2fffa4..99adbab 100644 --- a/report-window/email-content.tsx +++ b/report-window/email-content.tsx @@ -9,49 +9,50 @@ declare var PLUGIN_NAME: string; declare var PLUGIN_URL: string; export default function EmailContent({ - answers, - visited_url, - clusters, + answers, + visited_url, + clusters, }: { - answers: ParsedAnswers; - visited_url: string; - clusters: Record; + answers: ParsedAnswers; + visited_url: string; + clusters: Record; }) { - const _ = (key: string) => v(key, answers.zaimek); - const problems = deduceProblems(answers, clusters); - const explainers = Array.from( - new Set( - problems - .map((problem) => problem.getNecessaryExplainers()) - .reduce((a, b) => a.concat(b), []) - ) - ).map((explainer_key) => Explainers[explainer_key]); - return ( -
-
{JSON.stringify(answers, null, 3)}
-

Dzień dobry,

-

- w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po podejrzeniu ruchu - sieciowego generowanego przez tę stronę za pomocą wtyczki{' '} - {PLUGIN_NAME} w przeglądarce Firefox {_('mam')} pytania - dotyczące przetwarzania {_('moich')} danych osobowych, na które nie {_('znalazłem')}{' '} - odpowiedzi nigdzie na Państwa stronie. -

- {problems.map((problem) => problem.getEmailContent())} - {explainers.map((explainer) => explainer(answers.zaimek))} -

- {_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '} - - treści wyroku TSUE w sprawie C-40/17 - {' '} - poprzez wysyłanie moich danych w wyżej opisanym zakresie stają się Państwo - współadministratorem moich danych osobowych, dlatego ciąży na Państwu obowiązek - odpowiedzi na moje pytanie na mocy Art. 12 i 13 Rozporządzenia 2016/679 Parlamentu - Europejskiego i Rady (UE) z dnia 27 kwietnia 2016 r. w sprawie ochrony osób - fizycznych w związku z przetwarzaniem danych osobowych i w sprawie swobodnego - przepływu takich danych oraz uchylenia dyrektywy 95/46/WE (ogólne rozporządzenie o - ochronie danych, dalej: „RODO”). -

-
- ); + const _ = (key: string) => v(key, answers.zaimek); + const problems = deduceProblems(answers, clusters); + const explainers = Array.from( + new Set( + problems + .map((problem) => problem.getNecessaryExplainers()) + .reduce((a, b) => a.concat(b), []) + ) + ).map((explainer_key) => Explainers[explainer_key]); + return ( +
+
{JSON.stringify(answers, null, 3)}
+

Dzień dobry,

+

+ w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po podejrzeniu ruchu + sieciowego generowanego przez tę stronę za pomocą wtyczki{' '} + {PLUGIN_NAME} w przeglądarce Firefox {_('mam')} pytania + dotyczące przetwarzania {_('moich')} danych osobowych, na które nie {_('znalazłem')}{' '} + odpowiedzi nigdzie na Państwa stronie. +

+ {problems.map((problem) => problem.getEmailContent())} + {explainers.map((explainer) => explainer(answers.zaimek))} +

Państwa rola jako współadministratora danych osobowych

+

+ {_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '} + + treści wyroku TSUE w sprawie C-40/17 + {' '} + poprzez wysyłanie moich danych w wyżej opisanym zakresie stają się Państwo + współadministratorem moich danych osobowych, dlatego ciąży na Państwu obowiązek + odpowiedzi na moje pytanie na mocy Art. 12 i 13 Rozporządzenia 2016/679 Parlamentu + Europejskiego i Rady (UE) z dnia 27 kwietnia 2016 r. w sprawie ochrony osób fizycznych w + związku z przetwarzaniem danych osobowych i w sprawie swobodnego przepływu takich danych + oraz uchylenia dyrektywy 95/46/WE (ogólne rozporządzenie o ochronie danych, dalej: + „RODO”). +

+
+ ); } diff --git a/report-window/explainers.tsx b/report-window/explainers.tsx index 556f3f4..e7cc86c 100644 --- a/report-window/explainers.tsx +++ b/report-window/explainers.tsx @@ -1,14 +1,14 @@ export type ExplainerKey = 'cookies_are_pii'; export const Explainers: Record JSX.Element> = { - cookies_are_pii: () => ( - <> -

Ciasteczka stanowią dane osobowe

-

- Sztucznie wygenerowane identyfikatory przechowywane w plikach Cookies stanowią dane - osobowe. Wskazuje na to wprost Art. 4. pkt 1. RODO, wymieniając „identyfikator - internetowy” i „numer identyfikacyjny” jako przykłady danych osobowych. -

- - ), + cookies_are_pii: () => ( + <> +

Ciasteczka stanowią dane osobowe

+

+ Sztucznie wygenerowane identyfikatory przechowywane w plikach Cookies stanowią dane + osobowe. Wskazuje na to wprost Art. 4. pkt 1. RODO, wymieniając „identyfikator + internetowy” i „numer identyfikacyjny” jako przykłady danych osobowych. +

+ + ), }; diff --git a/report-window/problems/unlawful-cookies.tsx b/report-window/problems/unlawful-cookies.tsx index 50a8c27..ee7a2e6 100644 --- a/report-window/problems/unlawful-cookies.tsx +++ b/report-window/problems/unlawful-cookies.tsx @@ -1,4 +1,5 @@ import { RequestCluster } from '../../request-cluster'; +import { wordlist } from '../../util'; import { ExplainerKey } from '../explainers'; import { ParsedAnswers } from '../parse-answers'; import { v } from '../verbs'; @@ -145,7 +146,7 @@ export class UnlawfulCookieAccess extends Problem { {unnecessary_hosts.length > 0 ? (

W {_('mojej')} ocenie odczytywanie przez Państwa stronę treści plików cookies z{' '} - {unnecessary_hosts.join(', ')} nie jest konieczne do wyświetlenia treści Państwa + {wordlist(unnecessary_hosts)} nie jest konieczne do wyświetlenia treści Państwa strony, dlatego nie jest dla nich spełniony warunek 2. Jeżeli Państwa zdaniem jest inaczej, {_('proszę')} o wskazanie, co jest źródłem tej konieczności i co odróżnia Państwa stronę od wielu innych stron, które realizują te same funkcjonalności{' '} @@ -159,12 +160,16 @@ export class UnlawfulCookieAccess extends Problem { takiego przetwarzania {_('moich')} danych osobowych, czy przetwarzali je państwo bez ważnej podstawy prawnej?

-

- {_('Proszę')} też o wskazanie, czy dostęp do treści plików cookie z - {maybe_unnecessary_hosts.join(', ')} jest konieczny do poprawnego działania strony? - Jeżeli tak, to {_('proszę')} wskazać, w jaki sposób. Co sprawia, że strona nie może - działać bez nich? -

+ {maybe_unnecessary_hosts.length > 1 ? ( +

+ {_('Proszę')} też o wskazanie, czy dostęp do treści plików cookie z + {wordlist(maybe_unnecessary_hosts)} jest konieczny do poprawnego działania strony? + Jeżeli tak, to {_('proszę')} wskazać, w jaki sposób. Co sprawia, że strona nie + może działać bez nich? +

+ ) : ( + '' + )} ); } diff --git a/report-window/report-window.scss b/report-window/report-window.scss index 65e1245..0039e3e 100644 --- a/report-window/report-window.scss +++ b/report-window/report-window.scss @@ -1,38 +1,29 @@ @import '../sidebar/fonts.scss'; @import '../sidebar/colors.scss'; -* { - margin: 0px; - box-sizing: border-box; - text-rendering: optimizelegibility; - font-smooth: auto; - -webkit-font-smoothing: auto; - user-select: none; -} - html { - font-size: 1rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, - 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 1rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, + 'Open Sans', 'Helvetica Neue', sans-serif; } body { - background-color: #fff; + background-color: #fff; } nav { - position: sticky; - top: 0; - background-color: #fff; - display: flex; - flex-wrap: wrap; - justify-content: left; - align-items: center; - padding: 1rem 1rem; - border-bottom: 2px solid $ultra-light-grey; - height: 5rem; + position: sticky; + top: 0; + background-color: #fff; + display: flex; + flex-wrap: wrap; + justify-content: left; + align-items: center; + padding: 1rem 1rem; + border-bottom: 2px solid $ultra-light-grey; + height: 5rem; - img { - margin-right: 0.5rem; - } + img { + margin-right: 0.5rem; + } } diff --git a/util.ts b/util.ts index bfe5ecb..d42f849 100644 --- a/util.ts +++ b/util.ts @@ -6,266 +6,273 @@ export type Unarray = T extends Array ? R : T; export type Tab = Unarray>>; export type Request = { - cookieStoreId?: string; - documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined. - frameId: number; - incognito?: boolean; - method: string; - originUrl: string; - parentFrameId: number; - proxyInfo?: { - host: string; - port: number; - type: string; - username: string; - proxyDNS: boolean; - failoverTimeout: number; - }; - requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[]; - requestId: string; - tabId: number; - thirdParty?: boolean; - timeStamp: number; - type: string; - url: string; // the target of the request; - urlClassification?: { firstParty: string[]; thirdParty: string[] }; + cookieStoreId?: string; + documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined. + frameId: number; + incognito?: boolean; + method: string; + originUrl: string; + parentFrameId: number; + proxyInfo?: { + host: string; + port: number; + type: string; + username: string; + proxyDNS: boolean; + failoverTimeout: number; + }; + requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[]; + requestId: string; + tabId: number; + thirdParty?: boolean; + timeStamp: number; + type: string; + url: string; // the target of the request; + urlClassification?: { firstParty: string[]; thirdParty: string[] }; }; export function getshorthost(host: string) { - const parts = host - .replace(/^.*:\/\//, '') - .replace(/\/.*$/, '') - .split('.'); - let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; - if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { - lookback = -4; // to distinguish between google ads and stats - } else if (parts.at(-2) == 'google') { - lookback = -3; // to distinguish various google services - } - return parts.slice(lookback).join('.'); + const parts = host + .replace(/^.*:\/\//, '') + .replace(/\/.*$/, '') + .split('.'); + let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; + if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { + lookback = -4; // to distinguish between google ads and stats + } else if (parts.at(-2) == 'google') { + lookback = -3; // to distinguish various google services + } + return parts.slice(lookback).join('.'); } export function useEmitter( - e: EventEmitter + e: EventEmitter ): [ - Record, - React.Dispatch>> + Record, + React.Dispatch>> ] { - const [eventCounts, setEventCounts] = React.useState>({ - '*': 0, - }); - React.useEffect(() => { - const callback = (eventSubtype: string) => { - setEventCounts((eventCounts) => { - console.log({ - ...eventCounts, - ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, - ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, - }); - return { - ...eventCounts, - ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, - ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, - }; + const [eventCounts, setEventCounts] = React.useState>({ + '*': 0, + }); + React.useEffect(() => { + const callback = (eventSubtype: string) => { + setEventCounts((eventCounts) => { + console.log({ + ...eventCounts, + ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, + ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, }); - }; - e.on('change', callback); - return () => { - e.removeListener('change', callback); - }; - }, []); - return [eventCounts, setEventCounts]; + return { + ...eventCounts, + ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, + ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, + }; + }); + }; + e.on('change', callback); + return () => { + e.removeListener('change', callback); + }; + }, []); + return [eventCounts, setEventCounts]; } export function parseCookie(cookie: string): Record { - return cookie - .split(';') - .map((l) => l.split('=')) - .reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: value, - }), - {} - ); + return cookie + .split(';') + .map((l) => l.split('=')) + .reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value, + }), + {} + ); } export async function getTabByID(id: number) { - const tabs = await browser.tabs.query({ currentWindow: true }); - return tabs.find((tab) => tab.id == id); + const tabs = await browser.tabs.query({ currentWindow: true }); + return tabs.find((tab) => tab.id == id); } export function parseToObject(str: unknown): Record { - let result: Record; - let original_string: string; - if (typeof str === 'string') { - original_string = str; - result = JSON.parse(str); - } else if (typeof str == 'object') { - result = str as Record; - original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); - } - result[Symbol.for('originalString')] = original_string; - return result; + let result: Record; + let original_string: string; + if (typeof str === 'string') { + original_string = str; + result = JSON.parse(str); + } else if (typeof str == 'object') { + result = str as Record; + original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); + } + result[Symbol.for('originalString')] = original_string; + return result; } export function isJSONObject(str: unknown): str is Record | string | number { - try { - const firstChar = JSON.stringify(parseToObject(str))[0]; - return ['{', '['].includes(firstChar); - } catch (e) { - return false; - } + try { + const firstChar = JSON.stringify(parseToObject(str))[0]; + return ['{', '['].includes(firstChar); + } catch (e) { + return false; + } } export function isURL(str: unknown): str is string { - try { - return !!(typeof str === 'string' && new URL(str)); - } catch (e) { - return false; - } + try { + return !!(typeof str === 'string' && new URL(str)); + } catch (e) { + return false; + } } export function hyphenate(str: string): string { - return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`); + return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`); } export function unique(array: T[]): Array { - return Array.from(new Set(array)); + return Array.from(new Set(array)); } export function allSubhosts(host: string) { - const parts = host.split('.'); - const result = []; - for (let i = 0; i < parts.length - 2; i++) { - result.push(parts.slice(i).join('.')); - } - return result; + const parts = host.split('.'); + const result = []; + for (let i = 0; i < parts.length - 2; i++) { + result.push(parts.slice(i).join('.')); + } + return result; } export function reduceConcat(a: T[], b: T[]): T[] { - return a.concat(b); + return a.concat(b); } export function getDate() { - const d = new Date(); - return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d - .getDate() - .toString() - .padStart(2, '0')}`; + const d = new Date(); + return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d + .getDate() + .toString() + .padStart(2, '0')}`; } export function toBase64(file: File): Promise { - return new Promise((resolve) => { - const FR = new FileReader(); - FR.addEventListener('load', (e) => { - resolve(e.target.result as string); - }); - FR.readAsDataURL(file); - }); + return new Promise((resolve) => { + const FR = new FileReader(); + FR.addEventListener('load', (e) => { + resolve(e.target.result as string); + }); + FR.readAsDataURL(file); + }); } export function makeThrottle(interval: number) { - let last_emit = 0; - function emit(callback: () => void) { - if (Date.now() - last_emit > interval) { - callback(); - last_emit = Date.now(); - return true; - } else { - return false; - } - } - return function (callback: () => void) { - if (!emit(callback)) { - setTimeout(() => emit(callback), interval); - } - }; + let last_emit = 0; + function emit(callback: () => void) { + if (Date.now() - last_emit > interval) { + callback(); + last_emit = Date.now(); + return true; + } else { + return false; + } + } + return function (callback: () => void) { + if (!emit(callback)) { + setTimeout(() => emit(callback), interval); + } + }; } export function isSameURL(url1: string, url2: string): boolean { - if (url1 === url2) { - return true; - } - url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, ''); - url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, ''); - return url1 === url2; + if (url1 === url2) { + return true; + } + url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, ''); + url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, ''); + return url1 === url2; } export function isBase64(s: string): boolean { - try { - atob(s); - return true; - } catch (e) {} - return false; + try { + atob(s); + return true; + } catch (e) {} + return false; } export function isBase64JSON(s: unknown): s is string { - return typeof s === 'string' && isBase64(s) && isJSONObject(atob(s)); + return typeof s === 'string' && isBase64(s) && isJSONObject(atob(s)); } export function flattenObject( - obj: unknown, - parser: (to_parse: unknown) => string | Record = (id) => id.toString(), - key = '', - ret = [] as [string, string][], - parsed = false + obj: unknown, + parser: (to_parse: unknown) => string | Record = (id) => id.toString(), + key = '', + ret = [] as [string, string][], + parsed = false ): [string, string][] { - const prefix = key === '' ? '' : `${key}.`; - if (Array.isArray(obj)) { - if (obj.length == 1) { - flattenObject(obj[0], parser, key, ret); - } else { - for (let i in obj) { - flattenObject(obj[i], parser, prefix + i, ret); - } - } - } else if (typeof obj === 'object') { - for (const [subkey, value] of Object.entries(obj)) { - flattenObject(value, parser, prefix + subkey, ret); - } - } else if (!parsed) { - flattenObject(parser(obj), parser, key, ret, true); - } else if (typeof obj === 'string') { - ret.push([key, obj]); - } else { - throw new Error('Something went wrong when parsing ' + obj); - } - return ret; + const prefix = key === '' ? '' : `${key}.`; + if (Array.isArray(obj)) { + if (obj.length == 1) { + flattenObject(obj[0], parser, key, ret); + } else { + for (let i in obj) { + flattenObject(obj[i], parser, prefix + i, ret); + } + } + } else if (typeof obj === 'object') { + for (const [subkey, value] of Object.entries(obj)) { + flattenObject(value, parser, prefix + subkey, ret); + } + } else if (!parsed) { + flattenObject(parser(obj), parser, key, ret, true); + } else if (typeof obj === 'string') { + ret.push([key, obj]); + } else { + throw new Error('Something went wrong when parsing ' + obj); + } + return ret; } export function flattenObjectEntries( - entries: [string, unknown][], - parser: (to_parse: unknown) => string | Record = (id) => id.toString() + entries: [string, unknown][], + parser: (to_parse: unknown) => string | Record = (id) => id.toString() ): [string, string][] { - return flattenObject(Object.fromEntries(entries), parser); + return flattenObject(Object.fromEntries(entries), parser); } export function maskString( - str: string, - max_fraction_remaining: number, - max_chars_total: number + str: string, + max_fraction_remaining: number, + max_chars_total: number ): string { - const amount_of_chars_to_cut = - str.length - Math.min(str.length * max_fraction_remaining, max_chars_total); - if (amount_of_chars_to_cut == 0) { - return str; - } - return ( - str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) + - '(...)' + - str.slice(str.length / 2 + amount_of_chars_to_cut / 2) - ); + const amount_of_chars_to_cut = + str.length - Math.min(str.length * max_fraction_remaining, max_chars_total); + if (amount_of_chars_to_cut == 0) { + return str; + } + return ( + str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) + + '(...)' + + str.slice(str.length / 2 + amount_of_chars_to_cut / 2) + ); } export function safeDecodeURIComponent(s: string) { - try { - return decodeURIComponent(s); - } catch (e) { - return s; - } + try { + return decodeURIComponent(s); + } catch (e) { + return s; + } } export function normalizeForClassname(string: string) { - return string.replace(/[^a-z0-9]/gi, '-'); + return string.replace(/[^a-z0-9]/gi, '-'); +} + +export function wordlist(words: string[]) { + return words.reduce( + (acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, + '' + ); }