Formatting and wording

This commit is contained in:
Kuba Orlik 2022-02-10 19:54:51 +01:00
parent 54e5040348
commit 473424d88d
5 changed files with 280 additions and 276 deletions

View File

@ -9,49 +9,50 @@ declare var PLUGIN_NAME: string;
declare var PLUGIN_URL: string; declare var PLUGIN_URL: string;
export default function EmailContent({ export default function EmailContent({
answers, answers,
visited_url, visited_url,
clusters, clusters,
}: { }: {
answers: ParsedAnswers; answers: ParsedAnswers;
visited_url: string; visited_url: string;
clusters: Record<string, RequestCluster>; clusters: Record<string, RequestCluster>;
}) { }) {
const _ = (key: string) => v(key, answers.zaimek); const _ = (key: string) => v(key, answers.zaimek);
const problems = deduceProblems(answers, clusters); const problems = deduceProblems(answers, clusters);
const explainers = Array.from( const explainers = Array.from(
new Set( new Set(
problems problems
.map((problem) => problem.getNecessaryExplainers()) .map((problem) => problem.getNecessaryExplainers())
.reduce((a, b) => a.concat(b), []) .reduce((a, b) => a.concat(b), [])
) )
).map((explainer_key) => Explainers[explainer_key]); ).map((explainer_key) => Explainers[explainer_key]);
return ( return (
<div style={{ padding: '1rem' }}> <div style={{ padding: '1rem' }}>
<pre>{JSON.stringify(answers, null, 3)}</pre> <pre>{JSON.stringify(answers, null, 3)}</pre>
<p>Dzień dobry,</p> <p>Dzień dobry,</p>
<p> <p>
w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po podejrzeniu ruchu w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po podejrzeniu ruchu
sieciowego generowanego przez stronę za pomocą wtyczki{' '} sieciowego generowanego przez stronę za pomocą wtyczki{' '}
<a href={PLUGIN_URL}>{PLUGIN_NAME}</a> w przeglądarce Firefox {_('mam')} pytania <a href={PLUGIN_URL}>{PLUGIN_NAME}</a> w przeglądarce Firefox {_('mam')} pytania
dotyczące przetwarzania {_('moich')} danych osobowych, na które nie {_('znalazłem')}{' '} dotyczące przetwarzania {_('moich')} danych osobowych, na które nie {_('znalazłem')}{' '}
odpowiedzi nigdzie na Państwa stronie. odpowiedzi nigdzie na Państwa stronie.
</p> </p>
{problems.map((problem) => problem.getEmailContent())} {problems.map((problem) => problem.getEmailContent())}
{explainers.map((explainer) => explainer(answers.zaimek))} {explainers.map((explainer) => explainer(answers.zaimek))}
<p> <h2>Państwa rola jako współadministratora danych osobowych</h2>
{_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '} <p>
<a href="https://curia.europa.eu/juris/document/document.jsf?text=&docid=216555&pageIndex=0&doclang=PL&mode=lst&dir=&occ=first&part=1&cid=1254905"> {_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '}
treści wyroku TSUE w sprawie C-40/17 <a href="https://curia.europa.eu/juris/document/document.jsf?text=&docid=216555&pageIndex=0&doclang=PL&mode=lst&dir=&occ=first&part=1&cid=1254905">
</a>{' '} treści wyroku TSUE w sprawie C-40/17
poprzez wysyłanie moich danych w wyżej opisanym zakresie stają się Państwo </a>{' '}
współadministratorem moich danych osobowych, dlatego ciąży na Państwu obowiązek poprzez wysyłanie moich danych w wyżej opisanym zakresie stają się Państwo
odpowiedzi na moje pytanie na mocy Art. 12 i 13 Rozporządzenia 2016/679 Parlamentu współadministratorem moich danych osobowych, dlatego ciąży na Państwu obowiązek
Europejskiego i Rady (UE) z dnia 27 kwietnia 2016 r. w sprawie ochrony osób odpowiedzi na moje pytanie na mocy Art. 12 i 13 Rozporządzenia 2016/679 Parlamentu
fizycznych w związku z przetwarzaniem danych osobowych i w sprawie swobodnego Europejskiego i Rady (UE) z dnia 27 kwietnia 2016 r. w sprawie ochrony osób fizycznych w
przepływu takich danych oraz uchylenia dyrektywy 95/46/WE (ogólne rozporządzenie o związku z przetwarzaniem danych osobowych i w sprawie swobodnego przepływu takich danych
ochronie danych, dalej: RODO). oraz uchylenia dyrektywy 95/46/WE (ogólne rozporządzenie o ochronie danych, dalej:
</p> RODO).
</div> </p>
); </div>
);
} }

View File

@ -1,14 +1,14 @@
export type ExplainerKey = 'cookies_are_pii'; export type ExplainerKey = 'cookies_are_pii';
export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => JSX.Element> = { export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => JSX.Element> = {
cookies_are_pii: () => ( cookies_are_pii: () => (
<> <>
<h3>Ciasteczka stanowią dane osobowe</h3> <h2>Ciasteczka stanowią dane osobowe</h2>
<p> <p>
Sztucznie wygenerowane identyfikatory przechowywane w plikach Cookies stanowią dane Sztucznie wygenerowane identyfikatory przechowywane w plikach Cookies stanowią dane
osobowe. Wskazuje na to wprost Art. 4. pkt 1. RODO, wymieniając identyfikator osobowe. Wskazuje na to wprost Art. 4. pkt 1. RODO, wymieniając identyfikator
internetowy i numer identyfikacyjny jako przykłady danych osobowych. internetowy i numer identyfikacyjny jako przykłady danych osobowych.
</p> </p>
</> </>
), ),
}; };

View File

@ -1,4 +1,5 @@
import { RequestCluster } from '../../request-cluster'; import { RequestCluster } from '../../request-cluster';
import { wordlist } from '../../util';
import { ExplainerKey } from '../explainers'; import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers'; import { ParsedAnswers } from '../parse-answers';
import { v } from '../verbs'; import { v } from '../verbs';
@ -145,7 +146,7 @@ export class UnlawfulCookieAccess extends Problem {
{unnecessary_hosts.length > 0 ? ( {unnecessary_hosts.length > 0 ? (
<p> <p>
W {_('mojej')} ocenie odczytywanie przez Państwa stronę treści plików cookies z{' '} 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 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 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{' '} 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 takiego przetwarzania {_('moich')} danych osobowych, czy przetwarzali je państwo bez
ważnej podstawy prawnej? ważnej podstawy prawnej?
</p> </p>
<p> {maybe_unnecessary_hosts.length > 1 ? (
{_('Proszę')} też o wskazanie, czy dostęp do treści plików cookie z <p>
{maybe_unnecessary_hosts.join(', ')} jest konieczny do poprawnego działania strony? {_('Proszę')} też o wskazanie, czy dostęp do treści plików cookie z
Jeżeli tak, to {_('proszę')} wskazać, w jaki sposób. Co sprawia, że strona nie może {wordlist(maybe_unnecessary_hosts)} jest konieczny do poprawnego działania strony?
działać bez nich? Jeżeli tak, to {_('proszę')} wskazać, w jaki sposób. Co sprawia, że strona nie
</p> może działać bez nich?
</p>
) : (
''
)}
</> </>
); );
} }

View File

@ -1,38 +1,29 @@
@import '../sidebar/fonts.scss'; @import '../sidebar/fonts.scss';
@import '../sidebar/colors.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 { html {
font-size: 1rem; font-size: 1rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif; 'Open Sans', 'Helvetica Neue', sans-serif;
} }
body { body {
background-color: #fff; background-color: #fff;
} }
nav { nav {
position: sticky; position: sticky;
top: 0; top: 0;
background-color: #fff; background-color: #fff;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: left; justify-content: left;
align-items: center; align-items: center;
padding: 1rem 1rem; padding: 1rem 1rem;
border-bottom: 2px solid $ultra-light-grey; border-bottom: 2px solid $ultra-light-grey;
height: 5rem; height: 5rem;
img { img {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
} }

387
util.ts
View File

@ -6,266 +6,273 @@ export type Unarray<T> = T extends Array<infer R> ? R : T;
export type Tab = Unarray<Unpromisify<ReturnType<typeof browser.tabs.query>>>; export type Tab = Unarray<Unpromisify<ReturnType<typeof browser.tabs.query>>>;
export type Request = { export type Request = {
cookieStoreId?: 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. 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; frameId: number;
incognito?: boolean; incognito?: boolean;
method: string; method: string;
originUrl: string; originUrl: string;
parentFrameId: number; parentFrameId: number;
proxyInfo?: { proxyInfo?: {
host: string; host: string;
port: number; port: number;
type: string; type: string;
username: string; username: string;
proxyDNS: boolean; proxyDNS: boolean;
failoverTimeout: number; failoverTimeout: number;
}; };
requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[]; requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[];
requestId: string; requestId: string;
tabId: number; tabId: number;
thirdParty?: boolean; thirdParty?: boolean;
timeStamp: number; timeStamp: number;
type: string; type: string;
url: string; // the target of the request; url: string; // the target of the request;
urlClassification?: { firstParty: string[]; thirdParty: string[] }; urlClassification?: { firstParty: string[]; thirdParty: string[] };
}; };
export function getshorthost(host: string) { export function getshorthost(host: string) {
const parts = host const parts = host
.replace(/^.*:\/\//, '') .replace(/^.*:\/\//, '')
.replace(/\/.*$/, '') .replace(/\/.*$/, '')
.split('.'); .split('.');
let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3;
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
lookback = -4; // to distinguish between google ads and stats lookback = -4; // to distinguish between google ads and stats
} else if (parts.at(-2) == 'google') { } else if (parts.at(-2) == 'google') {
lookback = -3; // to distinguish various google services lookback = -3; // to distinguish various google services
} }
return parts.slice(lookback).join('.'); return parts.slice(lookback).join('.');
} }
export function useEmitter( export function useEmitter(
e: EventEmitter e: EventEmitter
): [ ): [
Record<string, number | undefined>, Record<string, number | undefined>,
React.Dispatch<React.SetStateAction<Record<string, number | undefined>>> React.Dispatch<React.SetStateAction<Record<string, number | undefined>>>
] { ] {
const [eventCounts, setEventCounts] = React.useState<Record<string, number | undefined>>({ const [eventCounts, setEventCounts] = React.useState<Record<string, number | undefined>>({
'*': 0, '*': 0,
}); });
React.useEffect(() => { React.useEffect(() => {
const callback = (eventSubtype: string) => { const callback = (eventSubtype: string) => {
setEventCounts((eventCounts) => { setEventCounts((eventCounts) => {
console.log({ console.log({
...eventCounts, ...eventCounts,
...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 },
...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 },
});
return {
...eventCounts,
...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 },
...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 },
};
}); });
}; return {
e.on('change', callback); ...eventCounts,
return () => { ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 },
e.removeListener('change', callback); ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 },
}; };
}, []); });
return [eventCounts, setEventCounts]; };
e.on('change', callback);
return () => {
e.removeListener('change', callback);
};
}, []);
return [eventCounts, setEventCounts];
} }
export function parseCookie(cookie: string): Record<string, string> { export function parseCookie(cookie: string): Record<string, string> {
return cookie return cookie
.split(';') .split(';')
.map((l) => l.split('=')) .map((l) => l.split('='))
.reduce( .reduce(
(acc, [key, value]) => ({ (acc, [key, value]) => ({
...acc, ...acc,
[key]: value, [key]: value,
}), }),
{} {}
); );
} }
export async function getTabByID(id: number) { export async function getTabByID(id: number) {
const tabs = await browser.tabs.query({ currentWindow: true }); const tabs = await browser.tabs.query({ currentWindow: true });
return tabs.find((tab) => tab.id == id); return tabs.find((tab) => tab.id == id);
} }
export function parseToObject(str: unknown): Record<string | symbol, unknown> { export function parseToObject(str: unknown): Record<string | symbol, unknown> {
let result: Record<string | symbol, unknown>; let result: Record<string | symbol, unknown>;
let original_string: string; let original_string: string;
if (typeof str === 'string') { if (typeof str === 'string') {
original_string = str; original_string = str;
result = JSON.parse(str); result = JSON.parse(str);
} else if (typeof str == 'object') { } else if (typeof str == 'object') {
result = str as Record<string | symbol, unknown>; result = str as Record<string | symbol, unknown>;
original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str);
} }
result[Symbol.for('originalString')] = original_string; result[Symbol.for('originalString')] = original_string;
return result; return result;
} }
export function isJSONObject(str: unknown): str is Record<string, unknown> | string | number { export function isJSONObject(str: unknown): str is Record<string, unknown> | string | number {
try { try {
const firstChar = JSON.stringify(parseToObject(str))[0]; const firstChar = JSON.stringify(parseToObject(str))[0];
return ['{', '['].includes(firstChar); return ['{', '['].includes(firstChar);
} catch (e) { } catch (e) {
return false; return false;
} }
} }
export function isURL(str: unknown): str is string { export function isURL(str: unknown): str is string {
try { try {
return !!(typeof str === 'string' && new URL(str)); return !!(typeof str === 'string' && new URL(str));
} catch (e) { } catch (e) {
return false; return false;
} }
} }
export function hyphenate(str: string): string { 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<T>(array: T[]): Array<T> { export function unique<T>(array: T[]): Array<T> {
return Array.from(new Set<T>(array)); return Array.from(new Set<T>(array));
} }
export function allSubhosts(host: string) { export function allSubhosts(host: string) {
const parts = host.split('.'); const parts = host.split('.');
const result = []; const result = [];
for (let i = 0; i < parts.length - 2; i++) { for (let i = 0; i < parts.length - 2; i++) {
result.push(parts.slice(i).join('.')); result.push(parts.slice(i).join('.'));
} }
return result; return result;
} }
export function reduceConcat<T>(a: T[], b: T[]): T[] { export function reduceConcat<T>(a: T[], b: T[]): T[] {
return a.concat(b); return a.concat(b);
} }
export function getDate() { export function getDate() {
const d = new Date(); const d = new Date();
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d
.getDate() .getDate()
.toString() .toString()
.padStart(2, '0')}`; .padStart(2, '0')}`;
} }
export function toBase64(file: File): Promise<string> { export function toBase64(file: File): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
const FR = new FileReader(); const FR = new FileReader();
FR.addEventListener('load', (e) => { FR.addEventListener('load', (e) => {
resolve(e.target.result as string); resolve(e.target.result as string);
}); });
FR.readAsDataURL(file); FR.readAsDataURL(file);
}); });
} }
export function makeThrottle(interval: number) { export function makeThrottle(interval: number) {
let last_emit = 0; let last_emit = 0;
function emit(callback: () => void) { function emit(callback: () => void) {
if (Date.now() - last_emit > interval) { if (Date.now() - last_emit > interval) {
callback(); callback();
last_emit = Date.now(); last_emit = Date.now();
return true; return true;
} else { } else {
return false; return false;
} }
} }
return function (callback: () => void) { return function (callback: () => void) {
if (!emit(callback)) { if (!emit(callback)) {
setTimeout(() => emit(callback), interval); setTimeout(() => emit(callback), interval);
} }
}; };
} }
export function isSameURL(url1: string, url2: string): boolean { export function isSameURL(url1: string, url2: string): boolean {
if (url1 === url2) { if (url1 === url2) {
return true; return true;
} }
url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, ''); url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, '');
url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, ''); url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, '');
return url1 === url2; return url1 === url2;
} }
export function isBase64(s: string): boolean { export function isBase64(s: string): boolean {
try { try {
atob(s); atob(s);
return true; return true;
} catch (e) {} } catch (e) {}
return false; return false;
} }
export function isBase64JSON(s: unknown): s is string { 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( export function flattenObject(
obj: unknown, obj: unknown,
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(), parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(),
key = '', key = '',
ret = [] as [string, string][], ret = [] as [string, string][],
parsed = false parsed = false
): [string, string][] { ): [string, string][] {
const prefix = key === '' ? '' : `${key}.`; const prefix = key === '' ? '' : `${key}.`;
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
if (obj.length == 1) { if (obj.length == 1) {
flattenObject(obj[0], parser, key, ret); flattenObject(obj[0], parser, key, ret);
} else { } else {
for (let i in obj) { for (let i in obj) {
flattenObject(obj[i], parser, prefix + i, ret); flattenObject(obj[i], parser, prefix + i, ret);
} }
} }
} else if (typeof obj === 'object') { } else if (typeof obj === 'object') {
for (const [subkey, value] of Object.entries(obj)) { for (const [subkey, value] of Object.entries(obj)) {
flattenObject(value, parser, prefix + subkey, ret); flattenObject(value, parser, prefix + subkey, ret);
} }
} else if (!parsed) { } else if (!parsed) {
flattenObject(parser(obj), parser, key, ret, true); flattenObject(parser(obj), parser, key, ret, true);
} else if (typeof obj === 'string') { } else if (typeof obj === 'string') {
ret.push([key, obj]); ret.push([key, obj]);
} else { } else {
throw new Error('Something went wrong when parsing ' + obj); throw new Error('Something went wrong when parsing ' + obj);
} }
return ret; return ret;
} }
export function flattenObjectEntries( export function flattenObjectEntries(
entries: [string, unknown][], entries: [string, unknown][],
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString() parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString()
): [string, string][] { ): [string, string][] {
return flattenObject(Object.fromEntries(entries), parser); return flattenObject(Object.fromEntries(entries), parser);
} }
export function maskString( export function maskString(
str: string, str: string,
max_fraction_remaining: number, max_fraction_remaining: number,
max_chars_total: number max_chars_total: number
): string { ): string {
const amount_of_chars_to_cut = const amount_of_chars_to_cut =
str.length - Math.min(str.length * max_fraction_remaining, max_chars_total); str.length - Math.min(str.length * max_fraction_remaining, max_chars_total);
if (amount_of_chars_to_cut == 0) { if (amount_of_chars_to_cut == 0) {
return str; return str;
} }
return ( return (
str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) + str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) +
'(...)' + '(...)' +
str.slice(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) { export function safeDecodeURIComponent(s: string) {
try { try {
return decodeURIComponent(s); return decodeURIComponent(s);
} catch (e) { } catch (e) {
return s; return s;
} }
} }
export function normalizeForClassname(string: string) { 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}`,
''
);
} }