Compare commits
32 Commits
647095ad9e
...
592122a7da
Author | SHA1 | Date | |
---|---|---|---|
|
592122a7da | ||
|
336d28c44d | ||
|
0efaa39d45 | ||
|
e47bb1e9b8 | ||
|
8492022a11 | ||
|
534f9f3db6 | ||
|
9a1287c35b | ||
|
6e121234f1 | ||
58c40efc79 | |||
|
48809c7cc5 | ||
|
83e8498459 | ||
|
ba22788331 | ||
|
aa6d0df3c5 | ||
|
974096bc56 | ||
|
7a2122089e | ||
|
24da4a34dd | ||
fa441b5b81 | |||
|
1961d072bf | ||
|
1dd756f1fa | ||
|
9f81fae747 | ||
|
7cf70b0b0c | ||
|
291189bda4 | ||
|
b92a472569 | ||
|
6faa078095 | ||
|
28ac0201b9 | ||
|
1ba2bea2fb | ||
|
d759727208 | ||
|
51dc008aa7 | ||
|
d8b6e05efb | ||
|
39a7e34cb4 | ||
eed5aba0ec | |||
|
51872621b7 |
2
.gitignore
vendored
|
@ -3,3 +3,5 @@ node_modules
|
|||
sidebar.js
|
||||
/web-ext-artifacts/
|
||||
lib/*
|
||||
/yarn-error.log
|
||||
/rentgen.zip
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
trailingComma: "es5"
|
||||
tabWidth: 3
|
||||
tabWidth: 4
|
||||
printWidth: 100
|
||||
semi: true
|
||||
singleQuote: true
|
||||
|
|
|
@ -15,6 +15,3 @@ Note: At the moment, we support Polish language because this extension generates
|
|||
3. `npm run build`
|
||||
4. The build code is located in the `lib/` directory
|
||||
|
||||
![screenshot](./screenshot.png)
|
||||
![screenshot](./screenshot2.png)
|
||||
![screenshot](./screenshot3.png)
|
||||
|
|
BIN
assets/fonts/open-sans/OpenSans-Bold-webfont.woff
Normal file
BIN
assets/fonts/open-sans/OpenSans-ExtraBold-webfont.woff
Normal file
BIN
assets/fonts/open-sans/OpenSans-Italic-webfont.woff
Normal file
BIN
assets/fonts/open-sans/OpenSans-Light-webfont.woff
Normal file
BIN
assets/fonts/open-sans/OpenSans-Regular-webfont.woff
Normal file
BIN
assets/fonts/open-sans/OpenSans-Semibold-webfont.woff
Normal file
3
assets/icons/bulb.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 814 B |
|
@ -1,40 +1,47 @@
|
|||
<?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="svg827"
|
||||
sodipodi:docname="cookie.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
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="defs831" />
|
||||
<sodipodi:namedview
|
||||
id="namedview829"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="31.291667"
|
||||
inkscape:cx="1.9174434"
|
||||
inkscape:cy="11.984021"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg827" />
|
||||
<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"
|
||||
fill="#2E3A59"
|
||||
id="path825"
|
||||
style="fill:#000000" />
|
||||
</svg>
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg827"
|
||||
sodipodi:docname="cookie.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
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"
|
||||
>
|
||||
<style>
|
||||
path:target {
|
||||
fill: #ff726b !important;
|
||||
}
|
||||
</style>
|
||||
<defs id="defs831" />
|
||||
<sodipodi:namedview
|
||||
id="namedview829"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="31.291667"
|
||||
inkscape:cx="1.9174434"
|
||||
inkscape:cy="11.984021"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg827"
|
||||
/>
|
||||
<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"
|
||||
fill="#2E3A59"
|
||||
id="color"
|
||||
style="fill:#000000"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.1 KiB |
3
assets/icons/file_find.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 764 B |
3
assets/icons/file_minus.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 798 B |
41
assets/icons/laptop.svg
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?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>
|
After Width: | Height: | Size: 1.3 KiB |
41
assets/icons/report.svg
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?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>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,40 +1,47 @@
|
|||
<?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="svg1282"
|
||||
sodipodi:docname="warning.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
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="defs1286" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1284"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="31.291667"
|
||||
inkscape:cx="12.015979"
|
||||
inkscape:cy="11.984021"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1282" />
|
||||
<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"
|
||||
fill="#2E3A59"
|
||||
id="path1280"
|
||||
style="fill:#000000" />
|
||||
</svg>
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1282"
|
||||
sodipodi:docname="warning.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
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"
|
||||
>
|
||||
<style>
|
||||
path:target {
|
||||
fill: #ffb900 !important;
|
||||
}
|
||||
</style>
|
||||
<defs id="defs1286" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1284"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="31.291667"
|
||||
inkscape:cx="12.015979"
|
||||
inkscape:cy="11.984021"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1282"
|
||||
/>
|
||||
<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"
|
||||
fill="#2E3A59"
|
||||
id="color"
|
||||
style="fill:#000000"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
19
components/report-window/deduce-problems.tsx
Normal 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;
|
||||
}
|
55
components/report-window/email-content.scss
Normal file
|
@ -0,0 +1,55 @@
|
|||
@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;
|
||||
}
|
||||
}
|
100
components/report-window/email-content.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
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 tę 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 ją do
|
||||
administratorów witryny {visited_url.split('/').slice(0, 3).join('/')}.
|
||||
</p>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
14
components/report-window/explainers.tsx
Normal 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>
|
||||
</>
|
||||
),
|
||||
};
|
399
components/report-window/generate-survey-questions.ts
Normal file
|
@ -0,0 +1,399 @@
|
|||
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} tę
|
||||
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),
|
||||
],
|
||||
};
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { HAREntry } from '../extended-request';
|
||||
import { StolenDataEntry } from '../stolen-data-entry';
|
||||
import { getshorthost, unique } from '../util';
|
||||
import { HAREntry } from '../../extended-request';
|
||||
import { StolenDataEntry } from '../../stolen-data-entry';
|
||||
import { getshorthost, unique } from '../../util';
|
||||
|
||||
function handleNewFile(
|
||||
element: HTMLInputElement,
|
||||
|
@ -11,13 +11,10 @@ function handleNewFile(
|
|||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const content = JSON.parse(reader.result as string);
|
||||
content.log.entries = content.log.entries.filter(
|
||||
(har_entry: HAREntry) =>
|
||||
entries.some((entry) => entry.matchesHAREntry(har_entry))
|
||||
);
|
||||
setFiltered(
|
||||
new Blob([JSON.stringify(content)], { type: 'application/json' })
|
||||
content.log.entries = content.log.entries.filter((har_entry: HAREntry) =>
|
||||
entries.some((entry) => entry.matchesHAREntry(har_entry))
|
||||
);
|
||||
setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' }));
|
||||
});
|
||||
reader.readAsText(element.files[0]);
|
||||
}
|
||||
|
@ -30,10 +27,7 @@ function generateFakeHAR(entries: StolenDataEntry[]) {
|
|||
} else if (request1.shorthost > request2.shorthost) {
|
||||
return 1;
|
||||
} else {
|
||||
return (
|
||||
request2.getBalancedPriority() -
|
||||
request1.getBalancedPriority()
|
||||
);
|
||||
return request2.getBalancedPriority() - request1.getBalancedPriority();
|
||||
}
|
||||
})
|
||||
.filter((_, index, array) => {
|
||||
|
@ -43,10 +37,7 @@ function generateFakeHAR(entries: StolenDataEntry[]) {
|
|||
}
|
||||
return true;
|
||||
})
|
||||
.sort(
|
||||
(entry1, entry2) =>
|
||||
entry2.getBalancedPriority() - entry1.getBalancedPriority()
|
||||
);
|
||||
.sort((entry1, entry2) => entry2.getBalancedPriority() - entry1.getBalancedPriority());
|
||||
|
||||
return {
|
||||
log: {
|
||||
|
@ -75,15 +66,10 @@ 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 [filename, setFilename] = React.useState('');
|
||||
const [fakeHAR, setFakeHAR] =
|
||||
React.useState<ReturnType<typeof generateFakeHAR>>();
|
||||
const [fakeHAR, setFakeHAR] = React.useState<ReturnType<typeof generateFakeHAR>>();
|
||||
React.useEffect(() => {
|
||||
setFakeHAR(generateFakeHAR(entries));
|
||||
}, []);
|
67
components/report-window/parse-answers.ts
Normal 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;
|
||||
}
|
45
components/report-window/problems/no-information-at-all.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
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 są właścicielami
|
||||
domen:
|
||||
</p>
|
||||
{this.getRangeDescription()}
|
||||
<p>
|
||||
Na stronie brakuje jednak jakichkolwiek informacji o tym, jakie są cele
|
||||
przetwarzania takich danych oraz jakie są 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;
|
||||
}
|
||||
}
|
38
components/report-window/problems/problem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
183
components/report-window/problems/unlawful-cookies.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
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>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
36
components/report-window/problems/unlawful-data.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
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 są właścicielami
|
||||
domen:
|
||||
</p>
|
||||
{this.getRangeDescription()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
getNecessaryExplainers() {
|
||||
return [] as ExplainerKey[];
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ export default function Questions({
|
|||
onComplete: (sender) => onComplete(sender.data),
|
||||
});
|
||||
if (!survey) {
|
||||
return <div>Wczytywanie....</div>;
|
||||
return <div>Wczytywanie...</div>;
|
||||
}
|
||||
return <Survey.Survey model={survey} />;
|
||||
}
|
60
components/report-window/raw-answers.ts
Normal 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;
|
37
components/report-window/report-window.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!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>
|
276
components/report-window/report-window.scss
Normal file
|
@ -0,0 +1,276 @@
|
|||
@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);
|
||||
}
|
84
components/report-window/report-window.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
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'));
|
108
components/report-window/screenshot-generator.scss
Normal file
|
@ -0,0 +1,108 @@
|
|||
@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;
|
||||
}
|
||||
}
|
199
components/report-window/screenshot-generator.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
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 są 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>
|
||||
);
|
||||
}
|
50
components/report-window/verbs.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
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;
|
||||
}
|
31
components/sidebar/sidebar.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!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>
|
|
@ -1,21 +1,18 @@
|
|||
@import './colors.scss';
|
||||
@import '../../styles/colors.scss';
|
||||
|
||||
.sidebar {
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: 6.67rem 1fr 1.25rem;
|
||||
grid-template-columns: 1.75rem 1fr 10rem;
|
||||
align-items: center;
|
||||
max-height: 3.5rem;
|
||||
min-height: 3.5rem;
|
||||
border-bottom: 1px solid $light-grey;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
background: #ffffff;
|
||||
z-index: 1;
|
||||
|
||||
&--without-logo {
|
||||
grid-template-columns: 1fr 1.25rem;
|
||||
}
|
||||
user-select: none;
|
||||
|
||||
.webpage-metadata {
|
||||
word-break: break-all;
|
||||
|
@ -87,9 +84,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.warning-container {
|
||||
background-color: $pale-yellow;
|
||||
border-left: 4px solid $contrast-yellow;
|
||||
.dialog-container {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
display: grid;
|
||||
|
@ -111,5 +106,52 @@
|
|||
display: flex;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
243
components/sidebar/sidebar.tsx
Normal file
|
@ -0,0 +1,243 @@
|
|||
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'));
|
|
@ -1,4 +1,4 @@
|
|||
@import './colors.scss';
|
||||
@import '../../styles/colors.scss';
|
||||
|
||||
.stolen-data-cluster-container {
|
||||
.domains-container {
|
||||
|
@ -7,11 +7,16 @@
|
|||
flex-flow: column;
|
||||
border-bottom: none;
|
||||
|
||||
.domain-checkbox {
|
||||
margin-right: 0.5rem;
|
||||
width: 0.875rem;
|
||||
}
|
||||
|
||||
.domain {
|
||||
color: $ultra-black-color;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.subdomains-container {
|
||||
|
@ -83,6 +88,7 @@
|
|||
word-break: break-all;
|
||||
text-overflow: clip;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.icons {
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { getMemory } from '../memory';
|
||||
import { StolenDataEntry } from '../stolen-data-entry';
|
||||
import { getMemory } from '../../memory';
|
||||
import { StolenDataEntry } from '../../stolen-data-entry';
|
||||
|
||||
import { maskString, useEmitter } from '../util';
|
||||
import { maskString, useEmitter } from '../../util';
|
||||
|
||||
import './stolen-data-cluster.scss';
|
||||
|
||||
|
@ -14,19 +14,17 @@ function StolenDataValue({ entry }: { entry: StolenDataEntry; prefixKey?: string
|
|||
if (!entry.value) {
|
||||
body = <></>;
|
||||
} else {
|
||||
body = (
|
||||
<div data-version={version}>{maskString(entry.value, 1, MAX_STRING_VALUE_LENGTH)}</div>
|
||||
);
|
||||
body = <div data-version={version}>{entry.value}</div>;
|
||||
}
|
||||
return (
|
||||
<td
|
||||
className="value"
|
||||
onClick={(e) => {
|
||||
entry.toggleMark();
|
||||
getMemory().emit('change', false, entry.request.shorthost, 'clicked value');
|
||||
getMemory().emit('change', entry.request.shorthost);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
title={maskString(entry.value, 1, MAX_STRING_VALUE_LENGTH)}
|
||||
title={entry.value}
|
||||
>
|
||||
{body}
|
||||
</td>
|
||||
|
@ -47,29 +45,11 @@ function StolenDataRow({ entry }: { entry: StolenDataEntry }) {
|
|||
checked={entry.isMarked}
|
||||
onChange={() => {
|
||||
entry.toggleMark();
|
||||
getMemory().emit(
|
||||
'change',
|
||||
false,
|
||||
entry.request.shorthost,
|
||||
'clicked checkbox'
|
||||
);
|
||||
getMemory().emit('change', entry.request.shorthost);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<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>
|
||||
<th title={`Nazwa: ${entry.name}\nŹródło: ${entry.source}`}>{entry.name}</th>
|
||||
<td className="icons">
|
||||
{entry.source === 'cookie' ? (
|
||||
<span title="Dane przechowywane w Cookies">
|
||||
|
@ -125,6 +105,7 @@ export default function StolenDataCluster({
|
|||
cookiesOnly,
|
||||
refreshToken,
|
||||
cookiesOrOriginOnly,
|
||||
detailsVisibility,
|
||||
}: {
|
||||
origin: string;
|
||||
shorthost: string;
|
||||
|
@ -132,53 +113,77 @@ export default function StolenDataCluster({
|
|||
minValueLength: number;
|
||||
cookiesOnly: boolean;
|
||||
cookiesOrOriginOnly: boolean;
|
||||
detailsVisibility: boolean;
|
||||
}) {
|
||||
const cluster = getMemory().getClustersForOrigin(origin)[shorthost];
|
||||
const fullHosts = cluster.getFullHosts();
|
||||
const [version] = useEmitter(cluster);
|
||||
|
||||
/* console.log('Stolen data cluster!', shorthost, refreshToken); */
|
||||
|
||||
console.log(cluster.getMarkedEntries());
|
||||
|
||||
return (
|
||||
<div className="stolen-data-cluster-container">
|
||||
<header className="domains-container">
|
||||
<a className="domain" href={'https://' + cluster.id}>
|
||||
{cluster.id}
|
||||
</a>
|
||||
<div>
|
||||
<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}
|
||||
</a>
|
||||
</div>
|
||||
<div className="subdomains-container">
|
||||
{fullHosts.map((host, index) => (
|
||||
<a className="subdomain" key={host} href={`https://${host}`}>
|
||||
<a
|
||||
className="subdomain"
|
||||
key={host}
|
||||
href={`https://${host}`}
|
||||
target="_blank"
|
||||
>
|
||||
{host} {`${fullHosts.length - 1 !== index ? '· ' : ''}`}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
<section>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="table-header" colSpan={4}>
|
||||
Wysłane dane:
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cluster
|
||||
.calculateRepresentativeStolenData({
|
||||
minValueLength,
|
||||
cookiesOnly,
|
||||
cookiesOrOriginOnly,
|
||||
})
|
||||
.map((entry) => (
|
||||
<StolenDataRow
|
||||
{...{
|
||||
entry,
|
||||
key: entry.id,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
{detailsVisibility ? (
|
||||
<section>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="table-header" colSpan={4}>
|
||||
Wysłane dane:
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cluster
|
||||
.calculateRepresentativeStolenData({
|
||||
minValueLength,
|
||||
cookiesOnly,
|
||||
cookiesOrOriginOnly,
|
||||
})
|
||||
.map((entry) => (
|
||||
<StolenDataRow
|
||||
{...{
|
||||
entry,
|
||||
key: entry.id,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@import './colors.scss';
|
||||
@import '../../styles/colors.scss';
|
||||
|
||||
.stolen-data-container {
|
||||
padding-top: .5rem;
|
|
@ -1,5 +1,5 @@
|
|||
import { getMemory } from '../memory';
|
||||
import { RequestCluster } from '../request-cluster';
|
||||
import { getMemory } from '../../memory';
|
||||
import { RequestCluster } from '../../request-cluster';
|
||||
|
||||
import StolenDataCluster from './stolen-data-cluster';
|
||||
|
||||
|
@ -11,12 +11,14 @@ export function StolenData({
|
|||
eventCounts,
|
||||
cookiesOnly,
|
||||
cookiesOrOriginOnly,
|
||||
detailsVisibility,
|
||||
}: {
|
||||
origin: string;
|
||||
eventCounts: Record<string, number>;
|
||||
minValueLength: number;
|
||||
cookiesOnly: boolean;
|
||||
cookiesOrOriginOnly: boolean;
|
||||
detailsVisibility: boolean;
|
||||
}) {
|
||||
if (!origin) {
|
||||
return (
|
||||
|
@ -33,7 +35,7 @@ export function StolenData({
|
|||
);
|
||||
return (
|
||||
<div className="stolen-data-container">
|
||||
<span>Domeny oraz przesłane informacje</span>
|
||||
<span>Domeny{detailsVisibility ? ' oraz przesłane informacje' : null}</span>
|
||||
|
||||
{clusters.map((cluster) => {
|
||||
return (
|
||||
|
@ -45,6 +47,7 @@ export function StolenData({
|
|||
minValueLength={minValueLength}
|
||||
cookiesOnly={cookiesOnly}
|
||||
cookiesOrOriginOnly={cookiesOrOriginOnly}
|
||||
detailsVisibility={detailsVisibility}
|
||||
/>
|
||||
);
|
||||
})}
|
26
components/toolbar/toolbar.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!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>
|
172
components/toolbar/toolbar.scss
Normal file
|
@ -0,0 +1,172 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
238
components/toolbar/toolbar.tsx
Normal file
|
@ -0,0 +1,238 @@
|
|||
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'));
|
|
@ -44,10 +44,13 @@ let skipReactImports = {
|
|||
esbuild
|
||||
.build({
|
||||
entryPoints: [
|
||||
'sidebar/sidebar.tsx',
|
||||
'components/toolbar/toolbar.tsx',
|
||||
'components/sidebar/sidebar.tsx',
|
||||
'test.ts',
|
||||
'report-window/report-window.tsx',
|
||||
'components/report-window/report-window.tsx',
|
||||
'background.ts',
|
||||
'styles/global.scss',
|
||||
'styles/fonts.scss'
|
||||
],
|
||||
bundle: true,
|
||||
// minify: true,
|
||||
|
|
|
@ -1,413 +1,406 @@
|
|||
import { StolenDataEntry } from "./stolen-data-entry";
|
||||
import { StolenDataEntry } from './stolen-data-entry';
|
||||
import {
|
||||
flattenObjectEntries,
|
||||
getshorthost,
|
||||
parseCookie,
|
||||
Request,
|
||||
safeDecodeURIComponent,
|
||||
} from "./util";
|
||||
flattenObjectEntries,
|
||||
getshorthost,
|
||||
parseCookie,
|
||||
Request,
|
||||
safeDecodeURIComponent,
|
||||
} from './util';
|
||||
|
||||
type NameValue = { name: string; value: string };
|
||||
|
||||
export type HAREntry = {
|
||||
pageref: string;
|
||||
startedDateTime: string;
|
||||
request: {
|
||||
bodySize: number;
|
||||
cookies: NameValue[];
|
||||
headers: NameValue[];
|
||||
headersSize: number;
|
||||
httpVersion: string;
|
||||
method: string;
|
||||
postData?: {
|
||||
mimeType: string;
|
||||
params: (NameValue & {
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
comment: "";
|
||||
})[];
|
||||
text: string;
|
||||
pageref: string;
|
||||
startedDateTime: string;
|
||||
request: {
|
||||
bodySize: number;
|
||||
cookies: NameValue[];
|
||||
headers: NameValue[];
|
||||
headersSize: number;
|
||||
httpVersion: string;
|
||||
method: string;
|
||||
postData?: {
|
||||
mimeType: string;
|
||||
params: (NameValue & {
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
comment: '';
|
||||
})[];
|
||||
text: string;
|
||||
};
|
||||
queryString: NameValue[];
|
||||
url: string;
|
||||
};
|
||||
queryString: NameValue[];
|
||||
url: string;
|
||||
};
|
||||
response: {
|
||||
status: number;
|
||||
statusText: string;
|
||||
httpVersion: string;
|
||||
headers: NameValue[];
|
||||
cookies: NameValue[];
|
||||
content: {
|
||||
mimeType: string;
|
||||
size: number;
|
||||
encoding: "base64";
|
||||
text: string;
|
||||
};
|
||||
redirectURL: "";
|
||||
headersSize: number;
|
||||
bodySize: number;
|
||||
}; // not relevant
|
||||
cache: {};
|
||||
timings: {};
|
||||
time: number;
|
||||
_securityState: string;
|
||||
serverIPAddress: string;
|
||||
connection: string;
|
||||
response: {
|
||||
status: number;
|
||||
statusText: string;
|
||||
httpVersion: string;
|
||||
headers: NameValue[];
|
||||
cookies: NameValue[];
|
||||
content: {
|
||||
mimeType: string;
|
||||
size: number;
|
||||
encoding: 'base64';
|
||||
text: string;
|
||||
};
|
||||
redirectURL: '';
|
||||
headersSize: number;
|
||||
bodySize: number;
|
||||
}; // not relevant
|
||||
cache: {};
|
||||
timings: {};
|
||||
time: number;
|
||||
_securityState: string;
|
||||
serverIPAddress: string;
|
||||
connection: string;
|
||||
};
|
||||
|
||||
const whitelisted_cookies = [
|
||||
/^Accept.*$/,
|
||||
/^Host$/,
|
||||
/^Connection$/,
|
||||
/^Sec-Fetch-.*$/,
|
||||
/^Content-Type$/,
|
||||
/^Cookie$/, // we're extracting it in getCookie separately anyway
|
||||
/^User-Agent$/,
|
||||
/^Accept.*$/,
|
||||
/^Host$/,
|
||||
/^Connection$/,
|
||||
/^Sec-Fetch-.*$/,
|
||||
/^Content-Type$/,
|
||||
/^Cookie$/, // we're extracting it in getCookie separately anyway
|
||||
/^User-Agent$/,
|
||||
];
|
||||
|
||||
type RequestBody = {
|
||||
error?: string;
|
||||
formData?: Record<string, string[]>;
|
||||
raw?: { bytes: ArrayBuffer; file?: string }[];
|
||||
error?: string;
|
||||
formData?: Record<string, string[]>;
|
||||
raw?: { bytes: ArrayBuffer; file?: string }[];
|
||||
};
|
||||
|
||||
export default class ExtendedRequest {
|
||||
public tabId: number;
|
||||
public url: string;
|
||||
public shorthost: string;
|
||||
public requestHeaders: Request["requestHeaders"] = [];
|
||||
public originalURL: string;
|
||||
public origin: string;
|
||||
public initialized = false;
|
||||
public stolenData: StolenDataEntry[];
|
||||
public originalPathname: string;
|
||||
public requestBody: RequestBody;
|
||||
public tabId: number;
|
||||
public url: string;
|
||||
public shorthost: string;
|
||||
public requestHeaders: Request['requestHeaders'] = [];
|
||||
public originalURL: string;
|
||||
public origin: string;
|
||||
public initialized = false;
|
||||
public stolenData: StolenDataEntry[];
|
||||
public originalPathname: string;
|
||||
public requestBody: RequestBody;
|
||||
|
||||
static by_id = {} as Record<string, ExtendedRequest>;
|
||||
static by_id = {} as Record<string, ExtendedRequest>;
|
||||
public data: Request;
|
||||
|
||||
constructor(public data: Request) {
|
||||
this.tabId = data.tabId;
|
||||
this.url = data.url;
|
||||
this.shorthost = getshorthost(data.url);
|
||||
this.requestBody =
|
||||
((data as any).requestBody as undefined | RequestBody) || {};
|
||||
if (this.url.includes("criteo")) {
|
||||
console.log(this);
|
||||
}
|
||||
ExtendedRequest.by_id[data.requestId] = this;
|
||||
}
|
||||
|
||||
addHeaders(headers: Request["requestHeaders"]) {
|
||||
this.requestHeaders = headers;
|
||||
return this;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.cacheOrigin();
|
||||
this.initialized = true;
|
||||
this.stolenData = this.getAllStolenData();
|
||||
}
|
||||
|
||||
async cacheOrigin(): Promise<void> {
|
||||
let url: string;
|
||||
if (this.data.tabId && this.data.tabId >= 0) {
|
||||
const tab = await browser.tabs.get(this.data.tabId);
|
||||
url = tab.url;
|
||||
} else if ((this.data as any)?.frameAncestors) {
|
||||
url = (this.data as any).frameAncestors[0].url || "";
|
||||
} else {
|
||||
const headers = Object.fromEntries(
|
||||
this.requestHeaders.map(({ name, value }) => [name, value])
|
||||
);
|
||||
if (headers.Referer) {
|
||||
url = headers.Referer;
|
||||
} else {
|
||||
url = this.data.url;
|
||||
}
|
||||
}
|
||||
|
||||
this.originalURL = url;
|
||||
this.origin = new URL(url).origin;
|
||||
this.originalPathname = new URL(url).pathname;
|
||||
}
|
||||
|
||||
isThirdParty() {
|
||||
const request_url = new URL(this.data.url);
|
||||
const origin_url = new URL(this.originalURL);
|
||||
if (request_url.host.includes(origin_url.host)) {
|
||||
return false;
|
||||
}
|
||||
if (getshorthost(request_url.host) == getshorthost(origin_url.host)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
request_url.origin != origin_url.origin ||
|
||||
(this.data as any).urlClassification.thirdParty.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
getReferer() {
|
||||
return (
|
||||
this.requestHeaders.filter((h) => h.name === "Referer")[0]?.value ||
|
||||
"missing-referrer"
|
||||
);
|
||||
}
|
||||
|
||||
exposesOrigin() {
|
||||
const url = new URL(this.originalURL);
|
||||
const host = url.host;
|
||||
const path = url.pathname;
|
||||
const shorthost = getshorthost(host);
|
||||
if (this.getReferer().includes(shorthost)) {
|
||||
return true;
|
||||
}
|
||||
for (const entry of this.stolenData) {
|
||||
if (
|
||||
entry.value.includes(host) ||
|
||||
entry.value.includes(path) ||
|
||||
entry.value.includes(shorthost)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getAllStolenData(): StolenDataEntry[] {
|
||||
return [
|
||||
...this.getPathParams(),
|
||||
...this.getCookieData(),
|
||||
...this.getQueryParams(),
|
||||
...this.getHeadersData(),
|
||||
...this.getRequestBodyData(),
|
||||
];
|
||||
}
|
||||
|
||||
getCookieData(): StolenDataEntry[] {
|
||||
if (!this.hasCookie() || this.getCookie() === undefined) {
|
||||
return [];
|
||||
}
|
||||
return flattenObjectEntries(
|
||||
Object.entries(parseCookie(this.getCookie())).map(([key, value]) => [
|
||||
key,
|
||||
value || "",
|
||||
]),
|
||||
StolenDataEntry.parseValue
|
||||
).map(([key, value]) => new StolenDataEntry(this, "cookie", key, value));
|
||||
}
|
||||
|
||||
getRequestBodyData(): StolenDataEntry[] {
|
||||
const ret = flattenObjectEntries(
|
||||
Object.entries({
|
||||
...this.requestBody.formData,
|
||||
...Object.fromEntries(
|
||||
Object.entries(
|
||||
this.requestBody.raw || {}
|
||||
).map(([key, value], index) => [`${key}.${index}`, value])
|
||||
),
|
||||
}).map(([key, value]) => {
|
||||
// to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/
|
||||
if (
|
||||
(Array.isArray(value) && value.length === 1 && !value[0]) ||
|
||||
!value
|
||||
) {
|
||||
return ["requestBody", key];
|
||||
} else if (!Array.isArray(value)) {
|
||||
return [
|
||||
"raw",
|
||||
String.fromCharCode.apply(null, new Uint8Array(value.bytes)),
|
||||
];
|
||||
} else {
|
||||
return [key, value || ""];
|
||||
constructor(data: Request) {
|
||||
this.tabId = data.tabId;
|
||||
this.url = data.url;
|
||||
this.shorthost = getshorthost(data.url);
|
||||
this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {};
|
||||
if (this.url.includes('criteo')) {
|
||||
console.log(this);
|
||||
}
|
||||
}),
|
||||
StolenDataEntry.parseValue
|
||||
).map(
|
||||
([key, value]) => new StolenDataEntry(this, "request_body", key, value)
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
ExtendedRequest.by_id[data.requestId] = this;
|
||||
|
||||
hasReferer() {
|
||||
return this.requestHeaders.some((h) => h.name === "Referer");
|
||||
}
|
||||
this.data = Object.assign({}, data);
|
||||
(this.data as any).frameAncestors = [
|
||||
...(data as any).frameAncestors.map((e: any) => ({ url: e.url })),
|
||||
];
|
||||
|
||||
hasCookie() {
|
||||
return this.requestHeaders.some((h) => h.name === "Cookie");
|
||||
}
|
||||
|
||||
getCookie(): string {
|
||||
return this.requestHeaders.find((h) => h.name == "Cookie")?.value;
|
||||
}
|
||||
|
||||
getPathParams(): StolenDataEntry[] {
|
||||
const url = new URL(this.data.url);
|
||||
const path = url.pathname;
|
||||
if (!path.includes(";")) {
|
||||
return [];
|
||||
// console.log('→→→',(this.data as any).frameAncestors, (data as any).frameAncestors);
|
||||
}
|
||||
return flattenObjectEntries(
|
||||
path
|
||||
.split(";")
|
||||
.map((e) => e.split("="))
|
||||
.map(([key, value]) => [key, value || ""])
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
StolenDataEntry.parseValue(safeDecodeURIComponent(value)),
|
||||
];
|
||||
})
|
||||
).map(([key, value]) => new StolenDataEntry(this, "pathname", key, value));
|
||||
}
|
||||
|
||||
getQueryParams(): StolenDataEntry[] {
|
||||
const url = new URL(this.data.url);
|
||||
return flattenObjectEntries(
|
||||
Array.from((url.searchParams as any).entries())
|
||||
.map(([key, value]) => [key, value || ""])
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
StolenDataEntry.parseValue(safeDecodeURIComponent(value)),
|
||||
];
|
||||
})
|
||||
).map(([key, value]) => {
|
||||
return new StolenDataEntry(this, "queryparams", key, value);
|
||||
});
|
||||
}
|
||||
addHeaders(headers: Request['requestHeaders']) {
|
||||
this.requestHeaders = headers;
|
||||
return this;
|
||||
}
|
||||
|
||||
getHeadersData(): StolenDataEntry[] {
|
||||
return flattenObjectEntries(
|
||||
this.requestHeaders
|
||||
.filter((header) => {
|
||||
for (const regex of whitelisted_cookies) {
|
||||
if (regex.test(header.name)) {
|
||||
return false;
|
||||
async init() {
|
||||
await this.cacheOrigin();
|
||||
this.initialized = true;
|
||||
this.stolenData = this.getAllStolenData();
|
||||
}
|
||||
|
||||
async cacheOrigin(): Promise<void> {
|
||||
let url: string;
|
||||
if (this.data.tabId && this.data.tabId >= 0) {
|
||||
const tab = await browser.tabs.get(this.data.tabId);
|
||||
url = tab.url;
|
||||
} else if (
|
||||
(this.data as any)?.frameAncestors &&
|
||||
(this.data as any).frameAncestors[0] !== undefined
|
||||
) {
|
||||
url = (this.data as any).frameAncestors[0].url || '';
|
||||
} else {
|
||||
const headers = Object.fromEntries(
|
||||
this.requestHeaders.map(({ name, value }) => [name, value])
|
||||
);
|
||||
if (headers.Referer) {
|
||||
url = headers.Referer;
|
||||
} else {
|
||||
url = this.data.url;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((header) => {
|
||||
return [
|
||||
header.name,
|
||||
StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)),
|
||||
];
|
||||
})
|
||||
).map(([key, value]) => new StolenDataEntry(this, "header", key, value));
|
||||
}
|
||||
}
|
||||
|
||||
hasMark() {
|
||||
return this.stolenData.some((data) => data.isMarked);
|
||||
}
|
||||
|
||||
getMarkedEntries() {
|
||||
return this.stolenData.filter((data) => data.isMarked);
|
||||
}
|
||||
|
||||
getHost() {
|
||||
return new URL(this.url).host;
|
||||
}
|
||||
|
||||
matchesHAREntry(har: HAREntry): boolean {
|
||||
const rq = this.data;
|
||||
const hrq = har.request;
|
||||
return rq.url == hrq.url;
|
||||
}
|
||||
|
||||
toHAR(): HAREntry {
|
||||
return {
|
||||
pageref: "page_1",
|
||||
startedDateTime: `${new Date().toJSON().replace("Z", "+01:00")}`,
|
||||
request: {
|
||||
bodySize:
|
||||
JSON.stringify(this.requestBody.formData || {}).length +
|
||||
(this.requestBody.raw || [])
|
||||
.map((e) => e.bytes.byteLength)
|
||||
.reduce((a, b) => a + b, 0),
|
||||
method: this.data.method,
|
||||
url: this.data.url,
|
||||
headersSize: JSON.stringify(this.requestHeaders).length,
|
||||
httpVersion: "HTTP/2",
|
||||
headers: this.requestHeaders as NameValue[],
|
||||
cookies: this.getCookieData().map((cookie) => ({
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
})),
|
||||
queryString: this.getQueryParams().map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
})),
|
||||
postData: {
|
||||
mimeType: "application/x-www-form-urlencoded",
|
||||
params: this.stolenData
|
||||
.filter((e) => e.source == "request_body")
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
value: e.value,
|
||||
fileName: "--" + Math.ceil(Math.random() * 1000000000),
|
||||
contentType: "text/plain",
|
||||
comment: "",
|
||||
})),
|
||||
text: this.stolenData
|
||||
.filter((e) => e.source == "request_body")
|
||||
.map((e) => `${e.name}:\t${StolenDataEntry.parseValue(e.value)}`)
|
||||
.join("\n\n"),
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
httpVersion: "HTTP/2",
|
||||
headers: [],
|
||||
cookies: [],
|
||||
content: {
|
||||
mimeType: "text/plain",
|
||||
size: this.getBalancedPriority(),
|
||||
encoding: "base64",
|
||||
text: "ZG9lc24ndCBtYXR0ZXIK",
|
||||
},
|
||||
redirectURL: "",
|
||||
headersSize: 15,
|
||||
bodySize: 15,
|
||||
},
|
||||
cache: {},
|
||||
timings: {
|
||||
blocked: -1,
|
||||
dns: 0,
|
||||
connect: 0,
|
||||
ssl: 0,
|
||||
send: 0,
|
||||
wait: 79,
|
||||
receive: 0,
|
||||
},
|
||||
time: 79,
|
||||
_securityState: "secure",
|
||||
serverIPAddress: "31.13.92.36",
|
||||
connection: "443",
|
||||
};
|
||||
}
|
||||
|
||||
getMaxPriority(): number {
|
||||
return Math.max(...this.stolenData.map((entry) => entry.getPriority()));
|
||||
}
|
||||
|
||||
getBalancedPriority(): number {
|
||||
let result = 0;
|
||||
if (this.stolenData.some((e) => e.exposesPath())) {
|
||||
result += 50;
|
||||
this.originalURL = url;
|
||||
this.origin = new URL(url).origin;
|
||||
this.originalPathname = new URL(url).pathname;
|
||||
}
|
||||
if (this.stolenData.some((e) => e.exposesHost())) {
|
||||
result += 50;
|
||||
|
||||
isThirdParty() {
|
||||
const request_url = new URL(this.data.url);
|
||||
const origin_url = new URL(this.originalURL);
|
||||
if (request_url.host.includes(origin_url.host)) {
|
||||
return false;
|
||||
}
|
||||
if (getshorthost(request_url.host) == getshorthost(origin_url.host)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
request_url.origin != origin_url.origin ||
|
||||
(this.data as any).urlClassification.thirdParty.length > 0
|
||||
);
|
||||
}
|
||||
if (this.hasCookie()) {
|
||||
result += 50;
|
||||
|
||||
getReferer() {
|
||||
return (
|
||||
this.requestHeaders.filter((h) => h.name === 'Referer')[0]?.value || 'missing-referrer'
|
||||
);
|
||||
}
|
||||
if (this.stolenData.some((e) => e.classification === "location")) {
|
||||
result += 300;
|
||||
|
||||
exposesOrigin() {
|
||||
const url = new URL(this.originalURL);
|
||||
const host = url.host;
|
||||
const path = url.pathname;
|
||||
const shorthost = getshorthost(host);
|
||||
if (this.getReferer().includes(shorthost)) {
|
||||
return true;
|
||||
}
|
||||
for (const entry of this.stolenData) {
|
||||
if (
|
||||
entry.value.includes(host) ||
|
||||
entry.value.includes(path) ||
|
||||
entry.value.includes(shorthost)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (this.url.includes("facebook")) {
|
||||
result += 50;
|
||||
|
||||
private getAllStolenData(): StolenDataEntry[] {
|
||||
return [
|
||||
...this.getPathParams(),
|
||||
...this.getCookieData(),
|
||||
...this.getQueryParams(),
|
||||
...this.getHeadersData(),
|
||||
...this.getRequestBodyData(),
|
||||
];
|
||||
}
|
||||
|
||||
getCookieData(): StolenDataEntry[] {
|
||||
if (!this.hasCookie() || this.getCookie() === undefined) {
|
||||
return [];
|
||||
}
|
||||
return flattenObjectEntries(
|
||||
Object.entries(parseCookie(this.getCookie())).map(([key, value]) => [key, value || '']),
|
||||
StolenDataEntry.parseValue
|
||||
).map(([key, value]) => new StolenDataEntry(this, 'cookie', key, value));
|
||||
}
|
||||
|
||||
getRequestBodyData(): StolenDataEntry[] {
|
||||
const ret = flattenObjectEntries(
|
||||
Object.entries({
|
||||
...this.requestBody.formData,
|
||||
...Object.fromEntries(
|
||||
Object.entries(this.requestBody.raw || {}).map(([key, value], index) => [
|
||||
`${key}.${index}`,
|
||||
value,
|
||||
])
|
||||
),
|
||||
}).map(([key, value]) => {
|
||||
// to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/
|
||||
if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) {
|
||||
return ['requestBody', key];
|
||||
} else if (!Array.isArray(value)) {
|
||||
return ['raw', String.fromCharCode.apply(null, new Uint8Array(value.bytes))];
|
||||
} else {
|
||||
return [key, value || ''];
|
||||
}
|
||||
}),
|
||||
StolenDataEntry.parseValue
|
||||
).map(([key, value]) => new StolenDataEntry(this, 'request_body', key, value));
|
||||
return ret;
|
||||
}
|
||||
|
||||
hasReferer() {
|
||||
return this.requestHeaders.some((h) => h.name === 'Referer');
|
||||
}
|
||||
|
||||
hasCookie() {
|
||||
return this.requestHeaders.some((h) => h.name === 'Cookie');
|
||||
}
|
||||
|
||||
getCookie(): string {
|
||||
return this.requestHeaders.find((h) => h.name == 'Cookie')?.value;
|
||||
}
|
||||
|
||||
getPathParams(): StolenDataEntry[] {
|
||||
const url = new URL(this.data.url);
|
||||
const path = url.pathname;
|
||||
if (!path.includes(';')) {
|
||||
return [];
|
||||
}
|
||||
return flattenObjectEntries(
|
||||
path
|
||||
.split(';')
|
||||
.map((e) => e.split('='))
|
||||
.map(([key, value]) => [key, value || ''])
|
||||
.map(([key, value]) => {
|
||||
return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))];
|
||||
})
|
||||
).map(([key, value]) => new StolenDataEntry(this, 'pathname', key, value));
|
||||
}
|
||||
|
||||
getQueryParams(): StolenDataEntry[] {
|
||||
const url = new URL(this.data.url);
|
||||
return flattenObjectEntries(
|
||||
Array.from((url.searchParams as any).entries())
|
||||
.map(([key, value]) => [key, value || ''])
|
||||
.map(([key, value]) => {
|
||||
return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))];
|
||||
})
|
||||
).map(([key, value]) => {
|
||||
return new StolenDataEntry(this, 'queryparams', key, value);
|
||||
});
|
||||
}
|
||||
|
||||
getHeadersData(): StolenDataEntry[] {
|
||||
return flattenObjectEntries(
|
||||
this.requestHeaders
|
||||
.filter((header) => {
|
||||
for (const regex of whitelisted_cookies) {
|
||||
if (regex.test(header.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((header) => {
|
||||
return [
|
||||
header.name,
|
||||
StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)),
|
||||
];
|
||||
})
|
||||
).map(([key, value]) => new StolenDataEntry(this, 'header', key, value));
|
||||
}
|
||||
|
||||
hasMark() {
|
||||
return this.stolenData.some((data) => data.isMarked);
|
||||
}
|
||||
|
||||
getMarkedEntries() {
|
||||
return this.stolenData.filter((data) => data.isMarked);
|
||||
}
|
||||
|
||||
getHost() {
|
||||
return new URL(this.url).host;
|
||||
}
|
||||
|
||||
matchesHAREntry(har: HAREntry): boolean {
|
||||
const rq = this.data;
|
||||
const hrq = har.request;
|
||||
return rq.url == hrq.url;
|
||||
}
|
||||
|
||||
toHAR(): HAREntry {
|
||||
return {
|
||||
pageref: 'page_1',
|
||||
startedDateTime: `${new Date().toJSON().replace('Z', '+01:00')}`,
|
||||
request: {
|
||||
bodySize:
|
||||
JSON.stringify(this.requestBody.formData || {}).length +
|
||||
(this.requestBody.raw || [])
|
||||
.map((e) => e.bytes.byteLength)
|
||||
.reduce((a, b) => a + b, 0),
|
||||
method: this.data.method,
|
||||
url: this.data.url,
|
||||
headersSize: JSON.stringify(this.requestHeaders).length,
|
||||
httpVersion: 'HTTP/2',
|
||||
headers: this.requestHeaders as NameValue[],
|
||||
cookies: this.getCookieData().map((cookie) => ({
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
})),
|
||||
queryString: this.getQueryParams().map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
})),
|
||||
postData: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: this.stolenData
|
||||
.filter((e) => e.source == 'request_body')
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
value: e.value,
|
||||
fileName: '--' + Math.ceil(Math.random() * 1000000000),
|
||||
contentType: 'text/plain',
|
||||
comment: '',
|
||||
})),
|
||||
text: this.stolenData
|
||||
.filter((e) => e.source == 'request_body')
|
||||
.map((e) => `${e.name}:\t${StolenDataEntry.parseValue(e.value)}`)
|
||||
.join('\n\n'),
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
httpVersion: 'HTTP/2',
|
||||
headers: [],
|
||||
cookies: [],
|
||||
content: {
|
||||
mimeType: 'text/plain',
|
||||
size: this.getBalancedPriority(),
|
||||
encoding: 'base64',
|
||||
text: 'ZG9lc24ndCBtYXR0ZXIK',
|
||||
},
|
||||
redirectURL: '',
|
||||
headersSize: 15,
|
||||
bodySize: 15,
|
||||
},
|
||||
cache: {},
|
||||
timings: {
|
||||
blocked: -1,
|
||||
dns: 0,
|
||||
connect: 0,
|
||||
ssl: 0,
|
||||
send: 0,
|
||||
wait: 79,
|
||||
receive: 0,
|
||||
},
|
||||
time: 79,
|
||||
_securityState: 'secure',
|
||||
serverIPAddress: '31.13.92.36',
|
||||
connection: '443',
|
||||
};
|
||||
}
|
||||
|
||||
getMaxPriority(): number {
|
||||
return Math.max(...this.stolenData.map((entry) => entry.getPriority()));
|
||||
}
|
||||
|
||||
getBalancedPriority(): number {
|
||||
let result = 0;
|
||||
if (this.stolenData.some((e) => e.exposesPath())) {
|
||||
result += 50;
|
||||
}
|
||||
if (this.stolenData.some((e) => e.exposesHost())) {
|
||||
result += 50;
|
||||
}
|
||||
if (this.hasCookie()) {
|
||||
result += 50;
|
||||
}
|
||||
if (this.stolenData.some((e) => e.classification === 'location')) {
|
||||
result += 300;
|
||||
}
|
||||
if (this.url.includes('facebook')) {
|
||||
result += 50;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"browser_action": {
|
||||
"default_icon": "assets/icon-addon.svg",
|
||||
"default_title": "Rentgen",
|
||||
"default_popup": "sidebar/sidebar.html"
|
||||
"default_popup": "components/toolbar/toolbar.html"
|
||||
},
|
||||
"icons": {
|
||||
"16": "assets/icon-addon.svg",
|
||||
|
|
41
memory.ts
|
@ -1,11 +1,18 @@
|
|||
import ExtendedRequest from './extended-request';
|
||||
import { getshorthost, makeThrottle } from './util';
|
||||
import { EventEmitter } from 'events';
|
||||
import { RequestCluster } from './request-cluster';
|
||||
import { SaferEmitter } from './safer-emitter';
|
||||
|
||||
export default class Memory extends EventEmitter {
|
||||
function setDomainsNumber(counter: number, tabId: number) {
|
||||
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>>;
|
||||
private throttle = makeThrottle(100);
|
||||
async register(request: ExtendedRequest) {
|
||||
await request.init();
|
||||
if (!request.isThirdParty()) {
|
||||
|
@ -20,7 +27,18 @@ export default class Memory extends EventEmitter {
|
|||
this.origin_to_history[request.origin][shorthost] = cluster;
|
||||
}
|
||||
this.origin_to_history[request.origin][shorthost].add(request);
|
||||
this.emit('change', false, shorthost, 'registered request(shorthost emit)');
|
||||
this.emit('change', shorthost);
|
||||
|
||||
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() {
|
||||
|
@ -45,22 +63,9 @@ export default class Memory extends EventEmitter {
|
|||
);
|
||||
}
|
||||
|
||||
emit(eventName: string, immediate = false, data = 'any', reason: string) {
|
||||
console.log('emitting!', eventName, data, reason);
|
||||
emit(eventName: string, data = 'any'): boolean {
|
||||
setTimeout(() => super.emit(eventName, data), 0);
|
||||
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> {
|
||||
|
|
50
options.scss
|
@ -1,4 +1,4 @@
|
|||
@import './sidebar/colors.scss';
|
||||
@import './styles/colors.scss';
|
||||
|
||||
.options-container {
|
||||
padding-top: 0.5rem;
|
||||
|
@ -17,9 +17,13 @@
|
|||
.label-checkbox {
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
|
@ -27,4 +31,48 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
206
options.tsx
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import './options.scss';
|
||||
|
||||
export default function Options({
|
||||
|
@ -10,8 +10,11 @@ export default function Options({
|
|||
setCookiesOrOriginOnly,
|
||||
warningDataDialogAck,
|
||||
setWarningDataDialogAck,
|
||||
logoVisibility,
|
||||
setLogoVisibility,
|
||||
detailsVisibility,
|
||||
setDetailsVisibility,
|
||||
setStolenDataView,
|
||||
removeCookies,
|
||||
removeRequests,
|
||||
}: {
|
||||
minValueLength: number;
|
||||
setMinValueLength: (n: number) => void;
|
||||
|
@ -21,99 +24,114 @@ export default function Options({
|
|||
setCookiesOrOriginOnly: (b: boolean) => void;
|
||||
warningDataDialogAck: boolean;
|
||||
setWarningDataDialogAck: (b: boolean) => void;
|
||||
logoVisibility: boolean;
|
||||
setLogoVisibility: (b: boolean) => void;
|
||||
detailsVisibility: boolean;
|
||||
setDetailsVisibility: (b: boolean) => void;
|
||||
setStolenDataView: (b: boolean) => void;
|
||||
removeCookies: () => void;
|
||||
removeRequests: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="options-container">
|
||||
<span>Interfejs</span>
|
||||
<fieldset>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="logoVisibility"
|
||||
checked={logoVisibility}
|
||||
onChange={(e) => {
|
||||
setLogoVisibility(e.target.checked);
|
||||
localStorage.setItem(
|
||||
'logoVisibility',
|
||||
e.target.checked as unknown as string
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label className="label-checkbox" htmlFor="logoVisibility">
|
||||
Wyświetlaj logo <i>Internet. Czas działać!</i>
|
||||
</label>
|
||||
<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">
|
||||
<span>Interfejs</span>
|
||||
<fieldset>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="detailsVisibility"
|
||||
checked={detailsVisibility}
|
||||
onChange={(e) => {
|
||||
setDetailsVisibility(e.target.checked);
|
||||
localStorage.setItem(
|
||||
'detailsVisibility',
|
||||
e.target.checked as unknown as string
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label className="label-checkbox" htmlFor="detailsVisibility">
|
||||
Wyświetlaj szczegóły pozyskanych danych
|
||||
</label>
|
||||
</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="warningDataDialogAck"
|
||||
checked={warningDataDialogAck}
|
||||
onChange={(e) => {
|
||||
setWarningDataDialogAck(e.target.checked);
|
||||
localStorage.setItem(
|
||||
'warningDataDialogAck',
|
||||
e.target.checked as unknown as string
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label className="label-checkbox" htmlFor="warningDataDialogAck">
|
||||
Wyświetlaj komunikat o pozyskiwanych danych
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<span>Ustawienia zaawansowane</span>
|
||||
<fieldset>
|
||||
<div className="input-container">
|
||||
<label htmlFor="minValueLength">
|
||||
Pokazuj tylko wartości o długości co najmniej{' '}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="minValueLength"
|
||||
min={1}
|
||||
value={minValueLength}
|
||||
onChange={(e) => {
|
||||
setMinValueLength(parseInt(e.target.value));
|
||||
localStorage.setItem('minValueLength', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cookiesOnly"
|
||||
checked={cookiesOnly}
|
||||
onChange={(e) => setCookiesOnly(e.target.checked)}
|
||||
/>
|
||||
<label className="label-checkbox" htmlFor="cookiesOnly">
|
||||
Pokazuj tylko dane z cookiesów
|
||||
</label>
|
||||
</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cookiesOrOriginOnly"
|
||||
checked={cookiesOrOriginOnly}
|
||||
onChange={(e) => setCookiesOrOriginOnly(e.target.checked)}
|
||||
/>
|
||||
<label className="label-checkbox" htmlFor="cookiesOrOriginOnly">
|
||||
Pokazuj tylko dane z cookiesów lub z częścią historii przeglądania
|
||||
</label>
|
||||
</div>
|
||||
</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 className="button-container">
|
||||
<button onClick={() => removeCookies()}>
|
||||
<img src="/assets/icons/cookie.svg" width="20" height="20" />
|
||||
<span>Wyczyść ciasteczka</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="warningDataDialogAck"
|
||||
checked={warningDataDialogAck}
|
||||
onChange={(e) => {
|
||||
setWarningDataDialogAck(e.target.checked);
|
||||
localStorage.setItem(
|
||||
'warningDataDialogAck',
|
||||
e.target.checked as unknown as string
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="label-checkbox"
|
||||
htmlFor="warningDataDialogAck"
|
||||
>
|
||||
Wyświetlaj komunikat o pozyskiwanych danych
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<span>Ustawienia zaawansowane</span>
|
||||
<fieldset>
|
||||
<div className="input-container">
|
||||
<label htmlFor="minValueLength">
|
||||
Pokazuj tylko wartości o długości co najmniej{' '}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="minValueLength"
|
||||
value={minValueLength}
|
||||
onChange={(e) => {
|
||||
setMinValueLength(parseInt(e.target.value));
|
||||
localStorage.setItem(
|
||||
'minValueLength',
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cookiesOnly"
|
||||
checked={cookiesOnly}
|
||||
onChange={(e) => setCookiesOnly(e.target.checked)}
|
||||
/>
|
||||
<label className="label-checkbox" htmlFor="cookiesOnly">
|
||||
Pokazuj tylko dane z cookiesów
|
||||
</label>
|
||||
</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cookiesOrOriginOnly"
|
||||
checked={cookiesOrOriginOnly}
|
||||
onChange={(e) =>
|
||||
setCookiesOrOriginOnly(e.target.checked)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className="label-checkbox"
|
||||
htmlFor="cookiesOrOriginOnly"
|
||||
>
|
||||
Pokazuj tylko dane z cookiesów lub z częścią historii
|
||||
przeglądania
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
3076
package-lock.json
generated
|
@ -46,6 +46,7 @@
|
|||
"addons-linter": "^4.7.0",
|
||||
"esbuild": "^0.14.14",
|
||||
"esbuild-plugin-sass": "^1.0.1",
|
||||
"typescript": "^4.6.4",
|
||||
"web-ext": "^6.7.0",
|
||||
"web-ext-types": "^3.2.1"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,58 +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';
|
||||
|
||||
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 tę 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
),
|
||||
};
|
|
@ -1,382 +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"`,
|
||||
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} tę 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),
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,44 +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 są właścicielami domen:
|
||||
</p>
|
||||
{this.getRangeDescription()}
|
||||
<p>
|
||||
Na stronie brakuje jednak jakichkolwiek informacji o tym, jakie są cele przetwarzania
|
||||
takich danych oraz jakie są 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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,176 +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>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,35 +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 są właścicielami domen:
|
||||
</p>
|
||||
{this.getRangeDescription()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
getNecessaryExplainers() {
|
||||
return [] as ExplainerKey[];
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -1,17 +0,0 @@
|
|||
<!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>
|
|
@ -1,37 +0,0 @@
|
|||
@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;
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
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'));
|
|
@ -1,45 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import ExtendedRequest from './extended-request';
|
||||
import { SaferEmitter } from './safer-emitter';
|
||||
import { Sources, StolenDataEntry } from './stolen-data-entry';
|
||||
|
||||
import { allSubhosts, isSameURL, reduceConcat, unique } from './util';
|
||||
|
||||
const source_priority: Array<Sources> = ['cookie', 'pathname', 'queryparams', 'header'];
|
||||
|
||||
export class RequestCluster extends EventEmitter {
|
||||
export class RequestCluster extends SaferEmitter {
|
||||
public requests: ExtendedRequest[] = [];
|
||||
public representativeStolenData: StolenDataEntry[] = [];
|
||||
public expanded: boolean;
|
||||
|
@ -174,4 +175,11 @@ export class RequestCluster extends EventEmitter {
|
|||
entry.autoMark();
|
||||
});
|
||||
}
|
||||
|
||||
undoMark() {
|
||||
this.calculateRepresentativeStolenData();
|
||||
this.representativeStolenData.forEach((entry) => {
|
||||
entry.unmark();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
57
safer-emitter.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
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
Before Width: | Height: | Size: 1023 KiB |
BIN
screenshot2.png
Before Width: | Height: | Size: 275 KiB |
BIN
screenshot3.png
Before Width: | Height: | Size: 265 KiB |
|
@ -1,49 +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;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<!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>
|
|
@ -1,221 +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 './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'));
|
|
@ -1,5 +1,6 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import ExtendedRequest, { HAREntry } from './extended-request';
|
||||
import { SaferEmitter } from './safer-emitter';
|
||||
|
||||
import {
|
||||
getshorthost,
|
||||
|
@ -33,7 +34,7 @@ const id = (function* id() {
|
|||
|
||||
export type DecodingSchema = 'base64' | 'raw';
|
||||
|
||||
export class StolenDataEntry extends EventEmitter {
|
||||
export class StolenDataEntry extends SaferEmitter {
|
||||
public isIAB = false;
|
||||
public id: number;
|
||||
private marked = false;
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
$ultra-black-color: #000;
|
||||
$black-color: hwb(217.5 18.4% 78.4%);
|
||||
$black-color: #1b1c1d;
|
||||
$disabled-grey: #8a949f;
|
||||
$light-grey: #d1d1d1;
|
||||
$ultra-light-grey: #ededed;
|
||||
$blue: #0048d9;
|
||||
$icd-yellow: #ffee2c;
|
||||
$icd-rentgen-color: #99ffdd;
|
||||
$contrast-icd-rentgen-color: #33f9b7;
|
||||
$highlight-color: #ccffee;
|
||||
$row-color: hsl(160, 100%, 94%);
|
||||
$pale-yellow: #fff8e5;
|
||||
$pale-red: hsl(10.8, 100%, 94.9%);
|
||||
$contrast-yellow: #ffb900;
|
||||
$contrast-red: #ff726b;
|
||||
$dark-red: hsl(2.8, 100%, 34%);
|
90
styles/fonts.scss
Normal file
|
@ -0,0 +1,90 @@
|
|||
@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;
|
||||
}
|
|
@ -10,14 +10,9 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 24rem;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 1rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family: 'OpenSans';
|
||||
}
|
||||
|
||||
button {
|
385
util.ts
|
@ -6,273 +6,268 @@ export type Unarray<T> = T extends Array<infer R> ? R : T;
|
|||
|
||||
export type Tab = Unarray<Unpromisify<ReturnType<typeof browser.tabs.query>>>;
|
||||
export type Request = {
|
||||
cookieStoreId?: string;
|
||||
documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined.
|
||||
frameId: number;
|
||||
incognito?: boolean;
|
||||
method: string;
|
||||
originUrl: string;
|
||||
parentFrameId: number;
|
||||
proxyInfo?: {
|
||||
host: string;
|
||||
port: number;
|
||||
type: string;
|
||||
username: string;
|
||||
proxyDNS: boolean;
|
||||
failoverTimeout: number;
|
||||
};
|
||||
requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[];
|
||||
requestId: string;
|
||||
tabId: number;
|
||||
thirdParty?: boolean;
|
||||
timeStamp: number;
|
||||
type: string;
|
||||
url: string; // the target of the request;
|
||||
urlClassification?: { firstParty: string[]; thirdParty: string[] };
|
||||
cookieStoreId?: string;
|
||||
documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined.
|
||||
frameId: number;
|
||||
incognito?: boolean;
|
||||
method: string;
|
||||
originUrl: string;
|
||||
parentFrameId: number;
|
||||
proxyInfo?: {
|
||||
host: string;
|
||||
port: number;
|
||||
type: string;
|
||||
username: string;
|
||||
proxyDNS: boolean;
|
||||
failoverTimeout: number;
|
||||
};
|
||||
requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[];
|
||||
requestId: string;
|
||||
tabId: number;
|
||||
thirdParty?: boolean;
|
||||
timeStamp: number;
|
||||
type: string;
|
||||
url: string; // the target of the request;
|
||||
urlClassification?: { firstParty: string[]; thirdParty: string[] };
|
||||
};
|
||||
|
||||
export function getshorthost(host: string) {
|
||||
const parts = host
|
||||
.replace(/^.*:\/\//, '')
|
||||
.replace(/\/.*$/, '')
|
||||
.split('.');
|
||||
let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3;
|
||||
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
|
||||
lookback = -4; // to distinguish between google ads and stats
|
||||
} else if (parts.at(-2) == 'google') {
|
||||
lookback = -3; // to distinguish various google services
|
||||
}
|
||||
return parts.slice(lookback).join('.');
|
||||
const parts = host
|
||||
.replace(/^.*:\/\//, '')
|
||||
.replace(/\/.*$/, '')
|
||||
.split('.');
|
||||
let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3;
|
||||
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
|
||||
lookback = -4; // to distinguish between google ads and stats
|
||||
} else if (parts.at(-2) == 'google') {
|
||||
lookback = -3; // to distinguish various google services
|
||||
}
|
||||
return parts.slice(lookback).join('.');
|
||||
}
|
||||
|
||||
export function useEmitter(
|
||||
e: EventEmitter
|
||||
e: EventEmitter
|
||||
): [
|
||||
Record<string, number | undefined>,
|
||||
React.Dispatch<React.SetStateAction<Record<string, number | undefined>>>
|
||||
Record<string, number | undefined>,
|
||||
React.Dispatch<React.SetStateAction<Record<string, number | undefined>>>
|
||||
] {
|
||||
const [eventCounts, setEventCounts] = React.useState<Record<string, number | undefined>>({
|
||||
'*': 0,
|
||||
});
|
||||
React.useEffect(() => {
|
||||
const callback = (eventSubtype: string) => {
|
||||
setEventCounts((eventCounts) => {
|
||||
console.log({
|
||||
...eventCounts,
|
||||
...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 },
|
||||
...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 },
|
||||
});
|
||||
return {
|
||||
...eventCounts,
|
||||
...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 },
|
||||
...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 },
|
||||
};
|
||||
});
|
||||
};
|
||||
e.on('change', callback);
|
||||
return () => {
|
||||
e.removeListener('change', callback);
|
||||
};
|
||||
}, []);
|
||||
return [eventCounts, setEventCounts];
|
||||
const [eventCounts, setEventCounts] = React.useState<Record<string, number | undefined>>({
|
||||
'*': 0,
|
||||
});
|
||||
React.useEffect(() => {
|
||||
const callback = (eventSubtype: string) => {
|
||||
setEventCounts((eventCounts) => ({
|
||||
...eventCounts,
|
||||
...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 },
|
||||
...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 },
|
||||
}));
|
||||
};
|
||||
e.on('change', callback);
|
||||
return () => {
|
||||
e.removeListener('change', callback);
|
||||
};
|
||||
}, []);
|
||||
return [eventCounts, setEventCounts];
|
||||
}
|
||||
|
||||
export function parseCookie(cookie: string): Record<string, string> {
|
||||
return cookie
|
||||
.split(';')
|
||||
.map((l) => l.split('='))
|
||||
.reduce(
|
||||
(acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
return cookie
|
||||
.split(';')
|
||||
.map((l) => l.split('='))
|
||||
.reduce(
|
||||
(acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTabByID(id: number) {
|
||||
const tabs = await browser.tabs.query({ currentWindow: true });
|
||||
return tabs.find((tab) => tab.id == id);
|
||||
const tabs = await browser.tabs.query({ currentWindow: true });
|
||||
return tabs.find((tab) => tab.id == id);
|
||||
}
|
||||
|
||||
export function parseToObject(str: unknown): Record<string | symbol, unknown> {
|
||||
let result: Record<string | symbol, unknown>;
|
||||
let original_string: string;
|
||||
if (typeof str === 'string') {
|
||||
original_string = str;
|
||||
result = JSON.parse(str);
|
||||
} else if (typeof str == 'object') {
|
||||
result = str as Record<string | symbol, unknown>;
|
||||
original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str);
|
||||
}
|
||||
result[Symbol.for('originalString')] = original_string;
|
||||
return result;
|
||||
let result: Record<string | symbol, unknown>;
|
||||
let original_string: string;
|
||||
if (typeof str === 'string') {
|
||||
original_string = str;
|
||||
result = JSON.parse(str);
|
||||
} else if (typeof str == 'object') {
|
||||
result = str as Record<string | symbol, unknown>;
|
||||
original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str);
|
||||
}
|
||||
result[Symbol.for('originalString')] = original_string;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isJSONObject(str: unknown): str is Record<string, unknown> | string | number {
|
||||
try {
|
||||
const firstChar = JSON.stringify(parseToObject(str))[0];
|
||||
return ['{', '['].includes(firstChar);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const firstChar = JSON.stringify(parseToObject(str))[0];
|
||||
return ['{', '['].includes(firstChar);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isURL(str: unknown): str is string {
|
||||
try {
|
||||
return !!(typeof str === 'string' && new URL(str));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return !!(typeof str === 'string' && new URL(str));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function hyphenate(str: string): string {
|
||||
return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`);
|
||||
return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`);
|
||||
}
|
||||
|
||||
export function unique<T>(array: T[]): Array<T> {
|
||||
return Array.from(new Set<T>(array));
|
||||
return Array.from(new Set<T>(array));
|
||||
}
|
||||
|
||||
export function allSubhosts(host: string) {
|
||||
const parts = host.split('.');
|
||||
const result = [];
|
||||
for (let i = 0; i < parts.length - 2; i++) {
|
||||
result.push(parts.slice(i).join('.'));
|
||||
}
|
||||
return result;
|
||||
const parts = host.split('.');
|
||||
const result = [];
|
||||
for (let i = 0; i < parts.length - 2; i++) {
|
||||
result.push(parts.slice(i).join('.'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function reduceConcat<T>(a: T[], b: T[]): T[] {
|
||||
return a.concat(b);
|
||||
return a.concat(b);
|
||||
}
|
||||
|
||||
export function getDate() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d
|
||||
.getDate()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d
|
||||
.getDate()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function toBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const FR = new FileReader();
|
||||
FR.addEventListener('load', (e) => {
|
||||
resolve(e.target.result as string);
|
||||
});
|
||||
FR.readAsDataURL(file);
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const FR = new FileReader();
|
||||
FR.addEventListener('load', (e) => {
|
||||
resolve(e.target.result as string);
|
||||
});
|
||||
FR.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export function makeThrottle(interval: number) {
|
||||
let last_emit = 0;
|
||||
function emit(callback: () => void) {
|
||||
if (Date.now() - last_emit > interval) {
|
||||
callback();
|
||||
last_emit = Date.now();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return function (callback: () => void) {
|
||||
if (!emit(callback)) {
|
||||
setTimeout(() => emit(callback), interval);
|
||||
}
|
||||
};
|
||||
let last_emit = 0;
|
||||
function emit(callback: () => void) {
|
||||
if (Date.now() - last_emit > interval) {
|
||||
callback();
|
||||
last_emit = Date.now();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return function (callback: () => void) {
|
||||
if (!emit(callback)) {
|
||||
setTimeout(() => emit(callback), interval);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isSameURL(url1: string, url2: string): boolean {
|
||||
if (url1 === url2) {
|
||||
return true;
|
||||
}
|
||||
url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
return url1 === url2;
|
||||
if (url1 === url2) {
|
||||
return true;
|
||||
}
|
||||
url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
return url1 === url2;
|
||||
}
|
||||
|
||||
export function isBase64(s: string): boolean {
|
||||
try {
|
||||
atob(s);
|
||||
return true;
|
||||
} catch (e) {}
|
||||
return false;
|
||||
try {
|
||||
atob(s);
|
||||
return true;
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isBase64JSON(s: unknown): s is string {
|
||||
return typeof s === 'string' && isBase64(s) && isJSONObject(atob(s));
|
||||
return typeof s === 'string' && isBase64(s) && isJSONObject(atob(s));
|
||||
}
|
||||
|
||||
export function flattenObject(
|
||||
obj: unknown,
|
||||
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(),
|
||||
key = '',
|
||||
ret = [] as [string, string][],
|
||||
parsed = false
|
||||
obj: unknown,
|
||||
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(),
|
||||
key = '',
|
||||
ret = [] as [string, string][],
|
||||
parsed = false
|
||||
): [string, string][] {
|
||||
const prefix = key === '' ? '' : `${key}.`;
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length == 1) {
|
||||
flattenObject(obj[0], parser, key, ret);
|
||||
} else {
|
||||
for (let i in obj) {
|
||||
flattenObject(obj[i], parser, prefix + i, ret);
|
||||
}
|
||||
}
|
||||
} else if (typeof obj === 'object') {
|
||||
for (const [subkey, value] of Object.entries(obj)) {
|
||||
flattenObject(value, parser, prefix + subkey, ret);
|
||||
}
|
||||
} else if (!parsed) {
|
||||
flattenObject(parser(obj), parser, key, ret, true);
|
||||
} else if (typeof obj === 'string') {
|
||||
ret.push([key, obj]);
|
||||
} else {
|
||||
throw new Error('Something went wrong when parsing ' + obj);
|
||||
}
|
||||
return ret;
|
||||
const prefix = key === '' ? '' : `${key}.`;
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length == 1) {
|
||||
flattenObject(obj[0], parser, key, ret);
|
||||
} else {
|
||||
for (let i in obj) {
|
||||
flattenObject(obj[i], parser, prefix + i, ret);
|
||||
}
|
||||
}
|
||||
} else if (obj === null) {
|
||||
ret.push([key, '']);
|
||||
} else if (typeof obj === 'object') {
|
||||
for (const [subkey, value] of Object.entries(obj)) {
|
||||
flattenObject(value, parser, prefix + subkey, ret);
|
||||
}
|
||||
} else if (!parsed) {
|
||||
flattenObject(parser(obj), parser, key, ret, true);
|
||||
} else if (typeof obj === 'string') {
|
||||
ret.push([key, obj]);
|
||||
} else {
|
||||
throw new Error('Something went wrong when parsing ' + obj);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function flattenObjectEntries(
|
||||
entries: [string, unknown][],
|
||||
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString()
|
||||
entries: [string, unknown][],
|
||||
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString()
|
||||
): [string, string][] {
|
||||
return flattenObject(Object.fromEntries(entries), parser);
|
||||
return flattenObject(Object.fromEntries(entries), parser);
|
||||
}
|
||||
|
||||
export function maskString(
|
||||
str: string,
|
||||
max_fraction_remaining: number,
|
||||
max_chars_total: number
|
||||
str: string,
|
||||
max_fraction_remaining: number,
|
||||
max_chars_total: number
|
||||
): string {
|
||||
const amount_of_chars_to_cut =
|
||||
str.length - Math.min(str.length * max_fraction_remaining, max_chars_total);
|
||||
if (amount_of_chars_to_cut == 0) {
|
||||
return str;
|
||||
}
|
||||
return (
|
||||
str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) +
|
||||
'(...)' +
|
||||
str.slice(str.length / 2 + amount_of_chars_to_cut / 2)
|
||||
);
|
||||
const amount_of_chars_to_cut =
|
||||
str.length - Math.min(str.length * max_fraction_remaining, max_chars_total);
|
||||
if (amount_of_chars_to_cut == 0) {
|
||||
return str;
|
||||
}
|
||||
return (
|
||||
str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) +
|
||||
'(...)' +
|
||||
str.slice(str.length / 2 + amount_of_chars_to_cut / 2)
|
||||
);
|
||||
}
|
||||
|
||||
export function safeDecodeURIComponent(s: string) {
|
||||
try {
|
||||
return decodeURIComponent(s);
|
||||
} catch (e) {
|
||||
return s;
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(s);
|
||||
} catch (e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeForClassname(string: string) {
|
||||
return string.replace(/[^a-z0-9]/gi, '-');
|
||||
return string.replace(/[^a-z0-9]/gi, '-');
|
||||
}
|
||||
|
||||
export function wordlist(words: string[]) {
|
||||
return words.reduce(
|
||||
(acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`,
|
||||
''
|
||||
);
|
||||
return words.reduce(
|
||||
(acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
|