Compare commits

..

No commits in common. "592122a7da3d1692865ec6f7970d76f4d7ec6607" and "647095ad9ec4e5f5b68cb4506d52a8a2baa8f5cb" have entirely different histories.

83 changed files with 5115 additions and 3919 deletions

2
.gitignore vendored
View File

@ -3,5 +3,3 @@ node_modules
sidebar.js sidebar.js
/web-ext-artifacts/ /web-ext-artifacts/
lib/* lib/*
/yarn-error.log
/rentgen.zip

View File

@ -1,5 +1,5 @@
trailingComma: "es5" trailingComma: "es5"
tabWidth: 4 tabWidth: 3
printWidth: 100 printWidth: 100
semi: true semi: true
singleQuote: true singleQuote: true

View File

@ -15,3 +15,6 @@ Note: At the moment, we support Polish language because this extension generates
3. `npm run build` 3. `npm run build`
4. The build code is located in the `lib/` directory 4. The build code is located in the `lib/` directory
![screenshot](./screenshot.png)
![screenshot](./screenshot2.png)
![screenshot](./screenshot3.png)

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 22H9V20H15V22ZM15 19H9L8.777 17C8.65703 16.3385 8.45863 15.6936 8.186 15.079C7.832 14.579 7.463 14.152 7.106 13.735C5.79411 12.5053 5.03465 10.7978 5 9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9C18.9593 10.7868 18.2057 12.4831 16.907 13.711L16.89 13.731C16.534 14.148 16.166 14.58 15.819 15.075C15.5466 15.6912 15.3476 16.3373 15.226 17L15 19ZM12 4C9.23995 4.00331 7.00331 6.23995 7 9C7 10.544 7.644 11.293 8.618 12.428C8.988 12.86 9.408 13.348 9.818 13.919C10.3156 14.8858 10.6555 15.9259 10.825 17H13.176C13.3499 15.929 13.6892 14.8916 14.182 13.925C14.582 13.354 15.001 12.863 15.37 12.431L15.385 12.413C16.357 11.273 17 10.52 17 9C16.9967 6.23995 14.7601 4.00331 12 4Z" fill="#000000"/>
</svg>

Before

Width:  |  Height:  |  Size: 814 B

View File

@ -11,14 +11,9 @@
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
> <defs
<style> id="defs831" />
path:target {
fill: #ff726b !important;
}
</style>
<defs id="defs831" />
<sodipodi:namedview <sodipodi:namedview
id="namedview829" id="namedview829"
pagecolor="#ffffff" pagecolor="#ffffff"
@ -36,12 +31,10 @@
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg827" inkscape:current-layer="svg827" />
/>
<path <path
d="M11.9837 21.9999C6.47237 21.9938 2.00605 17.5203 2 11.9999C2.39311 12.1112 2.79955 12.168 3.20803 12.1689C4.55933 12.1789 5.82888 11.5217 6.6025 10.412C7.29413 9.41154 7.44027 8.13091 6.99186 6.99997C7.27858 7.05119 7.5692 7.07729 7.86045 7.07797C9.1552 7.08764 10.3841 6.50698 11.2 5.49998C11.9984 4.52274 12.3106 3.2352 12.0486 2C17.5625 2.01795 22.0178 6.50963 21.9999 12.0324C21.982 17.5553 17.4976 22.0178 11.9837 21.9999ZM12.7455 18.5679C12.8991 18.634 13.0645 18.6681 13.2317 18.6679C13.7362 18.6674 14.1909 18.363 14.3842 17.8961C14.5775 17.4293 14.4714 16.8919 14.1152 16.5339C13.8805 16.2998 13.5629 16.1683 13.2317 16.1679C12.6392 16.1693 12.1294 16.5877 12.0115 17.1693C11.8937 17.7509 12.2004 18.3353 12.7455 18.5679ZM6.91199 16.8749C7.11205 16.9578 7.32647 17.0003 7.54296 16.9999C8.21842 16.9997 8.82678 16.5907 9.08272 15.9646C9.33866 15.3385 9.19143 14.6195 8.71006 14.1449C8.3989 13.838 7.97969 13.6659 7.54296 13.6659C6.62442 13.667 5.88022 14.4129 5.87967 15.3329C5.87984 16.0017 6.2781 16.606 6.89202 16.8689H6.89702H6.908L6.91199 16.8749ZM16.3276 14.3679C16.9987 14.6502 17.775 14.4627 18.2439 13.9051C18.7127 13.3475 18.7652 12.5493 18.3733 11.9349C18.2506 11.7436 18.0902 11.5793 17.9021 11.4519C17.3853 11.1024 16.7167 11.0716 16.17 11.3721C15.6233 11.6726 15.2902 12.254 15.307 12.8784C15.3238 13.5028 15.6877 14.0654 16.2498 14.3359H16.2378L16.2677 14.3489L16.2877 14.3569H16.2817C16.296 14.3615 16.31 14.3669 16.3236 14.3729L16.3276 14.3679ZM11.9837 10.333C11.5855 10.3323 11.2426 10.6141 11.1655 11.0054C11.0883 11.3967 11.2986 11.7879 11.6672 11.9389C12.0357 12.0899 12.4594 11.9583 12.6782 11.625C12.8969 11.2917 12.8493 10.8499 12.5648 10.571C12.4906 10.4978 12.4032 10.4394 12.3072 10.399L12.2892 10.391L12.2623 10.381C12.1729 10.349 12.0786 10.3328 11.9837 10.333ZM15.7276 6.16697C15.0888 6.16539 14.5526 6.64873 14.4871 7.28522C14.4216 7.92172 14.8481 8.50444 15.4738 8.63339C16.0995 8.76235 16.7211 8.39562 16.9118 7.78494C17.1025 7.17425 16.8004 6.51814 16.2128 6.26698H16.2028C16.0525 6.20267 15.8911 6.16869 15.7276 6.16697ZM4.49593 9.49996C4.03663 9.49996 3.66429 9.12701 3.66429 8.66696C3.66429 8.20691 4.03663 7.83397 4.49593 7.83397C4.95523 7.83397 5.32757 8.20691 5.32757 8.66696C5.32702 9.12679 4.95501 9.49941 4.49593 9.49996ZM3.24797 6.99997C2.55873 6.99997 2 6.44033 2 5.74998C2 5.05963 2.55873 4.49999 3.24797 4.49999C3.9372 4.49999 4.49593 5.05963 4.49593 5.74998C4.49483 6.43988 3.93674 6.99887 3.24797 6.99997ZM8.23983 5.33298C7.55059 5.33298 6.99186 4.77334 6.99186 4.08299C6.99186 3.39264 7.55059 2.833 8.23983 2.833C8.92906 2.833 9.48779 3.39264 9.48779 4.08299C9.48779 4.7735 8.92922 5.33343 8.23983 5.33398V5.33298ZM5.32857 3.66699C4.8694 3.66699 4.49711 3.29425 4.49693 2.83433C4.49675 2.37441 4.86873 2.00137 5.32791 2.001C5.78708 2.00063 6.15967 2.37308 6.16022 2.833C6.16022 3.29321 5.78804 3.66644 5.32857 3.66699Z" d="M11.9837 21.9999C6.47237 21.9938 2.00605 17.5203 2 11.9999C2.39311 12.1112 2.79955 12.168 3.20803 12.1689C4.55933 12.1789 5.82888 11.5217 6.6025 10.412C7.29413 9.41154 7.44027 8.13091 6.99186 6.99997C7.27858 7.05119 7.5692 7.07729 7.86045 7.07797C9.1552 7.08764 10.3841 6.50698 11.2 5.49998C11.9984 4.52274 12.3106 3.2352 12.0486 2C17.5625 2.01795 22.0178 6.50963 21.9999 12.0324C21.982 17.5553 17.4976 22.0178 11.9837 21.9999ZM12.7455 18.5679C12.8991 18.634 13.0645 18.6681 13.2317 18.6679C13.7362 18.6674 14.1909 18.363 14.3842 17.8961C14.5775 17.4293 14.4714 16.8919 14.1152 16.5339C13.8805 16.2998 13.5629 16.1683 13.2317 16.1679C12.6392 16.1693 12.1294 16.5877 12.0115 17.1693C11.8937 17.7509 12.2004 18.3353 12.7455 18.5679ZM6.91199 16.8749C7.11205 16.9578 7.32647 17.0003 7.54296 16.9999C8.21842 16.9997 8.82678 16.5907 9.08272 15.9646C9.33866 15.3385 9.19143 14.6195 8.71006 14.1449C8.3989 13.838 7.97969 13.6659 7.54296 13.6659C6.62442 13.667 5.88022 14.4129 5.87967 15.3329C5.87984 16.0017 6.2781 16.606 6.89202 16.8689H6.89702H6.908L6.91199 16.8749ZM16.3276 14.3679C16.9987 14.6502 17.775 14.4627 18.2439 13.9051C18.7127 13.3475 18.7652 12.5493 18.3733 11.9349C18.2506 11.7436 18.0902 11.5793 17.9021 11.4519C17.3853 11.1024 16.7167 11.0716 16.17 11.3721C15.6233 11.6726 15.2902 12.254 15.307 12.8784C15.3238 13.5028 15.6877 14.0654 16.2498 14.3359H16.2378L16.2677 14.3489L16.2877 14.3569H16.2817C16.296 14.3615 16.31 14.3669 16.3236 14.3729L16.3276 14.3679ZM11.9837 10.333C11.5855 10.3323 11.2426 10.6141 11.1655 11.0054C11.0883 11.3967 11.2986 11.7879 11.6672 11.9389C12.0357 12.0899 12.4594 11.9583 12.6782 11.625C12.8969 11.2917 12.8493 10.8499 12.5648 10.571C12.4906 10.4978 12.4032 10.4394 12.3072 10.399L12.2892 10.391L12.2623 10.381C12.1729 10.349 12.0786 10.3328 11.9837 10.333ZM15.7276 6.16697C15.0888 6.16539 14.5526 6.64873 14.4871 7.28522C14.4216 7.92172 14.8481 8.50444 15.4738 8.63339C16.0995 8.76235 16.7211 8.39562 16.9118 7.78494C17.1025 7.17425 16.8004 6.51814 16.2128 6.26698H16.2028C16.0525 6.20267 15.8911 6.16869 15.7276 6.16697ZM4.49593 9.49996C4.03663 9.49996 3.66429 9.12701 3.66429 8.66696C3.66429 8.20691 4.03663 7.83397 4.49593 7.83397C4.95523 7.83397 5.32757 8.20691 5.32757 8.66696C5.32702 9.12679 4.95501 9.49941 4.49593 9.49996ZM3.24797 6.99997C2.55873 6.99997 2 6.44033 2 5.74998C2 5.05963 2.55873 4.49999 3.24797 4.49999C3.9372 4.49999 4.49593 5.05963 4.49593 5.74998C4.49483 6.43988 3.93674 6.99887 3.24797 6.99997ZM8.23983 5.33298C7.55059 5.33298 6.99186 4.77334 6.99186 4.08299C6.99186 3.39264 7.55059 2.833 8.23983 2.833C8.92906 2.833 9.48779 3.39264 9.48779 4.08299C9.48779 4.7735 8.92922 5.33343 8.23983 5.33398V5.33298ZM5.32857 3.66699C4.8694 3.66699 4.49711 3.29425 4.49693 2.83433C4.49675 2.37441 4.86873 2.00137 5.32791 2.001C5.78708 2.00063 6.15967 2.37308 6.16022 2.833C6.16022 3.29321 5.78804 3.66644 5.32857 3.66699Z"
fill="#2E3A59" fill="#2E3A59"
id="color" id="path825"
style="fill:#000000" style="fill:#000000" />
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 22H6C4.89543 22 4 21.1046 4 20V4.00001C4 2.89544 4.89543 2.00001 6 2.00001H13C13.2654 1.99907 13.5201 2.10462 13.707 2.29301L19.707 8.29301C19.8954 8.47994 20.0009 8.73462 20 9.00001V20C20 21.1046 19.1046 22 18 22ZM6 4.00001V20H16.586L14.02 17.434C13.4101 17.8017 12.7121 17.9973 12 18C10.1612 18.0199 8.54049 16.7967 8.05545 15.0229C7.57041 13.2491 8.34318 11.3714 9.93625 10.4529C11.5293 9.53434 13.5415 9.80626 14.8337 11.1147C16.1258 12.4231 16.3724 14.4386 15.434 16.02L18 18.588V9.41401L12.586 4.00001H6ZM12 12C10.8954 12 10 12.8954 10 14C10 15.1046 10.8954 16 12 16C13.1046 16 14 15.1046 14 14C14 12.8954 13.1046 12 12 12Z" fill="#000000"/>
</svg>

Before

Width:  |  Height:  |  Size: 764 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 22H6C4.89543 22 4 21.1046 4 20V4C4 2.89543 4.89543 2 6 2H13C13.0109 2.00047 13.0217 2.00249 13.032 2.006C13.0418 2.00902 13.0518 2.01103 13.062 2.012C13.1502 2.01765 13.2373 2.0348 13.321 2.063L13.349 2.072C13.3717 2.07968 13.3937 2.08904 13.415 2.1C13.5239 2.14842 13.6232 2.21618 13.708 2.3L19.708 8.3C19.7918 8.38479 19.8596 8.48406 19.908 8.593C19.918 8.615 19.925 8.638 19.933 8.661L19.942 8.687C19.9699 8.77039 19.9864 8.85718 19.991 8.945C19.9926 8.95418 19.9949 8.96322 19.998 8.972C19.9998 8.98122 20.0004 8.99062 20.0001 9V20C20.0001 21.1046 19.1046 22 18 22ZM6 4V20H18V10H13C12.4477 10 12 9.55228 12 9V4H6ZM14 5.414V8H16.586L14 5.414ZM15 16H9V14H15V16Z" fill="#000000"/>
</svg>

Before

Width:  |  Height:  |  Size: 798 B

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg135"
sodipodi:docname="laptop.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs139" />
<sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="26.634355"
inkscape:cx="11.958239"
inkscape:cy="12.990741"
inkscape:window-width="1920"
inkscape:window-height="1024"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" />
<path
d="M21 19H3C1.89543 19 1 18.1046 1 17V16H3V7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V16H23V17C23 18.1046 22.1046 19 21 19ZM5 7V16H19V7H5Z"
fill="#2E3A59"
id="path133"
style="fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg4"
sodipodi:docname="report.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="37.666667"
inkscape:cx="12.013274"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="1024"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M13.66 4.3C13.5649 3.83433 13.1553 3.5 12.68 3.5H5.5C4.94772 3.5 4.5 3.94772 4.5 4.5V19.5C4.5 20.0523 4.94772 20.5 5.5 20.5C6.05228 20.5 6.5 20.0523 6.5 19.5V13.5H12.1L12.34 14.7C12.4307 15.1683 12.8431 15.5048 13.32 15.5H18.5C19.0523 15.5 19.5 15.0523 19.5 14.5V6.5C19.5 5.94772 19.0523 5.5 18.5 5.5H13.9L13.66 4.3Z"
fill="#2E3A59"
id="path2"
style="fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -11,14 +11,9 @@
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
> <defs
<style> id="defs1286" />
path:target {
fill: #ffb900 !important;
}
</style>
<defs id="defs1286" />
<sodipodi:namedview <sodipodi:namedview
id="namedview1284" id="namedview1284"
pagecolor="#ffffff" pagecolor="#ffffff"
@ -36,12 +31,10 @@
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg1282" inkscape:current-layer="svg1282" />
/>
<path <path
d="M21.2659 20.998H2.73288C2.37562 20.998 2.04551 20.8074 1.86688 20.498C1.68825 20.1886 1.68825 19.8074 1.86688 19.498L11.1329 3.49799C11.3117 3.1891 11.6415 2.9989 11.9984 2.9989C12.3553 2.9989 12.6851 3.1891 12.8639 3.49799L22.1299 19.498C22.3084 19.8072 22.3085 20.1882 22.1301 20.4975C21.9518 20.8069 21.622 20.9976 21.2649 20.998H21.2659ZM10.9999 15.998V17.998H11.9329H11.9979H12.0629H12.9979V15.998H10.9999ZM10.9999 8.99799V13.998H12.9999V8.99799H10.9999Z" d="M21.2659 20.998H2.73288C2.37562 20.998 2.04551 20.8074 1.86688 20.498C1.68825 20.1886 1.68825 19.8074 1.86688 19.498L11.1329 3.49799C11.3117 3.1891 11.6415 2.9989 11.9984 2.9989C12.3553 2.9989 12.6851 3.1891 12.8639 3.49799L22.1299 19.498C22.3084 19.8072 22.3085 20.1882 22.1301 20.4975C21.9518 20.8069 21.622 20.9976 21.2649 20.998H21.2659ZM10.9999 15.998V17.998H11.9329H11.9979H12.0629H12.9979V15.998H10.9999ZM10.9999 8.99799V13.998H12.9999V8.99799H10.9999Z"
fill="#2E3A59" fill="#2E3A59"
id="color" id="path1280"
style="fill:#000000" style="fill:#000000" />
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,19 +0,0 @@
import { RequestCluster } from '../../request-cluster';
import { ParsedAnswers } from './parse-answers';
import NoInformationAtAllProblem from './problems/no-information-at-all';
import { Problem } from './problems/problem';
import { UnlawfulCookieAccess } from './problems/unlawful-cookies';
export default function deduceProblems(
answers: ParsedAnswers,
clusters: Record<string, RequestCluster>
): Problem[] {
const problems = [];
if (answers.popup_type === 'none') {
problems.push(new NoInformationAtAllProblem(answers, clusters));
}
if (UnlawfulCookieAccess.qualifies(answers, Object.values(clusters))) {
problems.push(new UnlawfulCookieAccess(answers, clusters));
}
return problems;
}

View File

@ -1,55 +0,0 @@
@import './../../styles/colors.scss';
h1 {
font-size: 1.1rem;
margin-bottom: calc(24 / 16 * 1rem);
}
.generator-container {
max-width: 100ex;
margin: 0 auto;
font-size: calc(14 / 16 * 1rem);
}
.mail-container {
box-shadow: rgba(12, 12, 13, 0.1) 0px 1px 4px 0px;
background-color: #fff;
margin-top: 0.5rem;
max-width: 100ex;
margin: 0 auto;
font-size: calc(14 / 16 * 1rem);
&__header {
display: flex;
justify-content: flex-end;
}
&__content {
max-height: 50vh;
overflow-y: scroll;
padding: 1rem 2rem;
color: $black-color;
a {
color: $ultra-black-color;
}
}
h2 {
font-weight: 700;
font-size: 1rem;
}
p {
font-size: calc(14 / 16 * 1rem);
}
}
.greeting-text {
font-size: 1rem;
a {
color: $ultra-black-color;
font-weight: 700;
}
}

View File

@ -1,100 +0,0 @@
import { RequestCluster } from '../../request-cluster';
import { getDate } from '../../util';
import deduceProblems from './deduce-problems';
import { Explainers } from './explainers';
import { ParsedAnswers } from './parse-answers';
import { v } from './verbs';
import './email-content.scss';
import { Fragment, useState } from 'react';
declare var PLUGIN_NAME: string;
declare var PLUGIN_URL: string;
export default function EmailContent({
answers,
visited_url,
clusters,
}: {
answers: ParsedAnswers;
visited_url: string;
clusters: Record<string, RequestCluster>;
}) {
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]);
const [copied, setCopy] = useState<boolean>(false);
function copyTextToClipboard() {
// Should be changed in the future to Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write#browser_compatibility)
let r = document.createRange();
r.selectNode(document.querySelector('.mail-container__content'));
window.getSelection().addRange(r);
document.execCommand('copy');
window.getSelection().removeAllRanges();
setCopy(true);
}
return (
<Fragment>
<div className="generator-container">
<h1>Treść maila</h1>
<div className="mail-container">
<div className="mail-container__header">
<div className="mail-container__header--control"></div>
</div>
<article className="mail-container__content">
<p>Dzień dobry,</p>
<p>
w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po
podejrzeniu ruchu sieciowego generowanego przez stronę za pomocą
wtyczki <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')} odpowiedzi nigdzie na Państwa
stronie.
</p>
{problems.map((problem) => problem.getEmailContent())}
{explainers.map((explainer) => explainer(answers.zaimek))}
<h2>Państwa rola jako współadministratora danych osobowych</h2>
<p>
{_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '}
<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">
treści wyroku TSUE w sprawie C-40/17
</a>{' '}
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 pytania 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).
</p>
</article>
</div>
<div className="buttons-container">
<button className="sv_next_btn" onClick={() => copyTextToClipboard()}>
{copied ? 'Skopiowano!' : 'Kopiuj treść wiadomości'}
</button>
</div>
{copied ? (
<section className="greeting-text">
<strong>Przed Tobą ostatni krok! 😊</strong>
<p>
<a href="mailto:?subject=Zapytanie o przetwarzanie moich danych osobowych przez Państwa stronę">
Przejdź do swojego klienta pocztowego
</a>
, załącz zrzuty ekranów, wklej treść wiadomości i wyślij do
administratorów witryny {visited_url.split('/').slice(0, 3).join('/')}.
</p>
</section>
) : null}
</div>
</Fragment>
);
}

View File

@ -1,14 +0,0 @@
export type ExplainerKey = 'cookies_are_pii';
export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => JSX.Element> = {
cookies_are_pii: () => (
<>
<h2>Ciasteczka stanowią dane osobowe</h2>
<p>
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.
</p>
</>
),
};

View File

@ -1,399 +0,0 @@
function generateHostPage(
host: string,
index: number,
all_hosts: string[]
): { title: string; elements: any[]; visibleIf: string } {
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'),
visibleIf: '{popup_type} != "none"',
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"`,
defaultValueExpression:
'iif({popup_action} = "none" or {popup_action} = "closed_popup", "claims_consent_but_sends_before_consent", iif({popup_action} = "accept_all" and {rejection_is_hard} = "yes", "claims_consent_but_there_was_no_easy_refuse", ""))',
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: 'radiogroup',
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: 'radiogroup',
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"`,
isRequired: true,
choices: [
{ value: 'yes', text: 'Tak' },
{ value: 'no', text: 'Nie' },
{ value: 'not_sure', text: 'Nie wiem' },
],
},
{
type: 'radiogroup',
title: `Czy w {Twojej} ocenie wysłanie {Twoich} danych do właściciela domeny ${host} było konieczne do świadczenia zażądanej przez {Ciebie} usługi drogą elektroniczną?`,
name: f('was_processing_necessary'),
isRequired: true,
...defaultValue('was_processing_necessary'),
visibleIf: `{${f('legal_basis_type')}} = "legitimate_interest" or {${f(
'present'
)}} = "not_mentioned" or {popup_type} = "none"`,
choices: [
{ value: 'yes', text: 'Tak, było konieczne' },
{ value: 'no', text: 'Nie, nie było konieczne' },
{ value: 'not_sure', text: 'Nie mam zdania' },
],
},
],
};
}
export default function generateSurveyQuestions(hosts: string[]) {
return {
showQuestionNumbers: 'off',
showProgressBar: 'top',
pagePrevText: 'Wróć',
pageNextText: 'Dalej',
completeText: 'Dalej',
locale: 'pl',
clearInvisibleValues: 'onHidden',
pages: [
{
title: 'Tytuł - co to za ankieta?',
elements: [
{
type: 'html',
name: 'intro',
html: '<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab, odit dicta at aut esse culpa eveniet iure odio voluptates veniam sit. Libero explicabo, perspiciatis ad expedita officiis inventore impedit ducimus!</p>',
},
],
},
{
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 mechanizm 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: 'page',
text: 'Tylko w postaci tekstu na podstronie np. "prywatność" lub "polityka cookies"',
},
{
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' or {popup_type} = 'page'",
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" or {popup_type} = "page") ',
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ę”, „Brak zmiany ustawień przeglądarki oznacza zgodę”, „Klikając przycisk "X" (zamknij) wyrażasz 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. Na przykład: „Korzystając ze strony wyrażasz zgodę”, albo „Pozostawiając ustawienia przeglądarki bez zmian (..) wyrażasz zgodę”',
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: 'popup_action',
isRequired: true,
visibleIf: '{popup_type} = "some_choice" or {popup_type} = "passive_popup"',
title: 'Jaką akcję {podjąłeś} w ramach wyskakującego okienka?',
choices: [
{
value: 'none',
text: 'Nic nie {kliknąłem}',
},
{
value: 'closed_popup',
text: '{Zamknąłem} okienko za pomocą przycisku „X” lub „Zamknij”, lub podobnego',
},
{
value: 'accept_all',
text: '{Kliknąłem} przycisk od akceptacji wszystkich zgód',
},
{
value: 'deny_all',
text: '{Odmówiłem} wyrażenia zgody na wszystkie cele',
},
{
value: 'other',
text: 'Coś innego',
},
],
},
{
type: 'text',
name: 'popup_closed_how',
isRequired: true,
visibleIf: '{popup_action} = "closed_popup"',
title: 'W jaki sposób {zamknąłeś} okienko o zgodę? Opisz pełnym zdaniem',
defaultValueExpression: '{Kliknąłem} przycisk „X”.',
},
{
type: 'text',
name: 'popup_deny_all_how',
isRequired: true,
visibleIf: '{popup_action} = "deny_all"',
title: 'W jaki sposób {zamknąłeś} okienko o zgodę? Opisz pełnym zdaniem, np.: „{Kliknąłem} przycisk <Odrzuć wszystkie>” lub „{Odznaczyłem} wszystkie opcje w ustawieniach zaawansowanych”',
defaultValueExpression: '{Kliknąłem} przycisk „odmawiam wyrażenia zgody”.',
},
{
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',
visibleIf: "{popup_type} != 'none'",
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

@ -1,67 +0,0 @@
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'
| 'mentioned_in_policy'
| 'mentioned_in_popup';
legal_basis_type: 'consent' | 'legitimate_interes' | 'not_mentioned';
was_processing_necessary: 'yes' | 'no' | 'not_sure';
} & (
| {
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' }
)) & {
legitimate_interest_activity_specified: 'no' | 'precise' | 'vague';
outside_eu: 'yes' | 'no' | 'not_sure';
legitimate_interest_description?: string;
};
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 ParsedAnswers;
}

View File

@ -1,45 +0,0 @@
import { ExplainerKey } from '../explainers';
import { v } from '../verbs';
import { Problem } from './problem';
export default class NoInformationAtAllProblem extends Problem {
getEmailContent() {
const _ = (word: string) => v(word, this.answers.zaimek);
return (
<>
<h2>Brak informacji na temat przetwarzania danych osobowych</h2>
<p>
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami
domen:
</p>
{this.getRangeDescription()}
<p>
Na stronie brakuje jednak jakichkolwiek informacji o tym, jakie cele
przetwarzania takich danych oraz jakie podstawy prawne takiego przetwarzania.
</p>
<p>Zwracam się zatem do Państwa z następującymi pytaniami:</p>
<ul>
<li>Jaka jest tożsamość właścicieli tych domen?</li>
<li>Jaki jest cel takiego przetwarzania danych przez Państwa stronę?</li>
<li>
Jaka jest podstawa prawna takiego przetwarzania moich danych osobowych przez
Państwa stronę?
</li>
</ul>
</>
);
}
getNecessaryExplainers() {
const explainers = [] as Array<ExplainerKey>;
if (
this.getMarkedClusters().some((cluster) => {
console.log(cluster);
return cluster.hasMarkedCookies();
})
) {
explainers.push('cookies_are_pii');
}
return explainers;
}
}

View File

@ -1,38 +0,0 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers';
function formatRange(cluster: RequestCluster) {
const parts = [] as string[];
console.log(cluster);
if (cluster.hasMarkedCookies()) {
parts.push('mojego identyfikatora internetowego pozyskanego z Cookie');
}
if (cluster.exposesOrigin()) {
parts.push('części mojej historii przeglądania');
}
return parts.join(' oraz ');
}
export abstract class Problem {
constructor(public answers: ParsedAnswers, public clusters: Record<string, RequestCluster>) {}
abstract getEmailContent(): JSX.Element;
abstract getNecessaryExplainers(): ExplainerKey[];
getMarkedClusters() {
return Object.values(this.clusters).filter((c) => c.hasMarks());
}
getRangeDescription() {
return (
<ul>
{this.getMarkedClusters().map((cluster) => (
<li key={cluster.id}>
{cluster.id} (w zakresie: {formatRange(cluster)})
</li>
))}
</ul>
);
}
}

View File

@ -1,183 +0,0 @@
import { RequestCluster } from '../../../request-cluster';
import { wordlist } from '../../../util';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
export class UnlawfulCookieAccess extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
return [];
}
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {
// są cookiesy, nie było zgody, nie są konieczne do działania strony
const cookie_clusters = Object.values(clusters).filter((c) => c.hasMarkedCookies());
return cookie_clusters.some((cluster) => {
const hostAnswers = answers.hosts[cluster.id];
return (
(hostAnswers.present == 'not_mentioned' ||
hostAnswers.present == 'not_before_making_a_choice' ||
['none', 'closed_popup', 'deny_all'].includes(answers.popup_action) ||
answers.popup_type === 'none') &&
hostAnswers.was_processing_necessary != 'yes'
);
});
}
getEmailContent() {
const cookie_clusters = Object.values(this.clusters).filter((c) => c.hasMarkedCookies());
const unnecessary_hosts = Object.entries(this.answers.hosts)
.filter(([, answers]) => answers.was_processing_necessary === 'no')
.map(([host]) => host);
const maybe_unnecessary_hosts = Object.entries(this.answers.hosts)
.filter(([, answers]) => answers.was_processing_necessary === 'not_sure')
.map(([host]) => host);
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Dostęp do cookies niezgodny z ustawą Prawo Telekomunikacyjne</h2>
<p>
Państwa strona dokonała odczytu plików Cookie zapisanych na dysku twardym mojego
komputera. Dotyczy to plików cookie przypisanych do domen:
</p>
<ul>
{cookie_clusters.map((cluster, index) => {
const names = cluster
.getMarkedEntries()
.filter((e) => e.source === 'cookie')
.map((e) => e.name);
return (
<li>
{cluster.id} ({names.length > 1 ? 'pliki' : 'plik'}{' '}
{names.map((name, index) => {
return (
<>
{index > 0 ? ', ' : ''}
{name}
</>
);
})}
){index === cookie_clusters.length - 1 ? '.' : ';'}
</li>
);
})}
</ul>
<p>
Zgodnie z treścią Art. 173.{' '}
<a href="https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU20041711800/U/D20041800Lj.pdf">
ustawy Prawo Telekomunikacyjne
</a>
, strona może pozyskać dostęp do treści plików cookies pod warunkiem spełnienia
jednego z następujących warunków:
</p>
<ol>
<li>
Użytkownik wyraził zgodę na takie przetwarzanie danych <em>po</em> tym, jak
został poinformowany bezpośrednio o celu uzyskania dostępu do tej
informacji;
</li>
<li>
Dostęp do treści plików cookies jest konieczny do dostarczania usługi
świadczonej drogą elektroniczną zażądanej przez użytkownika.
</li>
</ol>
{(() => {
if (this.answers.popup_type == 'none' || this.answers.popup_type == 'page') {
return (
<p>
Jako, że strona nie pytała {_('mnie')} nigdy o zgodę, nie jest
spełniony warunek 1.
</p>
);
} else if (this.answers.popup_type === 'passive_popup') {
return (
<p>
Państwa strona nie dała mi nigdy faktycznego wyboru dotyczącego
wyrażenia lub odmówienia zgody na takie przetwarzanie danych
osobowych, dlatego nie jest spełniony warunek 1.{' '}
{this.answers.mentions_passive_consent ? (
<>
Zgody wyrażonej w sposób bierny lub milczący nie można uznać
za ważną w świetle obowiązujących przepisów rozporządzenia
2016/679. Dlatego zaniechanie zmiany ustawień przeglądarki
lub po prostu korzystanie ze strony nie stanowi ważnej
zgody. Takie jest{' '}
<a href="https://assets.midline.pl/pisma/2021-12-16%20odpowiedz%20UODO%20na%20skarg%C4%99%20i(n)Secure.pdf">
stanowisko polskiego UODO
</a>
.
</>
) : (
''
)}
</p>
);
} else if (this.answers.popup_type === 'some_choice') {
if (this.answers.popup_action === 'none') {
return (
<p>
Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')}{' '}
danych osobowych. W okienku pytającym o zgodję nic nie{' '}
{_('kliknąłem')}. Nie jest zatem spełniony warunek 1.
</p>
);
} else if (this.answers.popup_action === 'closed_popup') {
return (
<p>
Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')}{' '}
danych osobowych. {this.answers.popup_closed_how.trim()}
{this.answers.popup_closed_how.trim().at(-1) != '.'
? '.'
: ''}{' '}
Takiego działania nie można uznać za ważną zgodę na
przetwarzanie danych osobowych, gdyż nie spełnia warunku
jednoznaczności opisanego w Art. 4, pkt 11 RODO. Nie jest zatem
spełniony warunek 1.
</p>
);
} else if (this.answers.popup_action == 'deny_all') {
return (
<p>
{this.answers.popup_deny_all_how.trim()}
{this.answers.popup_closed_how.trim().at(-1) != '.'
? '.'
: ''}{' '}
Zatem nie jest spełniony warunek 1.
</p>
);
}
}
})()}
{unnecessary_hosts.length > 0 ? (
<p>
W {_('mojej')} ocenie odczytywanie przez Państwa stronę treści plików
cookies z {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 <em>bez</em> korzystania z plików Cookie.
</p>
) : (
''
)}
<p>
{_('Proszę')} o wskazanie, czy być może stosowali Państwo inną podstawę prawną
do takiego przetwarzania {_('moich')} danych osobowych, czy przetwarzali je
państwo bez ważnej podstawy prawnej?
</p>
{maybe_unnecessary_hosts.length > 1 ? (
<p>
{_('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?
</p>
) : (
''
)}
</>
);
}
}

View File

@ -1,36 +0,0 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers, ParsedHostAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
type UnlawfulDataClassification = 'no_purpose';
export function classifyUnlawfulData(
hostAnswers: ParsedHostAnswers,
cluster: RequestCluster
): UnlawfulDataClassification {
if (hostAnswers.present == 'not_mentioned' && hostAnswers.was_processing_necessary == 'no') {
return 'no_purpose';
}
}
export class UnlawfulData extends Problem {
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {}
getEmailContent() {
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Przetwarzanie danych osobowych bez ważnej podsawy prawnej</h2>
<p>
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami
domen:
</p>
{this.getRangeDescription()}
</>
);
}
getNecessaryExplainers() {
return [] as ExplainerKey[];
}
}

View File

@ -1,60 +0,0 @@
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_action: 'none' | 'closed_popup' | 'accept_all' | 'deny_all' | 'other';
popup_closed_how: string;
popup_deny_all_how: string;
} & (
| ({
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';
administrator_identity_available_before_choice: 'yes' | 'no';
cookie_wall: undefined;
passive_consent_description: undefined;
mentions_passive_consent: undefined;
}
| {
popup_type: 'none' | 'page';
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,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Rentgen - generowanie raportu</title>
<link
rel="stylesheet"
href="/lib/styles/fonts.css"
>
<link
rel="stylesheet"
href="/node_modules/survey-react/survey.css"
/>
<link
rel="stylesheet"
href="/node_modules/survey-react/modern.css"
/>
<link
rel="stylesheet"
href="/lib/components/report-window/report-window.css"
/>
<link
rel="shortcut icon"
href="../../../assets/icon-addon.svg"
type="image/x-icon"
>
</head>
<body>
<div id="app"></div>
<script src="/node_modules/react/umd/react.development.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.development.js"></script>
<script src="/lib/components/report-window/report-window.js"></script>
</body>
</html>

View File

@ -1,276 +0,0 @@
@import './../../styles/colors.scss';
* {
box-sizing: border-box;
font-family: 'OpenSans' !important;
}
html {
font-size: 1rem;
}
body {
background-color: #f9f9fa;
padding: 0rem 0.75rem;
margin: 0;
}
nav {
position: sticky;
top: 0;
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;
}
}
p,
li,
h1,
h2,
h3 {
max-width: 100ex;
}
h1 {
font-size: 1.1rem;
}
.header {
display: grid;
grid-template-columns: 1.75rem 1fr;
align-items: center;
max-height: 3.5rem;
min-height: 3.5rem;
position: sticky;
top: 0;
z-index: 1;
user-select: none;
.webpage-metadata {
word-break: break-all;
display: flex;
flex-flow: column;
flex-wrap: nowrap;
font-size: 0.875rem;
font-weight: 600;
justify-content: center;
padding-left: 1rem;
color: #000;
&--hyperlink {
font-weight: 400;
color: $ultra-black-color;
max-height: 2rem;
overflow: hidden;
}
}
}
.sv_main {
font-family: 'OpenSans' !important;
background-color: transparent;
.sv_p_root {
max-width: 100ex;
margin: 0 auto;
& > .sv_row:nth-child(2n) {
background-color: #fff;
}
& > .sv_row:nth-child(2n + 1) {
background-color: #fff;
}
& > .sv_row {
border-bottom: none;
background-color: #fff;
box-shadow: rgba(12, 12, 13, 0.1) 0px 1px 4px 0px;
}
& > .sv_row:last-child {
border-bottom: none;
}
}
.sv_container {
color: rgb(12, 12, 13);
padding: 0;
.sv_body {
padding: 0;
.sv_p_root {
& > .sv_row {
padding: 0.75rem 1.5rem;
margin-bottom: 1rem;
border-radius: 3px;
}
.sv_page_title {
font-weight: bold;
font-size: calc(15 / 16 * 1rem);
}
.sv_q {
padding: 0;
}
}
.sv_progress {
height: 0.25rem;
.sv_progress_bar {
margin-top: 0;
margin-bottom: 0.5rem;
}
}
}
}
.sv_body {
border: none !important;
background-color: transparent;
}
.sv_progress {
background-color: hsl(240, 9.1%, 87.8%);
margin-bottom: 4rem;
transition: all 200ms;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
& > span {
font-weight: 600;
}
}
.sv_progress_bar {
background-color: #000;
transition: all 200ms;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
}
}
.sv_main .sv_container .sv_body .sv_p_root .sv_page_title {
font-size: 1.1rem;
font-weight: bold;
}
.sv_q_radiogroup {
margin-top: 0.25rem;
}
.sv_main .sv_q_other input,
.sv_main .sv_q_text_root,
.sv_main .sv_q_dropdown_control,
.sv_main
input:not([type='button']):not([type='reset']):not([type='submit']):not([type='image']):not([type='checkbox']):not([type='radio']),
.sv_main select,
.sv_main textarea {
border: 1px solid #6d7072;
color: #000;
padding-left: 0.25rem !important;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.sv_q_radiogroup_label {
cursor: pointer;
}
.sv_nav {
display: flex;
margin: 2rem 0;
justify-content: center;
}
.sv_prev_btn,
.sv_next_btn,
.sv_complete_btn {
border: 0;
outline: 0;
font-size: 0.875rem !important;
line-height: 0.875rem !important;
height: 2.5rem;
cursor: pointer;
min-width: 100px;
}
.sv_next_btn,
.sv_complete_btn {
background-color: #000 !important;
font-weight: 800 !important;
padding: 0 1.5rem;
background-color: #000;
margin: 0 !important;
color: #fff !important;
&:hover {
color: $icd-rentgen-color !important;
background-image: linear-gradient(
to right,
$icd-rentgen-color 0%,
$icd-rentgen-color 4%,
#000 4%,
#000 100%
);
animation: slidebg 1s cubic-bezier(0.19, 1, 0.22, 1) infinite;
}
@keyframes slidebg {
to {
background-position: 155px;
}
}
}
.sv_prev_btn {
margin-right: 0.5rem !important;
color: #000 !important;
text-decoration: underline !important;
background-color: transparent !important;
font-weight: 700 !important;
}
.sv_main .sv_q_erbox:not([style*='display: none']):not([style*='display:none']) {
color: $dark-red;
}
.sv_main .sv_q_erbox:not([style*='display: none']):not([style*='display:none']) {
border: 1px solid $dark-red;
background-color: $pale-red;
}
.sv_main
.sv_container
.sv_body
.sv_p_root
.sv_q
.sv_q_erbox:not([style*='display: none']):not([style*='display:none']) {
margin: 0.5rem 0;
padding: 1rem;
}
.sv_qstn fieldset {
margin: 0px;
padding: 0px;
font-size: calc(14 / 16 * 1rem);
}
.sv_main .sv_custom_header {
background-color: transparent;
}
.sv_main .sv_q_other input:focus,
.sv_main .sv_q_text_root:focus,
.sv_main .sv_q_dropdown_control:focus,
.sv_main
input:not([type='button']):not([type='reset']):not([type='submit']):not([type='image']):not([type='checkbox']):not([type='radio']):focus,
.sv_main select:focus,
.sv_main textarea:focus {
border: 1px solid #000;
}
.sv_q_title,
.sv_main .sv_container .sv_body .sv_p_root .sv_q_title {
font-weight: 600;
font-size: calc(14 / 16 * 1rem);
}

View File

@ -1,84 +0,0 @@
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import { getMemory } from '../../memory';
import { useEmitter } from '../../util';
import './report-window.scss';
import Questions from './questions';
import EmailContent from './email-content';
import { parseAnswers, ParsedAnswers } from './parse-answers';
import ScreenshotGenerator from './screenshot-generator';
function Report() {
try {
const url = new URL(document.location.toString());
const origin = url.searchParams.get('origin');
const [counter] = useEmitter(getMemory());
const [answers, setAnswers] = React.useState<ParsedAnswers>(
url.searchParams.get('answers') ? JSON.parse(url.searchParams.get('answers')) : null
);
const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey');
const clusters = getMemory().getClustersForOrigin(origin);
React.useEffect(() => {
const url = new URL(document.location.toString());
url.searchParams.set('origin', origin);
url.searchParams.set('answers', JSON.stringify(answers));
url.searchParams.set('mode', mode);
history.pushState({}, 'Rentgen', url.toString());
}, [mode, answers, origin]);
const visited_url = Object.values(clusters)
.find((cluster) => cluster.getMarkedRequests().length > 0)
?.getMarkedRequests()[0].originalURL;
const result = (
<div {...{ 'data-version': counter }}>
{mode === 'survey' ? (
<Questions
hosts={Object.values(clusters)
.filter((cluster) => cluster.getMarkedRequests().length > 0)
.map((cluster) => cluster.id)}
onComplete={(answers) => {
setAnswers(parseAnswers(answers));
setMode('screenshots');
}}
></Questions>
) : (
''
)}
{mode === 'screenshots' ? (
<ScreenshotGenerator
{...{ visited_url, clusters, setReportWindowMode: setMode }}
/>
) : (
''
)}
{mode === 'preview' ? <EmailContent {...{ answers, visited_url, clusters }} /> : ''}
{/* <HARConverter {...{ entries }} /> */}
</div>
);
return (
<Fragment>
<header className="header">
<img src="../../assets/icon-addon.svg" height={32}></img>
<div className="webpage-metadata">
{origin ? (
<>
<span>Generowanie raportu </span>
<span className="webpage-metadata--hyperlink">{origin}</span>
</>
) : (
<span>Przejdź do wybranej strony internetowej</span>
)}
</div>
</header>
<section>{result}</section>
</Fragment>
);
} catch (e) {
console.error(e);
return <div>ERROR! {JSON.stringify(e)}</div>;
}
}
ReactDOM.render(<Report />, document.getElementById('app'));

View File

@ -1,108 +0,0 @@
@import './../../styles/colors.scss';
h1 {
font-size: 1.1rem;
margin-bottom: calc(24 / 16 * 1rem);
}
h2 {
font-weight: 600;
font-size: 0.875rem;
}
.images {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
width: 100%;
grid-gap: 1rem;
margin: 2rem 0 1rem;
max-height: 45vh;
overflow-y: scroll;
overflow-x: clip;
z-index: 1;
position: relative;
}
.generator-container {
max-width: 100ex;
margin: 0 auto;
font-size: calc(14 / 16 * 1rem);
a {
color: $ultra-black-color;
}
}
.buttons-container {
display: flex;
justify-content: center;
margin: 2rem 0;
padding: 1em 0;
}
.container {
padding: 0.75rem 1.5rem;
margin-bottom: 1rem;
border-radius: 3px;
box-shadow: rgba(12, 12, 13, 0.1) 0px 1px 4px 0px;
background-color: #fff;
}
.browser {
height: 8.667rem;
font-weight: 800 !important;
color: $disabled-grey !important;
border: 1px solid $disabled-grey;
background-image: linear-gradient(to bottom, $icd-rentgen-color 20%, #fff 20%, #fff 100%);
animation: xray 2s cubic-bezier(0, 1.43, 0.39, 1.43) infinite;
&--filled {
background-size: 100%;
background-position-y: 19px;
animation: none;
.browser__header {
background-color: #fff;
}
}
@keyframes xray {
to {
background-position-y: 11.1rem;
}
}
&__header {
height: 1.667rem;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.5rem;
font-size: 1.25rem;
border-bottom: 1px solid $disabled-grey;
&--address-bar {
border: 1px solid $disabled-grey;
height: 1rem;
width: 10rem;
font-size: 0.667rem;
font-weight: 400;
padding-left: 0.25rem;
color: #000;
overflow: visible;
word-break: break-all;
inline-size: 10rem;
}
&--controls {
padding-bottom: 0.25rem;
font-weight: 900;
}
}
&__content {
height: 6.667rem;
}
}

View File

@ -1,199 +0,0 @@
import React, { Fragment } from 'react';
import { RequestCluster } from '../../request-cluster';
import './screenshot-generator.scss';
const SS_URL = 'http://65.108.60.135:3000';
enum taskState {
WAITING = 'waiting',
RUNNING = 'running',
FINISHED = 'finished',
}
type Screenshot = {
url: string;
domain: string;
found_headers: string[];
};
interface screenshotTask {
url: string;
domains: string[];
id: string;
status: taskState;
output: string;
images: Screenshot[];
}
function createTaskEndpoint(visited_url: string, domains: string[]) {
return `${SS_URL}/api/requests?url=${visited_url}${domains.reduce(
(prev: string, curr: string) => prev + '&domains[]=' + curr,
''
)}`;
}
function createTask(visited_url: string, domains: string[]) {
return fetch(createTaskEndpoint(visited_url, domains), { method: 'POST' });
}
function pollTask(path: string): Promise<Response> {
return fetch(path, { method: 'GET' });
}
export default function ScreenshotGenerator({
visited_url,
clusters,
setReportWindowMode,
}: {
visited_url: string;
clusters: Record<string, RequestCluster>;
setReportWindowMode: Function;
}) {
const [mode, setMode] = React.useState<string>('idle');
const [images, setImages] = React.useState<Screenshot[]>([]);
const [taskId, setTaskId] = React.useState<string>(null);
async function subscribeTask(path: string): Promise<screenshotTask> {
let response = { status: taskState.WAITING };
while (response.status === taskState.WAITING || response.status === taskState.RUNNING) {
await new Promise((resolve) => setTimeout(resolve, 1000));
response = await (await pollTask(path)).json();
setImages((response as screenshotTask)?.images);
document.querySelector('.images').scrollTo({
top: document.querySelector('.images').scrollHeight,
behavior: 'smooth',
});
}
if (response.status === taskState.FINISHED) {
setMode('finished');
}
return response as screenshotTask;
}
function downloadFiles() {
const urls = images.map((el) => `${SS_URL}${el}`);
for (const url of urls) {
let a = document.createElement('a');
a.setAttribute('href', url);
a.setAttribute('download', '');
a.setAttribute('target', '_blank');
a.click();
}
setReportWindowMode('preview');
}
return (
<div className="generator-container">
{mode === 'idle' ? (
<Fragment>
<h1>Przygotowanie zrzutów ekranów</h1>
<div className="container">
<h2>Notka informacyjna</h2>
<p>
Dla potwierdzenia przechwyconych danych, warto załączyć zrzuty ekranów
narzędzi deweloperskich do maila dla administratora oraz Urzędu Ochrony
Danych Osobowych.
</p>
<p>
Jeżeli nie wiesz jak wykonać zrzuty ekranów, skorzystaj z{' '}
<a href="">naszej instrukcji</a> lub wtyczka Rentgen może wygenerować je
za Ciebie.
</p>
</div>
<div className="buttons-container">
<button
className="sv_prev_btn"
onClick={() => {
setReportWindowMode('preview');
}}
>
Pomiń
</button>
<button
className="sv_next_btn"
onClick={async () => {
setMode('in_progress');
const task = await createTask(visited_url, Object.keys(clusters));
const urlArr = task.url.split('/');
setTaskId(urlArr[urlArr.length - 1]);
const response = await subscribeTask(task.url);
setImages(response.images);
console.log('response.images', response.images);
console.log('output', response);
}}
>
Wygeneruj
</button>
</div>
</Fragment>
) : null}
{mode === 'in_progress' || mode === 'finished' ? (
<Fragment>
<h1>Przygotowanie zrzutów ekranów</h1>
<div className="container">
{mode === 'in_progress' ? (
<Fragment>
<h2>To może chwilkę zająć...</h2>
<p>
Nasz serwer właśnie odwiedza wskazaną przez Ciebie stronę
i przygotowuje zrzuty ekranów narzędzi deweloperskich.
</p>
</Fragment>
) : null}
{mode === 'finished' ? (
<Fragment>
<h2>Gotowe!</h2>
<p>Zrzuty ekranów narzędzi deweloperskich gotowe do pobrania.</p>
</Fragment>
) : null}
<div className="images">
{images.map((screenshot) => {
return (
<div
key={`${taskId}_${screenshot.url}`}
className="browser browser--filled"
style={{
backgroundImage: `url(${SS_URL}${screenshot.url})`,
}}
>
<div className="browser__header">
<div className="browser__header--address-bar">
{screenshot.url.split('-').slice(-2).join('-')}
</div>
<div className="browser__header--controls">···</div>
</div>
<div className="browser__content"></div>
</div>
);
})}
{mode === 'in_progress' ? (
<div className="browser">
<div className="browser__header">
<div className="browser__header--address-bar"></div>
<div className="browser__header--controls">···</div>
</div>
<div className="browser__content"></div>
</div>
) : null}
</div>
</div>
<div className="buttons-container">
{mode === 'finished' ? (
<Fragment>
<button className="sv_next_btn" onClick={() => downloadFiles()}>
Pobierz zrzuty ekranów i przejdź dalej
</button>
</Fragment>
) : null}
</div>
</Fragment>
) : null}
</div>
);
}

View File

@ -1,50 +0,0 @@
const words = {
ciebie: ['ciebie', 'ciebie', 'ciebie', 'was'],
dokonałeś: ['dokonałeś', 'dokonałaś', 'dokonałoś', 'dokonaliście'],
jesteś: ['jesteś', 'jesteś', 'jesteś', 'jesteście'],
kliknąłem: ['kliknąłem', 'kliknęłam', 'klinkęłom', 'kliknęliśmy'],
mam: ['mam', 'mam', 'mam', 'mamy'],
masz: ['masz', 'masz', 'masz', 'macie'],
mnie: ['mnie', 'mnie', 'mnie', 'nas'],
moich: ['moich', 'moich', 'moich', 'naszych'],
moje: ['moje', 'moje', 'moje', 'nasze'],
mojej: ['mojej', 'mojej', 'mojej', 'naszej'],
muszę: ['muszę', 'muszę', 'muszę', 'musimy'],
odkliknąłeś: ['odkliknąłeś', 'odkliknęłaś', 'odklikęłoś', 'odkliknęliście'],
odmówiłem: ['odmówiłem', 'odmówiłam', 'odmówiłom', 'odmówiliśmy'],
odmówiłeś: ['odmówiłeś', 'odmówiłaś', 'odmówiłoś', 'odmówiliście'],
odwiedzałeś: ['odwiedzałeś', 'odwiedzałaś', 'odwiedzałoś', 'odwiedzaliście'],
odwiedziłem: ['odwiedziłem', 'odwiedziłam', 'odwiedziłom', 'odwiedziliśmy'],
odznaczyłem: ['odznaczyłem', 'odznaczyłam', 'odznaczyłom', 'odznaczyliśmy'],
otwórz: ['otwórz', 'otwórz', 'otwórz', 'otwórzcie'],
podjąłem: ['podjąłem', 'podjęłam', 'podjęłom', 'podjęliśmy'],
podjąłeś: ['podjąłeś', 'podjęłaś', 'podjęłoś', 'podjęliście'],
proszę: ['proszę', 'proszę', 'proszę', 'prosimy'],
szukałem: ['szukałem', 'szukałam', 'szukałom', 'szukaliśmy'],
tobie: ['tobie', 'tobie', 'tobie', 'wam'],
twoich: ['twoich', 'twoich', 'twoich', 'waszych'],
twojej: ['twojej', 'twojej', 'twojej', 'waszej'],
usuń: ['usuń', 'usuń', 'usuń', 'usuńcie'],
widzę: ['widzę', 'widzę', 'widzę', 'widzimy'],
widziałem: ['widziałem', 'widziałam', 'widziałom', 'widzieliśmy'],
widzisz: ['widzisz', 'widzisz', 'widzisz', 'widzicie'],
wykonaj: ['wykonaj', 'wykonaj', 'wykonaj', 'wykonajcie'],
wyraziłem: ['wyraziłem', 'wyraziłam', 'wyraziłom', 'wyraziliśmy'],
wyraziłeś: ['wyraziłeś', 'wyraziłaś', 'wyraziłoś', 'wyraziliście'],
zamknąłem: ['zamknąłem', 'zamknęłam', 'zamknęłom', 'zamknęliśmy'],
zobacz: ['zobacz', 'zobacz', 'zobacz', 'zobaczcie'],
zamknąłeś: ['zamknąłeś', 'zamknęłaś', 'zamknęłoś', 'zamknęliście'],
znalazłem: ['znalazłem', 'znalazłam', 'znalazłom', 'znaleźliśmy'],
zrobiłem: ['zrobiłem', 'zrobiłam', 'zrobiłom', 'zrobiliśmy'],
zwracam: ['zwracam', 'zwracam', 'zwracam', 'zwracamy'],
} as { [key: string]: string[] };
export default words;
export function v(key: string, index: number) {
let result = words[key.toLowerCase()]?.[index] || key;
if (key[0] == key[0].toUpperCase()) {
result = [result[0].toUpperCase(), ...result.slice(1)].join('');
}
return result;
}

View File

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Rentgen - analiza strony</title>
<link
rel="stylesheet"
href="/lib/styles/global.css"
>
<link
rel="stylesheet"
href="/lib/components/sidebar/sidebar.css"
>
<link
rel="shortcut icon"
href="../../assets/icon-addon.svg"
type="image/x-icon"
>
</head>
<body>
<div id="app"></div>
<script src="/node_modules/react/umd/react.production.min.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.production.min.js"></script>
<script src="/lib/components/sidebar/sidebar.js"></script>
</body>
</html>

View File

@ -1,243 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Options from '../../options';
import { StolenData } from './stolen-data';
import { getshorthost, useEmitter } from '../../util';
import { getMemory } from '../../memory';
async function getCurrentTab() {
const [tab] = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
return tab;
}
import './../../styles/global.scss';
import './sidebar.scss';
const Sidebar = () => {
const url = new URL(document.location.toString());
const origin = url.searchParams.get('origin');
const [minValueLength, setMinValueLength] = React.useState<number | null>(
localStorage.getItem('minValueLength') === null
? 7
: (localStorage.getItem('minValueLength') as unknown as number)
);
const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false);
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false);
const [eventCounts, setEventCounts] = useEmitter(getMemory());
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false);
const [infoDataDialogAck, setInfoDataDialogAck] = React.useState<boolean>(
localStorage.getItem('infoDataDialogAck') === null
? true
: localStorage.getItem('infoDataDialogAck') == 'true'
? true
: false
);
const [warningDataDialogAck, setWarningDataDialogAck] = React.useState<boolean>(
localStorage.getItem('warningDataDialogAck') === null
? true
: localStorage.getItem('warningDataDialogAck') == 'true'
? true
: false
);
const [detailsVisibility, setDetailsVisibility] = React.useState<boolean>(
localStorage.getItem('detailsVisibility') === null
? false
: localStorage.getItem('detailsVisibility') == 'true'
? true
: false
);
React.useEffect(() => {
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) {
return setMarksOccurrence(true);
}
}
return setMarksOccurrence(false);
}, [eventCounts['*']]);
return (
<div className="sidebar">
<header className="header">
<img src="../../assets/icon-addon.svg" height={32}></img>
<div className="webpage-metadata">
{origin ? (
<>
<span>Analiza strony</span>
<span className="webpage-metadata--hyperlink">{origin}</span>
</>
) : (
<span>Przejdź do wybranej strony internetowej</span>
)}
</div>
<button
className="button button--report"
onClick={() => {
window.open(
`/components/report-window/report-window.html?origin=${origin}`,
'new_tab'
);
}}
>
Generuj raport
</button>
</header>
{stolenDataView ? (
<nav>
<button
onClick={() => {
window.open(
`/components/report-window/report-window.html?origin=${origin}`,
'new_tab'
);
}}
>
<img src="/assets/icons/report.svg" width="20" height="20" />
<span>Generuj raport</span>
</button>
<button
onClick={() => {
setDetailsVisibility(!detailsVisibility);
}}
>
<img
src={
detailsVisibility
? '/assets/icons/file_minus.svg'
: '/assets/icons/file_find.svg'
}
width="20"
height="20"
/>
<span>
{detailsVisibility ? 'Ukryj szczegóły' : 'Wyświetlaj szczegóły'}
</span>
</button>
<button onClick={() => setStolenDataView(!stolenDataView)}>
<img src="/assets/icons/settings.svg" width="20" height="20" />
<span>Ustawienia</span>
</button>
{localStorage.getItem('blottingBrowser') ===
'nikttakniesplamitwojejprzeglądarkijakspidersweb' ? (
<button
onClick={() => {
if (
window.confirm(
'Czy chcesz wczytać wszystkie domeny w celu „splamienia” twojej przeglądarki? Uwaga przeglądarka może zablokować otwieranie nowych kart. (Ten krok jest opcjonalny)'
)
) {
let deep_copy = JSON.parse(
JSON.stringify(
Object.values(
getMemory().getClustersForOrigin(origin)
).map((domain) => domain.id)
)
);
for (const domain of deep_copy) {
window.open(`https://${domain}`);
}
}
}}
>
<img src="/assets/icons/bulb.svg" width="20" height="20" />
<span>Odwiedź wszystkie domeny</span>
</button>
) : null}
</nav>
) : null}
<section>
{stolenDataView ? (
<>
{infoDataDialogAck ? (
<section className="dialog-container dialog-container--info">
<span>
<strong>
Rentgen automatycznie zaznacza wybrane domeny na podstawie
zebranych danych.
</strong>{' '}
Możesz teraz przejść do generowania raportu lub dokonać korekty.
</span>
<button
onClick={() => {
setInfoDataDialogAck(false);
localStorage.setItem(
'infoDataDialogAck',
false as unknown as string
);
}}
>
<img src="/assets/icons/close_big.svg" width="16" height="16" />
</button>
</section>
) : null}
{warningDataDialogAck ? (
<section className="dialog-container dialog-container--warning">
<span>
<strong>Uwaga!</strong> Niekoniecznie każda przesłana poniżej
informacja jest daną osobową. Niektóre z podanych domen mogą
należeć do właściciela strony i nie reprezentować podmiotów
trzecich.
</span>
<button
onClick={() => {
setWarningDataDialogAck(false);
localStorage.setItem(
'warningDataDialogAck',
false as unknown as string
);
}}
>
<img src="/assets/icons/close_big.svg" width="16" height="16" />
</button>
</section>
) : null}
<StolenData
origin={origin}
eventCounts={eventCounts}
minValueLength={minValueLength}
cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly}
detailsVisibility={detailsVisibility}
/>
</>
) : (
<Options
minValueLength={minValueLength}
setMinValueLength={setMinValueLength}
cookiesOnly={cookiesOnly}
setCookiesOnly={setCookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly}
setCookiesOrOriginOnly={setCookiesOrOriginOnly}
warningDataDialogAck={warningDataDialogAck}
setWarningDataDialogAck={setWarningDataDialogAck}
detailsVisibility={detailsVisibility}
setDetailsVisibility={setDetailsVisibility}
setStolenDataView={setStolenDataView}
removeCookies={() => {
getMemory().removeCookiesFor(origin);
getMemory().emit('change', origin);
setMarksOccurrence(false);
}}
removeRequests={() => {
getMemory().removeRequestsFor(origin);
getMemory().emit('change', origin);
setMarksOccurrence(false);
}}
/>
)}
</section>
</div>
);
};
ReactDOM.render(<Sidebar />, document.getElementById('app'));

View File

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link
rel="stylesheet"
href="/lib/styles/global.css"
>
<link
rel="stylesheet"
href="/lib/components/toolbar/toolbar.css"
>
</head>
<body>
<div id="toolbar"></div>
<script src="/node_modules/react/umd/react.production.min.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.production.min.js"></script>
<script src="/lib/components/toolbar/toolbar.js"></script>
</body>
</html>

View File

@ -1,172 +0,0 @@
@import '../../styles/colors.scss';
body {
width: 400px;
overflow-x: hidden;
user-select: none;
}
.toolbar {
padding: 0.125rem 1rem;
.header {
display: grid;
grid-template-columns: 1.75rem 1fr 1.25rem;
align-items: center;
max-height: 3.5rem;
min-height: 3.5rem;
border-bottom: 1px solid $light-grey;
position: sticky;
top: 0;
background: #ffffff;
z-index: 1;
user-select: none;
.webpage-metadata {
word-break: break-all;
display: flex;
flex-flow: column;
flex-wrap: nowrap;
font-size: 0.875rem;
font-weight: 700;
justify-content: center;
padding-left: 1rem;
color: #000;
&--hyperlink {
font-weight: 600;
color: $ultra-black-color;
max-height: 2rem;
overflow: hidden;
}
}
button {
border: none;
background: transparent;
cursor: pointer;
padding: 0;
}
}
.summary {
display: flex;
flex-flow: column;
justify-content: center;
padding-bottom: 1.5rem;
border-bottom: 1px solid $light-grey;
.counters-wrapper {
flex-flow: row;
display: flex;
justify-content: center;
.counters {
display: flex;
flex-flow: column;
align-content: flex-start;
justify-content: center;
margin-right: 1rem;
.counter {
font-size: 1.25rem;
font-weight: 700;
align-items: center;
display: flex;
img {
margin-right: 0.5rem;
}
span {
margin-right: 2rem;
}
&:nth-child(1) {
margin-bottom: 1rem;
}
}
}
.big-counter {
font-size: 6rem;
font-weight: 700;
}
}
.notice {
font-size: 0.875rem;
font-weight: 700;
text-align: center;
}
}
.details {
padding-top: 1.5rem;
p {
font-size: 0.875rem;
padding-bottom: 1rem;
}
}
.about {
&__no-errors {
text-align: center;
padding-bottom: 1.5rem;
}
p {
font-size: 0.875rem;
font-weight: 700;
}
}
.actions {
padding: 2rem 0;
display: flex;
justify-content: center;
align-items: center;
a {
font-size: 0.875rem;
font-weight: 700;
color: #000;
margin-right: 0.5rem;
}
.button {
border: 0;
outline: 0;
height: 3rem;
font-size: 0.875rem;
line-height: 0.875rem;
cursor: pointer;
&--report {
font-weight: 800;
padding: 0 1.5rem;
background-color: #000;
color: #fff;
&:hover {
color: $icd-rentgen-color;
background-image: linear-gradient(
to right,
$icd-rentgen-color 0%,
$icd-rentgen-color 4%,
#000 4%,
#000 100%
);
animation: slidebg 1s cubic-bezier(0.19, 1, 0.22, 1) infinite;
}
}
@keyframes slidebg {
to {
background-position: 155px;
}
}
}
}
}

View File

@ -1,238 +0,0 @@
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import { useEmitter } from '../../util';
import { getMemory } from '../../memory';
async function getCurrentTab() {
const [tab] = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
return tab;
}
import './../../styles/global.scss';
import './toolbar.scss';
const Toolbar = () => {
const [origin, setOrigin] = React.useState<string | null>(null);
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
const [eventCounts, setEventCounts] = useEmitter(getMemory());
const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null);
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false);
const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>(
null
);
const first_sentence_cookie = 'Strona dokonała zapisu i odczytu plików Cookie dla domen ';
const first_sentence_history =
'Część informacji o Twojej historii przeglądania została wysłana do ';
React.useEffect(() => {
const listener = async () => {
const tab = await getCurrentTab();
if (tab !== undefined) {
const url = new URL(tab.url);
if (url.origin.startsWith('moz-extension')) {
return;
}
setOrigin(url.origin);
} else {
console.warn('Out of the tab scope');
}
};
browser.tabs.onUpdated.addListener(listener);
listener();
return () => {
browser.tabs.onUpdated.removeListener(listener);
};
});
React.useEffect(() => {
const exposedOriginDomains = Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.exposesOrigin())
.map((cluster) => cluster.id);
setExposedOriginDomainCopy('');
switch (exposedOriginDomains.length) {
case 0:
return null;
case 1:
return setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`);
case 2:
return setExposedOriginDomainCopy(
`${exposedOriginDomains[0]} oraz ${exposedOriginDomains[1]}.`
);
case 3:
return setExposedOriginDomainCopy(
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} oraz ${exposedOriginDomains[2]}.`
);
default:
return setExposedOriginDomainCopy(
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} (i ${
exposedOriginDomains.length - 2 < 2 ? 2 : exposedOriginDomains.length - 2
} innych).`
);
}
}, [eventCounts['*'], origin]);
React.useEffect(() => {
const cookieDomains = Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.hasCookies())
.map((cluster) => cluster.id);
setCookieDomainCopy('');
switch (cookieDomains.length) {
case 0:
return null;
case 1:
return setCookieDomainCopy(`${cookieDomains[0]}.`);
case 2:
return setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`);
case 3:
return setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} oraz ${cookieDomains[2]}.`
);
default:
return setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} (i ${
cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2
} innych).`
);
}
}, [eventCounts['*'], origin]);
React.useEffect(() => {
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) {
return setMarksOccurrence(true);
}
}
return setMarksOccurrence(false);
}, [eventCounts['*']]);
function autoMark() {
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
cluster.autoMark();
}
return setMarksOccurrence(true);
}
return (
<div className="toolbar">
<header className="header">
<img src="../../assets/icon-addon.svg" height={32}></img>
<div className="webpage-metadata">
{origin ? (
<>
<span>Analiza strony</span>
<span className="webpage-metadata--hyperlink">{origin}</span>
</>
) : (
<span>Przejdź do wybranej strony internetowej</span>
)}
</div>
{origin ? (
<a href="https://internet-czas-dzialac.pl">
<img src="/assets/icons/info_circle_outline.svg" width="20" height="20" />
</a>
) : null}
</header>
<section className="summary">
<div className="counters-wrapper">
<div className="counters">
<div className="counter counter--browser-history">
<img src="/assets/icons/warning.svg#color" width="24" height="24" />
<span data-event={`${eventCounts['*']}`}>
{
Object.values(getMemory().getClustersForOrigin(origin)).filter(
(cluster) => cluster.exposesOrigin()
).length
}
</span>
</div>
<div className="counter counter--cookies">
<img src="/assets/icons/cookie.svg#color" width="24" height="24" />
<span data-event={`${eventCounts['*']}`}>
{
Object.values(getMemory().getClustersForOrigin(origin)).filter(
(cluster) => cluster.hasCookies()
).length
}
</span>
</div>
</div>
<div className="big-counter" data-event={`${eventCounts['*']}`}>
{Object.values(getMemory().getClustersForOrigin(origin)).length}
</div>
</div>
<span className="notice">Liczba wykrytych domen podmiotów trzecich</span>
</section>
<section className="details">
{exposedOriginDomainCopy ? (
<p
data-event={`${eventCounts['*']}`}
title={Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.exposesOrigin())
.map((domain) => domain.id)
.join(', ')}
>
{first_sentence_cookie}
<strong>{exposedOriginDomainCopy}</strong>
</p>
) : null}
{cookieDomainCopy ? (
<p
data-event={`${eventCounts['*']}`}
title={Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.hasCookies())
.map((domain) => domain.id)
.join(', ')}
>
{first_sentence_history}
<strong>{cookieDomainCopy}</strong>
</p>
) : null}
</section>
{exposedOriginDomainCopy || cookieDomainCopy ? (
<Fragment>
<section className="about">
<p>
Takie przetwarzanie danych może być niezgodne z prawem. Przejdź do
analizy aby pomóc ustalić, czy ta strona nie narusza RODO.
</p>
</section>
<section className="actions">
<button
className="button button--report"
onClick={() => {
autoMark();
window.open(
`/components/sidebar/sidebar.html?origin=${origin}`,
'new_tab'
);
window.close(); // close toolbar popup
}}
>
Przejdź do analizy
</button>
</section>
</Fragment>
) : (
<Fragment>
<section className="about about__no-errors">
<p>Nie znaleziono problemów na tej stronie.</p>
</section>
</Fragment>
)}
</div>
);
};
ReactDOM.render(<Toolbar />, document.getElementById('toolbar'));

View File

@ -44,13 +44,10 @@ let skipReactImports = {
esbuild esbuild
.build({ .build({
entryPoints: [ entryPoints: [
'components/toolbar/toolbar.tsx', 'sidebar/sidebar.tsx',
'components/sidebar/sidebar.tsx',
'test.ts', 'test.ts',
'components/report-window/report-window.tsx', 'report-window/report-window.tsx',
'background.ts', 'background.ts',
'styles/global.scss',
'styles/fonts.scss'
], ],
bundle: true, bundle: true,
// minify: true, // minify: true,

View File

@ -1,11 +1,11 @@
import { StolenDataEntry } from './stolen-data-entry'; import { StolenDataEntry } from "./stolen-data-entry";
import { import {
flattenObjectEntries, flattenObjectEntries,
getshorthost, getshorthost,
parseCookie, parseCookie,
Request, Request,
safeDecodeURIComponent, safeDecodeURIComponent,
} from './util'; } from "./util";
type NameValue = { name: string; value: string }; type NameValue = { name: string; value: string };
@ -24,7 +24,7 @@ export type HAREntry = {
params: (NameValue & { params: (NameValue & {
fileName: string; fileName: string;
contentType: string; contentType: string;
comment: ''; comment: "";
})[]; })[];
text: string; text: string;
}; };
@ -40,10 +40,10 @@ export type HAREntry = {
content: { content: {
mimeType: string; mimeType: string;
size: number; size: number;
encoding: 'base64'; encoding: "base64";
text: string; text: string;
}; };
redirectURL: ''; redirectURL: "";
headersSize: number; headersSize: number;
bodySize: number; bodySize: number;
}; // not relevant }; // not relevant
@ -75,7 +75,7 @@ export default class ExtendedRequest {
public tabId: number; public tabId: number;
public url: string; public url: string;
public shorthost: string; public shorthost: string;
public requestHeaders: Request['requestHeaders'] = []; public requestHeaders: Request["requestHeaders"] = [];
public originalURL: string; public originalURL: string;
public origin: string; public origin: string;
public initialized = false; public initialized = false;
@ -84,27 +84,20 @@ export default class ExtendedRequest {
public requestBody: RequestBody; public requestBody: RequestBody;
static by_id = {} as Record<string, ExtendedRequest>; static by_id = {} as Record<string, ExtendedRequest>;
public data: Request;
constructor(data: Request) { constructor(public data: Request) {
this.tabId = data.tabId; this.tabId = data.tabId;
this.url = data.url; this.url = data.url;
this.shorthost = getshorthost(data.url); this.shorthost = getshorthost(data.url);
this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {}; this.requestBody =
if (this.url.includes('criteo')) { ((data as any).requestBody as undefined | RequestBody) || {};
if (this.url.includes("criteo")) {
console.log(this); console.log(this);
} }
ExtendedRequest.by_id[data.requestId] = this; ExtendedRequest.by_id[data.requestId] = this;
this.data = Object.assign({}, data);
(this.data as any).frameAncestors = [
...(data as any).frameAncestors.map((e: any) => ({ url: e.url })),
];
// console.log('→→→',(this.data as any).frameAncestors, (data as any).frameAncestors);
} }
addHeaders(headers: Request['requestHeaders']) { addHeaders(headers: Request["requestHeaders"]) {
this.requestHeaders = headers; this.requestHeaders = headers;
return this; return this;
} }
@ -120,11 +113,8 @@ export default class ExtendedRequest {
if (this.data.tabId && this.data.tabId >= 0) { if (this.data.tabId && this.data.tabId >= 0) {
const tab = await browser.tabs.get(this.data.tabId); const tab = await browser.tabs.get(this.data.tabId);
url = tab.url; url = tab.url;
} else if ( } else if ((this.data as any)?.frameAncestors) {
(this.data as any)?.frameAncestors && url = (this.data as any).frameAncestors[0].url || "";
(this.data as any).frameAncestors[0] !== undefined
) {
url = (this.data as any).frameAncestors[0].url || '';
} else { } else {
const headers = Object.fromEntries( const headers = Object.fromEntries(
this.requestHeaders.map(({ name, value }) => [name, value]) this.requestHeaders.map(({ name, value }) => [name, value])
@ -158,7 +148,8 @@ export default class ExtendedRequest {
getReferer() { getReferer() {
return ( return (
this.requestHeaders.filter((h) => h.name === 'Referer')[0]?.value || 'missing-referrer' this.requestHeaders.filter((h) => h.name === "Referer")[0]?.value ||
"missing-referrer"
); );
} }
@ -197,9 +188,12 @@ export default class ExtendedRequest {
return []; return [];
} }
return flattenObjectEntries( return flattenObjectEntries(
Object.entries(parseCookie(this.getCookie())).map(([key, value]) => [key, value || '']), Object.entries(parseCookie(this.getCookie())).map(([key, value]) => [
key,
value || "",
]),
StolenDataEntry.parseValue StolenDataEntry.parseValue
).map(([key, value]) => new StolenDataEntry(this, 'cookie', key, value)); ).map(([key, value]) => new StolenDataEntry(this, "cookie", key, value));
} }
getRequestBodyData(): StolenDataEntry[] { getRequestBodyData(): StolenDataEntry[] {
@ -207,65 +201,78 @@ export default class ExtendedRequest {
Object.entries({ Object.entries({
...this.requestBody.formData, ...this.requestBody.formData,
...Object.fromEntries( ...Object.fromEntries(
Object.entries(this.requestBody.raw || {}).map(([key, value], index) => [ Object.entries(
`${key}.${index}`, this.requestBody.raw || {}
value, ).map(([key, value], index) => [`${key}.${index}`, value])
])
), ),
}).map(([key, value]) => { }).map(([key, value]) => {
// to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/ // to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/
if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) { if (
return ['requestBody', key]; (Array.isArray(value) && value.length === 1 && !value[0]) ||
!value
) {
return ["requestBody", key];
} else if (!Array.isArray(value)) { } else if (!Array.isArray(value)) {
return ['raw', String.fromCharCode.apply(null, new Uint8Array(value.bytes))]; return [
"raw",
String.fromCharCode.apply(null, new Uint8Array(value.bytes)),
];
} else { } else {
return [key, value || '']; return [key, value || ""];
} }
}), }),
StolenDataEntry.parseValue StolenDataEntry.parseValue
).map(([key, value]) => new StolenDataEntry(this, 'request_body', key, value)); ).map(
([key, value]) => new StolenDataEntry(this, "request_body", key, value)
);
return ret; return ret;
} }
hasReferer() { hasReferer() {
return this.requestHeaders.some((h) => h.name === 'Referer'); return this.requestHeaders.some((h) => h.name === "Referer");
} }
hasCookie() { hasCookie() {
return this.requestHeaders.some((h) => h.name === 'Cookie'); return this.requestHeaders.some((h) => h.name === "Cookie");
} }
getCookie(): string { getCookie(): string {
return this.requestHeaders.find((h) => h.name == 'Cookie')?.value; return this.requestHeaders.find((h) => h.name == "Cookie")?.value;
} }
getPathParams(): StolenDataEntry[] { getPathParams(): StolenDataEntry[] {
const url = new URL(this.data.url); const url = new URL(this.data.url);
const path = url.pathname; const path = url.pathname;
if (!path.includes(';')) { if (!path.includes(";")) {
return []; return [];
} }
return flattenObjectEntries( return flattenObjectEntries(
path path
.split(';') .split(";")
.map((e) => e.split('=')) .map((e) => e.split("="))
.map(([key, value]) => [key, value || '']) .map(([key, value]) => [key, value || ""])
.map(([key, value]) => { .map(([key, value]) => {
return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))]; return [
key,
StolenDataEntry.parseValue(safeDecodeURIComponent(value)),
];
}) })
).map(([key, value]) => new StolenDataEntry(this, 'pathname', key, value)); ).map(([key, value]) => new StolenDataEntry(this, "pathname", key, value));
} }
getQueryParams(): StolenDataEntry[] { getQueryParams(): StolenDataEntry[] {
const url = new URL(this.data.url); const url = new URL(this.data.url);
return flattenObjectEntries( return flattenObjectEntries(
Array.from((url.searchParams as any).entries()) Array.from((url.searchParams as any).entries())
.map(([key, value]) => [key, value || '']) .map(([key, value]) => [key, value || ""])
.map(([key, value]) => { .map(([key, value]) => {
return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))]; return [
key,
StolenDataEntry.parseValue(safeDecodeURIComponent(value)),
];
}) })
).map(([key, value]) => { ).map(([key, value]) => {
return new StolenDataEntry(this, 'queryparams', key, value); return new StolenDataEntry(this, "queryparams", key, value);
}); });
} }
@ -286,7 +293,7 @@ export default class ExtendedRequest {
StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)), StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)),
]; ];
}) })
).map(([key, value]) => new StolenDataEntry(this, 'header', key, value)); ).map(([key, value]) => new StolenDataEntry(this, "header", key, value));
} }
hasMark() { hasMark() {
@ -309,8 +316,8 @@ export default class ExtendedRequest {
toHAR(): HAREntry { toHAR(): HAREntry {
return { return {
pageref: 'page_1', pageref: "page_1",
startedDateTime: `${new Date().toJSON().replace('Z', '+01:00')}`, startedDateTime: `${new Date().toJSON().replace("Z", "+01:00")}`,
request: { request: {
bodySize: bodySize:
JSON.stringify(this.requestBody.formData || {}).length + JSON.stringify(this.requestBody.formData || {}).length +
@ -320,7 +327,7 @@ export default class ExtendedRequest {
method: this.data.method, method: this.data.method,
url: this.data.url, url: this.data.url,
headersSize: JSON.stringify(this.requestHeaders).length, headersSize: JSON.stringify(this.requestHeaders).length,
httpVersion: 'HTTP/2', httpVersion: "HTTP/2",
headers: this.requestHeaders as NameValue[], headers: this.requestHeaders as NameValue[],
cookies: this.getCookieData().map((cookie) => ({ cookies: this.getCookieData().map((cookie) => ({
name: cookie.name, name: cookie.name,
@ -331,35 +338,35 @@ export default class ExtendedRequest {
value: param.value, value: param.value,
})), })),
postData: { postData: {
mimeType: 'application/x-www-form-urlencoded', mimeType: "application/x-www-form-urlencoded",
params: this.stolenData params: this.stolenData
.filter((e) => e.source == 'request_body') .filter((e) => e.source == "request_body")
.map((e) => ({ .map((e) => ({
name: e.name, name: e.name,
value: e.value, value: e.value,
fileName: '--' + Math.ceil(Math.random() * 1000000000), fileName: "--" + Math.ceil(Math.random() * 1000000000),
contentType: 'text/plain', contentType: "text/plain",
comment: '', comment: "",
})), })),
text: this.stolenData text: this.stolenData
.filter((e) => e.source == 'request_body') .filter((e) => e.source == "request_body")
.map((e) => `${e.name}:\t${StolenDataEntry.parseValue(e.value)}`) .map((e) => `${e.name}:\t${StolenDataEntry.parseValue(e.value)}`)
.join('\n\n'), .join("\n\n"),
}, },
}, },
response: { response: {
status: 200, status: 200,
statusText: 'OK', statusText: "OK",
httpVersion: 'HTTP/2', httpVersion: "HTTP/2",
headers: [], headers: [],
cookies: [], cookies: [],
content: { content: {
mimeType: 'text/plain', mimeType: "text/plain",
size: this.getBalancedPriority(), size: this.getBalancedPriority(),
encoding: 'base64', encoding: "base64",
text: 'ZG9lc24ndCBtYXR0ZXIK', text: "ZG9lc24ndCBtYXR0ZXIK",
}, },
redirectURL: '', redirectURL: "",
headersSize: 15, headersSize: 15,
bodySize: 15, bodySize: 15,
}, },
@ -374,9 +381,9 @@ export default class ExtendedRequest {
receive: 0, receive: 0,
}, },
time: 79, time: 79,
_securityState: 'secure', _securityState: "secure",
serverIPAddress: '31.13.92.36', serverIPAddress: "31.13.92.36",
connection: '443', connection: "443",
}; };
} }
@ -395,10 +402,10 @@ export default class ExtendedRequest {
if (this.hasCookie()) { if (this.hasCookie()) {
result += 50; result += 50;
} }
if (this.stolenData.some((e) => e.classification === 'location')) { if (this.stolenData.some((e) => e.classification === "location")) {
result += 300; result += 300;
} }
if (this.url.includes('facebook')) { if (this.url.includes("facebook")) {
result += 50; result += 50;
} }
return result; return result;

View File

@ -19,7 +19,7 @@
"browser_action": { "browser_action": {
"default_icon": "assets/icon-addon.svg", "default_icon": "assets/icon-addon.svg",
"default_title": "Rentgen", "default_title": "Rentgen",
"default_popup": "components/toolbar/toolbar.html" "default_popup": "sidebar/sidebar.html"
}, },
"icons": { "icons": {
"16": "assets/icon-addon.svg", "16": "assets/icon-addon.svg",

View File

@ -1,18 +1,11 @@
import ExtendedRequest from './extended-request'; import ExtendedRequest from './extended-request';
import { getshorthost, makeThrottle } from './util'; import { getshorthost, makeThrottle } from './util';
import { EventEmitter } from 'events';
import { RequestCluster } from './request-cluster'; import { RequestCluster } from './request-cluster';
import { SaferEmitter } from './safer-emitter';
function setDomainsNumber(counter: number, tabId: number) { export default class Memory extends EventEmitter {
browser.browserAction.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId });
browser.browserAction.setTitle({
title: 'Rentgen',
tabId,
});
}
export default class Memory extends SaferEmitter {
origin_to_history = {} as Record<string, Record<string, RequestCluster>>; origin_to_history = {} as Record<string, Record<string, RequestCluster>>;
private throttle = makeThrottle(100);
async register(request: ExtendedRequest) { async register(request: ExtendedRequest) {
await request.init(); await request.init();
if (!request.isThirdParty()) { if (!request.isThirdParty()) {
@ -27,18 +20,7 @@ export default class Memory extends SaferEmitter {
this.origin_to_history[request.origin][shorthost] = cluster; this.origin_to_history[request.origin][shorthost] = cluster;
} }
this.origin_to_history[request.origin][shorthost].add(request); this.origin_to_history[request.origin][shorthost].add(request);
this.emit('change', shorthost); this.emit('change', false, shorthost, 'registered request(shorthost emit)');
Object.values(this.getClustersForOrigin(request.origin)).some((cluster) =>
cluster.hasCookies()
)
? browser.browserAction.setBadgeBackgroundColor({ color: '#ff726b' })
: browser.browserAction.setBadgeBackgroundColor({ color: '#ffb900' });
setDomainsNumber(
Object.values(this.getClustersForOrigin(request.origin)).length,
request.tabId
);
} }
constructor() { constructor() {
@ -63,9 +45,22 @@ export default class Memory extends SaferEmitter {
); );
} }
emit(eventName: string, data = 'any'): boolean { emit(eventName: string, immediate = false, data = 'any', reason: string) {
console.log('emitting!', eventName, data, reason);
setTimeout(() => super.emit(eventName, data), 0); setTimeout(() => super.emit(eventName, data), 0);
return; return;
try {
if (immediate) {
super.emit(eventName, data);
return;
} else {
this.throttle(() => super.emit(eventName, data));
}
return true;
} catch (e) {
// debugger;
console.error(e);
}
} }
getClustersForOrigin(origin: string): Record<string, RequestCluster> { getClustersForOrigin(origin: string): Record<string, RequestCluster> {

View File

@ -1,4 +1,4 @@
@import './styles/colors.scss'; @import './sidebar/colors.scss';
.options-container { .options-container {
padding-top: 0.5rem; padding-top: 0.5rem;
@ -17,13 +17,9 @@
.label-checkbox { .label-checkbox {
cursor: pointer; cursor: pointer;
margin-left: 0.5rem; margin-left: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
} }
.input-container { .input-container {
font-size: 0.875rem;
font-weight: 500;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
@ -31,48 +27,4 @@
width: 3rem; width: 3rem;
} }
} }
.buttons {
display: flex;
flex-flow: column;
padding: 0.5rem 0 1rem;
.button-container {
padding: 0.25rem 0;
}
button {
border: none;
cursor: pointer;
color: $ultra-black-color;
padding: 0;
display: flex;
flex-wrap: nowrap;
line-height: 1.25rem;
background: #fff;
width: 100%;
&:hover {
color: #000;
text-decoration: underline;
svg path {
fill: #000;
}
}
&:disabled {
cursor: not-allowed;
color: $disabled-grey;
svg path {
fill: $disabled-grey;
}
}
span {
padding-left: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
}
}
}
} }

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react'; import React from 'react';
import './options.scss'; import './options.scss';
export default function Options({ export default function Options({
@ -10,11 +10,8 @@ export default function Options({
setCookiesOrOriginOnly, setCookiesOrOriginOnly,
warningDataDialogAck, warningDataDialogAck,
setWarningDataDialogAck, setWarningDataDialogAck,
detailsVisibility, logoVisibility,
setDetailsVisibility, setLogoVisibility,
setStolenDataView,
removeCookies,
removeRequests,
}: { }: {
minValueLength: number; minValueLength: number;
setMinValueLength: (n: number) => void; setMinValueLength: (n: number) => void;
@ -24,38 +21,28 @@ export default function Options({
setCookiesOrOriginOnly: (b: boolean) => void; setCookiesOrOriginOnly: (b: boolean) => void;
warningDataDialogAck: boolean; warningDataDialogAck: boolean;
setWarningDataDialogAck: (b: boolean) => void; setWarningDataDialogAck: (b: boolean) => void;
detailsVisibility: boolean; logoVisibility: boolean;
setDetailsVisibility: (b: boolean) => void; setLogoVisibility: (b: boolean) => void;
setStolenDataView: (b: boolean) => void;
removeCookies: () => void;
removeRequests: () => void;
}) { }) {
return ( return (
<Fragment>
<nav>
<button onClick={() => setStolenDataView(true)}>
<img src="/assets/icons/short_left.svg" width="20" height="20" />
<span>Wróć do szczegółów</span>
</button>
</nav>
<div className="options-container"> <div className="options-container">
<span>Interfejs</span> <span>Interfejs</span>
<fieldset> <fieldset>
<div className="input-container"> <div className="input-container">
<input <input
type="checkbox" type="checkbox"
id="detailsVisibility" id="logoVisibility"
checked={detailsVisibility} checked={logoVisibility}
onChange={(e) => { onChange={(e) => {
setDetailsVisibility(e.target.checked); setLogoVisibility(e.target.checked);
localStorage.setItem( localStorage.setItem(
'detailsVisibility', 'logoVisibility',
e.target.checked as unknown as string e.target.checked as unknown as string
); );
}} }}
/> />
<label className="label-checkbox" htmlFor="detailsVisibility"> <label className="label-checkbox" htmlFor="logoVisibility">
Wyświetlaj szczegóły pozyskanych danych Wyświetlaj logo <i>Internet. Czas działać!</i>
</label> </label>
</div> </div>
<div className="input-container"> <div className="input-container">
@ -71,7 +58,10 @@ export default function Options({
); );
}} }}
/> />
<label className="label-checkbox" htmlFor="warningDataDialogAck"> <label
className="label-checkbox"
htmlFor="warningDataDialogAck"
>
Wyświetlaj komunikat o pozyskiwanych danych Wyświetlaj komunikat o pozyskiwanych danych
</label> </label>
</div> </div>
@ -85,11 +75,13 @@ export default function Options({
<input <input
type="number" type="number"
id="minValueLength" id="minValueLength"
min={1}
value={minValueLength} value={minValueLength}
onChange={(e) => { onChange={(e) => {
setMinValueLength(parseInt(e.target.value)); setMinValueLength(parseInt(e.target.value));
localStorage.setItem('minValueLength', e.target.value); localStorage.setItem(
'minValueLength',
e.target.value
);
}} }}
/> />
</div> </div>
@ -109,29 +101,19 @@ export default function Options({
type="checkbox" type="checkbox"
id="cookiesOrOriginOnly" id="cookiesOrOriginOnly"
checked={cookiesOrOriginOnly} checked={cookiesOrOriginOnly}
onChange={(e) => setCookiesOrOriginOnly(e.target.checked)} onChange={(e) =>
setCookiesOrOriginOnly(e.target.checked)
}
/> />
<label className="label-checkbox" htmlFor="cookiesOrOriginOnly"> <label
Pokazuj tylko dane z cookiesów lub z częścią historii przeglądania className="label-checkbox"
htmlFor="cookiesOrOriginOnly"
>
Pokazuj tylko dane z cookiesów lub z częścią historii
przeglądania
</label> </label>
</div> </div>
</fieldset> </fieldset>
<span>Narzędzia deweloperskie</span>
<div className="buttons">
<div className="button-container">
<button onClick={() => removeRequests()}>
<img src="/assets/icons/trash_full.svg" width="20" height="20" />
<span>Wyczyść historię wtyczki</span>
</button>
</div> </div>
<div className="button-container">
<button onClick={() => removeCookies()}>
<img src="/assets/icons/cookie.svg" width="20" height="20" />
<span>Wyczyść ciasteczka</span>
</button>
</div>
</div>
</div>
</Fragment>
); );
} }

3076
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,6 @@
"addons-linter": "^4.7.0", "addons-linter": "^4.7.0",
"esbuild": "^0.14.14", "esbuild": "^0.14.14",
"esbuild-plugin-sass": "^1.0.1", "esbuild-plugin-sass": "^1.0.1",
"typescript": "^4.6.4",
"web-ext": "^6.7.0", "web-ext": "^6.7.0",
"web-ext-types": "^3.2.1" "web-ext-types": "^3.2.1"
} }

View File

@ -0,0 +1,19 @@
import { RequestCluster } from '../request-cluster';
import { ParsedAnswers } from './parse-answers';
import NoInformationAtAllProblem from './problems/no-information-at-all';
import { Problem } from './problems/problem';
import { UnlawfulCookieAccess } from './problems/unlawful-cookies';
export default function deduceProblems(
answers: ParsedAnswers,
clusters: Record<string, RequestCluster>
): Problem[] {
const problems = [];
if (answers.popup_type === 'none') {
problems.push(new NoInformationAtAllProblem(answers, clusters));
}
if (UnlawfulCookieAccess.qualifies(answers, Object.values(clusters))) {
problems.push(new UnlawfulCookieAccess(answers, clusters));
}
return problems;
}

View File

@ -0,0 +1,58 @@
import { RequestCluster } from '../request-cluster';
import { getDate } from '../util';
import deduceProblems from './deduce-problems';
import { Explainers } from './explainers';
import { ParsedAnswers } from './parse-answers';
import { v } from './verbs';
declare var PLUGIN_NAME: string;
declare var PLUGIN_URL: string;
export default function EmailContent({
answers,
visited_url,
clusters,
}: {
answers: ParsedAnswers;
visited_url: string;
clusters: Record<string, RequestCluster>;
}) {
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 (
<div style={{ padding: '1rem' }}>
<pre>{JSON.stringify(answers, null, 3)}</pre>
<p>Dzień dobry,</p>
<p>
w dniu {getDate()} {_('odwiedziłem')} stronę {visited_url}. Po podejrzeniu ruchu
sieciowego generowanego przez stronę za pomocą wtyczki{' '}
<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')}{' '}
odpowiedzi nigdzie na Państwa stronie.
</p>
{problems.map((problem) => problem.getEmailContent())}
{explainers.map((explainer) => explainer(answers.zaimek))}
<h2>Państwa rola jako współadministratora danych osobowych</h2>
<p>
{_('Zwracam')} Państwa uwagę na fakt, że w myśl{' '}
<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">
treści wyroku TSUE w sprawie C-40/17
</a>{' '}
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 pytania 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).
</p>
</div>
);
}

View File

@ -0,0 +1,14 @@
export type ExplainerKey = 'cookies_are_pii';
export const Explainers: Record<ExplainerKey, (zaimek_index: 0 | 1 | 2 | 3) => JSX.Element> = {
cookies_are_pii: () => (
<>
<h2>Ciasteczka stanowią dane osobowe</h2>
<p>
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.
</p>
</>
),
};

View File

@ -0,0 +1,382 @@
function generateHostPage(
host: string,
index: number,
all_hosts: string[]
): { title: string; elements: any[]; visibleIf: string } {
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'),
visibleIf: '{popup_type} != "none"',
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"`,
defaultValueExpression:
'iif({popup_action} = "none" or {popup_action} = "closed_popup", "claims_consent_but_sends_before_consent", iif({popup_action} = "accept_all" and {rejection_is_hard} = "yes", "claims_consent_but_there_was_no_easy_refuse", ""))',
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: 'radiogroup',
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: 'radiogroup',
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' },
],
},
{
type: 'radiogroup',
title: `Czy w {Twojej} ocenie wysłanie {Twoich} danych do właściciela domeny ${host} było konieczne do świadczenia zażądanej przez {Ciebie} usługi drogą elektroniczną?`,
name: f('was_processing_necessary'),
...defaultValue('was_processing_necessary'),
visibleIf: `{${f('legal_basis_type')}} = "legitimate_interest" or {${f(
'present'
)}} = "not_mentioned" or {popup_type} = "none"`,
choices: [
{ value: 'yes', text: 'Tak, było konieczne' },
{ value: 'no', text: 'Nie, nie było konieczne' },
{ value: 'not_sure', text: 'Nie mam zdania' },
],
},
],
};
}
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: 'page',
text: 'Tylko w postaci tekstu na podstronie np. "prywatność" lub "polityka cookies"',
},
{
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' or {popup_type} = 'page'",
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" or {popup_type} = "page") ',
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ę”, „Brak zmiany ustawień przeglądarki oznacza zgodę”, „Klikając przycisk "X" (zamknij) wyrażasz 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. Na przykład: „Korzystając ze strony wyrażasz zgodę”, albo „Pozostawiając ustawienia przeglądarki bez zmian (..) wyrażasz zgodę”',
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: 'popup_action',
isRequired: true,
visibleIf: '{popup_type} = "some_choice" or {popup_type} = "passive_popup"',
title: 'Jaką akcję {podjąłeś} w ramach wyskakującego okienka?',
choices: [
{
value: 'none',
text: 'Nic nie {kliknąłem}',
},
{
value: 'closed_popup',
text: '{Zamknąłem} okienko za pomocą przycisku „X” lub „Zamknij”, lub podobnego',
},
{
value: 'accept_all',
text: '{Kliknąłem} przycisk od akceptacji wszystkich zgód',
},
{
value: 'deny_all',
text: '{Odmówiłem} wyrażenia zgody na wszystkie cele',
},
{
value: 'other',
text: 'Coś innego',
},
],
},
{
type: 'text',
name: 'popup_closed_how',
isRequired: true,
visibleIf: '{popup_action} = "closed_popup"',
title: 'W jaki sposób {zamknąłeś} okienko o zgodę? Opisz pełnym zdaniem',
defaultValueExpression: '{Kliknąłem} przycisk „X”.',
},
{
type: 'text',
name: 'popup_deny_all_how',
isRequired: true,
visibleIf: '{popup_action} = "deny_all"',
title: 'W jaki sposób {zamknąłeś} okienko o zgodę? Opisz pełnym zdaniem, np.: „{Kliknąłem} przycisk <Odrzuć wszystkie>” lub „{Odznaczyłem} wszystkie opcje w ustawieniach zaawansowanych”',
defaultValueExpression: '{Kliknąłem} przycisk „odmawiam wyrażenia zgody”.',
},
{
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',
visibleIf: "{popup_type} != 'none'",
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

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { HAREntry } from '../../extended-request'; import { HAREntry } from '../extended-request';
import { StolenDataEntry } from '../../stolen-data-entry'; import { StolenDataEntry } from '../stolen-data-entry';
import { getshorthost, unique } from '../../util'; import { getshorthost, unique } from '../util';
function handleNewFile( function handleNewFile(
element: HTMLInputElement, element: HTMLInputElement,
@ -11,10 +11,13 @@ function handleNewFile(
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener('load', () => { reader.addEventListener('load', () => {
const content = JSON.parse(reader.result as string); const content = JSON.parse(reader.result as string);
content.log.entries = content.log.entries.filter((har_entry: HAREntry) => content.log.entries = content.log.entries.filter(
(har_entry: HAREntry) =>
entries.some((entry) => entry.matchesHAREntry(har_entry)) entries.some((entry) => entry.matchesHAREntry(har_entry))
); );
setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' })); setFiltered(
new Blob([JSON.stringify(content)], { type: 'application/json' })
);
}); });
reader.readAsText(element.files[0]); reader.readAsText(element.files[0]);
} }
@ -27,7 +30,10 @@ function generateFakeHAR(entries: StolenDataEntry[]) {
} else if (request1.shorthost > request2.shorthost) { } else if (request1.shorthost > request2.shorthost) {
return 1; return 1;
} else { } else {
return request2.getBalancedPriority() - request1.getBalancedPriority(); return (
request2.getBalancedPriority() -
request1.getBalancedPriority()
);
} }
}) })
.filter((_, index, array) => { .filter((_, index, array) => {
@ -37,7 +43,10 @@ function generateFakeHAR(entries: StolenDataEntry[]) {
} }
return true; return true;
}) })
.sort((entry1, entry2) => entry2.getBalancedPriority() - entry1.getBalancedPriority()); .sort(
(entry1, entry2) =>
entry2.getBalancedPriority() - entry1.getBalancedPriority()
);
return { return {
log: { log: {
@ -66,10 +75,15 @@ function generateFakeHAR(entries: StolenDataEntry[]) {
}; };
} }
export default function HARConverter({ entries }: { entries: StolenDataEntry[] }) { export default function HARConverter({
entries,
}: {
entries: StolenDataEntry[];
}) {
const [filtered, setFiltered] = React.useState<Blob | null>(null); const [filtered, setFiltered] = React.useState<Blob | null>(null);
const [filename, setFilename] = React.useState(''); const [filename, setFilename] = React.useState('');
const [fakeHAR, setFakeHAR] = React.useState<ReturnType<typeof generateFakeHAR>>(); const [fakeHAR, setFakeHAR] =
React.useState<ReturnType<typeof generateFakeHAR>>();
React.useEffect(() => { React.useEffect(() => {
setFakeHAR(generateFakeHAR(entries)); setFakeHAR(generateFakeHAR(entries));
}, []); }, []);

View File

@ -0,0 +1,67 @@
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'
| 'mentioned_in_policy'
| 'mentioned_in_popup';
legal_basis_type: 'consent' | 'legitimate_interes' | 'not_mentioned';
was_processing_necessary: 'yes' | 'no' | 'not_sure';
} & (
| {
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' }
)) & {
legitimate_interest_activity_specified: 'no' | 'precise' | 'vague';
outside_eu: 'yes' | 'no' | 'not_sure';
legitimate_interest_description?: string;
};
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 ParsedAnswers;
}

View File

@ -0,0 +1,44 @@
import { ExplainerKey } from '../explainers';
import { v } from '../verbs';
import { Problem } from './problem';
export default class NoInformationAtAllProblem extends Problem {
getEmailContent() {
const _ = (word: string) => v(word, this.answers.zaimek);
return (
<>
<h2>Brak informacji na temat przetwarzania danych osobowych</h2>
<p>
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami domen:
</p>
{this.getRangeDescription()}
<p>
Na stronie brakuje jednak jakichkolwiek informacji o tym, jakie cele przetwarzania
takich danych oraz jakie podstawy prawne takiego przetwarzania.
</p>
<p>Zwracam się zatem do Państwa z następującymi pytaniami:</p>
<ul>
<li>Jaka jest tożsamość właścicieli tych domen?</li>
<li>Jaki jest cel takiego przetwarzania danych przez Państwa stronę?</li>
<li>
Jaka jest podstawa prawna takiego przetwarzania moich danych osobowych przez
Państwa stronę?
</li>
</ul>
</>
);
}
getNecessaryExplainers() {
const explainers = [] as Array<ExplainerKey>;
if (
this.getMarkedClusters().some((cluster) => {
console.log(cluster);
return cluster.hasMarkedCookies();
})
) {
explainers.push('cookies_are_pii');
}
return explainers;
}
}

View File

@ -0,0 +1,38 @@
import { RequestCluster } from '../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers';
function formatRange(cluster: RequestCluster) {
const parts = [] as string[];
console.log(cluster);
if (cluster.hasMarkedCookies()) {
parts.push('mojego identyfikatora internetowego pozyskanego z Cookie');
}
if (cluster.exposesOrigin()) {
parts.push('części mojej historii przeglądania');
}
return parts.join(' oraz ');
}
export abstract class Problem {
constructor(public answers: ParsedAnswers, public clusters: Record<string, RequestCluster>) {}
abstract getEmailContent(): JSX.Element;
abstract getNecessaryExplainers(): ExplainerKey[];
getMarkedClusters() {
return Object.values(this.clusters).filter((c) => c.hasMarks());
}
getRangeDescription() {
return (
<ul>
{this.getMarkedClusters().map((cluster) => (
<li key={cluster.id}>
{cluster.id} (w zakresie: {formatRange(cluster)})
</li>
))}
</ul>
);
}
}

View File

@ -0,0 +1,176 @@
import { RequestCluster } from '../../request-cluster';
import { wordlist } from '../../util';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
export class UnlawfulCookieAccess extends Problem {
getNecessaryExplainers(): ExplainerKey[] {
return [];
}
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {
// są cookiesy, nie było zgody, nie są konieczne do działania strony
const cookie_clusters = Object.values(clusters).filter((c) => c.hasMarkedCookies());
return cookie_clusters.some((cluster) => {
const hostAnswers = answers.hosts[cluster.id];
return (
(hostAnswers.present == 'not_mentioned' ||
hostAnswers.present == 'not_before_making_a_choice' ||
['none', 'closed_popup', 'deny_all'].includes(answers.popup_action) ||
answers.popup_type === 'none') &&
hostAnswers.was_processing_necessary != 'yes'
);
});
}
getEmailContent() {
const cookie_clusters = Object.values(this.clusters).filter((c) => c.hasMarkedCookies());
const unnecessary_hosts = Object.entries(this.answers.hosts)
.filter(([, answers]) => answers.was_processing_necessary === 'no')
.map(([host]) => host);
const maybe_unnecessary_hosts = Object.entries(this.answers.hosts)
.filter(([, answers]) => answers.was_processing_necessary === 'not_sure')
.map(([host]) => host);
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Dostęp do cookies niezgodny z ustawą Prawo Telekomunikacyjne</h2>
<p>
Państwa strona dokonała odczytu plików Cookie zapisanych na dysku twardym mojego
komputera. Dotyczy to plików cookie przypisanych do domen:
</p>
<ul>
{cookie_clusters.map((cluster, index) => {
const names = cluster
.getMarkedEntries()
.filter((e) => e.source === 'cookie')
.map((e) => e.name);
return (
<li>
{cluster.id} ({names.length > 1 ? 'pliki' : 'plik'}{' '}
{names.map((name, index) => {
return (
<>
{index > 0 ? ', ' : ''}
{name}
</>
);
})}
){index === cookie_clusters.length - 1 ? '.' : ';'}
</li>
);
})}
</ul>
<p>
Zgodnie z treścią Art. 173.{' '}
<a href="https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU20041711800/U/D20041800Lj.pdf">
ustawy Prawo Telekomunikacyjne
</a>
, strona może pozyskać dostęp do treści plików cookies pod warunkiem spełnienia
jednego z następujących warunków:
</p>
<ol>
<li>
Użytkownik wyraził zgodę na takie przetwarzanie danych <em>po</em> tym, jak został
poinformowany bezpośrednio o celu uzyskania dostępu do tej informacji;
</li>
<li>
Dostęp do treści plików cookies jest konieczny do dostarczania usługi świadczonej
drogą elektroniczną zażądanej przez użytkownika.
</li>
</ol>
{(() => {
if (this.answers.popup_type == 'none' || this.answers.popup_type == 'page') {
return (
<p>
Jako, że strona nie pytała {_('mnie')} nigdy o zgodę, nie jest spełniony
warunek 1.
</p>
);
} else if (this.answers.popup_type === 'passive_popup') {
return (
<p>
Państwa strona nie dała mi nigdy faktycznego wyboru dotyczącego wyrażenia
lub odmówienia zgody na takie przetwarzanie danych osobowych, dlatego nie
jest spełniony warunek 1.{' '}
{this.answers.mentions_passive_consent ? (
<>
Zgody wyrażonej w sposób bierny lub milczący nie można uznać za ważną
w świetle obowiązujących przepisów rozporządzenia 2016/679. Dlatego
zaniechanie zmiany ustawień przeglądarki lub po prostu korzystanie ze
strony nie stanowi ważnej zgody. Takie jest{' '}
<a href="https://assets.midline.pl/pisma/2021-12-16%20odpowiedz%20UODO%20na%20skarg%C4%99%20i(n)Secure.pdf">
stanowisko polskiego UODO
</a>
.
</>
) : (
''
)}
</p>
);
} else if (this.answers.popup_type === 'some_choice') {
if (this.answers.popup_action === 'none') {
return (
<p>
Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')} danych
osobowych. W okienku pytającym o zgodję nic nie {_('kliknąłem')}. Nie
jest zatem spełniony warunek 1.
</p>
);
} else if (this.answers.popup_action === 'closed_popup') {
return (
<p>
Nie {_('wyraziłem')} zgody na takie przetwarzanie {_('moich')} danych
osobowych. {this.answers.popup_closed_how.trim()}
{this.answers.popup_closed_how.trim().at(-1) != '.' ? '.' : ''} Takiego
działania nie można uznać za ważną zgodę na przetwarzanie danych
osobowych, gdyż nie spełnia warunku jednoznaczności opisanego w Art. 4,
pkt 11 RODO. Nie jest zatem spełniony warunek 1.
</p>
);
} else if (this.answers.popup_action == 'deny_all') {
return (
<p>
{this.answers.popup_deny_all_how.trim()}
{this.answers.popup_closed_how.trim().at(-1) != '.' ? '.' : ''} Zatem nie
jest spełniony warunek 1.
</p>
);
}
}
})()}
{unnecessary_hosts.length > 0 ? (
<p>
W {_('mojej')} ocenie odczytywanie przez Państwa stronę treści plików cookies z{' '}
{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{' '}
<em>bez</em> korzystania z plików Cookie.
</p>
) : (
''
)}
<p>
{_('Proszę')} o wskazanie, czy być może stosowali Państwo inną podstawę prawną do
takiego przetwarzania {_('moich')} danych osobowych, czy przetwarzali je państwo bez
ważnej podstawy prawnej?
</p>
{maybe_unnecessary_hosts.length > 1 ? (
<p>
{_('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?
</p>
) : (
''
)}
</>
);
}
}

View File

@ -0,0 +1,35 @@
import { RequestCluster } from '../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers, ParsedHostAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
type UnlawfulDataClassification = 'no_purpose';
export function classifyUnlawfulData(
hostAnswers: ParsedHostAnswers,
cluster: RequestCluster
): UnlawfulDataClassification {
if (hostAnswers.present == 'not_mentioned' && hostAnswers.was_processing_necessary == 'no') {
return 'no_purpose';
}
}
export class UnlawfulData extends Problem {
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {}
getEmailContent() {
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Przetwarzanie danych osobowych bez ważnej podsawy prawnej</h2>
<p>
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami domen:
</p>
{this.getRangeDescription()}
</>
);
}
getNecessaryExplainers() {
return [] as ExplainerKey[];
}
}

View File

@ -13,7 +13,7 @@ export default function Questions({
onComplete: (sender) => onComplete(sender.data), onComplete: (sender) => onComplete(sender.data),
}); });
if (!survey) { if (!survey) {
return <div>Wczytywanie...</div>; return <div>Wczytywanie....</div>;
} }
return <Survey.Survey model={survey} />; return <Survey.Survey model={survey} />;
} }

View File

@ -0,0 +1,60 @@
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_action: 'none' | 'closed_popup' | 'accept_all' | 'deny_all' | 'other';
popup_closed_how: string;
popup_deny_all_how: string;
} & (
| ({
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';
administrator_identity_available_before_choice: 'yes' | 'no';
cookie_wall: undefined;
passive_consent_description: undefined;
mentions_passive_consent: undefined;
}
| {
popup_type: 'none' | 'page';
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

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Treść maila do zgłoszenia</title>
<link rel="stylesheet" href="/lib/report-window/report-window.css" />
<link rel="stylesheet" href="/node_modules/survey-react/survey.css" />
<link rel="stylesheet" href="/node_modules/survey-react/modern.css" />
</head>
<body>
<div id="app"></div>
<script src="/node_modules/react/umd/react.development.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.development.js"></script>
<script src="/lib/report-window/report-window.js"></script>
</body>
</html>

View File

@ -0,0 +1,37 @@
@import '../sidebar/fonts.scss';
@import '../sidebar/colors.scss';
html {
font-size: 1rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
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;
img {
margin-right: 0.5rem;
}
}
p,
li,
h1,
h2,
h3 {
max-width: 70ex;
}

View File

@ -0,0 +1,78 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { getMemory } from '../memory';
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 url = new URL(document.location.toString());
const origin = url.searchParams.get('origin');
const [counter] = useEmitter(getMemory());
const [answers, setAnswers] = React.useState<ParsedAnswers>(
url.searchParams.get('answers') ? JSON.parse(url.searchParams.get('answers')) : null
);
const [mode, setMode] = React.useState(url.searchParams.get('mode') || '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...</>;
* } */
React.useEffect(() => {
const url = new URL(document.location.toString());
url.searchParams.set('origin', origin);
url.searchParams.set('answers', JSON.stringify(answers));
url.searchParams.set('mode', mode);
history.pushState({}, 'Rentgen', url.toString());
}, [mode, answers, origin]);
const visited_url = Object.values(clusters)
.find((cluster) => cluster.getMarkedRequests().length > 0)
?.getMarkedRequests()[0].originalURL;
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>
{mode === 'survey' ? (
<Questions
hosts={Object.values(clusters)
.filter((cluster) => cluster.getMarkedRequests().length > 0)
.map((cluster) => cluster.id)}
onComplete={(answers) => {
setAnswers(parseAnswers(answers));
setMode('preview');
}}
></Questions>
) : (
''
)}
{mode === 'preview' ? <EmailContent {...{ answers, visited_url, clusters }} /> : ''}
{/* <HARConverter {...{ entries }} /> */}
</div>
);
return result;
} catch (e) {
console.error(e);
return <div>ERROR! {JSON.stringify(e)}</div>;
}
}
ReactDOM.render(<Report />, document.getElementById('app'));

45
report-window/verbs.ts Normal file
View File

@ -0,0 +1,45 @@
const words = {
zrobiłem: ['zrobiłem', 'zrobiłam', 'zrobiłom', 'zrobiliśmy'],
szukałem: ['szukałem', 'szukałam', 'szukałom', 'szukaliśmy'],
znalazłem: ['znalazłem', 'znalazłam', 'znalazłom', 'znaleźliśmy'],
moje: ['moje', 'moje', 'moje', 'nasze'],
mojej: ['mojej', 'mojej', 'mojej', 'naszej'],
wyraziłem: ['wyraziłem', 'wyraziłam', 'wyraziłom', 'wyraziliśmy'],
kliknąłem: ['kliknąłem', 'kliknęłam', 'klinkęłom', 'kliknęliśmy'],
odwiedzałeś: ['odwiedzałeś', 'odwiedzałaś', 'odwiedzałoś', 'odwiedzaliście'],
wyraziłeś: ['wyraziłeś', 'wyraziłaś', 'wyraziłoś', 'wyraziliście'],
jesteś: ['jesteś', 'jesteś', 'jesteś', 'jesteście'],
twoich: ['twoich', 'twoich', 'twoich', 'waszych'],
tobie: ['tobie', 'tobie', 'tobie', 'wam'],
twojej: ['twojej', 'twojej', 'twojej', 'waszej'],
odkliknąłeś: ['odkliknąłeś', 'odkliknęłaś', 'odklikęłoś', 'odkliknęliście'],
otwórz: ['otwórz', 'otwórz', 'otwórz', 'otwórzcie'],
widzisz: ['widzisz', 'widzisz', 'widzisz', 'widzicie'],
widzę: ['widzę', 'widzę', 'widzę', 'widzimy'],
widziałem: ['widziałem', 'widziałam', 'widziałom', 'widzieliśmy'],
odwiedziłem: ['odwiedziłem', 'odwiedziłam', 'odwiedziłom', 'odwiedziliśmy'],
mam: ['mam', 'mam', 'mam', 'mamy'],
podjąłeś: ['podjąłeś', 'podjęłaś', 'podjęłoś', 'podjęliście'],
zamknąłem: ['zamknąłem', 'zamknęłom', 'zamknęłom', 'zamknęliśmy'],
zamknąłeś: ['zamknąłeś', 'zamknęłaś', 'zamknęłoś', 'zamknęliście'],
zwracam: ['zwracam', 'zwracam', 'zwracam', 'zwracamy'],
moich: ['moich', 'moich', 'moich', 'naszych'],
ciebie: ['ciebie', 'ciebie', 'ciebie', 'was'],
mnie: ['mnie', 'mnie', 'mnie', 'nas'],
podjąłem: ['podjąłem', 'podjęłam', 'podjęłom', 'podjęliśmy'],
dokonałeś: ['dokonałeś', 'dokonałaś', 'dokonałoś', 'dokonaliście'],
odmówiłeś: ['odmówiłeś', 'odmówiłaś', 'odmówiłoś', 'odmówiliście'],
odznaczyłem: ['odznaczyłem', 'odznaczyłam', 'odznaczyłom', 'odznaczyliśmy'],
proszę: ['proszę', 'proszę', 'proszę', 'prosimy'],
odmówiłem: ['odmówiłem', 'odmówiłam', 'odmówiłom', 'odmówiliśmy'],
} as { [key: string]: string[] };
export default words;
export function v(key: string, index: number) {
let result = words[key.toLowerCase()]?.[index] || key;
if (key[0] == key[0].toUpperCase()) {
result = [result[0].toUpperCase(), ...result.slice(1)].join('');
}
return result;
}

View File

@ -1,13 +1,12 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import ExtendedRequest from './extended-request'; import ExtendedRequest from './extended-request';
import { SaferEmitter } from './safer-emitter';
import { Sources, StolenDataEntry } from './stolen-data-entry'; import { Sources, StolenDataEntry } from './stolen-data-entry';
import { allSubhosts, isSameURL, reduceConcat, unique } from './util'; import { allSubhosts, isSameURL, reduceConcat, unique } from './util';
const source_priority: Array<Sources> = ['cookie', 'pathname', 'queryparams', 'header']; const source_priority: Array<Sources> = ['cookie', 'pathname', 'queryparams', 'header'];
export class RequestCluster extends SaferEmitter { export class RequestCluster extends EventEmitter {
public requests: ExtendedRequest[] = []; public requests: ExtendedRequest[] = [];
public representativeStolenData: StolenDataEntry[] = []; public representativeStolenData: StolenDataEntry[] = [];
public expanded: boolean; public expanded: boolean;
@ -175,11 +174,4 @@ export class RequestCluster extends SaferEmitter {
entry.autoMark(); entry.autoMark();
}); });
} }
undoMark() {
this.calculateRepresentativeStolenData();
this.representativeStolenData.forEach((entry) => {
entry.unmark();
});
}
} }

View File

@ -1,57 +0,0 @@
import EventEmitter from 'events';
export class SaferEmitter extends EventEmitter {
emit(type: string, ...args: unknown[]) {
let doError = type === 'error';
let events = (this as any)._events;
if (events !== undefined) doError = doError && events.error === undefined;
else if (!doError) return false;
// If there is no 'error' event listener then throw.
if (doError) {
let er;
if (args.length > 0) er = args[0];
if (er instanceof Error) {
// Note: The comments on the `throw` lines are intentional, they show
// up in Node's output if this results in an unhandled exception.
throw er; // Unhandled 'error' event
}
// At least give some kind of context to the user
let err = new Error('Unhandled error.' + (er ? ' (' + (er as any).message + ')' : ''));
(err as any).context = er;
throw err; // Unhandled 'error' event
}
let handler = events[type];
if (handler === undefined) return false;
if (typeof handler === 'function') {
try {
Reflect.apply(handler, this, args);
} catch (error) {
events[type] = undefined;
}
} else {
let listeners = [...handler];
listeners
.filter((e) => {
try {
e.call;
} catch (error) {
return false;
}
return true;
})
.forEach((listener) => {
try {
Reflect.apply(listener, this, args);
} catch (error) {
console.error(error);
debugger;
}
});
}
return true;
}
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 KiB

BIN
screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
screenshot3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

View File

@ -1,16 +1,11 @@
$ultra-black-color: #000; $ultra-black-color: #000;
$black-color: #1b1c1d; $black-color: hwb(217.5 18.4% 78.4%);
$disabled-grey: #8a949f; $disabled-grey: #8a949f;
$light-grey: #d1d1d1; $light-grey: #d1d1d1;
$ultra-light-grey: #ededed; $ultra-light-grey: #ededed;
$blue: #0048d9; $blue: #0048d9;
$icd-yellow: #ffee2c; $icd-yellow: #ffee2c;
$icd-rentgen-color: #99ffdd; $icd-rentgen-color: #99ffdd;
$contrast-icd-rentgen-color: #33f9b7;
$highlight-color: #ccffee; $highlight-color: #ccffee;
$row-color: hsl(160, 100%, 94%);
$pale-yellow: #fff8e5; $pale-yellow: #fff8e5;
$pale-red: hsl(10.8, 100%, 94.9%);
$contrast-yellow: #ffb900; $contrast-yellow: #ffb900;
$contrast-red: #ff726b;
$dark-red: hsl(2.8, 100%, 34%);

49
sidebar/fonts.scss Normal file
View File

@ -0,0 +1,49 @@
@font-face {
font-family: 'Fira Code';
src: url('./../assets/fonts/fira-code/woff2/FiraCode-Light.woff2') format('woff2'),
url('./../assets/fonts/fira-code/woff/FiraCode-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('./../assets/fonts/fira-code/woff2/FiraCode-Regular.woff2') format('woff2'),
url('./../assets/fonts/fira-code/woff/FiraCode-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('./../assets/fonts/fira-code/woff2/FiraCode-Medium.woff2') format('woff2'),
url('./../assets/fonts/fira-code/woff/FiraCode-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('./../assets/fonts/fira-code/woff2/FiraCode-SemiBold.woff2') format('woff2'),
url('./../assets/fonts/fira-code/woff/FiraCode-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('./../assets/fonts/fira-code/woff2/FiraCode-Bold.woff2') format('woff2'),
url('./../assets/fonts/fira-code/woff/FiraCode-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Fira Code VF';
src: url('./../assets/fonts/fira-code/woff2/FiraCode-VF.woff2')
format('woff2-variations'),
url('./../assets/fonts/fira-code/woff/FiraCode-VF.woff') format('woff-variations');
/* font-weight requires a range: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide#Using_a_variable_font_font-face_changes */
font-weight: 300 700;
font-style: normal;
}

View File

@ -10,9 +10,14 @@
user-select: none; user-select: none;
} }
body {
min-width: 24rem;
}
html { html {
font-size: 1rem; font-size: 1rem;
font-family: 'OpenSans'; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
} }
button { button {

27
sidebar/sidebar.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
</style>
</head>
<body>
<div id="app"></div>
<link
rel="stylesheet"
href="/lib/sidebar/global.css"
>
<link
rel="stylesheet"
href="/lib/sidebar/sidebar.css"
>
<script src="/node_modules/react/umd/react.production.min.js"></script>
<script src="/node_modules/react-dom/umd/react-dom.production.min.js"></script>
<script src="/lib/sidebar/sidebar.js"></script>
</body>
</html>

View File

@ -1,18 +1,21 @@
@import '../../styles/colors.scss'; @import './colors.scss';
.sidebar { .sidebar {
.header { .header {
display: grid; display: grid;
grid-template-columns: 1.75rem 1fr 10rem; grid-template-columns: 6.67rem 1fr 1.25rem;
align-items: center; align-items: center;
max-height: 3.5rem; max-height: 3.5rem;
min-height: 3.5rem; min-height: 3.5rem;
border-bottom: 1px solid $light-grey; border-bottom: 1px solid $light-grey;
position: sticky; position: sticky;
top: 0; top: 0;
background: #ffffff; background: #fff;
z-index: 1; z-index: 1;
user-select: none;
&--without-logo {
grid-template-columns: 1fr 1.25rem;
}
.webpage-metadata { .webpage-metadata {
word-break: break-all; word-break: break-all;
@ -84,7 +87,9 @@
} }
} }
.dialog-container { .warning-container {
background-color: $pale-yellow;
border-left: 4px solid $contrast-yellow;
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
display: grid; display: grid;
@ -106,52 +111,5 @@
display: flex; display: flex;
align-self: flex-start; align-self: flex-start;
} }
&--info {
background-color: $row-color;
border-left: 4px solid $contrast-icd-rentgen-color;
}
&--warning {
background-color: $pale-yellow;
border-left: 4px solid $contrast-yellow;
}
}
.button {
border: 0;
outline: 0;
height: 3rem;
font-size: 0.875rem;
line-height: 0.875rem;
height: 2.5rem;
cursor: pointer;
background-color: #000 !important;
&--report {
font-weight: 800;
padding: 0 1.5rem;
margin-left: 0.5rem;
background-color: #000;
color: #fff;
&:hover {
color: $icd-rentgen-color;
background-image: linear-gradient(
to right,
$icd-rentgen-color 0%,
$icd-rentgen-color 4%,
#000 4%,
#000 100%
);
animation: slidebg 1s cubic-bezier(0.19, 1, 0.22, 1) infinite;
}
}
@keyframes slidebg {
to {
background-position: 155px;
}
}
} }
} }

221
sidebar/sidebar.tsx Normal file
View File

@ -0,0 +1,221 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Options from '../options';
import { StolenData } from './stolen-data';
import { getshorthost, useEmitter } from '../util';
import { getMemory } from '../memory';
async function getCurrentTab() {
const [tab] = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
return tab;
}
import './global.scss';
import './sidebar.scss';
const Sidebar = () => {
const [origin, setOrigin] = React.useState<string | null>(null);
const [minValueLength, setMinValueLength] = React.useState<number | null>(
localStorage.getItem('minValueLength') === null
? 7
: (localStorage.getItem('minValueLength') as unknown as number)
);
const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false);
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false);
const [eventCounts, setEventCounts] = useEmitter(getMemory());
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false);
const [warningDataDialogAck, setWarningDataDialogAck] = React.useState<boolean>(
localStorage.getItem('warningDataDialogAck') === null
? true
: localStorage.getItem('warningDataDialogAck') == 'true'
? true
: false
);
const [logoVisibility, setLogoVisibility] = React.useState<boolean>(
localStorage.getItem('logoVisibility') === null
? false
: localStorage.getItem('logoVisibility') == 'true'
? true
: false
);
React.useEffect(() => {
const listener = async () => {
console.log('tab change!');
const tab = await getCurrentTab();
const url = new URL(tab.url);
if (url.origin.startsWith('moz-extension')) {
return;
}
setOrigin(url.origin);
};
browser.tabs.onUpdated.addListener(listener);
return () => {
browser.tabs.onUpdated.removeListener(listener);
};
});
React.useEffect(() => {
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) {
return setMarksOccurrence(true);
}
}
return setMarksOccurrence(false);
}, [eventCounts['*'], origin]);
return (
<div className="sidebar">
<header className={logoVisibility ? 'header' : 'header header--without-logo'}>
<img
src="../assets/logo-internet-czas-dzialac.svg"
height={40}
style={!logoVisibility ? { display: 'none' } : null}
></img>
<div
className={
logoVisibility
? 'webpage-metadata'
: 'webpage-metadata webpage-metadata--without-logo'
}
>
{origin ? (
<>
<span>Analiza strony</span>
<span className="webpage-metadata--hyperlink">{origin}</span>
</>
) : (
<span>Przejdź do wybranej strony internetowej</span>
)}
</div>
{stolenDataView ? (
<a href="https://internet-czas-dzialac.pl">
<img src="/assets/icons/info_circle_outline.svg" width="20" height="20" />
</a>
) : (
<button onClick={() => setStolenDataView(true)}>
<img src="/assets/icons/short_left.svg" width="20" height="20" />
</button>
)}
</header>
{stolenDataView ? (
<nav>
<button onClick={() => setStolenDataView(!stolenDataView)}>
<img src="/assets/icons/settings.svg" width="20" height="20" />
<span>Ustawienia</span>
</button>
<button
onClick={() => {
getMemory().removeRequestsFor(origin);
getMemory().emit('change', false, origin, 'clicked trash');
setMarksOccurrence(false);
}}
>
<img src="/assets/icons/trash_full.svg" width="20" height="20" />
<span>Wyczyść historię wtyczki</span>
</button>
<button
onClick={() => {
getMemory().removeCookiesFor(origin);
getMemory().emit('change', false, origin, 'clicked clear cookies');
setMarksOccurrence(false);
}}
>
<img src="/assets/icons/cookie.svg" width="20" height="20" />
<span>Wyczyść ciasteczka</span>
</button>
<button
disabled={!marksOccurrence}
title={
marksOccurrence
? 'Kliknij, aby wygenerować wiadomość'
: 'Zaznacz poniżej elementy, aby móc wygenerować wiadomość'
}
onClick={() => {
const params = [
'height=' + screen.height,
'width=' + screen.width,
'fullscreen=yes',
].join(',');
window.open(
`/report-window/report-window.html?origin=${origin}`,
'new_window',
params
);
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 20H4C2.89543 20 2 19.1046 2 18V5.913C2.04661 4.84255 2.92853 3.99899 4 4H20C21.1046 4 22 4.89543 22 6V18C22 19.1046 21.1046 20 20 20ZM4 7.868V18H20V7.868L12 13.2L4 7.868ZM4.8 6L12 10.8L19.2 6H4.8Z"
fill="#2E3A59"
/>
</svg>
<span>Utwórz wiadomość dla administratora witryny</span>
</button>
</nav>
) : null}
<section>
{stolenDataView ? (
<>
{warningDataDialogAck ? (
<section className="warning-container">
<span>
<strong>Uwaga!</strong> Niekoniecznie każda przechwycona poniżej
informacja jest daną osobową. Niektóre z podanych domen mogą
należeć do właściciela strony i nie reprezentować podmiotów
trzecich.
</span>
<button
onClick={() => {
setWarningDataDialogAck(false);
localStorage.setItem(
'warningDataDialogAck',
false as unknown as string
);
}}
>
<img src="/assets/icons/close_big.svg" width="16" height="16" />
</button>
</section>
) : null}
<StolenData
origin={origin}
eventCounts={eventCounts}
minValueLength={minValueLength}
cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly}
/>
</>
) : (
<Options
minValueLength={minValueLength}
setMinValueLength={setMinValueLength}
cookiesOnly={cookiesOnly}
setCookiesOnly={setCookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly}
setCookiesOrOriginOnly={setCookiesOrOriginOnly}
warningDataDialogAck={warningDataDialogAck}
setWarningDataDialogAck={setWarningDataDialogAck}
logoVisibility={logoVisibility}
setLogoVisibility={setLogoVisibility}
/>
)}
</section>
</div>
);
};
ReactDOM.render(<Sidebar />, document.getElementById('app'));

View File

@ -1,4 +1,4 @@
@import '../../styles/colors.scss'; @import './colors.scss';
.stolen-data-cluster-container { .stolen-data-cluster-container {
.domains-container { .domains-container {
@ -7,16 +7,11 @@
flex-flow: column; flex-flow: column;
border-bottom: none; border-bottom: none;
.domain-checkbox {
margin-right: 0.5rem;
width: 0.875rem;
}
.domain { .domain {
color: $ultra-black-color; color: $ultra-black-color;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
text-decoration: underline; text-decoration: none;
} }
.subdomains-container { .subdomains-container {
@ -88,7 +83,6 @@
word-break: break-all; word-break: break-all;
text-overflow: clip; text-overflow: clip;
padding-left: 0.25rem; padding-left: 0.25rem;
padding-right: 0.25rem;
} }
.icons { .icons {

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { getMemory } from '../../memory'; import { getMemory } from '../memory';
import { StolenDataEntry } from '../../stolen-data-entry'; import { StolenDataEntry } from '../stolen-data-entry';
import { maskString, useEmitter } from '../../util'; import { maskString, useEmitter } from '../util';
import './stolen-data-cluster.scss'; import './stolen-data-cluster.scss';
@ -14,17 +14,19 @@ function StolenDataValue({ entry }: { entry: StolenDataEntry; prefixKey?: string
if (!entry.value) { if (!entry.value) {
body = <></>; body = <></>;
} else { } else {
body = <div data-version={version}>{entry.value}</div>; body = (
<div data-version={version}>{maskString(entry.value, 1, MAX_STRING_VALUE_LENGTH)}</div>
);
} }
return ( return (
<td <td
className="value" className="value"
onClick={(e) => { onClick={(e) => {
entry.toggleMark(); entry.toggleMark();
getMemory().emit('change', entry.request.shorthost); getMemory().emit('change', false, entry.request.shorthost, 'clicked value');
e.stopPropagation(); e.stopPropagation();
}} }}
title={entry.value} title={maskString(entry.value, 1, MAX_STRING_VALUE_LENGTH)}
> >
{body} {body}
</td> </td>
@ -45,11 +47,29 @@ function StolenDataRow({ entry }: { entry: StolenDataEntry }) {
checked={entry.isMarked} checked={entry.isMarked}
onChange={() => { onChange={() => {
entry.toggleMark(); entry.toggleMark();
getMemory().emit('change', entry.request.shorthost); getMemory().emit(
'change',
false,
entry.request.shorthost,
'clicked checkbox'
);
}} }}
/> />
</td> </td>
<th title={`Nazwa: ${entry.name}\nŹródło: ${entry.source}`}>{entry.name}</th> <th
title={`Nazwa: ${entry.name}\nŹródło: ${entry.source}`}
onClick={() => {
entry.toggleMark();
getMemory().emit(
'change',
false,
entry.request.shorthost,
'Clicked entry name'
);
}}
>
{entry.name}
</th>
<td className="icons"> <td className="icons">
{entry.source === 'cookie' ? ( {entry.source === 'cookie' ? (
<span title="Dane przechowywane w Cookies"> <span title="Dane przechowywane w Cookies">
@ -105,7 +125,6 @@ export default function StolenDataCluster({
cookiesOnly, cookiesOnly,
refreshToken, refreshToken,
cookiesOrOriginOnly, cookiesOrOriginOnly,
detailsVisibility,
}: { }: {
origin: string; origin: string;
shorthost: string; shorthost: string;
@ -113,49 +132,26 @@ export default function StolenDataCluster({
minValueLength: number; minValueLength: number;
cookiesOnly: boolean; cookiesOnly: boolean;
cookiesOrOriginOnly: boolean; cookiesOrOriginOnly: boolean;
detailsVisibility: boolean;
}) { }) {
const cluster = getMemory().getClustersForOrigin(origin)[shorthost]; const cluster = getMemory().getClustersForOrigin(origin)[shorthost];
const fullHosts = cluster.getFullHosts(); const fullHosts = cluster.getFullHosts();
const [version] = useEmitter(cluster);
/* console.log('Stolen data cluster!', shorthost, refreshToken); */ /* console.log('Stolen data cluster!', shorthost, refreshToken); */
console.log(cluster.getMarkedEntries());
return ( return (
<div className="stolen-data-cluster-container"> <div className="stolen-data-cluster-container">
<header className="domains-container"> <header className="domains-container">
<div> <a className="domain" href={'https://' + cluster.id}>
<input
type="checkbox"
className="domain-checkbox"
data-version={version}
checked={cluster.hasMarks()}
onChange={() => {
cluster.hasMarks() ? cluster.undoMark() : cluster.autoMark();
getMemory().emit('change', cluster.id);
}}
/>
<a className="domain" href={'https://' + cluster.id} target="_blank">
{cluster.id} {cluster.id}
</a> </a>
</div>
<div className="subdomains-container"> <div className="subdomains-container">
{fullHosts.map((host, index) => ( {fullHosts.map((host, index) => (
<a <a className="subdomain" key={host} href={`https://${host}`}>
className="subdomain"
key={host}
href={`https://${host}`}
target="_blank"
>
{host} {`${fullHosts.length - 1 !== index ? '· ' : ''}`} {host} {`${fullHosts.length - 1 !== index ? '· ' : ''}`}
</a> </a>
))} ))}
</div> </div>
</header> </header>
{detailsVisibility ? (
<section> <section>
<table> <table>
<thead> <thead>
@ -183,7 +179,6 @@ export default function StolenDataCluster({
</tbody> </tbody>
</table> </table>
</section> </section>
) : null}
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
@import '../../styles/colors.scss'; @import './colors.scss';
.stolen-data-container { .stolen-data-container {
padding-top: .5rem; padding-top: .5rem;

View File

@ -1,5 +1,5 @@
import { getMemory } from '../../memory'; import { getMemory } from '../memory';
import { RequestCluster } from '../../request-cluster'; import { RequestCluster } from '../request-cluster';
import StolenDataCluster from './stolen-data-cluster'; import StolenDataCluster from './stolen-data-cluster';
@ -11,14 +11,12 @@ export function StolenData({
eventCounts, eventCounts,
cookiesOnly, cookiesOnly,
cookiesOrOriginOnly, cookiesOrOriginOnly,
detailsVisibility,
}: { }: {
origin: string; origin: string;
eventCounts: Record<string, number>; eventCounts: Record<string, number>;
minValueLength: number; minValueLength: number;
cookiesOnly: boolean; cookiesOnly: boolean;
cookiesOrOriginOnly: boolean; cookiesOrOriginOnly: boolean;
detailsVisibility: boolean;
}) { }) {
if (!origin) { if (!origin) {
return ( return (
@ -35,7 +33,7 @@ export function StolenData({
); );
return ( return (
<div className="stolen-data-container"> <div className="stolen-data-container">
<span>Domeny{detailsVisibility ? ' oraz przesłane informacje' : null}</span> <span>Domeny oraz przesłane informacje</span>
{clusters.map((cluster) => { {clusters.map((cluster) => {
return ( return (
@ -47,7 +45,6 @@ export function StolenData({
minValueLength={minValueLength} minValueLength={minValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly} cookiesOrOriginOnly={cookiesOrOriginOnly}
detailsVisibility={detailsVisibility}
/> />
); );
})} })}

View File

@ -1,6 +1,5 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import ExtendedRequest, { HAREntry } from './extended-request'; import ExtendedRequest, { HAREntry } from './extended-request';
import { SaferEmitter } from './safer-emitter';
import { import {
getshorthost, getshorthost,
@ -34,7 +33,7 @@ const id = (function* id() {
export type DecodingSchema = 'base64' | 'raw'; export type DecodingSchema = 'base64' | 'raw';
export class StolenDataEntry extends SaferEmitter { export class StolenDataEntry extends EventEmitter {
public isIAB = false; public isIAB = false;
public id: number; public id: number;
private marked = false; private marked = false;

View File

@ -1,90 +0,0 @@
@font-face {
font-family: 'Fira Code';
src: url('../assets/fonts/fira-code/woff2/FiraCode-Light.woff2') format('woff2'),
url('../assets/fonts/fira-code/woff/FiraCode-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('../assets/fonts/fira-code/woff2/FiraCode-Regular.woff2') format('woff2'),
url('../assets/fonts/fira-code/woff/FiraCode-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('../assets/fonts/fira-code/woff2/FiraCode-Medium.woff2') format('woff2'),
url('../assets/fonts/fira-code/woff/FiraCode-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('../assets/fonts/fira-code/woff2/FiraCode-SemiBold.woff2') format('woff2'),
url('../assets/fonts/fira-code/woff/FiraCode-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Fira Code';
src: url('../assets/fonts/fira-code/woff2/FiraCode-Bold.woff2') format('woff2'),
url('../assets/fonts/fira-code/woff/FiraCode-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Fira Code VF';
src: url('../assets/fonts/fira-code/woff2/FiraCode-VF.woff2') format('woff2-variations'),
url('../assets/fonts/fira-code/woff/FiraCode-VF.woff') format('woff-variations');
/* font-weight requires a range: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide#Using_a_variable_font_font-face_changes */
font-weight: 300 700;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('../assets/fonts/open-sans/OpenSans-Light-webfont.woff') format('woff');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('../assets/fonts/open-sans/OpenSans-Regular-webfont.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('../assets/fonts/open-sans/OpenSans-Italic-webfont.woff') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'OpenSans';
src: url('../assets/fonts/open-sans/OpenSans-Semibold-webfont.woff') format('woff');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('../assets/fonts/open-sans/OpenSans-Bold-webfont.woff') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('../assets/fonts/open-sans/OpenSans-ExtraBold-webfont.woff') format('woff');
font-weight: 800;
font-style: normal;
}

13
util.ts
View File

@ -56,11 +56,18 @@ export function useEmitter(
}); });
React.useEffect(() => { React.useEffect(() => {
const callback = (eventSubtype: string) => { const callback = (eventSubtype: string) => {
setEventCounts((eventCounts) => ({ setEventCounts((eventCounts) => {
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 },
};
});
}; };
e.on('change', callback); e.on('change', callback);
return () => { return () => {
@ -213,8 +220,6 @@ export function flattenObject(
flattenObject(obj[i], parser, prefix + i, ret); flattenObject(obj[i], parser, prefix + i, ret);
} }
} }
} else if (obj === null) {
ret.push([key, '']);
} 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);