Create complex types for the survey response to prepare for template writing

This commit is contained in:
Kuba Orlik 2022-02-07 21:11:25 +01:00
parent 7330ebf9f2
commit 8e72759a7e
20 changed files with 552 additions and 1390 deletions

View File

@ -1,40 +0,0 @@
/* import { EmailTemplate3Config } from './email-template-3';
* import hostSettingsRadio from './host-settings-radio';
*
* export default function ConsentProblems({
* settings,
* host_id,
* setConfig,
* pronoun,
* }: {
* host_id: string;
* setConfig: React.Dispatch<React.SetStateAction<EmailTemplate3Config>>;
* settings: EmailTemplate3Config['hosts_settings'][string];
* pronoun: 0 | 1 | 2 | 3;
* }) {
* if (settings.legal_basis_type !== 'consent') {
* return '';
* }
* const p = pronoun;
* return (
* <div>
* {hostSettingsRadio({
* host_id,
* setConfig,
* field: 'consent_problems' as const,
* value: settings.consent_problems,
* options: {
* claims_consent_but_sends_before_consent: `Strona wysłała
* ${p == 3 ? 'nasze' : 'moje'} dane do ${host_id} zanim
* ${['wyraziłem', 'wyraziłam', 'wyraziłom', 'wyraziliśmy'][p]} na to zgodę.`,
* claims_consent_but_there_was_no_easy_refuse: `${
* ['Kliknąłem', 'Kliknęłam', 'Kliknęłom', 'Kliknęliśmy'][p]
* }
* przycisk od wyrażania zgody, ale w okienku o zgodę nie było natychmiastowo
* dostępnego przycisku do niewyrażenia zgody jednym kliknięciem.`,
* none: 'Żadne z powyższych.',
* },
* })}
* </div>
* );
* } */

View File

@ -1,69 +0,0 @@
import { Classifications, StolenDataEntry } from '../stolen-data-entry';
export function DataPreview({
entries,
refresh,
}: {
entries: StolenDataEntry[];
refresh: () => void;
}) {
// currently not used, maybe scraped entirely in the future
return (
<table>
<thead>
<tr>
<th>Adres docelowy</th>
<th>Źródło danych</th>
<th>Treść danych</th>
<th>Klasyfikacja</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr
key={entry.id}
style={{
backgroundColor: entry.classification == 'id' ? 'yellow' : 'white',
}}
>
<td>{entry.request.shorthost}</td>
<td style={{ overflowWrap: 'anywhere' }}>
{entry.source}:{entry.name}
</td>
<td
style={{
width: '400px',
overflowWrap: 'anywhere',
backgroundColor: entry.isRelatedToID() ? '#ffff0054' : 'white',
}}
>
{entry.getValuePreview()}
{/* always gonna have
one key, because unwrapEntry is called above */}
</td>
<td>
<select
value={entry.classification}
onChange={(e) => {
entry.classification = e.target
.value as keyof typeof Classifications;
refresh();
}}
>
{[
['history', 'Historia przeglądania'],
['id', 'Identyfikator internetowy'],
['location', 'Lokalizacja'],
].map(([key, name]) => (
<option key={key} value={key}>
{name}
</option>
))}
</select>
</td>
</tr>
))}
</tbody>
</table>
);
}

View File

@ -1,42 +0,0 @@
import React from 'react';
import { RequestCluster } from '../request-cluster';
import { Classifications, Sources } from '../stolen-data-entry';
const emailClassifications: Record<keyof typeof Classifications, string> = {
id: 'mój identyfikator internetowy',
history: 'część mojej historii przeglądania',
location: 'informacje na temat mojej lokalizacji geograficznej',
};
const emailSources: Record<Sources, string> = {
header: 'w nagłówku HTTP',
cookie: 'z pliku Cookie',
pathname: 'jako części adresu URL',
queryparams: 'jako część adresu URL (query-params)',
request_body: 'w body zapytania POST',
};
export default function DomainSummary({
cluster,
}: {
cluster: RequestCluster;
}) {
return (
<li>
Właścicielowi domeny <strong>{cluster.id}</strong> zostały
ujawnione:{' '}
<ul>
{cluster.representativeStolenData
.filter((entry) => entry.isMarked)
.map((entry) => (
<li key={entry.id}>
{emailClassifications[entry.classification]}{' '}
{emailSources[entry.source]} (nazwa:{' '}
<code>{entry.name}</code>, wartość:{' '}
<code>{entry.getValuePreview()}</code>)
</li>
))}
</ul>
</li>
);
}

View File

@ -0,0 +1,10 @@
import { ParsedAnswers } from './parse-answers';
export default function EmailContent({ answers }: { answers: ParsedAnswers }) {
return (
<div>
<h1>Email template</h1>
<pre>{JSON.stringify(answers, null, 3)}</pre>
</div>
);
}

View File

@ -1,2 +0,0 @@
@import '../sidebar/fonts.scss';
@import '../sidebar/colors.scss';

View File

@ -1,67 +0,0 @@
import { EmailTemplate3Config } from './email-template-3';
/* import hostSettingsDropdown from './host-setting-dropdown'; */
/* import ConsentProblems from './consent-problems'; */
import LegitimateInteresProblems from './legitimate-interest-problems';
/* import { hostNeedsQuestions } from './host-needs-questions'; */
export function setHostSetting<
P1 extends keyof EmailTemplate3Config['hosts_settings'],
P2 extends keyof EmailTemplate3Config['hosts_settings'][P1]
>(
setConfig: React.Dispatch<React.SetStateAction<EmailTemplate3Config>>,
[p1, p2]: [P1, P2],
value: EmailTemplate3Config['hosts_settings'][P1][P2]
) {
setConfig((v) => {
console.log(v, {
...v,
hosts_settings: {
...v.hosts_settings,
[p1]: {
...v.hosts_settings[p2],
[p2]: value,
},
},
});
return {
...v,
hosts_settings: {
...v.hosts_settings,
[p1]: {
...v.hosts_settings[p1],
[p2]: value,
},
},
};
});
}
export default function emailHostSettings(
config: EmailTemplate3Config,
setConfig: React.Dispatch<React.SetStateAction<EmailTemplate3Config>>
) {
if (config.policy_readable == 'null') {
return '';
}
const p = config.pronoun;
return (
<aside>
{Object.entries(config.hosts_settings).map(([host_id, settings]) => (
<div key={host_id}>
{!['not_mentioned', 'null'].includes(settings.legal_basis_type) ? (
<div>
{ConsentProblems({ settings, host_id, pronoun: p, setConfig })}
{LegitimateInteresProblems({
settings,
host_id,
setConfig,
})}
</div>
) : (
''
)}
</div>
))}
</aside>
);
}

View File

@ -1,486 +0,0 @@
import React from 'react';
import { RequestCluster } from '../request-cluster';
import { StolenDataEntry } from '../stolen-data-entry';
import { getDate, unique } from '../util';
import DomainSummary from './domain-summary';
import EmailTemplate2Controls from './email-template-2-controls';
export type EmailTemplate2Config = {
popup_type: 'none' | 'passive_cookie_banner' | 'consent';
popup_action: 'ignored' | 'accepted' | 'closed';
popup_closed_how: string;
popup_screenshot_base64: string | null;
popup_accept_all_text: string;
popup_mentions_passive_consent: boolean;
popup_passive_consent_text: string;
};
function ClusterRangeSummary({ cluster }: { cluster: RequestCluster }) {
const range = unique(
cluster.getMarkedEntries().map((entry) => entry.classification)
);
const has_cookie_ids = cluster
.getMarkedEntries()
.filter((entry) => entry.source === 'cookie')
.some((entry) => entry.classification == 'id');
return (
<>
{[
range.includes('id')
? 'Pańskiego identyfikatora internetowego' +
(has_cookie_ids ? ' z cookie' : '')
: '',
range.includes('history')
? 'części Pańskiej historii przeglądania'
: '',
range.includes('location')
? 'informacji na temat Pana położenia'
: '',
]
.filter((e) => e !== '')
.join(', ')}
</>
);
}
function Placeholder({ children }: { children: string }) {
const invisbleCharacter = '';
return (
<span
style={{
textDecoration: 'underline',
fontSize: '0.8em',
position: 'relative',
textUnderlineOffset: '4px',
bottom: '3px',
}}
>
<span>{invisbleCharacter.repeat(6)}</span>
<span style={{ color: 'gray' }}>({children})</span>
<span>{invisbleCharacter.repeat(6)}</span>
</span>
);
}
function Base64Image({ base64 }: { base64: string }) {
return <img style={{ maxWidth: '100%' }} {...{ src: base64 }} />;
}
export default function EmailTemplate2({
entries,
clusters,
}: {
entries: StolenDataEntry[];
clusters: Record<string, RequestCluster>;
version: number;
}): JSX.Element {
const [config, setConfig] = React.useState<EmailTemplate2Config>({
popup_type: 'none',
popup_action: 'ignored',
popup_screenshot_base64: null,
popup_accept_all_text: 'Zaakceptuj wszystkie',
popup_mentions_passive_consent: false,
popup_passive_consent_text: '',
popup_closed_how: 'kliknięcie przycisku „X”',
});
const visited_url = entries[0].request.originalURL;
return (
<>
<EmailTemplate2Controls {...{ config, setConfig }} />
<article
style={{
boxShadow: '0 20px 40px rgba(0,0,0,.2)',
padding: '4rem 3rem',
borderRadius: '0.25rem',
margin: '2rem 0',
color: 'hsl(240, 5.7%, 15.8%);',
}}
>
<p>
Dzień dobry, w dniu {getDate()} odwiedziłem stronę{' '}
{visited_url}.
</p>
{config.popup_type === 'none' ? (
<p>
Nie ukazał mi się na stronie żaden mechanizm pozyskujący
zgodę na przetwarzanie moich danych osobowych lub
umożliwiający mi wyrażenie sprzeciwu wobec przetwarzania
przez stronę moich danych osobowych w zakresie
wykraczającym poza procesy konieczne do wyświetlenia
strony
</p>
) : config.popup_type == 'passive_cookie_banner' ? (
<>
<p>
Na stronie była widoczna informacja o plikach
Cookie.{' '}
</p>
<p>
<Base64Image
{...{ base64: config.popup_screenshot_base64 }}
/>
</p>
</>
) : (
<>
<p>
Ukazało mi się okienko z informacjami i pytaniami
dotyczącymi sposobów, w jaki strona przetwarza moje
dane osobowe.{' '}
</p>
<p>
<Base64Image
{...{ base64: config.popup_screenshot_base64 }}
/>
</p>
<p>
{config.popup_action === 'ignored'
? /* HTML */ `Nie kliknąłem żadnego przycisku w
tym okienku. W szczególności nie kliknąłem
przycisku ${config.popup_accept_all_text}.`
: config.popup_action === 'accepted'
? `Kliknąłem na widoczną w tym okienku opcję „${config.popup_accept_all_text}”.`
: ''}
</p>
</>
)}
<p>
W tym samym czasie rejestrowałem ruch sieciowy generowany
przez stronę za pomocą narzędzi w przeglądarce Firefox.
Okazało się, że Państwa strona wysłała moje dane osobowe do
następujących podmiotów:
</p>
<ul>
{Object.values(clusters)
.filter((cluster) => cluster.hasMarks())
.map((cluster) => (
<DomainSummary cluster={cluster} key={cluster.id} />
))}
</ul>
{config.popup_action === 'ignored' ? (
<p>
Dane te zostały wysłane, zanim kliknąłem cokolwiek na
tej stronie.
</p>
) : config.popup_action === 'accepted' ? (
<p>
Dane te zostały wysłane po tym, jak kliknąłem przycisk
{config.popup_accept_all_text}
</p>
) : (
''
)}
<p>
W załączeniu przesyłam część zrzutów ekranu dokumentujących
fakt wysłania tych danych przez Państwa stronę.{' '}
</p>
<h3>Podstawa prawna</h3>
<p>
Ustawa Prawo Telekomunikacyjne w art. 173 reguluje warunki,
które musi spełnić administrator strony, aby jego strona
mogła zapisywać i czytać treść plików cookie. Nie reguluje
jednak tego, jakim podmiotom i w jakim zakresie dane mogą
być <em>ujawniane</em> przez stronę. Tym zajmuje się
Rozporządzenie 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) RODO.
Zapis/odczyt plików cookie a ujawnianie ich treści podmiotom
trzecim to dwa różne procesy. Niniejsza wiadomość i pytania
w niej zawarte dotyczą właśnie <em>ujawniania</em> moich
danych osobowych (pochodzących m.in. z Cookies) podmiotom
trzecim.
</p>
<p>
W kontekście stron internetowych właściwie dopuszczalne
tylko trzy z sześciu wymienionych w Art. 6 pkt 1 RODO
podstaw prawnych dla przetwarzania danych osobowych:
</p>
<ol>
<li>
Zgoda &mdash; osoba, której dane dotyczą wyraziła
zgodę na przetwarzanie swoich danych osobowych w jednym
lub większej liczbie określonych celów (
<em>Art. 6 pkt 1 lit. a)</em>).
</li>
<li>
Niezbędność &mdash; przetwarzanie jest niezbędne do
wykonania umowy, której stroną jest osoba, której dane
dotyczą, lub do podjęcia działań na żądanie osoby,
której dane dotyczą, przed zawarciem umowy (
<em>Art. 6 pkt 1 lit. b)</em>);{' '}
</li>
<li>
Uzasadniony Interes &mdash; przetwarzanie jest
niezbędne do celów wynikających z prawnie uzasadnionych
interesów realizowanych przez administratora lub przez
stronę trzecią, z wyjątkiem sytuacji, w których
nadrzędny charakter wobec tych interesów mają interesy
lub podstawowe prawa i wolności osoby, której dane
dotyczą, wymagające ochrony danych osobowych, w
szczególności gdy osoba, której dane dotyczą, jest
dzieckiem (<em>Art. 6 pkt 1 lit. f)</em>
);
</li>
</ol>
<p>
W przypadku opisywanej przeze mnie mojej wizyty na Państwa
stronie nie ma zastosowania Zgoda, gdyż{' '}
{config.popup_action === 'ignored' ? (
<>
nie wyrażałem żadnej zgody na takie przetwarzanie
moich danych
{config.popup_type === 'consent' ? (
<>
&mdash; w szczególności nie kliknąłem
przycisku {config.popup_accept_all_text}
</>
) : (
''
)}
.
</>
) : config.popup_action === 'accepted' ? (
<>
o ile po wejściu na stronę wcisnąłem w wyskakującym
okienku przycisk {config.popup_accept_all_text}, o
tyle nie stanowi to według mnie ważnej w świetle
RODO zgody, gdyż brakowało w tym okienku równie
łatwo osiągalnego przycisku, którego kliknięcie
skutkowałoby zasygnalizowaniem braku mojej zgody na
takie przetwarzanie moich danych. Mówiąc wprost
&mdash; wyrażenie zgody było łatwiejsze niż jej
niewyrażenie. Niewyrażenie zgody wiąże się z
negatywną konsekwencją konieczności przechodzenia
przez dodatkowe kroki w wyskakującym okienku. Zatem
tak otrzymana przez Państwo moja zgoda nie jest
poprawną podstawą prawną do przetwarzania moich
danych osobowych, gdyż nie spełnia warunku
dobrowolności wspomnianego w motywie (42) RODO.
</>
) : config.popup_action === 'closed' ? (
<>
zamknąłem okienko pytające o zgodę poprzez{' '}
{config.popup_closed_how}. Nie może być to uznane za
zgodę, bo nie spełnia to warunku jednoznaczności
opisanego w motywie (32) Rozporządzenia 2016/679.{' '}
</>
) : (
''
)}{' '}
Za zgodę nie można też uznać posiadania włączonej obsługi
cookies w przeglądarce (gdyż aby zgoda była ważna, musi być
szczegółowa dla każdego celów z osobna), jakichkolwiek
innych ustawień przeglądarki, ani pasywnych działań z mojej
strony (np. kontynuowanie korzystania ze strony)
{config.popup_mentions_passive_consent ? (
<>
{' '}
&mdash; nieprawdą więc jest zawarty na Państwa
stronie komunikat
{config.popup_passive_consent_text.trim()} (por.
paragraf 97.{' '}
<a href="https://edpb.europa.eu/sites/default/files/files/file1/edpb_guidelines_202005_consent_pl.pdf">
oficjalnych wytycznych EROD dotyczących zgody na
mocy rozporządzenia 2016/679
</a>
)
</>
) : (
''
)}
.
</p>
<p>
W mojej ocenie Niezbędność nie ma zastosowania co do
opisanych powyżej sposobów przetwarzania danych. Nie widzę,
co miałoby sprawiać, aby wysyłanie moich danych osobowych do
wspomnianych powyżej podmiotów trzecich było konieczne do
wyświetlenia Państwa strony na ekranie mojego komputera
(zob.{' '}
<a href="https://edpb.europa.eu/system/files/2021-11/edpb_guidelines_082020_on_the_targeting_of_social_media_users_pl_0.pdf">
Wytyczne 8/2020 EROD dotyczące targetowania użytkowników
mediów społecznościowych
</a>
, par. 49);.{' '}
</p>
<p>
Pozostaje zatem Uzasadniony Interes. Aby Administrator
mógł używać uzasadnionego interesu jako podstawy prawnej
targetowania użytkowników Sieci, muszą zostać spełnione
m.in. następujące warunki:{' '}
</p>
<ol>
<li>
Administrator danych lub podmiot trzeci, któremu dane
ujawniane musi{' '}
<strong>
faktycznie realizować dany konkretny uzasadniony
interes
</strong>{' '}
(
<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">
Wyrok TSUE z dnia 29 lipca 2019 r. w sprawie Fashion
ID, C-40/17, ECLI:EU:C:2019:629
</a>
, pkt 95.)
</li>
<li>
Takie przetwarzanie danych jest{' '}
<strong>konieczne</strong> dla potrzeb wynikających z
danego uzasadnionego interesu (
<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">
Wyrok TSUE z dnia 29 lipca 2019 r. w sprawie Fashion
ID, C-40/17, ECLI:EU:C:2019:629
</a>
, pkt 95.)
</li>
<li>
Wybrany uzasadniony interes musi mieć pierwszeństwo nad
prawami i wolnościami osoby, której dotyczą przetwarzane
dane (
<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">
Wyrok TSUE z dnia 29 lipca 2019 r. w sprawie Fashion
ID, C-40/17, ECLI:EU:C:2019:629
</a>
, pkt 95.)
</li>
<li>
Osoby, których dane dotyczą, powinny mieć możliwość
wyrażenia sprzeciwu wobec przetwarzania ich danych do
celów związanych z targetowaniem{' '}
<strong>przed rozpoczęciem przetwarzania</strong> (zob.{' '}
<a href="https://edpb.europa.eu/system/files/2021-11/edpb_guidelines_082020_on_the_targeting_of_social_media_users_pl_0.pdf">
Wytyczne 8/2020 EROD dotyczące targetowania
użytkowników mediów społecznościowych
</a>
, par. 54);
</li>
</ol>
{config.popup_action !== 'accepted' ? (
<p>
Moje dane zostały ujawnione podmiotom trzecim tuż po
włączeniu strony, zatem nie jest spełniony warunek 4.
Apeluję o wdrożenie zmian na stronie, które sprawią, że
dopiero po świadomym niewyrażeniu sprzeciwu przez
użytkownika aktywowane procesy przetwarzania danych
osobowych, których podstawą prawną jest uzasadniony
interes.
</p>
) : (
''
)}
<p>
Jeżeli istnieją jednak inne niż uzasadniony interes ważne
podstawy prawne do takiego przetwarzania moich danych
osobowych przez Państwa stronę, proszę o ich wskazanie,{' '}
<em>dla każdego z wymienionych podmiotów z osobna</em>.
(Przypominam, że Art. 173 ustawy Prawo Telekomunikacyjne nie
ma tutaj zastosowania, ponieważ nie pytam o zapis/odczyt
plików na moim komputerze, tylko o ujawnianie moich danych
osobowych podmiotom trzecim). W przeciwnym wypadku, aby
ustalić, czy moje dane były przez Państwa przetwarzane na
mocy uzasadnionego interesu zgodnie z prawem, proszę o
wypełnienie następującego szablonu (lub udzielenie tych
samych informacji w innej postaci, przy zachowaniu zakresu i
szczegółowości informacji:
</p>
<div style={{ border: '1px solid black', padding: '1rem' }}>
<p>
W dniu {getDate()} strona {visited_url}:
</p>
<ul>
{Object.values(clusters)
.filter((cluster) => cluster.hasMarks())
.map((cluster) => (
<li
key={cluster.id}
style={{ paddingBottom: '1rem' }}
>
ujawniła pańskie dane w zakresie{' '}
<em>
<ClusterRangeSummary {...{ cluster }} />
</em>{' '}
firmie{' '}
<Placeholder>nazwa firmy</Placeholder>,
która jest właścicielem domeny{' '}
<strong>{cluster.id}</strong> i swoją
politykę prywatności publikuje pod adresem{' '}
<Placeholder>
adres URL polityki prywatności tej firmy
</Placeholder>
. Podstawą prawną takiego przetwarzania
danych przez naszą stronę jest uzasadniony
interes:{' '}
<Placeholder>
na czym polega ten uzasadniony interes,
tzn. bieżące działania podejmowane przez
podmiot realizujący ten interes lub
korzyści dla podmiotu realizującego ten
interes oczekiwane w bardzo bliskiej
przyszłości
</Placeholder>{' '}
realizowany przez{' '}
<Placeholder>
kogo? jaki podmiot podejmuje wspomniane
działania lub jest beneficjentem
wspomnianych korzyści?
</Placeholder>
. Ujawnienie{' '}
<ClusterRangeSummary {...{ cluster }} />{' '}
temu podmiotowi przez naszą stronę było
konieczne dla potrzeb wynikających z tego
interesu, ponieważ
<Placeholder>
uzasadnienie konieczności
</Placeholder>
.<br />
</li>
))}
</ul>
</div>
<p>
Proszę w szczególności zwrócić uwagę na podanie adresów do
polityk prywatności tych firm, abym wiedział, jak
skontaktować się z nimi i wnioskować o usunięcie z ich baz
wysłanych przez Państwa stronę moich danych. )
</p>
<p>
W odpowiedzi proszę się nie powoływać na IAB Europe i ich
rzekomą renomę w tworzeniu rozwiązań zgodnych z RODO. IAB
chroni interes reklamodawców, a nie Użytkowników, i ich
rozwiązania (np. TCF) {' '}
<a href="https://panoptykon.org/search/site/IAB">
notorycznie niezgodne z RODO i pozbawione szacunku dla
Użytkowników
</a>
.
</p>
<p>
Apeluję także o wprowadzenie stosownych zmian na stronie
tak, aby nie pozostawiać cienia wątpliwości odnośnie tego,
na mocy jakiej przesłanki legalizującej dane przetwarzane
przez wspomniane podmioty trzecie, lub tak, aby te dane po
prostu nie były wysyłane. Pomoże to zachować prywatność
innym użytkownikom Państwa strony. Polecam Państwa uwadze
<a href="https://edpb.europa.eu/sites/default/files/files/file1/edpb_guidelines_202005_consent_pl.pdf">
{' '}
oficjalne wytyczne EROD dotyczące zgody w kontekście
RODO
</a>
. Aby na przykład zapobiec automatycznemu wysyłaniu historii
przeglądania do podmiotów trzecich przez Państwa stronę,
można po prostu ustawić odpowiednio treść nagłówka{' '}
<a href="https://developer.mozilla.org/pl/docs/Web/HTTP/Headers/Referrer-Policy">
Referrer-Policy{' '}
</a>
.
</p>
</article>
</>
);
}

View File

@ -1,407 +0,0 @@
import React from 'react';
import * as Survey from 'survey-react';
import { toBase64 } from '../util';
import emailHostSettings from './email-host-settings';
import { EmailTemplate3Config } from './email-template-3';
import verbs from './verbs';
function generateHostPage(
host: string,
index: number,
all_hosts: string[]
): { title: string; elements: any[] } {
function f(name: string, h = host) {
return `${h.replace(/\./g, '_')}|${name}`;
}
const previous_host: string | null = index > 0 ? all_hosts[index - 1] : null;
function defaultValue(name: string) {
if (!previous_host) {
return {};
}
return { defaultValueExpression: `{${f(name, previous_host)}}` };
}
return {
title: host,
elements: [
{
type: 'dropdown',
name: f('present'),
isRequired: true,
title: `Cel ujawnienia danych właścicielowi domeny ${host}`,
...defaultValue('present'),
choices: [
{ value: 'not_mentioned', text: 'nie jest podany nigdzie na stronie' },
{
value: 'mentioned_in_policy',
text: 'jest podany w polityce prywatności',
visibleIf: "{policy_readable} = 'yes' ",
},
{ value: 'mentioned_in_popup', text: 'jest podany w okienku RODO' },
],
},
{
type: 'dropdown',
name: f('legal_basis_type'),
...defaultValue('legal_basis_type'),
isRequired: true,
title: `Podstawa prawna dla tego konkretnego celu`,
visibleIf: `{${f('present')}} notempty and {${f('present')}} != "not_mentioned"`,
choices: [
{ value: 'consent', text: 'to zgoda.' },
{
value: 'legitimate_interest',
text: 'to uzasadniony interes.',
},
{ value: 'not_mentioned', text: 'nie jest wskazana nigdzie na stronie.' },
],
},
{
type: 'radiogroup',
name: f('consent_problems'),
...defaultValue('consent_problems'),
isRequired: true,
title: `Jak ma się ta podstawa prawna do stanu faktycznego?`,
visibleIf: `{${f('legal_basis_type')}} = "consent"`,
choices: [
{
value: 'claims_consent_but_sends_before_consent',
text: `Strona wysłała {moje} dane do ${host} zanim {wyraziłem} na to zgodę`,
},
{
value: 'claims_consent_but_there_was_no_easy_refuse',
text: '{Kliknąłem} przycisk od wyrażania zgody, ale w okienku o zgodę nie było natychmiastowo dostępnego przycisku do niewyrażenia zgody jednym kliknięciem',
},
{ value: 'none', text: 'żadne z powyższych.' },
],
},
{
type: 'dropdown',
name: f('legitimate_interest_activity_specified'),
...defaultValue('legitimate_interest_activity_specified'),
isRequired: true,
title: /* HTML */ `Czy administrator strony opisał szczegółowo, na czym polega
uzasadniony interes w kontekście tego celu?`,
visibleIf: `{${f('legal_basis_type')}} = "legitimate_interest"`,
choices: [
{
value: 'precise',
text: /* HTML */ `Tak, wskazuje jasno na bieżące działania lub korzyści
wynikające z takiego przetwarzania danych.`,
},
{
value: 'vague',
text: `Wskazuje tylko ogólnie, jak np. „marketing” czy „statystyki”.`,
},
{
value: 'no',
text: `Nie. Nie wiadomo, na czym ten uzasadniony interes polega.`,
},
],
},
{
type: 'text',
title: `Jak administrator opisał to, na czym polega uzasadniony interes w kontekście ${host}?`,
name: f('legitimate_interest_description'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = 'vague'`,
defaultValueExpression:
index == 0
? 'marketing'
: `{${f('legitimate_interest_description', previous_host)}}`,
},
{
type: 'dropdown',
title: `Czy domena ${host} należy do podmiotu spoza Europy (np. Google, Facebook)?`,
name: f('outside_eu'),
...defaultValue('outside_eu'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = "precise" or {${f(
'consent_problems'
)}} = "none"`,
choices: [
{ value: 'yes', text: 'Tak' },
{ value: 'no', text: 'Nie' },
{ value: 'not_sure', text: 'Nie wiem' },
],
},
],
};
}
export default function EmailTemplate3Controls({ hosts }: { hosts: string[] }) {
const [survey, setSurvey] = React.useState<Survey.Model>(null);
React.useEffect(() => {
var json = {
showQuestionNumbers: 'off',
showProgressBar: 'top',
pages: [
{
title: 'Zaimki',
elements: [
{
type: 'dropdown',
name: 'zaimek',
title: 'Forma czasownika:',
isRequired: true,
choices: [
{ value: 0, text: 'Wysłałem' },
{ value: 1, text: 'Wysłałam' },
{ value: 2, text: 'Wysłałom' },
{ value: 3, text: 'Wysłaliśmy' },
],
},
],
},
{
title: 'Obowiązek informacyjny i machanizm pozyskiwania zgody',
elements: [
{
type: 'radiogroup',
title: 'Jaką formę informacji o przetwarzaniu danych osobowych stosuje ta strona?',
name: 'popup_type',
isRequired: true,
choices: [
{ value: 'none', text: 'Brak informacji' },
{
value: 'passive_popup',
text: /* HTML */ `Okienko o cookiesach, bez możliwości podjęcia
żadnego wyboru (np. tylko opcja zamknij)`,
},
{
value: 'some_choice',
text: 'Okienko o cookiesach, z możliwością podjęcia wyboru',
},
],
},
{
type: 'checkbox',
title: /* HTML */ `Istnieje możliwość, że okienko z informacjami i
wyborami dotyczącymi przetwarzania {Twoich} danych osobowych ukazało się
dawno temu w trakcie {twojej} wcześniejszej wizyty i wtedy je
{odkliknąłeś}. {Otwórz} samą stronę w Trybie Prywatnym (Incognito).
Co {widzisz}?`,
visibleIf: "{popup_type} = 'none'",
name: 'is_incognito_different',
isRequired: true,
choices: [
{
value: 'incognito_is_the_same',
text: 'W Trybie prywatnym {widzę} to samo, co {widziałem} w normalnym trybie',
},
],
},
{
type: 'html',
visibleIf: '{is_incognito_different} != "no" and {popup_type} = "none"',
html: /* HTML */ `Jeżeli w trybie incognito widzisz więcej okienek z
informacjami o przetwarzaniu danych osobowych, wykonaj analizę w
normalnym trybie ponownie - ale najpierw usuń pliki cookies tej
strony.
<a
href="https://support.mozilla.org/pl/kb/usuwanie-ciasteczek-i-danych-stron-firefox?redirectslug=usuwanie-ciasteczek&redirectlocale=pl"
target="_blank"
>
Zobacz, jak to zrobić
</a>`,
},
{
type: 'radiogroup',
name: 'mentions_passive_consent',
isRequired: true,
visibleIf: '{popup_type} = "passive_popup"',
title: 'Czy treść okienka wskazuje na zgodę wyrażoną pasywnie, np. „Korzystając z naszej strony wyrażasz zgodę” lub „Brak zmiany ustawień przeglądarki oznacza zgodę”?',
choices: [
{
value: 'yes',
text: 'Tak',
},
{
value: 'no',
text: 'Nie',
},
],
},
],
},
{
title: 'Polityka prywatności',
elements: [
{
type: 'dropdown',
title: 'Czy polityka prywatności jest dostępna i czytelna?',
name: 'policy_readable',
isRequired: true,
choices: [
{ value: 'yes', text: 'dostępna i czytelna' },
{
value: 'entirely_obscured_by_popup',
text: 'dostępna, ale nieczytelna. Zasłania ją popup o RODO',
},
{
value: 'cant_find',
text: `Niedostępna. {Szukałem}, ale nie {znalazłem} jej na stronie`,
},
],
},
],
},
...hosts.map(generateHostPage),
],
};
console.log(json);
const survey = new Survey.Model(json);
survey.onProcessTextValue.add(function (
sender: Survey.SurveyModel,
options: { name: string; value?: string }
) {
if (verbs[options.name.toLowerCase()]) {
options.value = verbs[options.name.toLowerCase()][sender.valuesHash.zaimek];
if (options.name[0] == options.name[0].toUpperCase()) {
options.value = [
options.value[0].toUpperCase(),
...options.value.slice(1),
].join('');
}
}
});
setSurvey(survey);
}, []);
if (!survey) {
return <div>Wczytywanie...</div>;
}
return <Survey.Survey model={survey} />;
}
export function _EmailTemplate3Controls({
config,
setConfig,
}: {
config: EmailTemplate3Config;
setConfig: React.Dispatch<React.SetStateAction<EmailTemplate3Config>>;
}): JSX.Element {
return (
<div>
{emailHostSettings(config, setConfig)}
<div>
<label htmlFor="poup_type">Typ okienka o RODO: </label>
<select
id="poup_type"
value={config.popup_type}
onChange={(e) =>
setConfig((v) => ({
...v,
popup_type: e.target.value as EmailTemplate3Config['popup_type'],
}))
}
>
<option value="none">Brak jakiejkolwiek informacji</option>
<option value="passive_cookie_banner">
Pasywne powiadomienie o cookiesach
</option>
<option value="consent">Okienko z pytaniem o zgodę</option>
</select>
</div>
{config.popup_type !== 'none' ? (
<div>
<label htmlFor="popup_screenshot">Zrzut ekranu okienka o RODO:</label>
<input
{...{
type: 'file',
id: 'popup_screenshot',
onChange: async (e) => {
const popup_screenshot_base64 = await toBase64(e.target.files[0]);
setConfig((v) => ({
...v,
popup_screenshot_base64,
}));
},
}}
/>
</div>
) : (
''
)}
<div>
<label htmlFor="popup_action">Czy coś klikn*ł*m w informacjach o RODO?</label>
<select
id="popup_action"
value={config.popup_type}
onChange={(e) =>
setConfig((v) => ({
...v,
popup_action: e.target.value as EmailTemplate3Config['popup_action'],
}))
}
>
<option value="ignored">Nic nie klin*ł*m</option>
<option value="accepted">Kliknięte {config.popup_accept_all_text}</option>
<option value="closed">Zamkn*ł*m okienko (np. przyciskiem "X")</option>
</select>
</div>
{config.popup_action === 'closed' ? (
<div>
<label htmlFor="popup_closed_how">Jak okienko zostało zamknięte? Poprzez</label>
<input
id="popup_closed_how"
type="text"
placeholder="kliknięcie przycisku „X”"
value={config.popup_closed_how}
style={{ width: '300px' }}
onChange={(e) =>
setConfig((v) => ({
...v,
popup_closed_how: e.target.value,
}))
}
/>
</div>
) : (
''
)}
{config.popup_type !== 'none' ? (
<div>
<input
type="checkbox"
id="popup_mentions_passive_consent"
checked={config.popup_mentions_passive_consent}
onChange={(e) =>
setConfig((v) => ({
...v,
popup_mentions_passive_consent: e.target.checked,
}))
}
/>
<label htmlFor="popup_mentions_passive_consent">
okienko wspomina o pasywnej zgodzie (np. korzystając ze strony wyrażasz
zgodę)
</label>
</div>
) : (
''
)}
{config.popup_mentions_passive_consent ? (
<div>
<label htmlFor="popup_passive_consent_text">
Jak okienko próbuje wmówić Ci, że wyrażasz zgodę? Przeklej z treści okienka:
</label>
<input
id="popup_passive_consent_text"
placeholder="Korzystając ze strony wyrażasz zgodę"
value={config.popup_passive_consent_text}
onChange={(e) =>
setConfig((v) => ({
...v,
popup_passive_consent_text: e.target.value,
}))
}
/>
</div>
) : (
''
)}
</div>
);
}

View File

@ -1,27 +0,0 @@
@import '../sidebar/fonts.scss';
@import '../sidebar/colors.scss';
.mail-wrapper {
display: grid;
grid-template-columns: 0.75fr minmax(20rem, 60rem);
height: calc(100vh - 5rem);
background-color: $ultra-light-grey;
}
.mail-content-wrapper {
background-color: $ultra-light-grey;
height: 100%;
display: block;
}
.mail-controls {
padding: 1rem;
background-color: #fff;
}
.mail-content {
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
background-color: #fff;
padding: 2rem 1.75rem;
margin: 1rem;
}

View File

@ -1,144 +0,0 @@
import React from 'react';
import { RequestCluster } from '../request-cluster';
import { StolenDataEntry } from '../stolen-data-entry';
import { getDate } from '../util';
import EmailTemplate3Controls from './email-template-3-controls';
import './email-template-3.scss';
declare var PLUGIN_NAME: string;
declare var PLUGIN_URL: string;
export type EmailTemplate3Config = {
pronoun: 0 | 1 | 2 | 3; // masc, fem, neutral, plural
policy_readable: 'null' | 'yes' | 'entirely_obscured_by_popup' | 'cant_find';
hosts_settings: Record<
string,
{
presence: 'null' | 'not_mentioned' | 'mentioned_in_popup' | 'mentioned_in_policy';
legal_basis_type: 'null' | 'consent' | 'legitimate_interest' | 'not_mentioned';
consent_problems?:
| 'claims_consent_but_sends_before_consent'
| 'claims_consent_but_there_was_no_easy_refuse'
| 'none'
| 'null';
legitimate_interest_activity_specified: 'null' | 'no' | 'precise' | 'vague';
legitimate_interest_activity_description: string;
}
>;
popup_type: 'none' | 'passive_cookie_banner' | 'consent';
popup_action: 'ignored' | 'accepted' | 'closed';
popup_closed_how: string;
popup_screenshot_base64: string | null;
popup_accept_all_text: string;
popup_mentions_passive_consent: boolean;
popup_passive_consent_text: string;
};
export default function EmailTemplate3({
entries,
clusters,
}: {
entries: StolenDataEntry[];
clusters: Record<string, RequestCluster>;
version: number;
}): JSX.Element {
const all_host_ids = Array.from(new Set(entries.map((entry) => entry.request.shorthost)));
const [config, setConfig] = React.useState<EmailTemplate3Config>({
pronoun: Math.round(Math.random()) as 0 | 1,
policy_readable: 'null',
hosts_settings: Object.fromEntries(
all_host_ids.map((cluster_id) => [
cluster_id,
{
presence: 'null',
legal_basis_presence: 'null',
legal_basis_type: 'null',
consent_problems: 'null',
legitimate_interest_activity_specified: 'null',
legitimate_interest_activity_description: '',
},
])
),
popup_type: 'none',
popup_action: 'ignored',
popup_screenshot_base64: null,
popup_accept_all_text: 'Zaakceptuj wszystkie',
popup_mentions_passive_consent: false,
popup_passive_consent_text: '',
popup_closed_how: 'kliknięcie przycisku „X”',
});
const visited_url = entries[0].request.originalURL;
const p = config.pronoun;
return (
<div className="mail-wrapper">
<div className="mail-controls">
<EmailTemplate3Controls {...{ config, setConfig, hosts: all_host_ids }} />
</div>
<div className="mail-content-wrapper">
<article className="mail-content">
<p>
Dzień dobry, w dniu {getDate()}{' '}
{['odwiedziłem', 'odwiedziłam', 'odwiedziłom', 'odwiedziliśmy'][p]} stronę{' '}
{visited_url} i {['zbadałem', 'zbadałam', 'zbadałom', 'zbadaliśmy'][p]}
za pomocą wtyczki <a href={PLUGIN_URL}>{PLUGIN_NAME}</a> w celu zbadania,
jakie informacje o {['mnie', 'mnie', 'mnie', 'nas'][p]} wysyła ta strona do
podmiotów trzecich.
</p>
<p>
{['Moją', 'Moją', 'Moją', 'Naszą'][p]} szczególną uwagę przykuło:
WYFILTROWANE WZGLĘDEM TEGO, CZY DANEGO PODMIOTU NIE MA W POLITYCE
PRYWATNOŚCI LUB POWIADOMIENIU O COOKIESACH{' '}
<ul>
<li>
- wysyłanie mojego identyfikatora internetowego [z Cookie] (value)
oraz części mojej historii przeglądania do właściciela domeny
(domain);
</li>
<li> - (...).</li>
</ul>
</p>
<p>
Dane te zostały wysłane zanim{' '}
{['miałem', 'miałam', 'miałom', 'mieliśmy'][p]} szansę przeczytać Państwa
politykę prywatności i w jakikolwiek czynny i jednoznaczny sposób wyrazić
zgodę na takie przetwarzanie moich danych osobowych.
</p>
{!['yes', 'null'].includes(config.policy_readable) ? (
<p>
{['Chciałem', 'Chciałam', 'Chciałom', 'Chcieliśmy'][p]} przeczytać
Państwa politykę prywatności przed akceptacją, ale{' '}
{config.policy_readable == 'cant_find' ? (
<>nie mogę znaleźć jej nigdzie na Państwa stronie.</>
) : (
''
)}{' '}
{config.policy_readable == 'entirely_obscured_by_popup' ? (
<>jest ona przesłonięta przez okienko o RODO.</>
) : (
''
)}
</p>
) : (
''
)}
<p>
Dane zostały udostępnione podmiotom, o których nie{' '}
{['znalazłem', 'znalazłam', 'znalazłom', 'znaleźliśmy'][p]} informacji ani w
Państwa polityce prywatności, ani w żadnym wyskakującym okienku na Państwa
stronie. Z tego powodu zwracam{p == 3 ? 'y' : ''} się do Państwa z pytaniem:
jakie były podstawy prawne takiego ujawnienia{' '}
{['moich', 'moich', 'moich', 'naszych'][p]} danych osobowych wyżej
wymienionym podmiotom? Uprzejmie{' '}
{['proszę', 'proszę', 'proszę', 'prosimy'][p]} o wskazanie podstawy prawnej
dla każdego z tych podmiotów z osobna.
</p>
</article>
</div>
</div>
);
}

View File

@ -1,19 +0,0 @@
import { RequestCluster } from '../request-cluster';
import { StolenDataEntry } from '../stolen-data-entry';
import EmailTemplate3 from './email-template-3';
export default function EmailTemplate({
entries,
clusters,
version,
}: {
entries: StolenDataEntry[];
clusters: Record<string, RequestCluster>;
version: number;
}) {
return (
<div>
<EmailTemplate3 {...{ entries, clusters, version }} />
</div>
);
}

View File

@ -0,0 +1,314 @@
function generateHostPage(
host: string,
index: number,
all_hosts: string[]
): { title: string; elements: any[] } {
function f(name: string, h = host) {
return `${h.replace(/\./g, '_')}|${name}`;
}
const previous_host: string | null = index > 0 ? all_hosts[index - 1] : null;
function defaultValue(name: string) {
if (!previous_host) {
return {};
}
return { defaultValueExpression: `{${f(name, previous_host)}}` };
}
return {
title: host,
elements: [
{
type: 'radiogroup',
name: f('present'),
isRequired: true,
title: `Cel ujawnienia danych właścicielowi domeny ${host}`,
...defaultValue('present'),
choices: [
{
value: 'not_mentioned',
text: 'nie jest podany nigdzie na stronie',
visibleIf: "{policy_readable} = 'yes' ",
},
{
value: 'not_before_making_a_choice',
text: 'nie jest podany w żadnym miejscu na stronie, do którego można się dostać bez podejmowania wyboru dotyczącego przetwarzania danych osobowych',
},
{
value: 'mentioned_in_policy',
text: 'jest podany w polityce prywatności',
visibleIf: "{policy_readable} = 'yes' ",
},
{
value: 'mentioned_in_popup',
text: 'jest podany w okienku RODO',
visibleIf: "{popup_type} != 'none' ",
},
],
},
{
type: 'radiogroup',
name: f('legal_basis_type'),
...defaultValue('legal_basis_type'),
isRequired: true,
title: `Podstawa prawna dla tego konkretnego celu`,
visibleIf: `{${f('present')}} notempty and {${f(
'present'
)}} != "not_mentioned" and {${f('present')}} != "not_before_making_a_choice"`,
choices: [
{ value: 'consent', text: 'to zgoda.' },
{
value: 'legitimate_interest',
text: 'to uzasadniony interes.',
},
{ value: 'not_mentioned', text: 'nie jest wskazana nigdzie na stronie.' },
],
},
{
type: 'radiogroup',
name: f('consent_problems'),
...defaultValue('consent_problems'),
isRequired: true,
title: `Jak ma się ta podstawa prawna do stanu faktycznego?`,
visibleIf: `{${f('legal_basis_type')}} = "consent"`,
choices: [
{
value: 'claims_consent_but_sends_before_consent',
text: `Strona wysłała {moje} dane do ${host} zanim {wyraziłem} na to zgodę`,
},
{
value: 'claims_consent_but_there_was_no_easy_refuse',
text: '{Kliknąłem} przycisk od wyrażania zgody, ale w okienku o zgodę nie było natychmiastowo dostępnego przycisku do niewyrażenia zgody jednym kliknięciem',
},
{ value: 'none', text: 'żadne z powyższych.' },
],
},
{
type: 'dropdown',
name: f('legitimate_interest_activity_specified'),
...defaultValue('legitimate_interest_activity_specified'),
isRequired: true,
title: /* HTML */ `Czy administrator strony opisał szczegółowo, na czym polega
uzasadniony interes w kontekście tego celu?`,
visibleIf: `{${f('legal_basis_type')}} = "legitimate_interest"`,
choices: [
{
value: 'precise',
text: /* HTML */ `Tak, wskazuje jasno na bieżące działania lub korzyści
wynikające z takiego przetwarzania danych.`,
},
{
value: 'vague',
text: `Wskazuje tylko ogólnie, jak np. „marketing” czy „statystyki”.`,
},
{
value: 'no',
text: `Nie. Nie wiadomo, na czym ten uzasadniony interes polega.`,
},
],
},
{
type: 'text',
title: `Jak administrator opisał to, na czym polega uzasadniony interes w kontekście ${host}?`,
name: f('legitimate_interest_description'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = 'vague'`,
placeholder: 'marketing',
defaultValueExpression:
index == 0
? 'marketing'
: `{${f('legitimate_interest_description', previous_host)}}`,
},
{
type: 'dropdown',
title: `Czy domena ${host} należy do podmiotu spoza Europy (np. Google, Facebook)?`,
name: f('outside_eu'),
...defaultValue('outside_eu'),
visibleIf: `{${f('legitimate_interest_activity_specified')}} = "precise" or {${f(
'consent_problems'
)}} = "none"`,
choices: [
{ value: 'yes', text: 'Tak' },
{ value: 'no', text: 'Nie' },
{ value: 'not_sure', text: 'Nie wiem' },
],
},
],
};
}
export default function generateSurveyQuestions(hosts: string[]) {
return {
showQuestionNumbers: 'off',
showProgressBar: 'top',
clearInvisibleValues: 'onHidden',
pages: [
{
title: 'Zaimki',
elements: [
{
type: 'radiogroup',
name: 'zaimek',
title: 'Forma czasownika:',
isRequired: true,
choices: [
{ value: 0, text: 'Wysłałem' },
{ value: 1, text: 'Wysłałam' },
{ value: 2, text: 'Wysłałom' },
{ value: 3, text: 'Wysłaliśmy' },
],
},
],
},
{
title: 'Obowiązek informacyjny i machanizm pozyskiwania zgody',
elements: [
{
type: 'radiogroup',
title: 'Jaką formę informacji o przetwarzaniu danych osobowych stosuje ta strona?',
name: 'popup_type',
isRequired: true,
choices: [
{ value: 'none', text: 'Brak informacji' },
{
value: 'passive_popup',
text: /* HTML */ `Okienko o cookiesach, bez możliwości podjęcia
żadnego wyboru (np. tylko opcja zamknij)`,
},
{
value: 'some_choice',
text: 'Okienko o cookiesach, z możliwością podjęcia wyboru',
},
],
},
{
type: 'checkbox',
title: /* HTML */ `Istnieje możliwość, że okienko z informacjami i wyborami
dotyczącymi przetwarzania {Twoich} danych osobowych ukazało się dawno temu w
trakcie {twojej} wcześniejszej wizyty i wtedy je {odkliknąłeś}. {Otwórz}
samą stronę w Trybie Prywatnym (Incognito). Co {widzisz}?`,
visibleIf: "{popup_type} = 'none'",
name: 'is_incognito_different',
isRequired: true,
choices: [
{
value: 'incognito_is_the_same',
text: 'W Trybie prywatnym {widzę} to samo, co {widziałem} w normalnym trybie',
},
],
},
{
type: 'html',
visibleIf: '{is_incognito_different} != "no" and {popup_type} = "none"',
html: /* HTML */ `Jeżeli w trybie incognito widzisz więcej okienek z
informacjami o przetwarzaniu danych osobowych, wykonaj analizę w
normalnym trybie ponownie - ale najpierw usuń pliki cookies tej strony.
<a
href="https://support.mozilla.org/pl/kb/usuwanie-ciasteczek-i-danych-stron-firefox?redirectslug=usuwanie-ciasteczek&redirectlocale=pl"
target="_blank"
>
Zobacz, jak to zrobić
</a>`,
},
{
type: 'radiogroup',
name: 'mentions_passive_consent',
isRequired: true,
visibleIf: '{popup_type} = "passive_popup"',
title: 'Czy treść okienka wskazuje na zgodę wyrażoną pasywnie, np. „Korzystając z naszej strony wyrażasz zgodę” lub „Brak zmiany ustawień przeglądarki oznacza zgodę”?',
choices: [
{
value: 'yes',
text: 'Tak',
},
{
value: 'no',
text: 'Nie',
},
],
},
{
type: 'text',
name: 'passive_consent_description',
isRequired: true,
visibleIf: '{mentions_passive_consent} = "yes"',
title: 'Jakimi słowami administrator opisuje to pasywne wyrażenie zgody? Zacytuj wprost',
defaultValue: 'Korzystając ze strony wyrażasz zgodę',
},
{
type: 'radiogroup',
name: 'cookie_wall',
isRequired: true,
visibleIf: '{popup_type} = "passive_popup"',
title: 'Czy treść strony jest wygodnie czytelna bez odkliknięcia tego okienka o RODO?',
choices: [
{
value: 'no', // wiem, że tu jest "no", a odpowiedź brzmi "tak" - ale nazwa pytania dotyczy obecności cookie walla
text: 'Tak, jest czytelna',
},
{
value: 'yes',
text: 'Nie. Jest zupełnie niewidoczna albo jest przesłonięta w stopniu uniemożliwiającym lub znacznie utrudniającym czytanie treści strony.',
},
],
},
{
type: 'radiogroup',
name: 'rejection_is_hard',
isRequired: true,
visibleIf: '{popup_type} = "some_choice"',
title: 'Czy wyrażenie zgody na wszystkie cele jest dokładnie tak samo łatwe, jak odmowa zgody na wszystkie cele?',
choices: [
{
value: 'no', // wiem, że tu jest "no", a odpowiedź brzmi "tak" - ale nazwa pytania dotyczy braku równowagi
text: 'Tak. Opcja odmowy zgody na wszystkie cele jest równie widoczna i łatwo dostępna, co opcja wyrażenia zgody.',
},
{
value: 'yes',
text: 'Nie. Muszę wykonać więcej czynności aby odmówić wszystkich zgód, albo opcja niewyrażenia zgody jest mało widoczna.',
},
],
},
{
type: 'radiogroup',
name: 'administrator_identity_available_before_choice',
isRequired: true,
visibleIf: '{popup_type} = "some_choice"',
title: 'Czy przed podjęciem wyboru dot. {Twoich} danych masz możliwość poznać tożsamość administratora strony?',
choices: [
{
value: 'yes',
text: 'Tak.',
},
{
value: 'no',
text: 'Nie.',
},
],
},
],
},
{
title: 'Obowiązek informacyjny, polityka prywatności',
elements: [
{
type: 'radiogroup',
title: 'Czy polityka prywatności jest dostępna i czytelna?',
name: 'policy_readable',
isRequired: true,
choices: [
{ value: 'yes', text: 'dostępna i czytelna' },
{
value: 'entirely_obscured_by_popup',
text: 'dostępna, ale nieczytelna. Zasłania ją całkowicie lub prawie całkowicie popup o RODO lub nie można się do niej doklikać bez podjęcia wyboru w okienku',
},
{
value: 'cant_find',
text: `Niedostępna. {Szukałem}, ale nie {znalazłem} jej na stronie`,
},
],
},
],
},
...hosts.map(generateHostPage),
],
};
}

View File

View File

@ -1,65 +0,0 @@
import React from 'react';
import { setHostSetting } from './email-host-settings';
import { EmailTemplate3Config } from './email-template-3';
export default function LegitimateInteresProblems({
settings,
host_id,
pronoun,
setConfig,
}: {
host_id: string;
settings: EmailTemplate3Config['hosts_settings'][string];
setConfig: React.Dispatch<React.SetStateAction<EmailTemplate3Config>>;
}) {
if (settings.legal_basis_type !== 'legitimate_interest') {
return '';
}
const [description, setDescription] = React.useState('marketing');
return (
<div>
<div>
Czy administrator strony opisał szczegółowo, na czym polega uzasadniony interes w
kontekście tego celu?
{/* {hostSettingsDropdown({
settings,
host_id,
setConfig,
field: 'legitimate_interest_activity_specified' as const,
value: settings.legitimate_interest_activity_specified,
options: {
precise: [
'Tak, wskazuje jasno na bieżące działania lub korzyści wynikające z takiego przetwarzania danych.',
],
vague: ['Wskazuje tylko ogólnie, jak np. „marketing” czy „statystyki”.'],
no: ['Nie. Nie wiadomo, na czym ten uzasadniony interes polega.'],
},
})} */}
</div>
{settings.legitimate_interest_activity_specified === 'vague' ? (
<div>
Jak administrator opisał to, na czym polega uzasadniony interes w kontekście{' '}
{host_id}?{' '}
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button
onClick={(e) =>
setHostSetting(
setConfig,
[host_id, 'legitimate_interest_activity_description'],
description
)
}
>
zatwierdź
</button>
</div>
) : (
''
)}
</div>
);
}

View File

@ -0,0 +1,83 @@
import RawAnswers, { BasicRawAnswers, HostRawAnswers } from './raw-answers';
export type RecordValue<T> = T extends Record<any, infer R> ? R : any;
export type ParsedHostAnswers =
| {
present: 'not_mentioned' | 'not_before_making_a_choice';
}
| ({
present: 'mentioned_in_policy' | 'mentioned_in_popup';
} & (
| ({
legal_basis_type: 'consent';
} & (
| {
consent_problems:
| 'claims_consent_but_sends_before_consent'
| 'claims_consent_but_there_was_no_easy_refuse';
}
| { consent_problems: 'none'; outside_eu: 'yes' | 'no' | 'not_sure' }
))
| ({
legal_basis_type: 'legitimate_interest';
} & (
| {
legitimate_interest_activity_specified: 'no';
}
| {
legitimate_interest_activity_specified: 'precise';
outside_eu: 'yes' | 'no' | 'not_sure';
}
| {
legitimate_interest_activity_specified: 'vague';
legitimate_interest_description: string;
}
))
| {
legal_basis_type: 'not_mentioned';
}
));
export type ParsedAnswers = BasicRawAnswers & { hosts: Record<string, ParsedHostAnswers> };
function parseHostAnswers(
raw_answers: Record<keyof HostRawAnswers, string>
): Record<string, ParsedHostAnswers> {
const result: Record<string, Record<string, string>> = {};
for (const [key, value] of Object.entries(raw_answers)) {
const [masked_host, attr] = key.split('|');
const host = masked_host.replace(/_/g, '.');
if (!result[host]) {
result[host] = {} as ParsedHostAnswers;
}
result[host][attr] = value;
}
return result as Record<string, ParsedHostAnswers>;
}
export function parseAnswers({
zaimek,
is_incognito_different,
policy_readable,
popup_type,
cookie_wall,
passive_consent_description,
mentions_passive_consent,
rejection_is_hard,
administrator_identity_available_before_choice,
...rest
}: RawAnswers): ParsedAnswers {
return {
zaimek,
is_incognito_different,
policy_readable,
popup_type,
cookie_wall,
passive_consent_description,
mentions_passive_consent,
rejection_is_hard,
administrator_identity_available_before_choice,
hosts: parseHostAnswers(rest),
} as RawAnswers;
}

View File

@ -0,0 +1,19 @@
import * as Survey from 'survey-react';
import RawAnswers from './raw-answers';
import useSurvey from './use-survey';
export default function Questions({
hosts,
onComplete,
}: {
hosts: string[];
onComplete: (data: RawAnswers) => void;
}) {
const survey = useSurvey(hosts, {
onComplete: (sender) => onComplete(sender.data),
});
if (!survey) {
return <div>Wczytywanie....</div>;
}
return <Survey.Survey model={survey} />;
}

View File

@ -0,0 +1,57 @@
export type HostRawAnswers = {
[key: `${string}|present`]:
| 'not_mentioned'
| 'not_before_making_a_choice'
| 'mentioned_in_policy'
| 'mentioned_in_popup';
[key: `${string}|legal_basis_type`]: 'consent' | 'legitimate_interest' | 'not_mentioned';
[key: `${string}|consent`]:
| 'claims_consent_but_sends_before_consent'
| 'claims_consent_but_there_was_no_easy_refuse'
| 'none';
[key: `${string}|legitimate_interest_activity_specified`]: 'precise' | 'vague' | 'no';
[key: `${string}|legitimate_interest_description`]: string;
[key: `${string}|outside_eu`]: 'yes' | 'no' | 'not_sure';
};
export type BasicRawAnswers = {
zaimek: 0 | 1 | 2 | 3;
is_incognito_different: [] | ['incognito_is_the_same'];
policy_readable: 'yes' | 'vague' | 'cant_find';
} & (
| ({
popup_type: 'passive_popup';
cookie_wall: 'yes' | 'no';
rejection_is_hard: undefined;
administrator_identity_available_before_choice: undefined;
} & (
| {
mentions_passive_consent?: 'yes';
passive_consent_description: string;
}
| {
mentions_passive_consent?: 'no';
passive_consent_description: undefined;
}
))
| {
popup_type: 'some_choice';
rejection_is_hard: 'yes' | 'no';
cookie_wall: undefined;
passive_consent_description: undefined;
mentions_passive_consent: undefined;
administrator_identity_available_before_choice: 'yes' | 'no';
}
| {
popup_type: 'none';
cookie_wall: undefined;
passive_consent_description: undefined;
mentions_passive_consent: undefined;
rejection_is_hard: undefined;
administrator_identity_available_before_choice: undefined;
}
);
type RawAnswers = BasicRawAnswers & HostRawAnswers;
export default RawAnswers;

View File

@ -1,40 +1,53 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { getMemory } from '../memory';
import { StolenDataEntry } from '../stolen-data-entry';
import { reduceConcat, useEmitter } from '../util';
import EmailTemplate from './email-template';
import { useEmitter } from '../util';
import './report-window.scss';
import Questions from './questions';
import EmailContent from './email-content';
import { parseAnswers, ParsedAnswers } from './parse-answers';
function Report() {
try {
const origin = new URL(document.location.toString()).searchParams.get('origin');
const [counter, setCounter] = useEmitter(getMemory());
const [counter] = useEmitter(getMemory());
const [answers, setAnswers] = React.useState<ParsedAnswers>(null);
const [mode, setMode] = React.useState('survey');
const clusters = getMemory().getClustersForOrigin(origin);
const [entries, setEntries] = React.useState<StolenDataEntry[]>([]);
React.useEffect(() => {
setEntries(
Object.values(clusters)
.map((cluster) => {
cluster.calculateRepresentativeStolenData();
return cluster.representativeStolenData;
})
.reduce(reduceConcat, [])
.filter((entry) => entry.isMarked)
);
}, []);
if (entries.length == 0) {
return <>Wczytywanie...</>;
}
/* const [entries, setEntries] = React.useState<StolenDataEntry[]>([]); */
/* React.useEffect(() => {
* setEntries(
* Object.values(clusters)
* .map((cluster) => {
* cluster.calculateRepresentativeStolenData();
* return cluster.representativeStolenData;
* })
* .reduce(reduceConcat, [])
* .filter((entry) => entry.isMarked)
* );
* }, []); */
/* if (entries.length == 0) {
* return <>Wczytywanie...</>;
* } */
const result = (
<div {...{ 'data-version': counter }}>
<nav>
<img src="../assets/icon-addon.svg" width={48} height={48}></img>{' '}
<h1>Rentgen - Generuj treść maila dla {origin}</h1>
</nav>
<EmailTemplate {...{ entries, clusters, version: counter }} />
{mode === 'survey' ? (
<Questions
hosts={Object.keys(clusters)}
onComplete={(answers) => {
setAnswers(parseAnswers(answers));
setMode('preview');
}}
></Questions>
) : (
''
)}
{mode === 'preview' ? <EmailContent {...{ answers }} /> : ''}
{/* <HARConverter {...{ entries }} /> */}
</div>
);

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import * as Survey from 'survey-react';
import generateSurveyQuestions from './generate-survey-questions';
import RawAnswers from './raw-answers';
import verbs from './verbs';
export default function useSurvey(
hosts: string[],
{ onComplete }: { onComplete: (sender: { data: RawAnswers }) => void }
): Survey.ReactSurveyModel {
const [survey, setSurvey] = React.useState<Survey.Model>(null);
React.useEffect(() => {
const model = generateSurveyQuestions(hosts);
const survey = new Survey.Model(model);
survey.onProcessTextValue.add(function (
sender: Survey.SurveyModel,
options: { name: string; value?: string }
) {
if (verbs[options.name.toLowerCase()]) {
options.value = verbs[options.name.toLowerCase()][sender.valuesHash.zaimek];
if (options.name[0] == options.name[0].toUpperCase()) {
options.value = [
options.value[0].toUpperCase(),
...options.value.slice(1),
].join('');
}
}
});
survey.onComplete.add(onComplete);
setSurvey(survey);
}, []);
return survey;
}

View File

@ -157,7 +157,7 @@ export default function StolenDataCluster({
<thead>
<tr>
<th className="table-header" colSpan={4}>
Znalezione ustawienia:
Wysłane dane:
</th>
</tr>
</thead>