Compare commits
1156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15704cb6dc | ||
|
|
3d7c87e4dc | ||
|
|
d3438a52ac | ||
|
|
03b86cf772 | ||
|
|
100aab06c3 | ||
|
|
6e69e57120 | ||
|
|
f0540a48e9 | ||
|
|
41c51c2a3c | ||
|
|
d238a40ffd | ||
|
|
cc178c0594 | ||
|
|
925b1c873f | ||
|
|
b59b447982 | ||
|
|
039815ae9d | ||
|
|
0b46308d46 | ||
|
|
f2b2fc3aa8 | ||
|
|
3bcb447718 | ||
|
|
ff40debb88 | ||
|
|
ccc271a76c | ||
|
|
01c4f0d6df | ||
|
|
6985547b49 | ||
|
|
e313d9f9e1 | ||
|
|
9e547c0648 | ||
|
|
d9c3fd094b | ||
|
|
279c76ce89 | ||
|
|
fab6c7cf6a | ||
|
|
986acb4e3e | ||
|
|
8abc1b6364 | ||
|
|
bd61d2fb3e | ||
|
|
edc95f42cb | ||
|
|
9294ecb09f | ||
|
|
aa56216e76 | ||
|
|
3b7bf2ae7e | ||
|
|
4224d2ba0a | ||
|
|
9e314fca7c | ||
|
|
1d9d37b5b9 | ||
|
|
302c436e94 | ||
|
|
1778345bd9 | ||
|
|
fce4e70c6e | ||
|
|
776839f97f | ||
|
|
7c0f227f15 | ||
|
|
da5f503dbc | ||
|
|
07eefcafdb | ||
|
|
f167f9de22 | ||
|
|
918366db1d | ||
|
|
d101a76962 | ||
|
|
2b53b76fed | ||
|
|
cffc717c86 | ||
|
|
bb79819de2 | ||
|
|
f946a32156 | ||
|
|
d6eccb5f9f | ||
|
|
53b45bdc6d | ||
|
|
549c87c97a | ||
|
|
821be1d1e0 | ||
|
|
a8d5a92a52 | ||
|
|
dc3e8a4ba4 | ||
|
|
04846eb29f | ||
|
|
6f89c4039b | ||
|
|
b947949bb1 | ||
|
|
92a84f08b0 | ||
|
|
cb94c763ff | ||
|
|
dd8477da64 | ||
|
|
53acb4ed8b | ||
|
|
ab2e8b4634 | ||
|
|
39e820ba34 | ||
|
|
40fcd8506a | ||
|
|
fc1d2f43da | ||
|
|
ea78382e5e | ||
|
|
28e31f4870 | ||
|
|
a28aad274d | ||
|
|
0371f1906a | ||
|
|
90b885fadc | ||
|
|
0fcb53763c | ||
|
|
42797d3409 | ||
|
|
7856b8b4a0 | ||
|
|
058d972ef0 | ||
|
|
ed0e6c03eb | ||
|
|
f4fd594452 | ||
|
|
23a98b1047 | ||
|
|
fa502954f6 | ||
|
|
deed7f920b | ||
|
|
868f666aa8 | ||
|
|
33c1bf650d | ||
|
|
745b71beff | ||
|
|
f1f2020f09 | ||
|
|
a6ed23616f | ||
|
|
c9cd8fd156 | ||
|
|
c1815d62d9 | ||
|
|
6d9444c6bb | ||
|
|
534598d6b0 | ||
|
|
d7ab4c4a65 | ||
|
|
1102f00658 | ||
|
|
27c5867e2b | ||
|
|
7464d46843 | ||
|
|
f9fd9940bd | ||
|
|
796b98680c | ||
|
|
68ef4008a6 | ||
|
|
fb0f0f0b5d | ||
|
|
be5ea12408 | ||
|
|
2dd425c46e | ||
|
|
92ae9c38ef | ||
|
|
ede3188986 | ||
|
|
36a68ff829 | ||
|
|
4dd659c9d9 | ||
|
|
fb9f35413c | ||
|
|
7f9674f494 | ||
|
|
7f31b54b69 | ||
|
|
b7f1769ea8 | ||
|
|
cb2e459858 | ||
|
|
a08d23fcf1 | ||
|
|
60fdcc05fa | ||
|
|
3b84bb0239 | ||
|
|
de5634a817 | ||
|
|
bd74be1414 | ||
|
|
8c2615c06e | ||
|
|
f58cc15bbd | ||
|
|
473774bb5b | ||
|
|
da2c918f44 | ||
|
|
b4c6a447e1 | ||
|
|
1846a74d89 | ||
|
|
112bfce07f | ||
|
|
d7e02d07f9 | ||
|
|
343aefaa03 | ||
|
|
10078eac94 | ||
|
|
032b6376ca | ||
|
|
70712188e1 | ||
|
|
70366e7c94 | ||
|
|
f80eed6e2f | ||
|
|
9183689200 | ||
|
|
85378c393a | ||
|
|
9dcddf1f71 | ||
|
|
67093d7c86 | ||
|
|
665ddeb416 | ||
|
|
2623740451 | ||
|
|
0f7efdee80 | ||
|
|
7bafbc99c5 | ||
|
|
a1e07b5439 | ||
|
|
58a11ccc63 | ||
|
|
02ad860ea0 | ||
|
|
3b2382f9d7 | ||
|
|
64615d7b76 | ||
|
|
39d57c6d11 | ||
|
|
dce8b6e525 | ||
|
|
d25c41190a | ||
|
|
6c23476713 | ||
|
|
e7b881e975 | ||
|
|
445811931f | ||
|
|
5e04f367c1 | ||
|
|
9d97a84f29 | ||
|
|
6ab577dabf | ||
|
|
5f6f8b826a | ||
|
|
43a2c5bbf3 | ||
|
|
f328d69d0c | ||
|
|
eab0074cdf | ||
|
|
1f27da19bc | ||
|
|
14dbcfd487 | ||
|
|
26a55c5258 | ||
|
|
18a7feaacb | ||
|
|
c8ea3a6138 | ||
|
|
0ba59bc324 | ||
|
|
7197cc3bea | ||
|
|
6ab006de61 | ||
|
|
befbd8c1c5 | ||
|
|
3666900f13 | ||
|
|
1ddfeac81f | ||
|
|
8f74ccdb48 | ||
|
|
4820f175dd | ||
|
|
1b9182aac7 | ||
|
|
4e7b205dfd | ||
|
|
0ef26b739f | ||
|
|
10c7f8ab43 | ||
|
|
421967ea81 | ||
|
|
3bd2996c14 | ||
|
|
7716ba6e77 | ||
|
|
eddabac191 | ||
|
|
a30ea6cddc | ||
|
|
5b03c64e8d | ||
|
|
beeb0d16c0 | ||
|
|
eb94a4fd14 | ||
|
|
72ee9c13d2 | ||
|
|
45a270ddce | ||
|
|
f968083f74 | ||
|
|
72b41c158b | ||
|
|
7c540b8903 | ||
|
|
4a40c75173 | ||
|
|
8e104d2a5b | ||
|
|
1529a86180 | ||
|
|
b6941345a7 | ||
|
|
0b8eae11de | ||
|
|
4cb6646cce | ||
|
|
d55f0247de | ||
|
|
1c44d1d0f9 | ||
|
|
8df400a676 | ||
|
|
cccc1a5383 | ||
|
|
c6cc5f4ac4 | ||
|
|
2dce066449 | ||
|
|
3f4cdd844c | ||
|
|
2c867e0ae8 | ||
|
|
c36466d952 | ||
|
|
2fd72096ae | ||
|
|
08dc5fa23b | ||
|
|
e19c711548 | ||
|
|
c23da8f6b0 | ||
|
|
c8e6e761d0 | ||
|
|
7449d1c351 | ||
|
|
47d75d8220 | ||
|
|
694e05f738 | ||
|
|
de88885189 | ||
|
|
9e919eddb5 | ||
|
|
c146c584f6 | ||
|
|
6fd3019aba | ||
|
|
f0a60d8c8e | ||
|
|
2169a49d4c | ||
|
|
b6a703d5de | ||
|
|
060b6835f1 | ||
|
|
fd2b58d96d | ||
|
|
b1e5073e59 | ||
|
|
9d5c6573d9 | ||
|
|
ec356b703f | ||
|
|
e5f9673521 | ||
|
|
2c12b614f0 | ||
|
|
2c27665e39 | ||
|
|
c1df061669 | ||
|
|
41bfe31d61 | ||
|
|
a42cec97f3 | ||
|
|
fd3caa3cc6 | ||
|
|
b76150d4d9 | ||
|
|
06c9b126a3 | ||
|
|
a2ede30f09 | ||
|
|
244fcf5e77 | ||
|
|
8eaacfea18 | ||
|
|
b30be6e4a2 | ||
|
|
5ce35a71ef | ||
|
|
6b6b04e93b | ||
|
|
21e3095dc9 | ||
|
|
2a8a8abfde | ||
|
|
4d8f27e96e | ||
|
|
be8e507d25 | ||
|
|
26d6606e09 | ||
|
|
1c61915b53 | ||
|
|
e9ee7d022c | ||
|
|
fc37fd1fe3 | ||
|
|
0fa963f1ab | ||
|
|
d50c0477ea | ||
|
|
9b37e92a81 | ||
|
|
d6bfb8b1fd | ||
|
|
052f50358e | ||
|
|
1a483cad55 | ||
|
|
03246532e3 | ||
|
|
cff91b2189 | ||
|
|
6b33192a55 | ||
|
|
6ef9b7be92 | ||
|
|
66f52b8c41 | ||
|
|
16224b3c02 | ||
|
|
41eaa668a1 | ||
|
|
5e4ad28f90 | ||
|
|
3ea728e4e0 | ||
|
|
bc2a8c6780 | ||
|
|
500b34b666 | ||
|
|
7afe684c3b | ||
|
|
cd8ab540f2 | ||
|
|
36225e4f92 | ||
|
|
b55960d743 | ||
|
|
9e3bde2cc9 | ||
|
|
5eb24065b6 | ||
|
|
87a517a169 | ||
|
|
c0d8300a8e | ||
|
|
d159825b27 | ||
|
|
779fe94550 | ||
|
|
8794b1d101 | ||
|
|
8227b1420c | ||
|
|
dece0969c2 | ||
|
|
7bf8bab864 | ||
|
|
97bf9fdf69 | ||
|
|
9afe688c79 | ||
|
|
9f8233c7e4 | ||
|
|
d8009a64d4 | ||
|
|
831e39c113 | ||
|
|
d1ea261dea | ||
|
|
73123a690b | ||
|
|
95ceff5cc6 | ||
|
|
16ba74e959 | ||
|
|
de0a1ddcaa | ||
|
|
01105a0a98 | ||
|
|
4cf27030a1 | ||
|
|
339755e292 | ||
|
|
7d051d5eaf | ||
|
|
d5a1271bbe | ||
|
|
e954a6cb62 | ||
|
|
a6a8fa6528 | ||
|
|
c90d811087 | ||
|
|
6f39d42165 | ||
|
|
57422d6406 | ||
|
|
ae256141da | ||
|
|
af61434b72 | ||
|
|
c0382dd327 | ||
|
|
32a77180b0 | ||
|
|
783d7bfa03 | ||
|
|
6e175dd5ca | ||
|
|
38f8a68827 | ||
|
|
f6640e4822 | ||
|
|
f402011c82 | ||
|
|
742b97cb4b | ||
|
|
3df32e2a22 | ||
|
|
52b4c382cb | ||
|
|
8e8570c357 | ||
|
|
4a633c8930 | ||
|
|
6cc374865e | ||
|
|
6fdb5f4f9f | ||
|
|
7f1ce41d9c | ||
|
|
e9a9ead762 | ||
|
|
1bb1e8123f | ||
|
|
65be118e82 | ||
|
|
ffe8045d82 | ||
|
|
39c1bec91a | ||
|
|
eb9fba3da6 | ||
|
|
4e0425ad7e | ||
|
|
06b25dcba6 | ||
|
|
e1bbff0047 | ||
|
|
c711b7ce42 | ||
|
|
f49782c8e3 | ||
|
|
7352e4a1b0 | ||
|
|
b863b10566 | ||
|
|
7f8700e612 | ||
|
|
d5ce677960 | ||
|
|
7a1de5d651 | ||
|
|
8c496e3bee | ||
|
|
5e5af03d11 | ||
|
|
0338de3de7 | ||
|
|
c71bd26789 | ||
|
|
fe3a64941d | ||
|
|
ac9e107094 | ||
|
|
08e0e35041 | ||
|
|
a0e3c7c2ec | ||
|
|
62ffaafeb7 | ||
|
|
a79400f99f | ||
|
|
c585c34c01 | ||
|
|
989137342b | ||
|
|
b71d30fca4 | ||
|
|
236fcc960f | ||
|
|
b7d1d6a632 | ||
|
|
a6c78470ea | ||
|
|
6cc12528b3 | ||
|
|
ac87fdff97 | ||
|
|
b1db7ac312 | ||
|
|
4557a352f8 | ||
|
|
6c4bf8b37a | ||
|
|
41d0076add | ||
|
|
cbc847b751 | ||
|
|
c71ef9ae84 | ||
|
|
e6f7100bad | ||
|
|
793dfb4f42 | ||
|
|
8cde99d2a2 | ||
|
|
d217ac14fc | ||
|
|
aa156558b6 | ||
|
|
09a4d7b9ee | ||
|
|
1eba2f09a3 | ||
|
|
e58ebc835c | ||
|
|
4fc4421398 | ||
|
|
61299dd8ea | ||
|
|
1eae5e8e08 | ||
|
|
d3f034c4c3 | ||
|
|
729e0c9f9d | ||
|
|
81cf84b131 | ||
|
|
e55aa7cb86 | ||
|
|
fb782aa147 | ||
|
|
d1212540a8 | ||
|
|
0640902350 | ||
|
|
7dd20f3c04 | ||
|
|
613a70f473 | ||
|
|
b6e7fa5496 | ||
|
|
bf2399ab88 | ||
|
|
31ff9aaabe | ||
|
|
87ac8b241b | ||
|
|
f4b6bab5d7 | ||
|
|
e55b3f828f | ||
|
|
5845fa5c05 | ||
|
|
d439dbe168 | ||
|
|
531584dbf1 | ||
|
|
015067648e | ||
|
|
5f8d9cb842 | ||
|
|
81659bce85 | ||
|
|
d4528848d9 | ||
|
|
416b9902cb | ||
|
|
5028351e6e | ||
|
|
9806dba408 | ||
|
|
1e62aa976d | ||
|
|
5afa4e5c9b | ||
|
|
eb1536878a | ||
|
|
ae4b8cf0d3 | ||
|
|
74d8a12c07 | ||
|
|
f761eb92ee | ||
|
|
c99697caae | ||
|
|
b80dd48434 | ||
|
|
0dc7398414 | ||
|
|
164f5e7d0e | ||
|
|
291f02855c | ||
|
|
b6517c5442 | ||
|
|
4ef303a2c9 | ||
|
|
fbad1ab55a | ||
|
|
91a8c66e0c | ||
|
|
6fdc430c4f | ||
|
|
32a55d00b2 | ||
|
|
9989f944c3 | ||
|
|
660a1947cc | ||
|
|
037c79730d | ||
|
|
a576d54d64 | ||
|
|
1528acde73 | ||
|
|
d67716f26b | ||
|
|
43c596fb99 | ||
|
|
c72e26c192 | ||
|
|
7013f5cf80 | ||
|
|
6184a70ba4 | ||
|
|
3dc0ca933b | ||
|
|
f9002df490 | ||
|
|
1d09a2e694 | ||
|
|
e9aa9d60af | ||
|
|
0631abe653 | ||
|
|
e264d0da62 | ||
|
|
cab6f1bafb | ||
|
|
23a6e338e8 | ||
|
|
2c1dfdbe07 | ||
|
|
994e77a38b | ||
|
|
ffac4ae5b1 | ||
|
|
e9d5d87691 | ||
|
|
173ca461a9 | ||
|
|
4eca951a1c | ||
|
|
064077d706 | ||
|
|
76d2d72fb0 | ||
|
|
99f6f53e40 | ||
|
|
732094a5ba | ||
|
|
6868af11cd | ||
|
|
ae89f1964f | ||
|
|
63796c8ccb | ||
|
|
be38392192 | ||
|
|
d60eb5e022 | ||
|
|
1ef80febcb | ||
|
|
43748295b0 | ||
|
|
7394d20cb7 | ||
|
|
eb79ce1835 | ||
|
|
77a5a377e3 | ||
|
|
53426b950a | ||
|
|
0cf95b30c0 | ||
|
|
d3a3e241bf | ||
|
|
1a7510070e | ||
|
|
1c063111cc | ||
|
|
cacb3a898d | ||
|
|
5ebe869c26 | ||
|
|
dee9c0a538 | ||
|
|
90f261a314 | ||
|
|
932a2a4576 | ||
|
|
6ba3be8a0f | ||
|
|
891ffc20af | ||
|
|
c359678226 | ||
|
|
26a943939d | ||
|
|
12e6eb1666 | ||
|
|
0e5202c470 | ||
|
|
7ad63ae004 | ||
|
|
d881755814 | ||
|
|
211404237a | ||
|
|
7838ad586d | ||
|
|
f0cfc19f8c | ||
|
|
cc85486414 | ||
|
|
2b81ff1fb3 | ||
|
|
5b939d2c95 | ||
|
|
8964387331 | ||
|
|
3bf7798323 | ||
|
|
9e8e604024 | ||
|
|
9bb36cd827 | ||
|
|
103aa8a0c8 | ||
|
|
62d507120c | ||
|
|
8ccb1c449a | ||
|
|
a07eb1ad1c | ||
|
|
78c6d83462 | ||
|
|
5d41da0e16 | ||
|
|
d87fb64390 | ||
|
|
5afe9ff2af | ||
|
|
97d4f1223f | ||
|
|
c1bbc97514 | ||
|
|
23932c0d4c | ||
|
|
76de2b29a5 | ||
|
|
23d629b80b | ||
|
|
bd02b7db8e | ||
|
|
7166f4e3d6 | ||
|
|
f7f8944e00 | ||
|
|
d560fc05cf | ||
|
|
f8964ebb99 | ||
|
|
a2eee15a7d | ||
|
|
a997a44a23 | ||
|
|
88db2c1cf4 | ||
|
|
307c9d3fa1 | ||
|
|
b0a09ef953 | ||
|
|
84657ed149 | ||
|
|
3d95848ba3 | ||
|
|
044138b540 | ||
|
|
3f098f340e | ||
|
|
6b9a735bec | ||
|
|
8b21a54ab4 | ||
|
|
a181bb3509 | ||
|
|
5ec2486c57 | ||
|
|
fe4eb1d582 | ||
|
|
294cd0dfa8 | ||
|
|
e2259ae737 | ||
|
|
b61bf3c867 | ||
|
|
7eb7590f06 | ||
|
|
c8bf3101aa | ||
|
|
603a352595 | ||
|
|
d34ff79fd7 | ||
|
|
7b8655a079 | ||
|
|
d3ba54d05a | ||
|
|
83e2aec3f5 | ||
|
|
135f40f65d | ||
|
|
fb83b324ab | ||
|
|
0abf890fc4 | ||
|
|
93e82cf953 | ||
|
|
e3087f008c | ||
|
|
ec5d301814 | ||
|
|
da9f4d0ce5 | ||
|
|
99055b1342 | ||
|
|
17a0393ce0 | ||
|
|
3d2c8c2ce2 | ||
|
|
38fcd7227d | ||
|
|
7dcf4bcdb9 | ||
|
|
942457b357 | ||
|
|
3a162d47c5 | ||
|
|
dc3cffe63b | ||
|
|
bf83b43866 | ||
|
|
7ccf462bf8 | ||
|
|
6d29cebabb | ||
|
|
cdc15596df | ||
|
|
20528eb0d1 | ||
|
|
a8b305a84e | ||
|
|
76f9d3ee35 | ||
|
|
17ee4e0058 | ||
|
|
041c8ffdd2 | ||
|
|
35a6c7324d | ||
|
|
71ea4e74f6 | ||
|
|
690a705be9 | ||
|
|
a1fa36b79c | ||
|
|
7c6aabc388 | ||
|
|
59224516b7 | ||
|
|
025f040388 | ||
|
|
71b24b05d0 | ||
|
|
df1c4ef913 | ||
|
|
353fab8486 | ||
|
|
976fd61f23 | ||
|
|
00550872d7 | ||
|
|
85185d048c | ||
|
|
fb7176d989 | ||
|
|
718d74fa50 | ||
|
|
c7876f6b7f | ||
|
|
8305b9bd2f | ||
|
|
5c542008ab | ||
|
|
70bc2b7656 | ||
|
|
7a48c5201a | ||
|
|
071e283f87 | ||
|
|
29bafe1bae | ||
|
|
37c2926252 | ||
|
|
650d30c4bc | ||
|
|
b89bef6e89 | ||
|
|
452ccd068b | ||
|
|
4ee497a83f | ||
|
|
bf16e5c8a9 | ||
|
|
49b9a1f452 | ||
|
|
e1d6467de4 | ||
|
|
13057804ab | ||
|
|
7dc34ba646 | ||
|
|
d5a30b710d | ||
|
|
894545a6f0 | ||
|
|
4c64593262 | ||
|
|
c9ae76b209 | ||
|
|
e42ad175db | ||
|
|
a83e0cfbda | ||
|
|
e97b8ff42d | ||
|
|
3e1f0f1f56 | ||
|
|
c13b63ec9c | ||
|
|
a0edff1ead | ||
|
|
4fabd539d1 | ||
|
|
9501bf7270 | ||
|
|
9bd96210e5 | ||
|
|
d14aeb29e9 | ||
|
|
fd1a3dc251 | ||
|
|
ebbb174c66 | ||
|
|
7bf104960e | ||
|
|
28131243f9 | ||
|
|
b2aed06328 | ||
|
|
ac1c7c2363 | ||
|
|
e3601055fc | ||
|
|
a80d007e0c | ||
|
|
5c7b4ace9a | ||
|
|
5e9e63944b | ||
|
|
c90310405c | ||
|
|
5483dc2506 | ||
|
|
2afe79c941 | ||
|
|
7673715c65 | ||
|
|
1bd85ee656 | ||
|
|
38fd349d9b | ||
|
|
45452c7153 | ||
|
|
f32ebd913a | ||
|
|
86f2a531d4 | ||
|
|
f923ff4f87 | ||
|
|
282cf0b595 | ||
|
|
f12d3abe79 | ||
|
|
f58b6194ce | ||
|
|
59ba89262f | ||
|
|
f4f8332f96 | ||
|
|
23c347175a | ||
|
|
dd0cb78ea2 | ||
|
|
3a8172fa63 | ||
|
|
2a63a5b103 | ||
|
|
787d227761 | ||
|
|
d906e927ed | ||
|
|
1a78f57515 | ||
|
|
527e9f09c9 | ||
|
|
5677390a45 | ||
|
|
ddeaf8076d | ||
|
|
5ff92c6452 | ||
|
|
ebf6bda467 | ||
|
|
b76899a353 | ||
|
|
05696cffd9 | ||
|
|
ff7969a7ef | ||
|
|
dfea1e96fb | ||
|
|
16d7af843b | ||
|
|
6db3009e5f | ||
|
|
921df9e1aa | ||
|
|
7e30fe8d33 | ||
|
|
f98bc0878c | ||
|
|
62ed0a411f | ||
|
|
1f13cf2c5f | ||
|
|
cc7ea73513 | ||
|
|
38ef52d3ba | ||
|
|
5f79a9fb6d | ||
|
|
a74a560f0a | ||
|
|
9472d5eaa6 | ||
|
|
93072c0c1e | ||
|
|
126ea8c7e6 | ||
|
|
beccd80902 | ||
|
|
9bfdf86bec | ||
|
|
9d04514f8e | ||
|
|
a4cf46c0eb | ||
|
|
b3ad207326 | ||
|
|
dced61eb30 | ||
|
|
b5d7e99ba5 | ||
|
|
c103c7fd7a | ||
|
|
c157e4d31c | ||
|
|
dafe4884fc | ||
|
|
42574af2cc | ||
|
|
ab699ebcc6 | ||
|
|
12ccce3016 | ||
|
|
e4a0028f5d | ||
|
|
863a2e9c29 | ||
|
|
ff092d3d84 | ||
|
|
207179484c | ||
|
|
1bd7e4d486 | ||
|
|
de37804973 | ||
|
|
7654ec3b7c | ||
|
|
fa84c653ea | ||
|
|
7f9b43753e | ||
|
|
a0c221750b | ||
|
|
bcb3936e08 | ||
|
|
66ec29eee9 | ||
|
|
58535c8c2e | ||
|
|
563686849f | ||
|
|
5b607af8d1 | ||
|
|
c5061ec51e | ||
|
|
32074a9bab | ||
|
|
26478fc444 | ||
|
|
7c66c07634 | ||
|
|
4ef6f8e3bd | ||
|
|
5591fb03f1 | ||
|
|
fc35acc6e4 | ||
|
|
5c8c6d56a4 | ||
|
|
4f9e63beca | ||
|
|
6b7f6426a1 | ||
|
|
1b6fad9d87 | ||
|
|
016d9bcf91 | ||
|
|
f199c8b96a | ||
|
|
06321126e7 | ||
|
|
08f9ad5046 | ||
|
|
9aefe7f7c9 | ||
|
|
ea2ff28f32 | ||
|
|
339bf12857 | ||
|
|
7c605ac7af | ||
|
|
288b878700 | ||
|
|
db84b331c2 | ||
|
|
2fde21c522 | ||
|
|
211b1a8576 | ||
|
|
fb3747785b | ||
|
|
93d151a29a | ||
|
|
e28a92848d | ||
|
|
f83784e033 | ||
|
|
11173c520b | ||
|
|
608112d56c | ||
|
|
480a06c426 | ||
|
|
f0530975ac | ||
|
|
34cb970f11 | ||
|
|
a8fef2c0a5 | ||
|
|
fdef37287d | ||
|
|
6d4973391a | ||
|
|
0edfc8405f | ||
|
|
5274b732b2 | ||
|
|
0a71c8c724 | ||
|
|
af7a262ef0 | ||
|
|
9feb6866ee | ||
|
|
f95d6d062c | ||
|
|
196d4211b6 | ||
|
|
a50762ebd7 | ||
|
|
a81c795627 | ||
|
|
3e65f3a906 | ||
|
|
bea7d30836 | ||
|
|
6acf58f9e9 | ||
|
|
33993eda88 | ||
|
|
cbe3c819fb | ||
|
|
6baf385058 | ||
|
|
82bd1b2adf | ||
|
|
b269712c32 | ||
|
|
d50a3d4a1f | ||
|
|
8242e2088d | ||
|
|
48457f7ac1 | ||
|
|
8a496022f4 | ||
|
|
9c398ad98b | ||
|
|
dc1b754692 | ||
|
|
dc7203ea59 | ||
|
|
af9973e35b | ||
|
|
aeb44379c8 | ||
|
|
7841dec5d8 | ||
|
|
7d62a23b36 | ||
|
|
f36ac24ac5 | ||
|
|
e6c2736f1f | ||
|
|
0d46997ab3 | ||
|
|
896a0f035b | ||
|
|
fdd07a06be | ||
|
|
8f8595e750 | ||
|
|
7a83fc6d8f | ||
|
|
30df33e189 | ||
|
|
7aac0426e0 | ||
|
|
bc423d4d78 | ||
|
|
3ddfc822d1 | ||
|
|
f99e4db25f | ||
|
|
fd71e7f957 | ||
|
|
2bfeb75380 | ||
|
|
21f7fd7dbc | ||
|
|
3e08c35740 | ||
|
|
54e78b6274 | ||
|
|
f1499abbe8 | ||
|
|
1ad78e2844 | ||
|
|
af436f9506 | ||
|
|
4dfdc8b0c7 | ||
|
|
b4c0c36f3a | ||
|
|
d52ca850cb | ||
|
|
c3e0787d12 | ||
|
|
3f65e55f86 | ||
|
|
2db56fac3a | ||
|
|
464c0c4c47 | ||
|
|
71ed7d71fb | ||
|
|
cda38f9bcf | ||
|
|
cc9b622bde | ||
|
|
fb91fd03cc | ||
|
|
77e3b5a3e6 | ||
|
|
0ed5c7f1e7 | ||
|
|
5afadd4ff1 | ||
|
|
0f53c718a2 | ||
|
|
ad4e6c8dec | ||
|
|
9e0195deaa | ||
|
|
253216e6fc | ||
|
|
78eab6335d | ||
|
|
1d20b5ba11 | ||
|
|
1edc571b36 | ||
|
|
e3556aa7e1 | ||
|
|
aa94a75da9 | ||
|
|
ecd61830aa | ||
|
|
da82ef814b | ||
|
|
b840173429 | ||
|
|
e1dc1687fc | ||
|
|
3e6a88d31d | ||
|
|
94714ecb62 | ||
|
|
07a817266c | ||
|
|
706708876c | ||
|
|
fc4cbe4b74 | ||
|
|
9e8429cff3 | ||
|
|
f8db7ca923 | ||
|
|
70e4d9eeb0 | ||
|
|
410ec72fff | ||
|
|
a42a517896 | ||
|
|
d9c9d95b89 | ||
|
|
0ed10649ef | ||
|
|
4edf82cc21 | ||
|
|
775726ae6d | ||
|
|
5aeaf2e987 | ||
|
|
773244f320 | ||
|
|
5079d9a317 | ||
|
|
18e1609cb3 | ||
|
|
41796840c4 | ||
|
|
171b64bc98 | ||
|
|
cfc94fd9af | ||
|
|
914394054e | ||
|
|
7a237b9b68 | ||
|
|
80e9f129d8 | ||
|
|
fddf1c40dc | ||
|
|
557db53d39 | ||
|
|
c16e00e5af | ||
|
|
cd7da20024 | ||
|
|
5f44ed2598 | ||
|
|
5c0cfdcf38 | ||
|
|
2de5378208 | ||
|
|
a2aca89550 | ||
|
|
8d658dc159 | ||
|
|
fb8e0afd85 | ||
|
|
5263839731 | ||
|
|
484063a596 | ||
|
|
5650c7f778 | ||
|
|
74728782f3 | ||
|
|
0a0980f9e3 | ||
|
|
26c46b8488 | ||
|
|
c469696687 | ||
|
|
27550a7781 | ||
|
|
e79bacd268 | ||
|
|
fd2dfcc4f2 | ||
|
|
74b9e364fe | ||
|
|
d2412679ea | ||
|
|
98e6fc3b4a | ||
|
|
b64b2a3091 | ||
|
|
6275f3abf7 | ||
|
|
22e836c98a | ||
|
|
d6c0489fa3 | ||
|
|
78728ce4ca | ||
|
|
6803a34b51 | ||
|
|
42b6383283 | ||
|
|
147f009279 | ||
|
|
b899781b69 | ||
|
|
64d0819ab4 | ||
|
|
3ea4af816a | ||
|
|
037d2c6974 | ||
|
|
fe2c664474 | ||
|
|
a3d429b9c3 | ||
|
|
d0e6f4118d | ||
|
|
75fb58e454 | ||
|
|
6563b7fd11 | ||
|
|
e1b78174e2 | ||
|
|
aab7e25f80 | ||
|
|
24b2ba7b35 | ||
|
|
20ae667b3b | ||
|
|
d8cb9da483 | ||
|
|
8abce2ccf3 | ||
|
|
aa3ebd3bd2 | ||
|
|
4929437283 | ||
|
|
389dd19a8a | ||
|
|
a8b6b3335b | ||
|
|
a925e2fae0 | ||
|
|
dc79b5923e | ||
|
|
b39bbaf6fb | ||
|
|
14b40d820b | ||
|
|
354d5963ec | ||
|
|
6a32b94336 | ||
|
|
5c68437b1c | ||
|
|
0106f280f0 | ||
|
|
b567aaac69 | ||
|
|
36b419202b | ||
|
|
c80d01c648 | ||
|
|
1d00646b17 | ||
|
|
e4b98fe65a | ||
|
|
12443db891 | ||
|
|
03f08de32f | ||
|
|
c18f488be7 | ||
|
|
099012fac9 | ||
|
|
fa510af65a | ||
|
|
46b514cc61 | ||
|
|
019c8814f6 | ||
|
|
46249935b2 | ||
|
|
ebecc6bb81 | ||
|
|
130ddca135 | ||
|
|
f2661989dc | ||
|
|
3f6cb8c356 | ||
|
|
228d9cca6c | ||
|
|
d08a1dd2ca | ||
|
|
7d76a60db7 | ||
|
|
e4f0865067 | ||
|
|
cad4bd7c04 | ||
|
|
f85aaf0370 | ||
|
|
312d78617d | ||
|
|
af5aa12fa1 | ||
|
|
1c7e2edae0 | ||
|
|
a49eee9685 | ||
|
|
61938c8e66 | ||
|
|
76d10f5920 | ||
|
|
2cf85926e9 | ||
|
|
afb099f9df | ||
|
|
343627eb82 | ||
|
|
86f2477c63 | ||
|
|
2684150141 | ||
|
|
3f8d8d055d | ||
|
|
e775e0542e | ||
|
|
7992ba5bc1 | ||
|
|
d24bbaa65a | ||
|
|
58a91e1b86 | ||
|
|
cfed1d0230 | ||
|
|
b14c70f4b6 | ||
|
|
ad77fc20c6 | ||
|
|
029633f3d3 | ||
|
|
78459d759c | ||
|
|
a0d5a3dd07 | ||
|
|
d76d983d5d | ||
|
|
68b8e7dc7c | ||
|
|
1a7aff5102 | ||
|
|
677edffb80 | ||
|
|
2cd90c998b | ||
|
|
346e604f34 | ||
|
|
8d41111cd6 | ||
|
|
3163edcbe4 | ||
|
|
18df43c9cb | ||
|
|
4f2179c4d7 | ||
|
|
b89546ac22 | ||
|
|
c0ad7635f2 | ||
|
|
a82688163e | ||
|
|
29c36ee110 | ||
|
|
43698f8d61 | ||
|
|
b5b29aeb17 | ||
|
|
fdcf4c152a | ||
|
|
dcfda9521b | ||
|
|
950c9cdaeb | ||
|
|
1e9641a40e | ||
|
|
3fd2537311 | ||
|
|
6d470b8eba | ||
|
|
71ad81a67d | ||
|
|
9a1852ea05 | ||
|
|
629a86de99 | ||
|
|
3539868683 | ||
|
|
71d4566df5 | ||
|
|
caaa613ce9 | ||
|
|
cf36a33aea | ||
|
|
a777a808ee | ||
|
|
a78150b7ad | ||
|
|
deb177c6bb | ||
|
|
1c5e47b4c4 | ||
|
|
aae61f9451 | ||
|
|
807c44f057 | ||
|
|
9782007f7e | ||
|
|
77d5f1e603 | ||
|
|
81ee6de0a3 | ||
|
|
82fe65ada2 | ||
|
|
ce79d7b745 | ||
|
|
755cc4f5ec | ||
|
|
fde4d311e3 | ||
|
|
b08f40aaa3 | ||
|
|
e12ade6b31 | ||
|
|
43fc80ef41 | ||
|
|
6ca96157f6 | ||
|
|
856181ea54 | ||
|
|
f84bd46cdc | ||
|
|
0b43924ee2 | ||
|
|
16d3fd3828 | ||
|
|
cf5defa6a9 | ||
|
|
3de760db12 | ||
|
|
1366f0b68e | ||
|
|
be498e0bd3 | ||
|
|
a4e13f032a | ||
|
|
fef3136b1b | ||
|
|
6b318c248f | ||
|
|
fd6a3a5579 | ||
|
|
2292267e39 | ||
|
|
dbfae53222 | ||
|
|
d39ed267f3 | ||
|
|
9d04ad704a | ||
|
|
11cbb4df23 | ||
|
|
232911f725 | ||
|
|
41a0c6c73f | ||
|
|
10e80edb1d | ||
|
|
af3848586c | ||
|
|
48807bf030 | ||
|
|
21a14cb225 | ||
|
|
b725d07744 | ||
|
|
94e707da8a | ||
|
|
4fb4041f13 | ||
|
|
545da556d2 | ||
|
|
9f3da3454f | ||
|
|
15105426b1 | ||
|
|
b2d2e27945 | ||
|
|
d93a4cf9f3 | ||
|
|
8c3ec5da3d | ||
|
|
d067b1d55a | ||
|
|
55070ace8b | ||
|
|
0f35961173 | ||
|
|
b63f68bf74 | ||
|
|
8c6d6db07e | ||
|
|
a74a70fd8c | ||
|
|
97ad674be2 | ||
|
|
81534d2c13 | ||
|
|
51b7036456 | ||
|
|
e40fdad251 | ||
|
|
e4b0e97ac0 | ||
|
|
003320ca34 | ||
|
|
780addc50b | ||
|
|
8892a5f43d | ||
|
|
75e52ebca7 | ||
|
|
e7b3bbcd0e | ||
|
|
b9d60639e2 | ||
|
|
2ce0ce65e1 | ||
|
|
a32bdcb06a | ||
|
|
f9f8dff8b4 | ||
|
|
5c5cd1e501 | ||
|
|
89bcd1c4a8 | ||
|
|
6b7b142961 | ||
|
|
565e47aef8 | ||
|
|
dd448cb3ed | ||
|
|
7a9a19e3b9 | ||
|
|
a6e94441aa | ||
|
|
68d66d8ab3 | ||
|
|
aa5cc7ff9f | ||
|
|
eb8ede8376 | ||
|
|
4dc92bae4e | ||
|
|
9e1a975b23 | ||
|
|
e5a26fa0be | ||
|
|
4035a3a1e5 | ||
|
|
0380d286cd | ||
|
|
91dc3aa5d2 | ||
|
|
4bbffcf35e | ||
|
|
bb73eeb7ea | ||
|
|
546f8a12e4 | ||
|
|
3a865d4aaa | ||
|
|
05feb6b1b0 | ||
|
|
efa6fe3ef1 | ||
|
|
e917927979 | ||
|
|
61689ed451 | ||
|
|
b74919c376 | ||
|
|
67cc4f9600 | ||
|
|
ec749de3fa | ||
|
|
b99cd7ed3f | ||
|
|
87b9c955ca | ||
|
|
00aee2771e | ||
|
|
291276494d | ||
|
|
fc3823f0af | ||
|
|
855ef73a9a | ||
|
|
517a8d2104 | ||
|
|
a042795ec1 | ||
|
|
e427d65bf5 | ||
|
|
5ce319456f | ||
|
|
03828bfdaa | ||
|
|
7c836d121c | ||
|
|
b2ee3c5f30 | ||
|
|
04205783b8 | ||
|
|
f27e71fbd8 | ||
|
|
fbca679334 | ||
|
|
5b594ba0b4 | ||
|
|
3d4b5b0c1e | ||
|
|
c7574de7dc | ||
|
|
c8a3bb5f81 | ||
|
|
118edef773 | ||
|
|
050a1aff83 | ||
|
|
9ad74e9cbf | ||
|
|
09f8decc59 | ||
|
|
0decdf156b | ||
|
|
9316655b8c | ||
|
|
1bcc4fd2d7 | ||
|
|
239a4da145 | ||
|
|
d8e66c9b6a | ||
|
|
72279f4995 | ||
|
|
48b21de011 | ||
|
|
f7dc86ab2b | ||
|
|
39bfe6d2cb | ||
|
|
d6f534c3c0 | ||
|
|
45067d0354 | ||
|
|
da1ff63f72 | ||
|
|
5d715c50de | ||
|
|
47072ae1fe | ||
|
|
72f301fa45 | ||
|
|
e9b89629a6 | ||
|
|
3ef5ef166f | ||
|
|
37ca7a706a | ||
|
|
734c65fbda | ||
|
|
145605d628 | ||
|
|
7ac432fbc5 | ||
|
|
0c92cec2ea | ||
|
|
f6a788b36f | ||
|
|
72dffcf46b | ||
|
|
3140cdd148 | ||
|
|
d87adbce63 | ||
|
|
ffdf2bc0cd | ||
|
|
be5d7a1c9f | ||
|
|
94288b5cef | ||
|
|
63abbf5949 | ||
|
|
49214281f7 | ||
|
|
9688dde1a4 | ||
|
|
fdcc31f049 | ||
|
|
55ed6100e0 | ||
|
|
7fb11ba912 | ||
|
|
f3d77fdcf2 | ||
|
|
6489ab6a56 | ||
|
|
bace117ada | ||
|
|
76175d61af | ||
|
|
87110095a0 | ||
|
|
50ba8bec5a | ||
|
|
1741b1c686 | ||
|
|
99097baf9d | ||
|
|
bac1cc8243 | ||
|
|
096489d486 | ||
|
|
9811a9a3e1 | ||
|
|
910cde4380 | ||
|
|
4f1ccf83c8 | ||
|
|
9501c1ce4b | ||
|
|
069f0e53e1 | ||
|
|
aedfba795e | ||
|
|
9f162c0703 | ||
|
|
7b96c46e39 | ||
|
|
3d48ea71b9 | ||
|
|
24ee984a2e | ||
|
|
4255cbe540 | ||
|
|
fe16f24c41 | ||
|
|
a6e1fc5c44 | ||
|
|
8434312728 | ||
|
|
46f641aaec | ||
|
|
c246d8d517 | ||
|
|
4ec1aeafaf | ||
|
|
27cfd04ea7 | ||
|
|
797cfcb98d | ||
|
|
96a9b52e6d | ||
|
|
3b7462070b | ||
|
|
f08dd5960b | ||
|
|
9972196f70 | ||
|
|
ebbf06787c | ||
|
|
1d2b0cb093 | ||
|
|
6f27c6e4aa | ||
|
|
7b6008c37e | ||
|
|
cf5405fbe4 | ||
|
|
b4ec7402fc | ||
|
|
ff9a107a29 | ||
|
|
417ad87bcc | ||
|
|
265f99f327 | ||
|
|
0f8c3caf18 | ||
|
|
3a7677b46d | ||
|
|
3fd64d9956 | ||
|
|
1d26f4b24f | ||
|
|
1a4cea7b4f | ||
|
|
bbb1c42641 | ||
|
|
7ce2320eda | ||
|
|
1729b30f89 | ||
|
|
25d6595d5e | ||
|
|
3b31b9d65b | ||
|
|
e9e64fed3f | ||
|
|
3b88ea9b1c | ||
|
|
623f76fa6c | ||
|
|
19efcb1a0d | ||
|
|
31c8916622 | ||
|
|
7a0b189a1d | ||
|
|
fa0d56d57a | ||
|
|
85670bbc6a | ||
|
|
fc9a85b6ad | ||
|
|
8760b132da | ||
|
|
b80ee8d778 | ||
|
|
c8e168aa3e | ||
|
|
106aef579f | ||
|
|
d7084829c3 | ||
|
|
243d72eed5 | ||
|
|
aa60e97d5d | ||
|
|
4ae12db99c | ||
|
|
98ad058e2e | ||
|
|
58cc381abf | ||
|
|
58840e2c00 |
@@ -1,8 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
|
.tox
|
||||||
.DS_Store
|
.DS_Store
|
||||||
firefox
|
firefox
|
||||||
assets
|
assets
|
||||||
docs
|
docs
|
||||||
public
|
|
||||||
test
|
test
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
dist
|
dist
|
||||||
assets
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
coverage
|
||||||
|
app/locale.js
|
||||||
|
app/capabilities.js
|
||||||
@@ -14,6 +14,9 @@ plugins:
|
|||||||
root: true
|
root: true
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
node/no-deprecated-api: off
|
||||||
|
node/no-unsupported-features/es-syntax: off
|
||||||
|
node/no-unsupported-features/node-builtins: off
|
||||||
node/no-unpublished-require: off
|
node/no-unpublished-require: off
|
||||||
|
|
||||||
security/detect-non-literal-fs-filename: off
|
security/detect-non-literal-fs-filename: off
|
||||||
|
|||||||
13
.gitignore
vendored
@@ -1,2 +1,15 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
coverage
|
||||||
dist
|
dist
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
|
.tox
|
||||||
|
.pytest_cache
|
||||||
|
*.iml
|
||||||
|
android/app/src/main/assets
|
||||||
|
ios/send-ios/assets/ios.js
|
||||||
|
ios/send-ios/assets/vendor.js
|
||||||
|
ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/*
|
||||||
|
ios/send-ios.xcodeproj/xcuserdata/*
|
||||||
|
test/integration/downloads
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
assets/*.js
|
android/app/src/main/assets
|
||||||
|
android/app/build
|
||||||
|
coverage
|
||||||
@@ -10,3 +10,4 @@ rules:
|
|||||||
declaration-colon-newline-after: null
|
declaration-colon-newline-after: null
|
||||||
selector-list-comma-newline-after: null
|
selector-list-comma-newline-after: null
|
||||||
value-list-comma-newline-after: null
|
value-list-comma-newline-after: null
|
||||||
|
at-rule-no-unknown: null
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
93
CHANGELOG.md
@@ -1,5 +1,98 @@
|
|||||||
## Change Log
|
## Change Log
|
||||||
|
|
||||||
|
### v2.5.1 (2018/03/12 19:26 +00:00)
|
||||||
|
- [#789](https://github.com/mozilla/send/pull/789) Fixed #775 : Made text not-selectable (@RCMainak)
|
||||||
|
|
||||||
|
### v2.5.0 (2018/03/08 19:31 +00:00)
|
||||||
|
- [#782](https://github.com/mozilla/send/pull/782) updated docs (@dannycoates)
|
||||||
|
- [#781](https://github.com/mozilla/send/pull/781) Don't translate URL-safe chars, b64 is doing it for us (@timvisee)
|
||||||
|
- [#779](https://github.com/mozilla/send/pull/779) implemented crypto polyfills for ms edge (@dannycoates)
|
||||||
|
|
||||||
|
### v2.4.1 (2018/02/28 17:05 +00:00)
|
||||||
|
- [#777](https://github.com/mozilla/send/pull/777) use a separate circle in the progress svg for indefinite progress (@dannycoates)
|
||||||
|
|
||||||
|
### v2.4.0 (2018/02/27 01:55 +00:00)
|
||||||
|
- [#769](https://github.com/mozilla/send/pull/769) removed unsafe-inline styles via svgo-loader (@dannycoates)
|
||||||
|
- [#767](https://github.com/mozilla/send/pull/767) added coverage artifact to circleci (@dannycoates)
|
||||||
|
- [#766](https://github.com/mozilla/send/pull/766) Some frontend unit tests [WIP] (@dannycoates)
|
||||||
|
- [#761](https://github.com/mozilla/send/pull/761) added maxPasswordLength and passwordError messages (@dannycoates)
|
||||||
|
- [#764](https://github.com/mozilla/send/pull/764) added indefinite progress mode (@dannycoates)
|
||||||
|
- [#760](https://github.com/mozilla/send/pull/760) refactored css: phase 1 (@dannycoates)
|
||||||
|
- [#759](https://github.com/mozilla/send/pull/759) Switch en-US FTL file to new syntax (@flodolo)
|
||||||
|
- [#758](https://github.com/mozilla/send/pull/758) refactored server (@dannycoates)
|
||||||
|
- [#757](https://github.com/mozilla/send/pull/757) Update to fluent 0.4.3 (@stasm)
|
||||||
|
|
||||||
|
### v2.3.0 (2018/02/01 23:27 +00:00)
|
||||||
|
- [#536](https://github.com/mozilla/send/pull/536) use redis expire event to delete stored data immediately (@ehuggett)
|
||||||
|
- [#744](https://github.com/mozilla/send/pull/744) Gradient experiment (@dannycoates)
|
||||||
|
- [#739](https://github.com/mozilla/send/pull/739) added /api/info/:id route (@dannycoates)
|
||||||
|
- [#737](https://github.com/mozilla/send/pull/737) big refactor (@dannycoates)
|
||||||
|
- [#722](https://github.com/mozilla/send/pull/722) Add localization note to 'Time' and 'Downloads' string (@flodolo)
|
||||||
|
- [#721](https://github.com/mozilla/send/pull/721) show download Limits on page; Fixes #661 (@shikhar-scs)
|
||||||
|
- [#694](https://github.com/mozilla/send/pull/694) Passwords can now be changed (#687) (@himanish-star)
|
||||||
|
- [#702](https://github.com/mozilla/send/pull/702) Restricted the banner from showing on unsupported browsers (@himanish-star)
|
||||||
|
- [#701](https://github.com/mozilla/send/pull/701) improved popup for mobile display; Fixes #699 (@shikhar-scs)
|
||||||
|
- [#683](https://github.com/mozilla/send/pull/683) API changes to accommodate 3rd party clients (@ehuggett)
|
||||||
|
- [#698](https://github.com/mozilla/send/pull/698) Popup for delete button attached (@himanish-star)
|
||||||
|
- [#695](https://github.com/mozilla/send/pull/695) Show Warning, Cancel and Redirect on size > 2GB ; fixes #578 (@shikhar-scs)
|
||||||
|
- [#684](https://github.com/mozilla/send/pull/684) delete btn popup attached (@himanish-star)
|
||||||
|
- [#686](https://github.com/mozilla/send/pull/686) Hide password while Typing and after Entering: Fixes #670 (@shikhar-scs)
|
||||||
|
- [#679](https://github.com/mozilla/send/pull/679) changed font to sans sherif: Solves #676 (@shikhar-scs)
|
||||||
|
- [#693](https://github.com/mozilla/send/pull/693) README: Fix query link for "good first bugs" (@jspam)
|
||||||
|
- [#685](https://github.com/mozilla/send/pull/685) checkbox now has a hover effect: fixes #635 (@himanish-star)
|
||||||
|
- [#668](https://github.com/mozilla/send/pull/668) Add possibility to bind to a specific IP address (@TwizzyDizzy)
|
||||||
|
- [#682](https://github.com/mozilla/send/pull/682) [Docs] - README.md - minor spelling fixes (@tmm2018)
|
||||||
|
- [#672](https://github.com/mozilla/send/pull/672) Use EXPIRE_SECONDS to calculate file ttl for static content (@derektamsen)
|
||||||
|
- [#680](https://github.com/mozilla/send/pull/680) adjusted line height of label : fixes #609 (@himanish-star)
|
||||||
|
|
||||||
|
### v2.2.2 (2017/12/19 18:06 +00:00)
|
||||||
|
- [#667](https://github.com/mozilla/send/pull/667) Make develop the default NODE_ENV (@claudijd)
|
||||||
|
|
||||||
|
### v2.2.1 (2017/12/08 18:00 +00:00)
|
||||||
|
- [#665](https://github.com/mozilla/send/pull/665) stop drag target from flickering when dragging over children (@ericawright)
|
||||||
|
|
||||||
|
### v2.2.0 (2017/12/06 23:57 +00:00)
|
||||||
|
- [#654](https://github.com/mozilla/send/pull/654) Multiple download UI (@dannycoates)
|
||||||
|
- [#650](https://github.com/mozilla/send/pull/650) #634: overwrite appearance of password submit input (@ovlb)
|
||||||
|
- [#649](https://github.com/mozilla/send/pull/649) #609 share interface: align text in input and button (@ovlb)
|
||||||
|
|
||||||
|
### v2.1.2 (2017/11/16 19:03 +00:00)
|
||||||
|
- [#645](https://github.com/mozilla/send/pull/645) Remove the leak of the password into the console (@laurentj)
|
||||||
|
|
||||||
|
### v2.1.0 (2017/11/15 03:07 +00:00)
|
||||||
|
- [#641](https://github.com/mozilla/send/pull/641) Added experiment for firefox download promo (@dannycoates)
|
||||||
|
- [#640](https://github.com/mozilla/send/pull/640) use fluent-langneg for subtag support (@dannycoates)
|
||||||
|
- [#639](https://github.com/mozilla/send/pull/639) wrap number localization in try/catch (@dannycoates)
|
||||||
|
|
||||||
|
### v2.0.0 (2017/11/08 05:31 +00:00)
|
||||||
|
- [#633](https://github.com/mozilla/send/pull/633) Keyboard navigation/visual feedback regression (@ehuggett)
|
||||||
|
- [#632](https://github.com/mozilla/send/pull/632) display the 'add password' button only when the input field isn't empty (@dannycoates)
|
||||||
|
- [#626](https://github.com/mozilla/send/pull/626) Partial fix for #623 (@ehuggett)
|
||||||
|
- [#624](https://github.com/mozilla/send/pull/624) set a default MIME type in file metadata (@ehuggett)
|
||||||
|
- [#612](https://github.com/mozilla/send/pull/612) Password UI nits (@dannycoates, @ericawright)
|
||||||
|
- [#617](https://github.com/mozilla/send/pull/617) allow drag and drop if navigating from shared page (@ericawright)
|
||||||
|
- [#608](https://github.com/mozilla/send/pull/608) disable copying link when password not completed (@ericawright)
|
||||||
|
- [#605](https://github.com/mozilla/send/pull/605) align the "Password" and "Copy to clipboard" fields. (@ericawright)
|
||||||
|
- [#582](https://github.com/mozilla/send/pull/582) Add optional password to the download url (@dannycoates)
|
||||||
|
|
||||||
|
### v1.2.4 (2017/10/10 17:34 +00:00)
|
||||||
|
- [#583](https://github.com/mozilla/send/pull/583) Promote the beefy UI to default (@dannycoates)
|
||||||
|
- [#581](https://github.com/mozilla/send/pull/581) introducing ToC to README.md (@tmm2018)
|
||||||
|
- [#579](https://github.com/mozilla/send/pull/579) Hide cancel button when upload reaches 100% (@ericawright)
|
||||||
|
- [#580](https://github.com/mozilla/send/pull/580) Change Favicon in to look better in a variety of cases (@ericawright)
|
||||||
|
- [#571](https://github.com/mozilla/send/pull/571) Centre logo (@ehuggett)
|
||||||
|
- [#574](https://github.com/mozilla/send/pull/574) Make upload button focusable (accessibility/tab navigation) (@ehuggett)
|
||||||
|
|
||||||
|
### v1.2.0 (2017/09/12 22:42 +00:00)
|
||||||
|
- [#559](https://github.com/mozilla/send/pull/559) added first A/B experiment (@dannycoates)
|
||||||
|
- [#542](https://github.com/mozilla/send/pull/542) fix docker link typo (@ehuggett)
|
||||||
|
- [#541](https://github.com/mozilla/send/pull/541) removed .title and .alt attributes from ftl (@dannycoates)
|
||||||
|
- [#537](https://github.com/mozilla/send/pull/537) a few changes to make A/B testing easier (@dannycoates)
|
||||||
|
- [#533](https://github.com/mozilla/send/pull/533) minor UI fixes (@youwenliang)
|
||||||
|
- [#531](https://github.com/mozilla/send/pull/531) Add CHANGELOG script (@pdehaan)
|
||||||
|
- [#535](https://github.com/mozilla/send/pull/535) Fixed minimum NodeJS version in README (@LuFlo)
|
||||||
|
- [#528](https://github.com/mozilla/send/pull/528) adding separators to README (@tmm2018)
|
||||||
|
|
||||||
### v1.1.1 (2017/08/17 01:29 +00:00)
|
### v1.1.1 (2017/08/17 01:29 +00:00)
|
||||||
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
||||||
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
||||||
|
|||||||
65
CONTRIBUTORS
@@ -1,74 +1,126 @@
|
|||||||
|
Abdalrahman Hwoij
|
||||||
Abhinav Adduri
|
Abhinav Adduri
|
||||||
|
Adnan Kičin
|
||||||
|
Alberto Castro
|
||||||
Alexander Slovesnik
|
Alexander Slovesnik
|
||||||
|
Alfredos-Panagiotis Damkalis
|
||||||
Amin Mahmudian
|
Amin Mahmudian
|
||||||
Andreas Pettersson
|
Andreas Pettersson
|
||||||
Arash Mousavi
|
Arash Mousavi
|
||||||
|
Artem Polivanchuk
|
||||||
|
Ashikur Rahman
|
||||||
Balázs Meskó
|
Balázs Meskó
|
||||||
Belayet Hossain
|
Belayet Hossain
|
||||||
|
Besnik Bleta
|
||||||
Bjørn I
|
Bjørn I
|
||||||
Boopesh Mahendran
|
Boopesh Mahendran
|
||||||
|
Breana Gonzales
|
||||||
Chuck Harmston
|
Chuck Harmston
|
||||||
Cláudio Esperança
|
Cláudio Esperança
|
||||||
|
Cristian Silaghi
|
||||||
Cynthia Pereira
|
Cynthia Pereira
|
||||||
Daniel Thorn
|
Daniel Thorn
|
||||||
Daniela Arcese
|
Daniela Arcese
|
||||||
Danny Coates
|
Danny Coates
|
||||||
|
Derek Tamsen
|
||||||
|
Edmund Huggett
|
||||||
|
Elisa X
|
||||||
Emin Mastizada
|
Emin Mastizada
|
||||||
Enol
|
Enol
|
||||||
Erica
|
Erica
|
||||||
Erica Wright
|
Erica Wright
|
||||||
|
Filip Hruška
|
||||||
Fjoerfoks
|
Fjoerfoks
|
||||||
Francesco Lodolo
|
Francesco Lodolo
|
||||||
Francesco Lodolo [:flod]
|
Francesco Lodolo [:flod]
|
||||||
|
Frederick Villaluna
|
||||||
|
Gabriela
|
||||||
Gautam krishna.R
|
Gautam krishna.R
|
||||||
|
Georgianizator
|
||||||
|
Gonçalo Matos
|
||||||
|
Hyeonseok Shin
|
||||||
Håvar Henriksen
|
Håvar Henriksen
|
||||||
Jae Hyeon Park
|
Jae Hyeon Park
|
||||||
Jakub Rychlý
|
Jakub Rychlý
|
||||||
Jamie
|
Jamie
|
||||||
Jim Spentzos
|
Jim Spentzos
|
||||||
|
Jobava
|
||||||
Johann-S
|
Johann-S
|
||||||
John Gruen
|
John Gruen
|
||||||
Jon Vadillo
|
Jon Vadillo
|
||||||
|
Jonathan Claudius
|
||||||
|
Jordi Cuevas
|
||||||
Jordi Serratosa
|
Jordi Serratosa
|
||||||
|
Juan Esteban Ajsivinac Sián
|
||||||
Juraj Cigáň
|
Juraj Cigáň
|
||||||
|
Kerim Kalamujić
|
||||||
|
Khaled Hosny
|
||||||
Kohei Yoshino
|
Kohei Yoshino
|
||||||
Lan Glad
|
Lan Glad
|
||||||
|
Laurent Jouanneau
|
||||||
|
Lobodzets
|
||||||
|
LuFlo
|
||||||
|
Luiz Carlos de Morais
|
||||||
Luna Jernberg
|
Luna Jernberg
|
||||||
Marcelo Poli
|
Marcelo Poli
|
||||||
Marco Aurélio
|
Marco Aurélio
|
||||||
|
Mark Heijl
|
||||||
Mark Liang
|
Mark Liang
|
||||||
|
Marko Andrejić
|
||||||
Matjaž Horvat
|
Matjaž Horvat
|
||||||
Maykon Chagas
|
Maykon Chagas
|
||||||
|
Melo46
|
||||||
|
Merike Sell
|
||||||
Michael Köhler
|
Michael Köhler
|
||||||
Michael Wolf
|
Michael Wolf
|
||||||
Michal Stanke
|
Michal Stanke
|
||||||
Michal Vašíček
|
Michal Vašíček
|
||||||
|
Mozilla Pontoon
|
||||||
Moḥend Belqasem
|
Moḥend Belqasem
|
||||||
|
Muḥend Belqasem
|
||||||
Nicholas Skinsacos
|
Nicholas Skinsacos
|
||||||
|
Nihad
|
||||||
|
Nihad Suljić
|
||||||
|
Oscar
|
||||||
Peter deHaan
|
Peter deHaan
|
||||||
Pierre Neter
|
Pierre Neter
|
||||||
Pin-guang Chen
|
Pin-guang Chen
|
||||||
|
Radu Popescu
|
||||||
Rhoslyn Prys
|
Rhoslyn Prys
|
||||||
|
RickieES
|
||||||
Rizky Ariestiyansyah
|
Rizky Ariestiyansyah
|
||||||
Roberto Alvarado
|
Roberto Alvarado
|
||||||
Rodrigo
|
Rodrigo
|
||||||
|
Rodrigo Guerra
|
||||||
Rok Žerdin
|
Rok Žerdin
|
||||||
Sahithi
|
Sahithi
|
||||||
Sairam Raavi
|
Sairam Raavi
|
||||||
|
Sander Lepik
|
||||||
Sandro
|
Sandro
|
||||||
|
Sara Todaro
|
||||||
|
Sav22999
|
||||||
Schieck :)
|
Schieck :)
|
||||||
Selim Şumlu
|
Selim Şumlu
|
||||||
Slimane Amiri
|
Slimane Amiri
|
||||||
|
Soumya Himanish Mohapatra
|
||||||
|
Staś Małolepszy
|
||||||
|
Tema
|
||||||
|
Thomas Dalichow
|
||||||
Théo Chevalier
|
Théo Chevalier
|
||||||
|
Tiago Morais Morgado
|
||||||
Tomáš Zelina
|
Tomáš Zelina
|
||||||
Ton
|
Ton
|
||||||
Tymur Faradzhev
|
Tymur Faradzhev
|
||||||
|
Uccen Marzuq
|
||||||
Varghese Thomas
|
Varghese Thomas
|
||||||
Victor Bychek
|
Victor Bychek
|
||||||
|
Vitaliy Krutko
|
||||||
Weihang Lo
|
Weihang Lo
|
||||||
Wil Clouser
|
Wil Clouser
|
||||||
YFdyh000
|
YFdyh000
|
||||||
You-Wen Liang (Mark)
|
You-Wen Liang (Mark)
|
||||||
|
aefgh39622
|
||||||
|
albertdcastro
|
||||||
alex_mayorga
|
alex_mayorga
|
||||||
ariestiyansyah
|
ariestiyansyah
|
||||||
avelper
|
avelper
|
||||||
@@ -77,16 +129,29 @@ ehuggett
|
|||||||
eljuno
|
eljuno
|
||||||
erdem cobanoglu
|
erdem cobanoglu
|
||||||
gautamkrishnar
|
gautamkrishnar
|
||||||
|
gmontagu
|
||||||
goofy
|
goofy
|
||||||
|
hello
|
||||||
hi
|
hi
|
||||||
jesferman1993
|
jesferman1993
|
||||||
|
jlG
|
||||||
josotrix
|
josotrix
|
||||||
|
jspam
|
||||||
kenrick95
|
kenrick95
|
||||||
manxmensch
|
manxmensch
|
||||||
|
mirzet.omerovic.1992
|
||||||
ravmn
|
ravmn
|
||||||
|
rcmainak
|
||||||
reza.habibi2008
|
reza.habibi2008
|
||||||
|
savemore99.sm
|
||||||
|
shikhar-scs
|
||||||
siparon
|
siparon
|
||||||
skystar-p
|
skystar-p
|
||||||
|
tiagomoraismorgado
|
||||||
|
timvisee
|
||||||
xcffl
|
xcffl
|
||||||
|
ybouhamam
|
||||||
Μιχάλης
|
Μιχάλης
|
||||||
Марко Костић (Marko Kostić)
|
Марко Костић (Marko Kostić)
|
||||||
|
صفا الفليج
|
||||||
|
వీవెన్
|
||||||
|
|||||||
18
Dockerfile
@@ -1,15 +1,17 @@
|
|||||||
FROM node:8-alpine
|
FROM node:10 AS builder
|
||||||
|
RUN addgroup --gid 10001 app && adduser --disabled-password --gecos '' --gid 10001 --home /app --uid 10001 app
|
||||||
|
COPY package*.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
RUN apk add --no-cache git
|
FROM node:10-slim
|
||||||
RUN addgroup -S -g 10001 app && adduser -S -D -G app -u 10001 app
|
RUN addgroup --gid 10001 app && adduser --disabled-password --gecos '' --gid 10001 --home /app --uid 10001 app
|
||||||
COPY . /app
|
|
||||||
RUN chown -R app /app
|
|
||||||
USER app
|
USER app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN mkdir static
|
COPY --chown=app:app --from=builder /app .
|
||||||
RUN npm install --production && npm cache clean --force
|
COPY --chown=app:app . .
|
||||||
|
|
||||||
ENV PORT=1443
|
ENV PORT=1443
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
|
|
||||||
CMD ["npm", "run", "prod"]
|
CMD ["node", "server/bin/prod.js"]
|
||||||
|
|||||||
22
README.md
@@ -1,9 +1,8 @@
|
|||||||
# Firefox Send
|
# Firefox Send
|
||||||
|
|
||||||
[](https://circleci.com/gh/mozilla/send)
|
[](https://circleci.com/gh/mozilla/send)
|
||||||
[](https://testpilot.firefox.com/experiments/send)
|
|
||||||
|
|
||||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [Metrics](docs/metrics.md), [More](docs/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,6 +16,7 @@
|
|||||||
* [Localization](#localization)
|
* [Localization](#localization)
|
||||||
* [Contributing](#contributing)
|
* [Contributing](#contributing)
|
||||||
* [Testing](#testing)
|
* [Testing](#testing)
|
||||||
|
* [Android](#android)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -29,7 +29,7 @@ A file sharing experiment which allows you to send encrypted files to other user
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Node.js 8.2+](https://nodejs.org/)
|
- [Node.js 10.0+](https://nodejs.org/)
|
||||||
- [Redis server](https://redis.io/) (optional for development)
|
- [Redis server](https://redis.io/) (optional for development)
|
||||||
- [AWS S3](https://aws.amazon.com/s3/) or compatible service. (optional)
|
- [AWS S3](https://aws.amazon.com/s3/) or compatible service. (optional)
|
||||||
|
|
||||||
@@ -69,13 +69,15 @@ The server is configured with environment variables. See [server/config.js](serv
|
|||||||
|
|
||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||||
|
|
||||||
|
see also [docs/localization.md](docs/localization.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
Pull requests are always welcome! Feel free to check out the list of ["good first issues"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -84,8 +86,14 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
|
|||||||
| ENVIRONMENT | URL
|
| ENVIRONMENT | URL
|
||||||
|-------------|-----
|
|-------------|-----
|
||||||
| Production | <https://send.firefox.com/>
|
| Production | <https://send.firefox.com/>
|
||||||
| Stage | <https://send.stage.mozaws.net/>
|
| Stage | <https://stage.send.nonprod.cloudops.mozgcp.net/>
|
||||||
| Development | <https://send.dev.mozaws.net/>
|
| Development | <https://send2.dev.lcip.org/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
The android implementation is contained in the `android` directory, and can be viewed locally for easy testing and editing by running `ANDROID=1 npm start` and then visiting <http://localhost:8080>. CSS and image files are located in the `android/app/src/main/assets` directory.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
6
android/.eslintrc.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
|
||||||
|
parserOptions:
|
||||||
|
sourceType: module
|
||||||
|
|
||||||
4
android/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
local.properties
|
||||||
|
.gradle
|
||||||
|
build
|
||||||
|
|
||||||
9
android/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Readme
|
||||||
|
=====
|
||||||
|
|
||||||
|
The Send Android app allows you to choose any file from your android device, encrypt it with a password, and get a URL which will allow secure download of the file. By default, this URL will expire after one download or 24 hours.
|
||||||
|
|
||||||
|
Building the Send Android app.
|
||||||
|
=====
|
||||||
|
|
||||||
|
First, install Android Studio. Open the `android` directory in Android Studio, plug in your android phone, and press the run button.
|
||||||
19
android/SendAndroid.iml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module external.linked.project.id="SendAndroid" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="java-gradle" name="Java-Gradle">
|
||||||
|
<configuration>
|
||||||
|
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
||||||
|
<option name="BUILDABLE" value="false" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
96
android/android.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* global window, navigator */
|
||||||
|
import 'fluent-intl-polyfill';
|
||||||
|
import choo from 'choo';
|
||||||
|
import html from 'choo/html';
|
||||||
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
import { setApiUrlPrefix, getConstants } from '../app/api';
|
||||||
|
import metrics from '../app/metrics';
|
||||||
|
//import assets from '../common/assets';
|
||||||
|
import Archive from '../app/archive';
|
||||||
|
import Header from '../app/ui/header';
|
||||||
|
import storage from '../app/storage';
|
||||||
|
import controller from '../app/controller';
|
||||||
|
import User from './user';
|
||||||
|
import intents from './stores/intents';
|
||||||
|
import home from './pages/home';
|
||||||
|
import upload from './pages/upload';
|
||||||
|
import share from './pages/share';
|
||||||
|
import preferences from './pages/preferences';
|
||||||
|
import error from './pages/error';
|
||||||
|
import { getTranslator } from '../app/locale';
|
||||||
|
import { delay } from '../app/utils';
|
||||||
|
|
||||||
|
if (navigator.userAgent === 'Send Android') {
|
||||||
|
setApiUrlPrefix('https://send2.dev.lcip.org');
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = choo();
|
||||||
|
//app.use(state);
|
||||||
|
app.use(controller);
|
||||||
|
app.use(intents);
|
||||||
|
|
||||||
|
window.finishLogin = async function(accountInfo) {
|
||||||
|
while (!(app.state && app.state.user)) {
|
||||||
|
await delay();
|
||||||
|
}
|
||||||
|
await app.state.user.finishLogin(accountInfo);
|
||||||
|
await app.state.user.syncFileList();
|
||||||
|
app.emitter.emit('replaceState', '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
function body(main) {
|
||||||
|
return function(state, emit) {
|
||||||
|
/*
|
||||||
|
Disable the preferences menu for now since it is ugly and isn't
|
||||||
|
relevant to the beta
|
||||||
|
function clickPreferences(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('pushState', '/preferences');
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = html`<a
|
||||||
|
id="hamburger"
|
||||||
|
class="absolute pin-t pin-r z-50"
|
||||||
|
href="#"
|
||||||
|
onclick="${clickPreferences}"
|
||||||
|
>
|
||||||
|
<img src="${assets.get('preferences.png')}" />
|
||||||
|
</a>`;
|
||||||
|
*/
|
||||||
|
return html`
|
||||||
|
<body
|
||||||
|
class="flex flex-col items-center font-sans bg-grey-lightest h-screen"
|
||||||
|
>
|
||||||
|
${state.cache(Header, 'header').render()} ${main(state, emit)}
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(async function start() {
|
||||||
|
const translate = await getTranslator('en-US');
|
||||||
|
const { LIMITS, DEFAULTS } = await getConstants();
|
||||||
|
app.use(state => {
|
||||||
|
state.LIMITS = LIMITS;
|
||||||
|
state.DEFAULTS = DEFAULTS;
|
||||||
|
state.translate = translate;
|
||||||
|
state.capabilities = {
|
||||||
|
account: true
|
||||||
|
}; //TODO
|
||||||
|
state.archive = new Archive([], DEFAULTS.EXPIRE_SECONDS);
|
||||||
|
state.storage = storage;
|
||||||
|
state.user = new User(storage, LIMITS);
|
||||||
|
state.raven = Raven;
|
||||||
|
});
|
||||||
|
app.use(metrics);
|
||||||
|
app.route('/', body(home));
|
||||||
|
app.route('/upload', upload);
|
||||||
|
app.route('/share/:id', share);
|
||||||
|
app.route('/preferences', preferences);
|
||||||
|
app.route('/error', error);
|
||||||
|
//app.route('/debugging', require('./pages/debugging').default);
|
||||||
|
// add /api/filelist
|
||||||
|
app.mount('body');
|
||||||
|
})();
|
||||||
|
|
||||||
|
window.app = app;
|
||||||
1
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
43
android/app/build.gradle
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 27
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "org.mozilla.sendandroid"
|
||||||
|
minSdkVersion 26
|
||||||
|
targetSdkVersion 27
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
|
implementation 'com.github.delight-im:Android-AdvancedWebView:v3.0.0'
|
||||||
|
implementation "org.mozilla.components:service-firefox-accounts:${rootProject.ext.android_components_version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
task generateAndLinkBundle(type: Exec, description: 'Generate the android.js bundle and link it into the assets directory') {
|
||||||
|
commandLine './buildAssets.sh'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
compileTask -> compileTask.dependsOn generateAndLinkBundle
|
||||||
|
}
|
||||||
12
android/app/buildAssets.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
if [ -d "../../node_modules" ]
|
||||||
|
then
|
||||||
|
echo "node_modules already present."
|
||||||
|
else
|
||||||
|
echo "node_modules not present, running npm install."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
npm run build
|
||||||
|
rm -rf src/main/assets
|
||||||
|
mkdir -p src/main/assets
|
||||||
|
cp -R ../../dist/* src/main/assets
|
||||||
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
35
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.mozilla.sendandroid">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" android:value="false" />
|
||||||
|
<activity android:name=".MainActivity" android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
BIN
android/app/src/main/ic_launcher-web.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,220 @@
|
|||||||
|
package org.mozilla.sendandroid
|
||||||
|
|
||||||
|
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.os.Bundle
|
||||||
|
import im.delight.android.webview.AdvancedWebView
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.net.Uri
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebMessage
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import mozilla.components.service.fxa.Config
|
||||||
|
import mozilla.components.service.fxa.FirefoxAccount
|
||||||
|
import mozilla.components.service.fxa.OAuthInfo
|
||||||
|
import mozilla.components.service.fxa.Profile
|
||||||
|
import mozilla.components.service.fxa.FxaResult
|
||||||
|
|
||||||
|
internal class LoggingWebChromeClient : WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(cm: ConsoleMessage): Boolean {
|
||||||
|
Log.w("CONTENT", String.format("%s @ %d: %s",
|
||||||
|
cm.message(), cm.lineNumber(), cm.sourceId()))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebAppInterface(private val mContext: MainActivity) {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun beginOAuthFlow() {
|
||||||
|
mContext.beginOAuthFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun shareUrl(url: String) {
|
||||||
|
mContext.shareUrl(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity(), AdvancedWebView.Listener {
|
||||||
|
private var mWebView: AdvancedWebView? = null
|
||||||
|
private var mToShare: String? = null
|
||||||
|
private var mToCall: String? = null
|
||||||
|
private var mAccount: FirefoxAccount? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
// https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews
|
||||||
|
// WebView.setWebContentsDebuggingEnabled(true); // TODO only dev builds
|
||||||
|
|
||||||
|
mWebView = findViewById<WebView>(R.id.webview) as AdvancedWebView
|
||||||
|
mWebView!!.setListener(this, this)
|
||||||
|
mWebView!!.setWebChromeClient(LoggingWebChromeClient())
|
||||||
|
mWebView!!.addJavascriptInterface(WebAppInterface(this), "Android")
|
||||||
|
mWebView!!.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||||
|
|
||||||
|
val webSettings = mWebView!!.getSettings()
|
||||||
|
webSettings.setUserAgentString("Send Android")
|
||||||
|
webSettings.setAllowUniversalAccessFromFileURLs(true)
|
||||||
|
webSettings.setJavaScriptEnabled(true)
|
||||||
|
|
||||||
|
val intent = getIntent()
|
||||||
|
val action = intent.getAction()
|
||||||
|
val type = intent.getType()
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||||
|
if (type.equals("text/plain")) {
|
||||||
|
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
Log.w("INTENT", "text/plain " + sharedText)
|
||||||
|
mToShare = "data:text/plain;base64," + Base64.encodeToString(sharedText.toByteArray(), 16).trim()
|
||||||
|
} else if (type.startsWith("image/")) {
|
||||||
|
val imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri
|
||||||
|
Log.w("INTENT", "image/ " + imageUri)
|
||||||
|
mToShare = "data:text/plain;base64," + Base64.encodeToString(imageUri.path.toByteArray(), 16).trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mWebView!!.loadUrl("file:///android_asset/android.html")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun beginOAuthFlow() {
|
||||||
|
Config.release().then(fun (value: Config): FxaResult<Unit> {
|
||||||
|
mAccount = FirefoxAccount(value, "20f7931c9054d833", "https://send.firefox.com/fxa/android-redirect.html")
|
||||||
|
mAccount?.beginOAuthFlow(arrayOf("profile", "https://identity.mozilla.com/apps/send"), true)?.then(fun (url: String): FxaResult<Unit> {
|
||||||
|
Log.w("CONFIG", "GOT A URL " + url)
|
||||||
|
this@MainActivity.runOnUiThread({
|
||||||
|
mWebView!!.loadUrl(url)
|
||||||
|
})
|
||||||
|
return FxaResult.fromValue(Unit)
|
||||||
|
})
|
||||||
|
Log.w("CONFIG", "CREATED FIREFOXACCOUNT")
|
||||||
|
return FxaResult.fromValue(Unit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareUrl(url: String) {
|
||||||
|
val shareIntent = Intent()
|
||||||
|
shareIntent.action = Intent.ACTION_SEND
|
||||||
|
shareIntent.type = "text/plain"
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
val chooser = Intent.createChooser(shareIntent, "")
|
||||||
|
chooser.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(ComponentName(applicationContext, MainActivity::class.java)))
|
||||||
|
startActivity(chooser)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
mWebView!!.onResume()
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onPause() {
|
||||||
|
mWebView!!.onPause()
|
||||||
|
// ...
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mWebView!!.onDestroy()
|
||||||
|
// ...
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, intent)
|
||||||
|
mWebView!!.onActivityResult(requestCode, resultCode, intent)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (!mWebView!!.onBackPressed()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageStarted(url: String, favicon: Bitmap?) {
|
||||||
|
if (url.startsWith("https://send.firefox.com/fxa/android-redirect.html")) {
|
||||||
|
// We load this here so the user doesn't see the android-redirect.html page
|
||||||
|
mWebView!!.loadUrl("file:///android_asset/android.html")
|
||||||
|
|
||||||
|
val parsed = Uri.parse(url)
|
||||||
|
val code = parsed.getQueryParameter("code")
|
||||||
|
val state = parsed.getQueryParameter("state")
|
||||||
|
|
||||||
|
code?.let { code ->
|
||||||
|
state?.let { state ->
|
||||||
|
mAccount?.completeOAuthFlow(code, state)?.whenComplete { info ->
|
||||||
|
//displayAndPersistProfile(code, state)
|
||||||
|
val profile = mAccount?.getProfile(false)?.then(fun (profile: Profile): FxaResult<Unit> {
|
||||||
|
val accessToken = info.accessToken
|
||||||
|
val keys = info.keys
|
||||||
|
val avatar = profile.avatar
|
||||||
|
val displayName = profile.displayName
|
||||||
|
val email = profile.email
|
||||||
|
val uid = profile.uid
|
||||||
|
val toPass = "{\"accessToken\": \"${accessToken}\", \"keys\": '${keys}', \"avatar\": \"${avatar}\", \"displayName\": \"${displayName}\", \"email\": \"${email}\", \"uid\": \"${uid}\"}"
|
||||||
|
mToCall = "finishLogin(${toPass})"
|
||||||
|
this@MainActivity.runOnUiThread({
|
||||||
|
// Clear the history so that the user can't use the back button to see broken pages
|
||||||
|
// that were inserted into the history by the login process.
|
||||||
|
mWebView!!.clearHistory()
|
||||||
|
|
||||||
|
// We also reload this here because we need to make sure onPageFinished runs after mToCall has been set.
|
||||||
|
// We can't guarantee that onPageFinished wasn't already called at this point.
|
||||||
|
mWebView!!.loadUrl("file:///android_asset/android.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return FxaResult.fromValue(Unit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.w("MAIN", "onPageStarted");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(url: String) {
|
||||||
|
Log.w("MAIN", "onPageFinished")
|
||||||
|
if (mToShare != null) {
|
||||||
|
Log.w("INTENT", mToShare)
|
||||||
|
|
||||||
|
mWebView?.postWebMessage(WebMessage(mToShare), Uri.EMPTY)
|
||||||
|
mToShare = null
|
||||||
|
}
|
||||||
|
if (mToCall != null) {
|
||||||
|
this@MainActivity.runOnUiThread({
|
||||||
|
mWebView?.evaluateJavascript(mToCall, fun (value: String) {
|
||||||
|
mToCall = null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageError(errorCode: Int, description: String, failingUrl: String) {
|
||||||
|
Log.w("MAIN", "onPageError " + description)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDownloadRequested(url: String, suggestedFilename: String, mimeType: String, contentLength: Long, contentDisposition: String, userAgent: String) {
|
||||||
|
Log.w("MAIN", "onDownloadRequested")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onExternalPageRequest(url: String) {
|
||||||
|
Log.w("MAIN", "onExternalPageRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="92.5"
|
||||||
|
android:viewportHeight="92.5">
|
||||||
|
<group android:translateX="27.75"
|
||||||
|
android:translateY="28.25">
|
||||||
|
<path
|
||||||
|
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
|
||||||
|
android:fillType="nonZero">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="2.9809632"
|
||||||
|
android:startX="25.805717"
|
||||||
|
android:endY="31.687763"
|
||||||
|
android:endX="8.569217"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFF980E"/>
|
||||||
|
<item android:offset="0.21" android:color="#FFFF7139"/>
|
||||||
|
<item android:offset="0.36" android:color="#FFFF5854"/>
|
||||||
|
<item android:offset="0.46" android:color="#FFFF4F5E"/>
|
||||||
|
<item android:offset="0.69" android:color="#FFFF3750"/>
|
||||||
|
<item android:offset="0.86" android:color="#FFF92261"/>
|
||||||
|
<item android:offset="1" android:color="#FFF5156C"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
|
||||||
|
android:fillType="nonZero">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="2.9809632"
|
||||||
|
android:startX="25.805717"
|
||||||
|
android:endY="31.687763"
|
||||||
|
android:endX="8.569217"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#CCFFF44F"/>
|
||||||
|
<item android:offset="0.75" android:color="#00FFF44F"/>
|
||||||
|
<item android:offset="1" android:color="#00FFF44F"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.0303,3.9483C26.3833,4.8003 31.4203,9.6773 32.3113,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C35.4103,19.6643 36.2633,18.8133 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="20.534323"
|
||||||
|
android:startX="22.366518"
|
||||||
|
android:endY="7.772023"
|
||||||
|
android:endX="30.234228"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF3A8EE6"/>
|
||||||
|
<item android:offset="0.24" android:color="#FF5C79F0"/>
|
||||||
|
<item android:offset="0.63" android:color="#FF9059FF"/>
|
||||||
|
<item android:offset="1" android:color="#FFC139E6"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.2333,15.4453C33.5123,16.4903 34.8293,17.4963 36.0693,18.5803C36.1853,18.3483 36.2633,18.0773 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483C26.2283,4.7613 31.1873,9.4843 32.2333,15.4453Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="8.195093"
|
||||||
|
android:startX="30.235817"
|
||||||
|
android:endY="12.836453"
|
||||||
|
android:endX="26.934916"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#7E6E008B"/>
|
||||||
|
<item android:offset="0.5" android:color="#00C846CB"/>
|
||||||
|
<item android:offset="1" android:color="#00C846CB"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.0013,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C34.9453,19.6643 35.4883,19.3933 35.8373,18.9673C34.5583,17.9223 33.2793,16.9163 32.0013,15.8713Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="18.076923"
|
||||||
|
android:startX="31.69962"
|
||||||
|
android:endY="17.594997"
|
||||||
|
android:endX="23.366179"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#006A2BEA"/>
|
||||||
|
<item android:offset="0.14" android:color="#006A2BEA"/>
|
||||||
|
<item android:offset="0.3" android:color="#15662CE6"/>
|
||||||
|
<item android:offset="0.47" android:color="#2C592FDB"/>
|
||||||
|
<item android:offset="0.64" android:color="#424534C9"/>
|
||||||
|
<item android:offset="0.82" android:color="#59283BAF"/>
|
||||||
|
<item android:offset="0.99" android:color="#7003448D"/>
|
||||||
|
<item android:offset="1" android:color="#7200458B"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
97
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="92.5"
|
||||||
|
android:viewportHeight="92.5">
|
||||||
|
<group android:translateX="27.75"
|
||||||
|
android:translateY="28.25">
|
||||||
|
<path
|
||||||
|
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
|
||||||
|
android:fillType="nonZero">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="2.9809632"
|
||||||
|
android:startX="25.805717"
|
||||||
|
android:endY="31.687763"
|
||||||
|
android:endX="8.569217"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFF980E"/>
|
||||||
|
<item android:offset="0.21" android:color="#FFFF7139"/>
|
||||||
|
<item android:offset="0.36" android:color="#FFFF5854"/>
|
||||||
|
<item android:offset="0.46" android:color="#FFFF4F5E"/>
|
||||||
|
<item android:offset="0.69" android:color="#FFFF3750"/>
|
||||||
|
<item android:offset="0.86" android:color="#FFF92261"/>
|
||||||
|
<item android:offset="1" android:color="#FFF5156C"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
|
||||||
|
android:fillType="nonZero">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="2.9809632"
|
||||||
|
android:startX="25.805717"
|
||||||
|
android:endY="31.687763"
|
||||||
|
android:endX="8.569217"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#CCFFF44F"/>
|
||||||
|
<item android:offset="0.75" android:color="#00FFF44F"/>
|
||||||
|
<item android:offset="1" android:color="#00FFF44F"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.0303,3.9483C26.3833,4.8003 31.4203,9.6773 32.3113,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C35.4103,19.6643 36.2633,18.8133 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="20.534323"
|
||||||
|
android:startX="22.366518"
|
||||||
|
android:endY="7.772023"
|
||||||
|
android:endX="30.234228"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF3A8EE6"/>
|
||||||
|
<item android:offset="0.24" android:color="#FF5C79F0"/>
|
||||||
|
<item android:offset="0.63" android:color="#FF9059FF"/>
|
||||||
|
<item android:offset="1" android:color="#FFC139E6"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.2333,15.4453C33.5123,16.4903 34.8293,17.4963 36.0693,18.5803C36.1853,18.3483 36.2633,18.0773 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483C26.2283,4.7613 31.1873,9.4843 32.2333,15.4453Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="8.195093"
|
||||||
|
android:startX="30.235817"
|
||||||
|
android:endY="12.836453"
|
||||||
|
android:endX="26.934916"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#7E6E008B"/>
|
||||||
|
<item android:offset="0.5" android:color="#00C846CB"/>
|
||||||
|
<item android:offset="1" android:color="#00C846CB"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.0013,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C34.9453,19.6643 35.4883,19.3933 35.8373,18.9673C34.5583,17.9223 33.2793,16.9163 32.0013,15.8713Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="18.076923"
|
||||||
|
android:startX="31.69962"
|
||||||
|
android:endY="17.594997"
|
||||||
|
android:endX="23.366179"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#006A2BEA"/>
|
||||||
|
<item android:offset="0.14" android:color="#006A2BEA"/>
|
||||||
|
<item android:offset="0.3" android:color="#15662CE6"/>
|
||||||
|
<item android:offset="0.47" android:color="#2C592FDB"/>
|
||||||
|
<item android:offset="0.64" android:color="#424534C9"/>
|
||||||
|
<item android:offset="0.82" android:color="#59283BAF"/>
|
||||||
|
<item android:offset="0.99" android:color="#7003448D"/>
|
||||||
|
<item android:offset="1" android:color="#7200458B"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
13
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<im.delight.android.webview.AdvancedWebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
6
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#220033</color>
|
||||||
|
</resources>
|
||||||
3
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Send</string>
|
||||||
|
</resources>
|
||||||
11
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
32
android/build.gradle
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.3.21'
|
||||||
|
ext.android_components_version = '0.26.0'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.3.1'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.20"
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
maven {
|
||||||
|
url "https://maven.mozilla.org/maven2"
|
||||||
|
}
|
||||||
|
jcenter()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
13
android/gradle.properties
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#Tue Feb 19 08:34:25 EST 2019
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||||
172
android/gradlew
vendored
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
84
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
6
android/pages/.eslintrc.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
|
||||||
|
parserOptions:
|
||||||
|
sourceType: module
|
||||||
|
|
||||||
12
android/pages/error.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
export default function error(_state, _emit) {
|
||||||
|
return html`
|
||||||
|
<body>
|
||||||
|
<div id="white">
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>Sorry, an error occurred.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
}
|
||||||
73
android/pages/home.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const { list } = require('../../app/utils');
|
||||||
|
const archiveTile = require('../../app/ui/archiveTile');
|
||||||
|
const modal = require('../../app/ui/modal');
|
||||||
|
const intro = require('../../app/ui/intro');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
function onchange(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const newFiles = Array.from(event.target.files);
|
||||||
|
|
||||||
|
emit('addFiles', { files: newFiles });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onclick() {
|
||||||
|
document.getElementById('file-upload').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
const archives = state.storage.files
|
||||||
|
.filter(archive => !archive.expired)
|
||||||
|
.map(archive => archiveTile(state, emit, archive))
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
let button = html`
|
||||||
|
<div
|
||||||
|
class="bg-blue rounded-full m-4 flex items-center justify-center shadow-lg"
|
||||||
|
style="width: 56px; height: 56px"
|
||||||
|
onclick="${onclick}"
|
||||||
|
>
|
||||||
|
<img src="${assets.get('add.svg')}" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
if (state.uploading) {
|
||||||
|
content = archiveTile.uploading(state, emit);
|
||||||
|
button = '';
|
||||||
|
} else if (state.archive.numFiles > 0) {
|
||||||
|
content = archiveTile.wip(state, emit);
|
||||||
|
button = '';
|
||||||
|
} else {
|
||||||
|
content =
|
||||||
|
archives.length < 1
|
||||||
|
? intro(state)
|
||||||
|
: list(
|
||||||
|
archives,
|
||||||
|
'list-reset h-full overflow-y-auto w-full',
|
||||||
|
'mb-3 w-full'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<main class="main">
|
||||||
|
${state.modal && modal(state, emit)}
|
||||||
|
<section
|
||||||
|
class="h-full w-full p-6 z-10 overflow-hidden md:flex md:flex-row md:rounded-lg md:shadow-big"
|
||||||
|
>
|
||||||
|
${content}
|
||||||
|
</section>
|
||||||
|
<div class="fixed pin-r pin-b z-20">
|
||||||
|
${button}
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
class="hidden"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onchange="${onchange}"
|
||||||
|
onclick="${e => e.stopPropagation()}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
};
|
||||||
34
android/pages/preferences.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
import { setFileProtocolWssUrl, getFileProtocolWssUrl } from '../../app/api';
|
||||||
|
|
||||||
|
export default function preferences(state, emit) {
|
||||||
|
const wssURL = getFileProtocolWssUrl();
|
||||||
|
|
||||||
|
function updateWssUrl(event) {
|
||||||
|
state.wssURL = event.target.value;
|
||||||
|
setFileProtocolWssUrl(state.wssURL);
|
||||||
|
emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickDone(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<body>
|
||||||
|
<div id="white">
|
||||||
|
<div id="preferences">
|
||||||
|
<a onclick="${clickDone}" href="#"> done </a>
|
||||||
|
<dl>
|
||||||
|
<dt>wss url:</dt>
|
||||||
|
<dd>
|
||||||
|
<input type="text" onchange="${updateWssUrl}" value="${wssURL}" />
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
}
|
||||||
51
android/pages/share.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
export default function uploadComplete(state, emit) {
|
||||||
|
const file = state.storage.files[state.storage.files.length - 1];
|
||||||
|
function onclick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
input.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
input.selectionEnd = input.selectionStart;
|
||||||
|
copyText.textContent = 'Copied!';
|
||||||
|
setTimeout(function() {
|
||||||
|
copyText.textContent = 'Copy link';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.target;
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('pushState', '/upload');
|
||||||
|
emit('addFiles', { files: [file] });
|
||||||
|
emit('upload', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = html`
|
||||||
|
<input id="url" value="${file.url}" readonly="true" />
|
||||||
|
`;
|
||||||
|
const copyText = html`
|
||||||
|
<span>Copy link</span>
|
||||||
|
`;
|
||||||
|
return html`<body>
|
||||||
|
<div id="white">
|
||||||
|
<div class="card">
|
||||||
|
<div>The card contents will be here.</div>
|
||||||
|
<div>Expires after: <span class="expires-after">exp</span></div>
|
||||||
|
${input}
|
||||||
|
<div id="copy-link" onclick=${onclick}>
|
||||||
|
<img id="copy-image" src=${state.getAsset('copy-link.png')} />
|
||||||
|
${copyText}
|
||||||
|
</div>
|
||||||
|
<label id="label" for="input">
|
||||||
|
<img src=${state.getAsset('cloud-upload.png')} />
|
||||||
|
</label>
|
||||||
|
<input id="input" name="input" type="file" onchange=${uploadFile} />
|
||||||
|
</div>
|
||||||
|
</body>`;
|
||||||
|
}
|
||||||
26
android/pages/upload.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
export default function progressBar(state, emit) {
|
||||||
|
let percent = 0;
|
||||||
|
if (state.transfer && state.transfer.progress) {
|
||||||
|
percent = Math.floor(state.transfer.progressRatio * 100);
|
||||||
|
}
|
||||||
|
function onclick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.uploading) {
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<body>
|
||||||
|
<div id="white">
|
||||||
|
<div class="card">
|
||||||
|
<div>${percent}%</div>
|
||||||
|
<span class="progress" style="width: ${percent}%">.</span>
|
||||||
|
<div class="cancel" onclick="${onclick}">CANCEL</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
|
}
|
||||||
1
android/settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
include ':app'
|
||||||
20
android/stores/intents.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
export default function intentHandler(state, emitter) {
|
||||||
|
window.addEventListener(
|
||||||
|
'message',
|
||||||
|
event => {
|
||||||
|
if (typeof event.data !== 'string' || !event.data.startsWith('data:')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(event.data)
|
||||||
|
.then(res => res.blob())
|
||||||
|
.then(blob => {
|
||||||
|
emitter.emit('addFiles', { files: [blob] });
|
||||||
|
emitter.emit('upload', {});
|
||||||
|
})
|
||||||
|
.catch(e => console.error('ERROR ' + e + ' ' + e.stack));
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
41
android/stores/state.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import User from '../user';
|
||||||
|
import storage from '../../app/storage';
|
||||||
|
|
||||||
|
export default function initialState(state, emitter) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
Object.assign(state, {
|
||||||
|
prefix: '/android_asset',
|
||||||
|
user: new User(storage),
|
||||||
|
getAsset(name) {
|
||||||
|
return `${state.prefix}/${name}`;
|
||||||
|
},
|
||||||
|
raven: {
|
||||||
|
captureException: e => {
|
||||||
|
console.error('ERROR ' + e + ' ' + e.stack);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
files,
|
||||||
|
remove: function(fileId) {
|
||||||
|
console.log('REMOVE FILEID', fileId);
|
||||||
|
},
|
||||||
|
writeFile: function(file) {
|
||||||
|
console.log('WRITEFILE', file);
|
||||||
|
},
|
||||||
|
addFile: function(file) {
|
||||||
|
console.log('addfile' + JSON.stringify(file));
|
||||||
|
files.push(file);
|
||||||
|
emitter.emit('pushState', `/share/${file.id}`);
|
||||||
|
},
|
||||||
|
totalUploads: 0
|
||||||
|
},
|
||||||
|
transfer: null,
|
||||||
|
uploading: false,
|
||||||
|
settingPassword: false,
|
||||||
|
passwordSetError: null,
|
||||||
|
route: '/'
|
||||||
|
});
|
||||||
|
}
|
||||||
30
android/user.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* global Android */
|
||||||
|
import User from '../app/user';
|
||||||
|
import { deriveFileListKey } from '../app/fxa';
|
||||||
|
|
||||||
|
export default class AndroidUser extends User {
|
||||||
|
constructor(storage, limits) {
|
||||||
|
super(storage, limits);
|
||||||
|
}
|
||||||
|
|
||||||
|
async login() {
|
||||||
|
Android.beginOAuthFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
startAuthFlow() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async finishLogin(accountInfo) {
|
||||||
|
const jwks = JSON.parse(accountInfo.keys);
|
||||||
|
const ikm = jwks['https://identity.mozilla.com/apps/send'].k;
|
||||||
|
const profile = {
|
||||||
|
displayName: accountInfo.displayName,
|
||||||
|
email: accountInfo.email,
|
||||||
|
avatar: accountInfo.avatar,
|
||||||
|
access_token: accountInfo.accessToken
|
||||||
|
};
|
||||||
|
profile.fileListKey = await deriveFileListKey(ikm);
|
||||||
|
this.info = profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
414
app/api.js
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
import { arrayToB64, b64ToArray, delay } from './utils';
|
||||||
|
import { ECE_RECORD_SIZE } from './ece';
|
||||||
|
|
||||||
|
let fileProtocolWssUrl = null;
|
||||||
|
try {
|
||||||
|
fileProtocolWssUrl = localStorage.getItem('wssURL');
|
||||||
|
} catch (e) {
|
||||||
|
// NOOP
|
||||||
|
}
|
||||||
|
if (!fileProtocolWssUrl) {
|
||||||
|
fileProtocolWssUrl = 'wss://send2.dev.lcip.org/api/ws';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setFileProtocolWssUrl(url) {
|
||||||
|
localStorage && localStorage.setItem('wssURL', url);
|
||||||
|
fileProtocolWssUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileProtocolWssUrl() {
|
||||||
|
return fileProtocolWssUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiUrlPrefix = '';
|
||||||
|
export function getApiUrl(path) {
|
||||||
|
return apiUrlPrefix + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setApiUrlPrefix(prefix) {
|
||||||
|
apiUrlPrefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(obj, bearerToken) {
|
||||||
|
const h = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
if (bearerToken) {
|
||||||
|
h['Authentication'] = `Bearer ${bearerToken}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Headers(h),
|
||||||
|
body: JSON.stringify(obj)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseNonce(header) {
|
||||||
|
header = header || '';
|
||||||
|
return header.split(' ')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, params, keychain) {
|
||||||
|
const result = {};
|
||||||
|
params = params || {};
|
||||||
|
const h = await keychain.authHeader();
|
||||||
|
params.headers = new Headers({ Authorization: h });
|
||||||
|
const response = await fetch(url, params);
|
||||||
|
result.response = response;
|
||||||
|
result.ok = response.ok;
|
||||||
|
const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
|
||||||
|
result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
|
||||||
|
keychain.nonce = nonce;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuthAndRetry(url, params, keychain) {
|
||||||
|
const result = await fetchWithAuth(url, params, keychain);
|
||||||
|
if (result.shouldRetry) {
|
||||||
|
return fetchWithAuth(url, params, keychain);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(id, owner_token) {
|
||||||
|
const response = await fetch(
|
||||||
|
getApiUrl(`/api/delete/${id}`),
|
||||||
|
post({ owner_token })
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setParams(id, owner_token, bearerToken, params) {
|
||||||
|
const response = await fetch(
|
||||||
|
getApiUrl(`/api/params/${id}`),
|
||||||
|
post(
|
||||||
|
{
|
||||||
|
owner_token,
|
||||||
|
dlimit: params.dlimit
|
||||||
|
},
|
||||||
|
bearerToken
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileInfo(id, owner_token) {
|
||||||
|
const response = await fetch(
|
||||||
|
getApiUrl(`/api/info/${id}`),
|
||||||
|
post({ owner_token })
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const obj = await response.json();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function metadata(id, keychain) {
|
||||||
|
const result = await fetchWithAuthAndRetry(
|
||||||
|
getApiUrl(`/api/metadata/${id}`),
|
||||||
|
{ method: 'GET' },
|
||||||
|
keychain
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
const data = await result.response.json();
|
||||||
|
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
|
||||||
|
return {
|
||||||
|
size: meta.size,
|
||||||
|
ttl: data.ttl,
|
||||||
|
iv: meta.iv,
|
||||||
|
name: meta.name,
|
||||||
|
type: meta.type,
|
||||||
|
manifest: meta.manifest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(result.response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPassword(id, owner_token, keychain) {
|
||||||
|
const auth = await keychain.authKeyB64();
|
||||||
|
const response = await fetch(
|
||||||
|
getApiUrl(`/api/password/${id}`),
|
||||||
|
post({ owner_token, auth })
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asyncInitWebSocket(server) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const ws = new WebSocket(server);
|
||||||
|
ws.onopen = () => {
|
||||||
|
resolve(ws);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenForResponse(ws, canceller) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
function handleMessage(msg) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(msg.data);
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
} else {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ws.close();
|
||||||
|
canceller.cancelled = true;
|
||||||
|
canceller.error = e;
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.addEventListener('message', handleMessage, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(
|
||||||
|
stream,
|
||||||
|
metadata,
|
||||||
|
verifierB64,
|
||||||
|
timeLimit,
|
||||||
|
dlimit,
|
||||||
|
bearerToken,
|
||||||
|
onprogress,
|
||||||
|
canceller
|
||||||
|
) {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
const port = window.location.port;
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const endpoint =
|
||||||
|
window.location.protocol === 'file:'
|
||||||
|
? fileProtocolWssUrl
|
||||||
|
: `${protocol}//${host}${port ? ':' : ''}${port}/api/ws`;
|
||||||
|
|
||||||
|
const ws = await asyncInitWebSocket(endpoint);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metadataHeader = arrayToB64(new Uint8Array(metadata));
|
||||||
|
const fileMeta = {
|
||||||
|
fileMetadata: metadataHeader,
|
||||||
|
authorization: `send-v1 ${verifierB64}`,
|
||||||
|
bearer: bearerToken,
|
||||||
|
timeLimit,
|
||||||
|
dlimit
|
||||||
|
};
|
||||||
|
const uploadInfoResponse = listenForResponse(ws, canceller);
|
||||||
|
ws.send(JSON.stringify(fileMeta));
|
||||||
|
const uploadInfo = await uploadInfoResponse;
|
||||||
|
|
||||||
|
const completedResponse = listenForResponse(ws, canceller);
|
||||||
|
|
||||||
|
const reader = stream.getReader();
|
||||||
|
let state = await reader.read();
|
||||||
|
let size = 0;
|
||||||
|
while (!state.done) {
|
||||||
|
const buf = state.value;
|
||||||
|
if (canceller.cancelled) {
|
||||||
|
throw canceller.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(buf);
|
||||||
|
|
||||||
|
onprogress(size);
|
||||||
|
size += buf.length;
|
||||||
|
state = await reader.read();
|
||||||
|
while (ws.bufferedAmount > ECE_RECORD_SIZE * 2) {
|
||||||
|
await delay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const footer = new Uint8Array([0]);
|
||||||
|
ws.send(footer);
|
||||||
|
|
||||||
|
await completedResponse;
|
||||||
|
ws.close();
|
||||||
|
return uploadInfo;
|
||||||
|
} catch (e) {
|
||||||
|
ws.close(4000);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadWs(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
verifierB64,
|
||||||
|
timeLimit,
|
||||||
|
dlimit,
|
||||||
|
bearerToken,
|
||||||
|
onprogress
|
||||||
|
) {
|
||||||
|
const canceller = { cancelled: false };
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: function() {
|
||||||
|
canceller.error = new Error(0);
|
||||||
|
canceller.cancelled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
result: upload(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
verifierB64,
|
||||||
|
timeLimit,
|
||||||
|
dlimit,
|
||||||
|
bearerToken,
|
||||||
|
onprogress,
|
||||||
|
canceller
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////
|
||||||
|
|
||||||
|
async function downloadS(id, keychain, signal) {
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
|
||||||
|
const response = await fetch(getApiUrl(`/api/download/${id}`), {
|
||||||
|
signal: signal,
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Authorization: auth }
|
||||||
|
});
|
||||||
|
|
||||||
|
const authHeader = response.headers.get('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDownloadStream(id, keychain, signal, tries = 1) {
|
||||||
|
try {
|
||||||
|
const result = await downloadS(id, keychain, signal);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401' && --tries > 0) {
|
||||||
|
return tryDownloadStream(id, keychain, signal, tries);
|
||||||
|
}
|
||||||
|
if (e.name === 'AbortError') {
|
||||||
|
throw new Error('0');
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadStream(id, keychain) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
function cancel() {
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cancel,
|
||||||
|
result: tryDownloadStream(id, keychain, controller.signal, 2)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////
|
||||||
|
|
||||||
|
function download(id, keychain, onprogress, canceller) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
canceller.oncancel = function() {
|
||||||
|
xhr.abort();
|
||||||
|
};
|
||||||
|
return new Promise(async function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
canceller.oncancel = function() {};
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([xhr.response]);
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('progress', function(event) {
|
||||||
|
if (event.target.status === 200) {
|
||||||
|
onprogress(event.loaded);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
xhr.open('get', getApiUrl(`/api/download/blob/${id}`));
|
||||||
|
xhr.setRequestHeader('Authorization', auth);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
onprogress(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDownload(id, keychain, onprogress, canceller, tries = 1) {
|
||||||
|
try {
|
||||||
|
const result = await download(id, keychain, onprogress, canceller);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401' && --tries > 0) {
|
||||||
|
return tryDownload(id, keychain, onprogress, canceller, tries);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadFile(id, keychain, onprogress) {
|
||||||
|
const canceller = {
|
||||||
|
oncancel: function() {} // download() sets this
|
||||||
|
};
|
||||||
|
function cancel() {
|
||||||
|
canceller.oncancel();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cancel,
|
||||||
|
result: tryDownload(id, keychain, onprogress, canceller, 2)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileList(bearerToken, kid) {
|
||||||
|
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
|
||||||
|
const response = await fetch(getApiUrl(`/api/filelist/${kid}`), { headers });
|
||||||
|
if (response.ok) {
|
||||||
|
const encrypted = await response.blob();
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setFileList(bearerToken, kid, data) {
|
||||||
|
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
|
||||||
|
const response = await fetch(getApiUrl(`/api/filelist/${kid}`), {
|
||||||
|
headers,
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendMetrics(blob) {
|
||||||
|
if (!navigator.sendBeacon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
navigator.sendBeacon(getApiUrl('/api/metrics'), blob);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConstants() {
|
||||||
|
const response = await fetch(getApiUrl('/config'));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const obj = await response.json();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
83
app/archive.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { blobStream, concatStream } from './streams';
|
||||||
|
|
||||||
|
function isDupe(newFile, array) {
|
||||||
|
for (const file of array) {
|
||||||
|
if (
|
||||||
|
newFile.name === file.name &&
|
||||||
|
newFile.size === file.size &&
|
||||||
|
newFile.lastModified === file.lastModified
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Archive {
|
||||||
|
constructor(files = [], defaultTimeLimit = 86400) {
|
||||||
|
this.files = Array.from(files);
|
||||||
|
this.defaultTimeLimit = defaultTimeLimit;
|
||||||
|
this.timeLimit = defaultTimeLimit;
|
||||||
|
this.dlimit = 1;
|
||||||
|
this.password = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.files.length > 1 ? 'Send-Archive.zip' : this.files[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this.files.length > 1 ? 'send-archive' : this.files[0].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.files.reduce((total, file) => total + file.size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get numFiles() {
|
||||||
|
return this.files.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get manifest() {
|
||||||
|
return {
|
||||||
|
files: this.files.map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get stream() {
|
||||||
|
return concatStream(this.files.map(file => blobStream(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
addFiles(files, maxSize, maxFiles) {
|
||||||
|
if (this.files.length + files.length > maxFiles) {
|
||||||
|
throw new Error('tooManyFiles');
|
||||||
|
}
|
||||||
|
const newFiles = files.filter(
|
||||||
|
file => file.size > 0 && !isDupe(file, this.files)
|
||||||
|
);
|
||||||
|
const newSize = newFiles.reduce((total, file) => total + file.size, 0);
|
||||||
|
if (this.size + newSize > maxSize) {
|
||||||
|
throw new Error('fileTooBig');
|
||||||
|
}
|
||||||
|
this.files = this.files.concat(newFiles);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(file) {
|
||||||
|
const index = this.files.indexOf(file);
|
||||||
|
if (index > -1) {
|
||||||
|
this.files.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.files = [];
|
||||||
|
this.dlimit = 1;
|
||||||
|
this.timeLimit = this.defaultTimeLimit;
|
||||||
|
this.password = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
app/capabilities.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/* global AUTH_CONFIG LOCALE */
|
||||||
|
import { browserName } from './utils';
|
||||||
|
|
||||||
|
async function checkCrypto() {
|
||||||
|
try {
|
||||||
|
const key = await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
await crypto.subtle.exportKey('raw', key);
|
||||||
|
await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: crypto.getRandomValues(new Uint8Array(12)),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
new ArrayBuffer(8)
|
||||||
|
);
|
||||||
|
await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
crypto.getRandomValues(new Uint8Array(16)),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
crypto.getRandomValues(new Uint8Array(16)),
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
try {
|
||||||
|
window.asmCrypto = await import('asmcrypto.js');
|
||||||
|
await import('@dannycoates/webcrypto-liner/build/shim');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStreams() {
|
||||||
|
try {
|
||||||
|
new ReadableStream({
|
||||||
|
pull() {}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function polyfillStreams() {
|
||||||
|
try {
|
||||||
|
await import('@mattiasbuelens/web-streams-polyfill');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function getCapabilities() {
|
||||||
|
const serviceWorker =
|
||||||
|
'serviceWorker' in navigator && browserName() !== 'edge';
|
||||||
|
let crypto = await checkCrypto();
|
||||||
|
const nativeStreams = checkStreams();
|
||||||
|
let polyStreams = false;
|
||||||
|
if (!nativeStreams) {
|
||||||
|
polyStreams = await polyfillStreams();
|
||||||
|
}
|
||||||
|
let account = typeof AUTH_CONFIG !== 'undefined';
|
||||||
|
try {
|
||||||
|
account = account && !!localStorage;
|
||||||
|
} catch (e) {
|
||||||
|
account = false;
|
||||||
|
}
|
||||||
|
const share =
|
||||||
|
typeof navigator.share === 'function' && LOCALE.startsWith('en'); // en until strings merge
|
||||||
|
|
||||||
|
return {
|
||||||
|
account,
|
||||||
|
crypto,
|
||||||
|
serviceWorker,
|
||||||
|
streamUpload: nativeStreams || polyStreams,
|
||||||
|
streamDownload:
|
||||||
|
nativeStreams && serviceWorker && browserName() !== 'safari',
|
||||||
|
multifile: nativeStreams || polyStreams,
|
||||||
|
share
|
||||||
|
};
|
||||||
|
}
|
||||||
302
app/controller.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import FileSender from './fileSender';
|
||||||
|
import FileReceiver from './fileReceiver';
|
||||||
|
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
|
||||||
|
import * as metrics from './metrics';
|
||||||
|
import { bytes } from './utils';
|
||||||
|
import okDialog from './ui/okDialog';
|
||||||
|
import copyDialog from './ui/copyDialog';
|
||||||
|
import shareDialog from './ui/shareDialog';
|
||||||
|
import signupDialog from './ui/signupDialog';
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
let lastRender = 0;
|
||||||
|
let updateTitle = false;
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
emitter.emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFiles() {
|
||||||
|
const changes = await state.user.syncFileList();
|
||||||
|
const rerender = changes.incoming || changes.downloadCount;
|
||||||
|
if (rerender) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
if (updateTitle) {
|
||||||
|
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.addEventListener('blur', () => (updateTitle = true));
|
||||||
|
document.addEventListener('focus', () => {
|
||||||
|
updateTitle = false;
|
||||||
|
emitter.emit('DOMTitleChange', 'Firefox Send');
|
||||||
|
});
|
||||||
|
checkFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('render', () => {
|
||||||
|
lastRender = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('login', email => {
|
||||||
|
state.user.login(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('logout', () => {
|
||||||
|
state.user.logout();
|
||||||
|
metrics.loggedOut({ trigger: 'button' });
|
||||||
|
emitter.emit('pushState', '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('removeUpload', file => {
|
||||||
|
state.archive.remove(file);
|
||||||
|
if (state.archive.numFiles === 0) {
|
||||||
|
state.archive.clear();
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('delete', async ownedFile => {
|
||||||
|
try {
|
||||||
|
metrics.deletedUpload({
|
||||||
|
size: ownedFile.size,
|
||||||
|
time: ownedFile.time,
|
||||||
|
speed: ownedFile.speed,
|
||||||
|
type: ownedFile.type,
|
||||||
|
ttl: ownedFile.expiresAt - Date.now(),
|
||||||
|
location
|
||||||
|
});
|
||||||
|
state.storage.remove(ownedFile.id);
|
||||||
|
await ownedFile.del();
|
||||||
|
} catch (e) {
|
||||||
|
state.raven.captureException(e);
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('cancel', () => {
|
||||||
|
state.transfer.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('addFiles', async ({ files }) => {
|
||||||
|
if (files.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maxSize = state.user.maxSize;
|
||||||
|
try {
|
||||||
|
state.archive.addFiles(
|
||||||
|
files,
|
||||||
|
maxSize,
|
||||||
|
state.LIMITS.MAX_FILES_PER_ARCHIVE
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === 'fileTooBig' && maxSize < state.LIMITS.MAX_FILE_SIZE) {
|
||||||
|
return emitter.emit('signup-cta', 'size');
|
||||||
|
}
|
||||||
|
state.modal = okDialog(
|
||||||
|
state.translate(e.message, {
|
||||||
|
size: bytes(maxSize),
|
||||||
|
count: state.LIMITS.MAX_FILES_PER_ARCHIVE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('signup-cta', source => {
|
||||||
|
const query = state.query;
|
||||||
|
state.user.startAuthFlow(source, {
|
||||||
|
campaign: query.utm_campaign,
|
||||||
|
content: query.utm_content,
|
||||||
|
medium: query.utm_medium,
|
||||||
|
source: query.utm_source,
|
||||||
|
term: query.utm_term
|
||||||
|
});
|
||||||
|
state.modal = signupDialog(source);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('authenticate', async (code, oauthState) => {
|
||||||
|
try {
|
||||||
|
await state.user.finishLogin(code, oauthState);
|
||||||
|
await state.user.syncFileList();
|
||||||
|
emitter.emit('replaceState', '/');
|
||||||
|
} catch (e) {
|
||||||
|
emitter.emit('replaceState', '/error');
|
||||||
|
setTimeout(render);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('upload', async () => {
|
||||||
|
if (state.storage.files.length >= state.LIMITS.MAX_ARCHIVES_PER_USER) {
|
||||||
|
state.modal = okDialog(
|
||||||
|
state.translate('tooManyArchives', {
|
||||||
|
count: state.LIMITS.MAX_ARCHIVES_PER_USER
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
const archive = state.archive;
|
||||||
|
const sender = new FileSender();
|
||||||
|
|
||||||
|
sender.on('progress', updateProgress);
|
||||||
|
sender.on('encrypting', render);
|
||||||
|
sender.on('complete', render);
|
||||||
|
state.transfer = sender;
|
||||||
|
state.uploading = true;
|
||||||
|
render();
|
||||||
|
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
await delay(200);
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const ownedFile = await sender.upload(archive, state.user.bearerToken);
|
||||||
|
state.storage.totalUploads += 1;
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
metrics.completedUpload(archive, duration);
|
||||||
|
|
||||||
|
state.storage.addFile(ownedFile);
|
||||||
|
// TODO integrate password into /upload request
|
||||||
|
if (archive.password) {
|
||||||
|
emitter.emit('password', {
|
||||||
|
password: archive.password,
|
||||||
|
file: ownedFile
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.modal = state.capabilities.share
|
||||||
|
? shareDialog(ownedFile.name, ownedFile.url)
|
||||||
|
: copyDialog(ownedFile.name, ownedFile.url);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === '0') {
|
||||||
|
//cancelled. do nothing
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
metrics.cancelledUpload(archive, duration);
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedUpload(archive);
|
||||||
|
emitter.emit('pushState', '/error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
archive.clear();
|
||||||
|
state.uploading = false;
|
||||||
|
state.transfer = null;
|
||||||
|
await state.user.syncFileList();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('password', async ({ password, file }) => {
|
||||||
|
try {
|
||||||
|
state.settingPassword = true;
|
||||||
|
render();
|
||||||
|
await file.setPassword(password);
|
||||||
|
state.storage.writeFile(file);
|
||||||
|
await delay(1000);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
state.passwordSetError = err;
|
||||||
|
} finally {
|
||||||
|
state.settingPassword = false;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('getMetadata', async () => {
|
||||||
|
const file = state.fileInfo;
|
||||||
|
|
||||||
|
const receiver = new FileReceiver(file);
|
||||||
|
try {
|
||||||
|
await receiver.getMetadata();
|
||||||
|
state.transfer = receiver;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401' || e.message === '404') {
|
||||||
|
file.password = null;
|
||||||
|
if (!file.requiresPassword) {
|
||||||
|
return emitter.emit('pushState', '/404');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('download', async file => {
|
||||||
|
state.transfer.on('progress', updateProgress);
|
||||||
|
state.transfer.on('decrypting', render);
|
||||||
|
state.transfer.on('complete', render);
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
const size = file.size;
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const dl = state.transfer.download({
|
||||||
|
stream: state.capabilities.streamDownload
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
await dl;
|
||||||
|
state.storage.totalDownloads += 1;
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
metrics.completedDownload({
|
||||||
|
size,
|
||||||
|
duration,
|
||||||
|
password_protected: file.requiresPassword
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === '0') {
|
||||||
|
// download cancelled
|
||||||
|
state.transfer.reset();
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
state.transfer = null;
|
||||||
|
const location = err.message === '404' ? '/404' : '/error';
|
||||||
|
if (location === '/error') {
|
||||||
|
state.raven.captureException(err);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
metrics.stoppedDownload({
|
||||||
|
size,
|
||||||
|
duration,
|
||||||
|
password_protected: file.requiresPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
emitter.emit('pushState', location);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('copy', ({ url }) => {
|
||||||
|
copyToClipboard(url);
|
||||||
|
// metrics.copiedLink({ location });
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for updates of the upload list
|
||||||
|
if (!state.modal && state.route === '/') {
|
||||||
|
checkFiles();
|
||||||
|
}
|
||||||
|
}, 2 * 60 * 1000);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for rerendering the file list countdown timers
|
||||||
|
if (
|
||||||
|
!state.modal &&
|
||||||
|
state.route === '/' &&
|
||||||
|
state.storage.files.length > 0 &&
|
||||||
|
Date.now() - lastRender > 30000
|
||||||
|
) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
@@ -6,18 +6,16 @@ export default function(state, emitter) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.addEventListener('drop', event => {
|
document.body.addEventListener('drop', event => {
|
||||||
if (state.route === '/' && !state.transfer) {
|
if (
|
||||||
|
state.route === '/' &&
|
||||||
|
!state.uploading &&
|
||||||
|
event.dataTransfer &&
|
||||||
|
event.dataTransfer.files
|
||||||
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.querySelector('.upload-window').classList.remove('ondrag');
|
emitter.emit('addFiles', {
|
||||||
const target = event.dataTransfer;
|
files: Array.from(event.dataTransfer.files)
|
||||||
if (target.files.length === 0) {
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.files.length > 1 || target.files[0].size === 0) {
|
|
||||||
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
|
||||||
}
|
|
||||||
const file = target.files[0];
|
|
||||||
emitter.emit('upload', { file, type: 'drop' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
310
app/ece.js
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import 'buffer';
|
||||||
|
import { transformStream } from './streams';
|
||||||
|
|
||||||
|
const NONCE_LENGTH = 12;
|
||||||
|
const TAG_LENGTH = 16;
|
||||||
|
const KEY_LENGTH = 16;
|
||||||
|
const MODE_ENCRYPT = 'encrypt';
|
||||||
|
const MODE_DECRYPT = 'decrypt';
|
||||||
|
export const ECE_RECORD_SIZE = 1024 * 64;
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
function generateSalt(len) {
|
||||||
|
const randSalt = new Uint8Array(len);
|
||||||
|
crypto.getRandomValues(randSalt);
|
||||||
|
return randSalt.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ECETransformer {
|
||||||
|
constructor(mode, ikm, rs, salt) {
|
||||||
|
this.mode = mode;
|
||||||
|
this.prevChunk;
|
||||||
|
this.seq = 0;
|
||||||
|
this.firstchunk = true;
|
||||||
|
this.rs = rs;
|
||||||
|
this.ikm = ikm.buffer;
|
||||||
|
this.salt = salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateKey() {
|
||||||
|
const inputKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.ikm,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: this.salt,
|
||||||
|
info: encoder.encode('Content-Encoding: aes128gcm\0'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
inputKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true, // Edge polyfill requires key to be extractable to encrypt :/
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateNonceBase() {
|
||||||
|
const inputKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.ikm,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
const base = await crypto.subtle.exportKey(
|
||||||
|
'raw',
|
||||||
|
await crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: this.salt,
|
||||||
|
info: encoder.encode('Content-Encoding: nonce\0'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
inputKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Buffer.from(base.slice(0, NONCE_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNonce(seq) {
|
||||||
|
if (seq > 0xffffffff) {
|
||||||
|
throw new Error('record sequence number exceeds limit');
|
||||||
|
}
|
||||||
|
const nonce = Buffer.from(this.nonceBase);
|
||||||
|
const m = nonce.readUIntBE(nonce.length - 4, 4);
|
||||||
|
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
|
||||||
|
nonce.writeUIntBE(xor, nonce.length - 4, 4);
|
||||||
|
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
pad(data, isLast) {
|
||||||
|
const len = data.length;
|
||||||
|
if (len + TAG_LENGTH >= this.rs) {
|
||||||
|
throw new Error('data too large for record size');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
const padding = Buffer.alloc(1);
|
||||||
|
padding.writeUInt8(2, 0);
|
||||||
|
return Buffer.concat([data, padding]);
|
||||||
|
} else {
|
||||||
|
const padding = Buffer.alloc(this.rs - len - TAG_LENGTH);
|
||||||
|
padding.fill(0);
|
||||||
|
padding.writeUInt8(1, 0);
|
||||||
|
return Buffer.concat([data, padding]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unpad(data, isLast) {
|
||||||
|
for (let i = data.length - 1; i >= 0; i--) {
|
||||||
|
if (data[i]) {
|
||||||
|
if (isLast) {
|
||||||
|
if (data[i] !== 2) {
|
||||||
|
throw new Error('delimiter of final record is not 2');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data[i] !== 1) {
|
||||||
|
throw new Error('delimiter of not final record is not 1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data.slice(0, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('no delimiter found');
|
||||||
|
}
|
||||||
|
|
||||||
|
createHeader() {
|
||||||
|
const nums = Buffer.alloc(5);
|
||||||
|
nums.writeUIntBE(this.rs, 0, 4);
|
||||||
|
nums.writeUIntBE(0, 4, 1);
|
||||||
|
return Buffer.concat([Buffer.from(this.salt), nums]);
|
||||||
|
}
|
||||||
|
|
||||||
|
readHeader(buffer) {
|
||||||
|
if (buffer.length < 21) {
|
||||||
|
throw new Error('chunk too small for reading header');
|
||||||
|
}
|
||||||
|
const header = {};
|
||||||
|
header.salt = buffer.buffer.slice(0, KEY_LENGTH);
|
||||||
|
header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
|
||||||
|
const idlen = buffer.readUInt8(KEY_LENGTH + 4);
|
||||||
|
header.length = idlen + KEY_LENGTH + 5;
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptRecord(buffer, seq, isLast) {
|
||||||
|
const nonce = this.generateNonce(seq);
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv: nonce },
|
||||||
|
this.key,
|
||||||
|
this.pad(buffer, isLast)
|
||||||
|
);
|
||||||
|
return Buffer.from(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptRecord(buffer, seq, isLast) {
|
||||||
|
const nonce = this.generateNonce(seq);
|
||||||
|
const data = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: nonce,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
this.key,
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.unpad(Buffer.from(data), isLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(controller) {
|
||||||
|
if (this.mode === MODE_ENCRYPT) {
|
||||||
|
this.key = await this.generateKey();
|
||||||
|
this.nonceBase = await this.generateNonceBase();
|
||||||
|
controller.enqueue(this.createHeader());
|
||||||
|
} else if (this.mode !== MODE_DECRYPT) {
|
||||||
|
throw new Error('mode must be either encrypt or decrypt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async transformPrevChunk(isLast, controller) {
|
||||||
|
if (this.mode === MODE_ENCRYPT) {
|
||||||
|
controller.enqueue(
|
||||||
|
await this.encryptRecord(this.prevChunk, this.seq, isLast)
|
||||||
|
);
|
||||||
|
this.seq++;
|
||||||
|
} else {
|
||||||
|
if (this.seq === 0) {
|
||||||
|
//the first chunk during decryption contains only the header
|
||||||
|
const header = this.readHeader(this.prevChunk);
|
||||||
|
this.salt = header.salt;
|
||||||
|
this.rs = header.rs;
|
||||||
|
this.key = await this.generateKey();
|
||||||
|
this.nonceBase = await this.generateNonceBase();
|
||||||
|
} else {
|
||||||
|
controller.enqueue(
|
||||||
|
await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.seq++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async transform(chunk, controller) {
|
||||||
|
if (!this.firstchunk) {
|
||||||
|
await this.transformPrevChunk(false, controller);
|
||||||
|
}
|
||||||
|
this.firstchunk = false;
|
||||||
|
this.prevChunk = Buffer.from(chunk.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async flush(controller) {
|
||||||
|
//console.log('ece stream ends')
|
||||||
|
if (this.prevChunk) {
|
||||||
|
await this.transformPrevChunk(true, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamSlicer {
|
||||||
|
constructor(rs, mode) {
|
||||||
|
this.mode = mode;
|
||||||
|
this.rs = rs;
|
||||||
|
this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21;
|
||||||
|
this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved
|
||||||
|
this.offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(buf, controller) {
|
||||||
|
controller.enqueue(buf);
|
||||||
|
if (this.chunkSize === 21 && this.mode === MODE_DECRYPT) {
|
||||||
|
this.chunkSize = this.rs;
|
||||||
|
}
|
||||||
|
this.partialChunk = new Uint8Array(this.chunkSize);
|
||||||
|
this.offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//reslice input into record sized chunks
|
||||||
|
transform(chunk, controller) {
|
||||||
|
//console.log('Received chunk with %d bytes.', chunk.byteLength)
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
if (this.offset > 0) {
|
||||||
|
const len = Math.min(chunk.byteLength, this.chunkSize - this.offset);
|
||||||
|
this.partialChunk.set(chunk.slice(0, len), this.offset);
|
||||||
|
this.offset += len;
|
||||||
|
i += len;
|
||||||
|
|
||||||
|
if (this.offset === this.chunkSize) {
|
||||||
|
this.send(this.partialChunk, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < chunk.byteLength) {
|
||||||
|
const remainingBytes = chunk.byteLength - i;
|
||||||
|
if (remainingBytes >= this.chunkSize) {
|
||||||
|
const record = chunk.slice(i, i + this.chunkSize);
|
||||||
|
i += this.chunkSize;
|
||||||
|
this.send(record, controller);
|
||||||
|
} else {
|
||||||
|
const end = chunk.slice(i, i + remainingBytes);
|
||||||
|
i += end.byteLength;
|
||||||
|
this.partialChunk.set(end);
|
||||||
|
this.offset = end.byteLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush(controller) {
|
||||||
|
if (this.offset > 0) {
|
||||||
|
controller.enqueue(this.partialChunk.slice(0, this.offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
input: a ReadableStream containing data to be transformed
|
||||||
|
key: Uint8Array containing key of size KEY_LENGTH
|
||||||
|
rs: int containing record size, optional
|
||||||
|
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
|
||||||
|
*/
|
||||||
|
export function encryptStream(
|
||||||
|
input,
|
||||||
|
key,
|
||||||
|
rs = ECE_RECORD_SIZE,
|
||||||
|
salt = generateSalt(KEY_LENGTH)
|
||||||
|
) {
|
||||||
|
const mode = 'encrypt';
|
||||||
|
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
|
||||||
|
return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
input: a ReadableStream containing data to be transformed
|
||||||
|
key: Uint8Array containing key of size KEY_LENGTH
|
||||||
|
rs: int containing record size, optional
|
||||||
|
*/
|
||||||
|
export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
|
||||||
|
const mode = 'decrypt';
|
||||||
|
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
|
||||||
|
return transformStream(inputStream, new ECETransformer(mode, key, rs));
|
||||||
|
}
|
||||||
@@ -1,28 +1,6 @@
|
|||||||
import hash from 'string-hash';
|
import hash from 'string-hash';
|
||||||
|
|
||||||
const experiments = {
|
const experiments = {};
|
||||||
XnN0idVWSxO6A0kiNkxzGw: {
|
|
||||||
id: 'XnN0idVWSxO6A0kiNkxzGw',
|
|
||||||
run: function(variant, state, emitter) {
|
|
||||||
state.promo = variant === 1 ? 'blue' : 'grey';
|
|
||||||
emitter.emit('render');
|
|
||||||
},
|
|
||||||
eligible: function() {
|
|
||||||
return (
|
|
||||||
!/firefox/i.test(navigator.userAgent) &&
|
|
||||||
document.querySelector('html').lang === 'en-US'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
variant: function(state) {
|
|
||||||
return this.luckyNumber(state) > 0.5 ? 1 : 0;
|
|
||||||
},
|
|
||||||
luckyNumber: function(state) {
|
|
||||||
return luckyNumber(
|
|
||||||
`${this.id}:${state.storage.get('testpilot_ga__cid')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//Returns a number between 0 and 1
|
//Returns a number between 0 and 1
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
/* global EXPIRE_SECONDS */
|
|
||||||
import FileSender from './fileSender';
|
|
||||||
import FileReceiver from './fileReceiver';
|
|
||||||
import { copyToClipboard, delay, fadeOut, percent } from './utils';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
|
|
||||||
function saveFile(file) {
|
|
||||||
const dataView = new DataView(file.plaintext);
|
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
if (window.navigator.msSaveBlob) {
|
|
||||||
return window.navigator.msSaveBlob(blob, file.name);
|
|
||||||
}
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = downloadUrl;
|
|
||||||
a.download = file.name;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openLinksInNewTab(links, should = true) {
|
|
||||||
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
|
||||||
if (should) {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.setAttribute('target', '_blank');
|
|
||||||
l.setAttribute('rel', 'noopener noreferrer');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.removeAttribute('target');
|
|
||||||
l.removeAttribute('rel');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return links;
|
|
||||||
}
|
|
||||||
|
|
||||||
function exists(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
|
||||||
resolve(xhr.status === 200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => resolve(false);
|
|
||||||
xhr.ontimeout = () => resolve(false);
|
|
||||||
xhr.open('get', '/api/exists/' + id);
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(state, emitter) {
|
|
||||||
let lastRender = 0;
|
|
||||||
let updateTitle = false;
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
emitter.emit('render');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkFiles() {
|
|
||||||
const files = state.storage.files;
|
|
||||||
let rerender = false;
|
|
||||||
for (const file of files) {
|
|
||||||
const ok = await exists(file.id);
|
|
||||||
if (!ok) {
|
|
||||||
state.storage.remove(file.id);
|
|
||||||
rerender = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rerender) {
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress() {
|
|
||||||
if (updateTitle) {
|
|
||||||
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
|
|
||||||
}
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter.on('DOMContentLoaded', () => {
|
|
||||||
document.addEventListener('blur', () => (updateTitle = true));
|
|
||||||
document.addEventListener('focus', () => {
|
|
||||||
updateTitle = false;
|
|
||||||
emitter.emit('DOMTitleChange', 'Firefox Send');
|
|
||||||
});
|
|
||||||
checkFiles();
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('navigate', checkFiles);
|
|
||||||
|
|
||||||
emitter.on('render', () => {
|
|
||||||
lastRender = Date.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('changeLimit', async ({ file, value }) => {
|
|
||||||
await FileSender.changeLimit(file.id, file.ownerToken, value);
|
|
||||||
file.dlimit = value;
|
|
||||||
state.storage.writeFiles();
|
|
||||||
metrics.changedDownloadLimit(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('delete', async ({ file, location }) => {
|
|
||||||
try {
|
|
||||||
metrics.deletedUpload({
|
|
||||||
size: file.size,
|
|
||||||
time: file.time,
|
|
||||||
speed: file.speed,
|
|
||||||
type: file.type,
|
|
||||||
ttl: file.expiresAt - Date.now(),
|
|
||||||
location
|
|
||||||
});
|
|
||||||
state.storage.remove(file.id);
|
|
||||||
await FileSender.delete(file.id, file.ownerToken);
|
|
||||||
} catch (e) {
|
|
||||||
state.raven.captureException(e);
|
|
||||||
}
|
|
||||||
state.fileInfo = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('cancel', () => {
|
|
||||||
state.transfer.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('upload', async ({ file, type }) => {
|
|
||||||
const size = file.size;
|
|
||||||
const sender = new FileSender(file);
|
|
||||||
sender.on('progress', updateProgress);
|
|
||||||
sender.on('encrypting', render);
|
|
||||||
state.transfer = sender;
|
|
||||||
render();
|
|
||||||
const links = openLinksInNewTab();
|
|
||||||
await delay(200);
|
|
||||||
try {
|
|
||||||
const start = Date.now();
|
|
||||||
metrics.startedUpload({ size, type });
|
|
||||||
const info = await sender.upload();
|
|
||||||
const time = Date.now() - start;
|
|
||||||
const speed = size / (time / 1000);
|
|
||||||
metrics.completedUpload({ size, time, speed, type });
|
|
||||||
document.getElementById('cancel-upload').hidden = 'hidden';
|
|
||||||
await delay(1000);
|
|
||||||
await fadeOut('upload-progress');
|
|
||||||
info.name = file.name;
|
|
||||||
info.size = size;
|
|
||||||
info.type = type;
|
|
||||||
info.time = time;
|
|
||||||
info.speed = speed;
|
|
||||||
info.createdAt = Date.now();
|
|
||||||
info.url = `${info.url}#${info.secretKey}`;
|
|
||||||
info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
|
|
||||||
state.fileInfo = info;
|
|
||||||
state.storage.addFile(state.fileInfo);
|
|
||||||
openLinksInNewTab(links, false);
|
|
||||||
state.transfer = null;
|
|
||||||
state.storage.totalUploads += 1;
|
|
||||||
emitter.emit('pushState', `/share/${info.id}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
state.transfer = null;
|
|
||||||
if (err.message === '0') {
|
|
||||||
//cancelled. do nothing
|
|
||||||
metrics.cancelledUpload({ size, type });
|
|
||||||
return render();
|
|
||||||
}
|
|
||||||
state.raven.captureException(err);
|
|
||||||
metrics.stoppedUpload({ size, type, err });
|
|
||||||
emitter.emit('pushState', '/error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('password', async ({ password, file }) => {
|
|
||||||
try {
|
|
||||||
await FileSender.setPassword(password, file);
|
|
||||||
metrics.addedPassword({ size: file.size });
|
|
||||||
file.password = password;
|
|
||||||
state.storage.writeFiles();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('preview', async () => {
|
|
||||||
const file = state.fileInfo;
|
|
||||||
const url = `/api/download/${file.id}`;
|
|
||||||
const receiver = new FileReceiver(url, file);
|
|
||||||
receiver.on('progress', updateProgress);
|
|
||||||
receiver.on('decrypting', render);
|
|
||||||
state.transfer = receiver;
|
|
||||||
try {
|
|
||||||
await receiver.getMetadata(file.nonce);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === '401') {
|
|
||||||
file.password = null;
|
|
||||||
if (!file.pwd) {
|
|
||||||
return emitter.emit('pushState', '/404');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('download', async file => {
|
|
||||||
state.transfer.on('progress', render);
|
|
||||||
state.transfer.on('decrypting', render);
|
|
||||||
const links = openLinksInNewTab();
|
|
||||||
const size = file.size;
|
|
||||||
try {
|
|
||||||
const start = Date.now();
|
|
||||||
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
|
||||||
const f = await state.transfer.download(file.nonce);
|
|
||||||
const time = Date.now() - start;
|
|
||||||
const speed = size / (time / 1000);
|
|
||||||
await delay(1000);
|
|
||||||
await fadeOut('download-progress');
|
|
||||||
saveFile(f);
|
|
||||||
state.storage.totalDownloads += 1;
|
|
||||||
state.transfer = null;
|
|
||||||
metrics.completedDownload({ size, time, speed });
|
|
||||||
emitter.emit('pushState', '/completed');
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
// TODO cancelled download
|
|
||||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
|
||||||
if (location === '/error') {
|
|
||||||
state.raven.captureException(err);
|
|
||||||
metrics.stoppedDownload({ size, err });
|
|
||||||
}
|
|
||||||
emitter.emit('pushState', location);
|
|
||||||
} finally {
|
|
||||||
state.transfer = null;
|
|
||||||
openLinksInNewTab(links, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('copy', ({ url, location }) => {
|
|
||||||
copyToClipboard(url);
|
|
||||||
metrics.copiedLink({ location });
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
// poll for rerendering the file list countdown timers
|
|
||||||
if (
|
|
||||||
state.route === '/' &&
|
|
||||||
state.storage.files.length > 0 &&
|
|
||||||
Date.now() - lastRender > 30000
|
|
||||||
) {
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
}, 60000);
|
|
||||||
}
|
|
||||||
@@ -1,110 +1,29 @@
|
|||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import { arrayToB64, b64ToArray, bytes } from './utils';
|
import Keychain from './keychain';
|
||||||
|
import { delay, bytes, streamToArrayBuffer } from './utils';
|
||||||
|
import { downloadFile, metadata, getApiUrl } from './api';
|
||||||
|
import { blobStream } from './streams';
|
||||||
|
import Zip from './zip';
|
||||||
|
|
||||||
export default class FileReceiver extends Nanobus {
|
export default class FileReceiver extends Nanobus {
|
||||||
constructor(url, file) {
|
constructor(fileInfo) {
|
||||||
super('FileReceiver');
|
super('FileReceiver');
|
||||||
this.secretKeyPromise = window.crypto.subtle.importKey(
|
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
|
||||||
'raw',
|
if (fileInfo.requiresPassword) {
|
||||||
b64ToArray(file.key),
|
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||||
'HKDF',
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
return window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('encryption'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
sk,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (file.pwd) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
this.authKeyPromise = window.crypto.subtle
|
|
||||||
.importKey(
|
|
||||||
'raw',
|
|
||||||
encoder.encode(file.password),
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
)
|
|
||||||
.then(pwdKey =>
|
|
||||||
window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt: encoder.encode(file.url),
|
|
||||||
iterations: 100,
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
pwdKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.authKeyPromise = this.secretKeyPromise.then(sk => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
return window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('authentication'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
sk,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: { name: 'SHA-256' }
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.metaKeyPromise = this.secretKeyPromise.then(sk => {
|
this.fileInfo = fileInfo;
|
||||||
const encoder = new TextEncoder();
|
this.reset();
|
||||||
return window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('metadata'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
sk,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.file = file;
|
|
||||||
this.url = url;
|
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
this.state = 'initialized';
|
|
||||||
this.progress = [0, 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get progressRatio() {
|
get progressRatio() {
|
||||||
return this.progress[0] / this.progress[1];
|
return this.progress[0] / this.progress[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get progressIndefinite() {
|
||||||
|
return this.state !== 'downloading';
|
||||||
|
}
|
||||||
|
|
||||||
get sizes() {
|
get sizes() {
|
||||||
return {
|
return {
|
||||||
partialSize: bytes(this.progress[0]),
|
partialSize: bytes(this.progress[0]),
|
||||||
@@ -113,159 +32,204 @@ export default class FileReceiver extends Nanobus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
// TODO
|
if (this.downloadRequest) {
|
||||||
}
|
this.downloadRequest.cancel();
|
||||||
|
|
||||||
async fetchMetadata(nonce) {
|
|
||||||
const authHeader = await this.getAuthHeader(nonce);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
|
|
||||||
this.file.nonce = nonce;
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
return resolve(xhr.response);
|
|
||||||
}
|
|
||||||
const err = new Error(xhr.status);
|
|
||||||
err.nonce = nonce;
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => reject(new Error(0));
|
|
||||||
xhr.ontimeout = () => reject(new Error(0));
|
|
||||||
xhr.open('get', `/api/metadata/${this.file.id}`);
|
|
||||||
xhr.setRequestHeader('Authorization', authHeader);
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMetadata(nonce) {
|
|
||||||
let data = null;
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
data = await this.fetchMetadata(nonce);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === '401' && nonce !== e.nonce) {
|
|
||||||
// allow one retry for changed nonce
|
|
||||||
data = await this.fetchMetadata(e.nonce);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const metaKey = await this.metaKeyPromise;
|
|
||||||
const json = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: new Uint8Array(12),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
metaKey,
|
|
||||||
b64ToArray(data.metadata)
|
|
||||||
);
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const meta = JSON.parse(decoder.decode(json));
|
|
||||||
this.file.name = meta.name;
|
|
||||||
this.file.type = meta.type;
|
|
||||||
this.file.iv = meta.iv;
|
|
||||||
this.file.size = data.size;
|
|
||||||
this.file.ttl = data.ttl;
|
|
||||||
this.state = 'ready';
|
|
||||||
} catch (e) {
|
|
||||||
this.state = 'invalid';
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(nonce) {
|
reset() {
|
||||||
const authHeader = await this.getAuthHeader(nonce);
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.state = 'initialized';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata() {
|
||||||
|
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||||
|
this.fileInfo.name = meta.name;
|
||||||
|
this.fileInfo.type = meta.type;
|
||||||
|
this.fileInfo.iv = meta.iv;
|
||||||
|
this.fileInfo.size = +meta.size;
|
||||||
|
this.fileInfo.manifest = meta.manifest;
|
||||||
|
this.state = 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessageToSw(msg) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
xhr.onprogress = event => {
|
channel.port1.onmessage = function(event) {
|
||||||
if (event.lengthComputable && event.target.status !== 404) {
|
if (event.data === undefined) {
|
||||||
this.progress = [event.loaded, event.total];
|
reject('bad response from serviceWorker');
|
||||||
this.emit('progress', this.progress);
|
} else if (event.data.error !== undefined) {
|
||||||
|
reject(event.data.error);
|
||||||
|
} else {
|
||||||
|
resolve(event.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onload = event => {
|
navigator.serviceWorker.controller.postMessage(msg, [channel.port2]);
|
||||||
if (xhr.status === 404) {
|
|
||||||
reject(new Error('notfound'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
const err = new Error(xhr.status);
|
|
||||||
err.nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([xhr.response]);
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
resolve(this.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('get', this.url);
|
|
||||||
xhr.setRequestHeader('Authorization', authHeader);
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
xhr.send();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAuthHeader(nonce) {
|
async downloadBlob(noSave = false) {
|
||||||
const authKey = await this.authKeyPromise;
|
|
||||||
const sig = await window.crypto.subtle.sign(
|
|
||||||
{
|
|
||||||
name: 'HMAC'
|
|
||||||
},
|
|
||||||
authKey,
|
|
||||||
b64ToArray(nonce)
|
|
||||||
);
|
|
||||||
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async download(nonce) {
|
|
||||||
this.state = 'downloading';
|
this.state = 'downloading';
|
||||||
this.emit('progress', this.progress);
|
this.downloadRequest = await downloadFile(
|
||||||
try {
|
this.fileInfo.id,
|
||||||
const encryptKey = await this.encryptKeyPromise;
|
this.keychain,
|
||||||
let ciphertext = null;
|
p => {
|
||||||
try {
|
this.progress = [p, this.fileInfo.size];
|
||||||
ciphertext = await this.downloadFile(nonce);
|
this.emit('progress');
|
||||||
} catch (e) {
|
|
||||||
if (e.message === '401' && nonce !== e.nonce) {
|
|
||||||
ciphertext = await this.downloadFile(e.nonce);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const ciphertext = await this.downloadRequest.result;
|
||||||
|
this.downloadRequest = null;
|
||||||
this.msg = 'decryptingFile';
|
this.msg = 'decryptingFile';
|
||||||
|
this.state = 'decrypting';
|
||||||
this.emit('decrypting');
|
this.emit('decrypting');
|
||||||
const plaintext = await window.crypto.subtle.decrypt(
|
let size = this.fileInfo.size;
|
||||||
{
|
let plainStream = this.keychain.decryptStream(blobStream(ciphertext));
|
||||||
name: 'AES-GCM',
|
if (this.fileInfo.type === 'send-archive') {
|
||||||
iv: b64ToArray(this.file.iv),
|
const zip = new Zip(this.fileInfo.manifest, plainStream);
|
||||||
tagLength: 128
|
plainStream = zip.stream;
|
||||||
},
|
size = zip.size;
|
||||||
encryptKey,
|
}
|
||||||
ciphertext
|
const plaintext = await streamToArrayBuffer(plainStream, size);
|
||||||
);
|
if (!noSave) {
|
||||||
|
await saveFile({
|
||||||
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.fileInfo.name),
|
||||||
|
type: this.fileInfo.type
|
||||||
|
});
|
||||||
|
}
|
||||||
this.msg = 'downloadFinish';
|
this.msg = 'downloadFinish';
|
||||||
|
this.emit('complete');
|
||||||
this.state = 'complete';
|
this.state = 'complete';
|
||||||
return {
|
|
||||||
plaintext,
|
|
||||||
name: decodeURIComponent(this.file.name),
|
|
||||||
type: this.file.type
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.state = 'invalid';
|
this.downloadRequest = null;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadStream(noSave = false) {
|
||||||
|
const onprogress = p => {
|
||||||
|
this.progress = [p, this.fileInfo.size];
|
||||||
|
this.emit('progress');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.downloadRequest = {
|
||||||
|
cancel: () => {
|
||||||
|
this.sendMessageToSw({ request: 'cancel', id: this.fileInfo.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.state = 'downloading';
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
request: 'init',
|
||||||
|
id: this.fileInfo.id,
|
||||||
|
filename: this.fileInfo.name,
|
||||||
|
type: this.fileInfo.type,
|
||||||
|
manifest: this.fileInfo.manifest,
|
||||||
|
key: this.fileInfo.secretKey,
|
||||||
|
requiresPassword: this.fileInfo.requiresPassword,
|
||||||
|
password: this.fileInfo.password,
|
||||||
|
url: this.fileInfo.url,
|
||||||
|
size: this.fileInfo.size,
|
||||||
|
nonce: this.keychain.nonce,
|
||||||
|
noSave
|
||||||
|
};
|
||||||
|
await this.sendMessageToSw(info);
|
||||||
|
|
||||||
|
onprogress(0);
|
||||||
|
|
||||||
|
if (noSave) {
|
||||||
|
const res = await fetch(getApiUrl(`/api/download/${this.fileInfo.id}`));
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(res.status);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const downloadPath = `/api/download/${this.fileInfo.id}`;
|
||||||
|
let downloadUrl = getApiUrl(downloadPath);
|
||||||
|
if (downloadUrl === downloadPath) {
|
||||||
|
downloadUrl = `${location.protocol}//${location.host}/api/download/${
|
||||||
|
this.fileInfo.id
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
let prog = 0;
|
||||||
|
while (prog < this.fileInfo.size) {
|
||||||
|
const msg = await this.sendMessageToSw({
|
||||||
|
request: 'progress',
|
||||||
|
id: this.fileInfo.id
|
||||||
|
});
|
||||||
|
prog = msg.progress;
|
||||||
|
onprogress(prog);
|
||||||
|
await delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadRequest = null;
|
||||||
|
this.msg = 'downloadFinish';
|
||||||
|
this.emit('complete');
|
||||||
|
this.state = 'complete';
|
||||||
|
} catch (e) {
|
||||||
|
this.downloadRequest = null;
|
||||||
|
if (e === 'cancelled' || e.message === '400') {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
download(options) {
|
||||||
|
if (options.stream) {
|
||||||
|
return this.downloadStream(options.noSave);
|
||||||
|
}
|
||||||
|
return this.downloadBlob(options.noSave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile(file) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const dataView = new DataView(file.plaintext);
|
||||||
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
|
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
navigator.msSaveBlob(blob, file.name);
|
||||||
|
return resolve();
|
||||||
|
} else if (/iPhone|fxios/i.test(navigator.userAgent)) {
|
||||||
|
// This method is much slower but createObjectURL
|
||||||
|
// is buggy on iOS
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('loadend', function() {
|
||||||
|
if (reader.error) {
|
||||||
|
return reject(reader.error);
|
||||||
|
}
|
||||||
|
if (reader.result) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = reader.result;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
} else {
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,27 @@
|
|||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import { arrayToB64, b64ToArray, bytes } from './utils';
|
import OwnedFile from './ownedFile';
|
||||||
|
import Keychain from './keychain';
|
||||||
async function getAuthHeader(authKey, nonce) {
|
import { arrayToB64, bytes } from './utils';
|
||||||
const sig = await window.crypto.subtle.sign(
|
import { uploadWs } from './api';
|
||||||
{
|
import { encryptedSize } from './utils';
|
||||||
name: 'HMAC'
|
|
||||||
},
|
|
||||||
authKey,
|
|
||||||
b64ToArray(nonce)
|
|
||||||
);
|
|
||||||
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendPassword(file, authKey, rawAuth) {
|
|
||||||
const authHeader = await getAuthHeader(authKey, file.nonce);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
return resolve(xhr.response);
|
|
||||||
}
|
|
||||||
if (xhr.status === 401) {
|
|
||||||
const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
|
|
||||||
file.nonce = nonce;
|
|
||||||
}
|
|
||||||
reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => reject(new Error(0));
|
|
||||||
xhr.ontimeout = () => reject(new Error(0));
|
|
||||||
xhr.open('post', `/api/password/${file.id}`);
|
|
||||||
xhr.setRequestHeader('Authorization', authHeader);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send(JSON.stringify({ auth: arrayToB64(new Uint8Array(rawAuth)) }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class FileSender extends Nanobus {
|
export default class FileSender extends Nanobus {
|
||||||
constructor(file) {
|
constructor() {
|
||||||
super('FileSender');
|
super('FileSender');
|
||||||
this.file = file;
|
this.keychain = new Keychain();
|
||||||
this.msg = 'importingFile';
|
this.reset();
|
||||||
this.progress = [0, 1];
|
|
||||||
this.cancelled = false;
|
|
||||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
this.uploadXHR = new XMLHttpRequest();
|
|
||||||
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
this.secretKey = window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
this.rawSecret,
|
|
||||||
'HKDF',
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete(id, token) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!id || !token) {
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', `/api/delete/${id}`);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(JSON.stringify({ owner_token: token }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static changeLimit(id, owner_token, dlimit) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!id || !owner_token) {
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', `/api/params/${id}`);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(JSON.stringify({ owner_token, dlimit }));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get progressRatio() {
|
get progressRatio() {
|
||||||
return this.progress[0] / this.progress[1];
|
return this.progress[0] / this.progress[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get progressIndefinite() {
|
||||||
|
return (
|
||||||
|
['fileSizeProgress', 'notifyUploadEncryptDone'].indexOf(this.msg) === -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get sizes() {
|
get sizes() {
|
||||||
return {
|
return {
|
||||||
partialSize: bytes(this.progress[0]),
|
partialSize: bytes(this.progress[0]),
|
||||||
@@ -107,216 +29,80 @@ export default class FileSender extends Nanobus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.msg = 'importingFile';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
if (this.msg === 'fileSizeProgress') {
|
if (this.uploadRequest) {
|
||||||
this.uploadXHR.abort();
|
this.uploadRequest.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readFile() {
|
async upload(archive, bearerToken) {
|
||||||
return new Promise((resolve, reject) => {
|
const start = Date.now();
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsArrayBuffer(this.file);
|
|
||||||
reader.onload = function(event) {
|
|
||||||
const plaintext = new Uint8Array(this.result);
|
|
||||||
resolve(plaintext);
|
|
||||||
};
|
|
||||||
reader.onerror = function(err) {
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadFile(encrypted, metadata, rawAuth) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dataView = new DataView(encrypted);
|
|
||||||
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('data', blob);
|
|
||||||
|
|
||||||
const xhr = this.uploadXHR;
|
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', e => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
this.progress = [e.loaded, e.total];
|
|
||||||
this.emit('progress', this.progress);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
const nonce = xhr
|
|
||||||
.getResponseHeader('WWW-Authenticate')
|
|
||||||
.split(' ')[1];
|
|
||||||
this.progress = [1, 1];
|
|
||||||
this.msg = 'notifyUploadDone';
|
|
||||||
const responseObj = JSON.parse(xhr.responseText);
|
|
||||||
return resolve({
|
|
||||||
url: responseObj.url,
|
|
||||||
id: responseObj.id,
|
|
||||||
secretKey: arrayToB64(this.rawSecret),
|
|
||||||
ownerToken: responseObj.owner,
|
|
||||||
nonce
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.msg = 'errorPageHeader';
|
|
||||||
reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('post', '/api/upload', true);
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'X-File-Metadata',
|
|
||||||
arrayToB64(new Uint8Array(metadata))
|
|
||||||
);
|
|
||||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(rawAuth)}`);
|
|
||||||
xhr.send(fd);
|
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload() {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const secretKey = await this.secretKey;
|
|
||||||
const encryptKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('encryption'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
const authKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('authentication'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
const metaKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('metadata'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
const plaintext = await this.readFile();
|
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
throw new Error(0);
|
throw new Error(0);
|
||||||
}
|
}
|
||||||
this.msg = 'encryptingFile';
|
this.msg = 'encryptingFile';
|
||||||
this.emit('encrypting');
|
this.emit('encrypting');
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
const totalSize = encryptedSize(archive.size);
|
||||||
{
|
const encStream = await this.keychain.encryptStream(archive.stream);
|
||||||
name: 'AES-GCM',
|
const metadata = await this.keychain.encryptMetadata(archive);
|
||||||
iv: this.iv,
|
const authKeyB64 = await this.keychain.authKeyB64();
|
||||||
tagLength: 128
|
|
||||||
},
|
this.uploadRequest = uploadWs(
|
||||||
encryptKey,
|
encStream,
|
||||||
plaintext
|
metadata,
|
||||||
|
authKeyB64,
|
||||||
|
archive.timeLimit,
|
||||||
|
archive.dlimit,
|
||||||
|
bearerToken,
|
||||||
|
p => {
|
||||||
|
this.progress = [p, totalSize];
|
||||||
|
this.emit('progress');
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const metadata = await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: new Uint8Array(12),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
metaKey,
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
iv: arrayToB64(this.iv),
|
|
||||||
name: this.file.name,
|
|
||||||
type: this.file.type || 'application/octet-stream'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
throw new Error(0);
|
throw new Error(0);
|
||||||
}
|
}
|
||||||
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
|
|
||||||
}
|
|
||||||
|
|
||||||
static async setPassword(password, file) {
|
this.msg = 'fileSizeProgress';
|
||||||
const encoder = new TextEncoder();
|
this.emit('progress'); // HACK to kick MS Edge
|
||||||
const secretKey = await window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
b64ToArray(file.secretKey),
|
|
||||||
'HKDF',
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
const authKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('authentication'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
const pwdKey = await window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
encoder.encode(password),
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
const newAuthKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt: encoder.encode(file.url),
|
|
||||||
iterations: 100,
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
pwdKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey);
|
|
||||||
try {
|
try {
|
||||||
await sendPassword(file, authKey, rawAuth);
|
const result = await this.uploadRequest.result;
|
||||||
|
const time = Date.now() - start;
|
||||||
|
this.msg = 'notifyUploadEncryptDone';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.progress = [1, 1];
|
||||||
|
const secretKey = arrayToB64(this.keychain.rawSecret);
|
||||||
|
const ownedFile = new OwnedFile({
|
||||||
|
id: result.id,
|
||||||
|
url: `${result.url}#${secretKey}`,
|
||||||
|
name: archive.name,
|
||||||
|
size: archive.size,
|
||||||
|
manifest: archive.manifest,
|
||||||
|
time: time,
|
||||||
|
speed: archive.size / (time / 1000),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + archive.timeLimit * 1000,
|
||||||
|
secretKey: secretKey,
|
||||||
|
nonce: this.keychain.nonce,
|
||||||
|
ownerToken: result.ownerToken,
|
||||||
|
dlimit: archive.dlimit,
|
||||||
|
timeLimit: archive.timeLimit
|
||||||
|
});
|
||||||
|
|
||||||
|
return ownedFile;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === '401' && file.nonce !== e.nonce) {
|
this.msg = 'errorPageHeader';
|
||||||
await sendPassword(file, authKey, rawAuth);
|
this.uploadRequest = null;
|
||||||
} else {
|
throw e;
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
181
app/fxa.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/* global AUTH_CONFIG */
|
||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
function getOtherInfo(enc) {
|
||||||
|
const name = encoder.encode(enc);
|
||||||
|
const length = 256;
|
||||||
|
const buffer = new ArrayBuffer(name.length + 16);
|
||||||
|
const dv = new DataView(buffer);
|
||||||
|
const result = new Uint8Array(buffer);
|
||||||
|
let i = 0;
|
||||||
|
dv.setUint32(i, name.length);
|
||||||
|
i += 4;
|
||||||
|
result.set(name, i);
|
||||||
|
i += name.length;
|
||||||
|
dv.setUint32(i, 0);
|
||||||
|
i += 4;
|
||||||
|
dv.setUint32(i, 0);
|
||||||
|
i += 4;
|
||||||
|
dv.setUint32(i, length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function concat(b1, b2) {
|
||||||
|
const result = new Uint8Array(b1.length + b2.length);
|
||||||
|
result.set(b1, 0);
|
||||||
|
result.set(b2, b1.length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function concatKdf(key, enc) {
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error('unsupported key length');
|
||||||
|
}
|
||||||
|
const otherInfo = getOtherInfo(enc);
|
||||||
|
const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
|
||||||
|
const dv = new DataView(buffer);
|
||||||
|
const concat = new Uint8Array(buffer);
|
||||||
|
dv.setUint32(0, 1);
|
||||||
|
concat.set(key, 4);
|
||||||
|
concat.set(otherInfo, key.length + 4);
|
||||||
|
const result = await crypto.subtle.digest('SHA-256', concat);
|
||||||
|
return new Uint8Array(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareScopedBundleKey(storage) {
|
||||||
|
const keys = await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
|
||||||
|
const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
|
||||||
|
const kid = await crypto.subtle.digest(
|
||||||
|
'SHA-256',
|
||||||
|
encoder.encode(JSON.stringify(publicJwk))
|
||||||
|
);
|
||||||
|
privateJwk.kid = kid;
|
||||||
|
publicJwk.kid = kid;
|
||||||
|
storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
|
||||||
|
return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptBundle(storage, bundle) {
|
||||||
|
const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
|
||||||
|
storage.remove('scopedBundlePrivateKey');
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
privateJwk,
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
const jweParts = bundle.split('.');
|
||||||
|
if (jweParts.length !== 5) {
|
||||||
|
throw new Error('invalid jwe');
|
||||||
|
}
|
||||||
|
const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
|
||||||
|
const additionalData = encoder.encode(jweParts[0]);
|
||||||
|
const iv = b64ToArray(jweParts[2]);
|
||||||
|
const ciphertext = b64ToArray(jweParts[3]);
|
||||||
|
const tag = b64ToArray(jweParts[4]);
|
||||||
|
|
||||||
|
if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
|
||||||
|
throw new Error('unsupported jwe type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
header.epk,
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const sharedBits = await crypto.subtle.deriveBits(
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
public: publicKey
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
256
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
|
||||||
|
const sharedKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
rawSharedKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM'
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
const plaintext = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
additionalData: additionalData,
|
||||||
|
tagLength: tag.length * 8
|
||||||
|
},
|
||||||
|
sharedKey,
|
||||||
|
concat(ciphertext, tag)
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preparePkce(storage) {
|
||||||
|
const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
|
||||||
|
storage.set('pkceVerifier', verifier);
|
||||||
|
const challenge = await crypto.subtle.digest(
|
||||||
|
'SHA-256',
|
||||||
|
encoder.encode(verifier)
|
||||||
|
);
|
||||||
|
return arrayToB64(new Uint8Array(challenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deriveFileListKey(ikm) {
|
||||||
|
const baseKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(ikm),
|
||||||
|
{ name: 'HKDF' },
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const fileListKey = await crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('fileList'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
baseKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
const rawFileListKey = await crypto.subtle.exportKey('raw', fileListKey);
|
||||||
|
return arrayToB64(new Uint8Array(rawFileListKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileListKey(storage, bundle) {
|
||||||
|
const jwks = await decryptBundle(storage, bundle);
|
||||||
|
const jwk = jwks[AUTH_CONFIG.key_scope];
|
||||||
|
return deriveFileListKey(jwk.k);
|
||||||
|
}
|
||||||
181
app/keychain.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
import { decryptStream, encryptStream } from './ece.js';
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
export default class Keychain {
|
||||||
|
constructor(secretKeyB64, nonce) {
|
||||||
|
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
|
||||||
|
if (secretKeyB64) {
|
||||||
|
this.rawSecret = b64ToArray(secretKeyB64);
|
||||||
|
} else {
|
||||||
|
this.rawSecret = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
}
|
||||||
|
this.secretKeyPromise = crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.rawSecret,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('encryption'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('metadata'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: { name: 'SHA-256' }
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get nonce() {
|
||||||
|
return this._nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
set nonce(n) {
|
||||||
|
if (n && n !== this._nonce) {
|
||||||
|
this._nonce = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword(password, shareUrl) {
|
||||||
|
this.authKeyPromise = crypto.subtle
|
||||||
|
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
|
||||||
|
'deriveKey'
|
||||||
|
])
|
||||||
|
.then(passwordKey =>
|
||||||
|
crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: encoder.encode(shareUrl),
|
||||||
|
iterations: 100,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
passwordKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthKey(authKeyB64) {
|
||||||
|
this.authKeyPromise = crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(authKeyB64),
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authKeyB64() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const rawAuth = await crypto.subtle.exportKey('raw', authKey);
|
||||||
|
return arrayToB64(new Uint8Array(rawAuth));
|
||||||
|
}
|
||||||
|
|
||||||
|
async authHeader() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const sig = await crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(this.nonce)
|
||||||
|
);
|
||||||
|
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptMetadata(metadata) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const ciphertext = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
name: metadata.name,
|
||||||
|
size: metadata.size,
|
||||||
|
type: metadata.type || 'application/octet-stream',
|
||||||
|
manifest: metadata.manifest || {}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptStream(plainStream) {
|
||||||
|
return encryptStream(plainStream, this.rawSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptStream(cryptotext) {
|
||||||
|
return decryptStream(cryptotext, this.rawSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptMetadata(ciphertext) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const plaintext = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/locale.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { FluentBundle } from 'fluent';
|
||||||
|
|
||||||
|
function makeBundle(locale, ftl) {
|
||||||
|
const bundle = new FluentBundle(locale, { useIsolating: false });
|
||||||
|
bundle.addMessages(ftl);
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTranslator(locale) {
|
||||||
|
const bundles = [];
|
||||||
|
const { default: en } = await import('../public/locales/en-US/send.ftl');
|
||||||
|
if (locale !== 'en-US') {
|
||||||
|
const {
|
||||||
|
default: ftl
|
||||||
|
} = await import(`../public/locales/${locale}/send.ftl`);
|
||||||
|
bundles.push(makeBundle(locale, ftl));
|
||||||
|
}
|
||||||
|
bundles.push(makeBundle('en-US', en));
|
||||||
|
return function(id, data) {
|
||||||
|
for (let bundle of bundles) {
|
||||||
|
if (bundle.hasMessage(id)) {
|
||||||
|
return bundle.format(bundle.getMessage(id), data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
289
app/main.css
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
@tailwind preflight;
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
:not(input) {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: 1px dotted grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url('../assets/bg.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@apply bg-blue-dark;
|
||||||
|
@apply text-white;
|
||||||
|
@apply cursor-pointer;
|
||||||
|
@apply py-4;
|
||||||
|
@apply px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
@apply bg-blue-darker;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
@apply bg-blue-darker;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
@apply leading-normal;
|
||||||
|
@apply select-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > input[type='checkbox'] {
|
||||||
|
@apply absolute;
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > label {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > label::before {
|
||||||
|
/* @apply bg-grey-lightest; */
|
||||||
|
@apply border;
|
||||||
|
@apply rounded-sm;
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > label:hover::before {
|
||||||
|
@apply border-blue-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > input:focus + label::before {
|
||||||
|
@apply border-blue-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > input:checked + label::before {
|
||||||
|
@apply bg-blue-dark;
|
||||||
|
@apply border-blue-dark;
|
||||||
|
|
||||||
|
background-image: url('../assets/lock.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > input:disabled + label {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox > input:disabled + label::before {
|
||||||
|
@apply bg-blue-dark;
|
||||||
|
@apply border-blue-dark;
|
||||||
|
|
||||||
|
background-image: url('../assets/lock.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
details > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details > summary > svg {
|
||||||
|
transition: all 0.25s cubic-bezier(0.07, 0.95, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > summary > svg {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer li:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-link {
|
||||||
|
background-color: #000;
|
||||||
|
background-image: url('../assets/feedback.svg');
|
||||||
|
background-position: 0.125rem 0.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1.125rem;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 0.75rem;
|
||||||
|
padding: 0.375rem 0.375rem 0.375rem 1.25rem;
|
||||||
|
text-indent: 0.125rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
max-width: 100%;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
max-width: 64rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > section {
|
||||||
|
@apply bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mozilla-logo {
|
||||||
|
background-image: url('../assets/mozilla-logo.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100px, 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: 120%;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
height: 32px;
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#password-msg::after {
|
||||||
|
content: '\200b';
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
@apply bg-grey-light;
|
||||||
|
@apply rounded-sm;
|
||||||
|
@apply w-full;
|
||||||
|
@apply h-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
@apply bg-grey-light;
|
||||||
|
@apply rounded-sm;
|
||||||
|
@apply w-full;
|
||||||
|
@apply h-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
/* stylelint-disable */
|
||||||
|
background-image: -webkit-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
transparent 20%,
|
||||||
|
rgba(255, 255, 255, 0.4) 20%,
|
||||||
|
rgba(255, 255, 255, 0.4) 40%,
|
||||||
|
transparent 40%,
|
||||||
|
transparent 60%,
|
||||||
|
rgba(255, 255, 255, 0.4) 60%,
|
||||||
|
rgba(255, 255, 255, 0.4) 80%,
|
||||||
|
transparent 80%
|
||||||
|
),
|
||||||
|
-webkit-linear-gradient(left, #0a84ff, #0a84ff);
|
||||||
|
/* stylelint-enable */
|
||||||
|
border-radius: 2px;
|
||||||
|
background-size: 21px 20px, 100% 100%, 100% 100%;
|
||||||
|
-webkit-animation: animate-stripes 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
/* stylelint-disable */
|
||||||
|
background-image: -moz-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
transparent 20%,
|
||||||
|
rgba(255, 255, 255, 0.4) 20%,
|
||||||
|
rgba(255, 255, 255, 0.4) 40%,
|
||||||
|
transparent 40%,
|
||||||
|
transparent 60%,
|
||||||
|
rgba(255, 255, 255, 0.4) 60%,
|
||||||
|
rgba(255, 255, 255, 0.4) 80%,
|
||||||
|
transparent 80%
|
||||||
|
),
|
||||||
|
-moz-linear-gradient(left, #0a84ff, #0a84ff);
|
||||||
|
/* stylelint-enable */
|
||||||
|
border-radius: 2px;
|
||||||
|
background-size: 21px 20px, 100% 100%, 100% 100%;
|
||||||
|
animation: animate-stripes 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animate-stripes {
|
||||||
|
100% {
|
||||||
|
background-position: -21px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animate-stripes {
|
||||||
|
100% {
|
||||||
|
background-position: -21px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background-image: url('../assets/select-arrow.svg');
|
||||||
|
background-position: calc(100% - 0.75rem);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
.intro {
|
||||||
|
max-width: unset;
|
||||||
|
height: unset;
|
||||||
|
margin-bottom: -3rem;
|
||||||
|
margin-right: -7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
@apply flex-1;
|
||||||
|
@apply self-center;
|
||||||
|
@apply items-center;
|
||||||
|
@apply m-auto;
|
||||||
|
@apply py-8;
|
||||||
|
|
||||||
|
min-height: 36rem;
|
||||||
|
max-height: 40rem;
|
||||||
|
width: calc(100% - 3rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@responsive {
|
||||||
|
.shadow-light {
|
||||||
|
box-shadow: 0 0 8px 0 rgba(12, 12, 13, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-big {
|
||||||
|
box-shadow: 0 0 32px 0 rgba(12, 12, 13, 0.1),
|
||||||
|
0 2px 16px 0 rgba(12, 12, 13, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@variants focus {
|
||||||
|
.outline {
|
||||||
|
outline: 1px dotted grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-break-all {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
89
app/main.js
@@ -1,51 +1,64 @@
|
|||||||
|
/* global DEFAULTS LIMITS LOCALE */
|
||||||
|
import 'core-js';
|
||||||
|
import 'fast-text-encoding'; // MS Edge support
|
||||||
import 'fluent-intl-polyfill';
|
import 'fluent-intl-polyfill';
|
||||||
import app from './routes';
|
import choo from 'choo';
|
||||||
import locale from '../common/locales';
|
import nanotiming from 'nanotiming';
|
||||||
import fileManager from './fileManager';
|
import routes from './routes';
|
||||||
|
import getCapabilities from './capabilities';
|
||||||
|
import controller from './controller';
|
||||||
import dragManager from './dragManager';
|
import dragManager from './dragManager';
|
||||||
import { canHasSend } from './utils';
|
import pasteManager from './pasteManager';
|
||||||
import assets from '../common/assets';
|
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
import metrics from './metrics';
|
import metrics from './metrics';
|
||||||
import experiments from './experiments';
|
import experiments from './experiments';
|
||||||
import Raven from 'raven-js';
|
import Raven from 'raven-js';
|
||||||
|
import './main.css';
|
||||||
|
import User from './user';
|
||||||
|
import { getTranslator } from './locale';
|
||||||
|
import Archive from './archive';
|
||||||
|
|
||||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use((state, emitter) => {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
// init state
|
nanotiming.disabled = true;
|
||||||
state.transfer = null;
|
}
|
||||||
state.fileInfo = null;
|
|
||||||
state.translate = locale.getTranslator();
|
|
||||||
state.storage = storage;
|
|
||||||
state.raven = Raven;
|
|
||||||
emitter.on('DOMContentLoaded', async () => {
|
|
||||||
let reason = null;
|
|
||||||
if (
|
|
||||||
/firefox/i.test(navigator.userAgent) &&
|
|
||||||
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) <=
|
|
||||||
49
|
|
||||||
) {
|
|
||||||
reason = 'outdated';
|
|
||||||
}
|
|
||||||
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
|
||||||
reason = 'edge';
|
|
||||||
}
|
|
||||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
|
||||||
if (!ok) {
|
|
||||||
reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
|
||||||
}
|
|
||||||
if (reason) {
|
|
||||||
setTimeout(() => emitter.emit('replaceState', `/unsupported/${reason}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(metrics);
|
(async function start() {
|
||||||
app.use(fileManager);
|
const capabilities = await getCapabilities();
|
||||||
app.use(dragManager);
|
if (
|
||||||
app.use(experiments);
|
!capabilities.crypto &&
|
||||||
|
window.location.pathname !== '/unsupported/crypto'
|
||||||
|
) {
|
||||||
|
return window.location.assign('/unsupported/crypto');
|
||||||
|
}
|
||||||
|
if (capabilities.serviceWorker) {
|
||||||
|
await navigator.serviceWorker.register('/serviceWorker.js');
|
||||||
|
await navigator.serviceWorker.ready;
|
||||||
|
}
|
||||||
|
|
||||||
app.mount('body');
|
const translate = await getTranslator(LOCALE);
|
||||||
|
window.initialState = {
|
||||||
|
LIMITS,
|
||||||
|
DEFAULTS,
|
||||||
|
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
|
||||||
|
capabilities,
|
||||||
|
translate,
|
||||||
|
storage,
|
||||||
|
raven: Raven,
|
||||||
|
user: new User(storage, LIMITS, window.AUTH_CONFIG),
|
||||||
|
transfer: null,
|
||||||
|
fileInfo: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = routes(choo());
|
||||||
|
window.app = app;
|
||||||
|
app.use(metrics);
|
||||||
|
app.use(controller);
|
||||||
|
app.use(dragManager);
|
||||||
|
app.use(experiments);
|
||||||
|
app.use(pasteManager);
|
||||||
|
app.mount('body');
|
||||||
|
})();
|
||||||
|
|||||||
356
app/metrics.js
@@ -1,294 +1,178 @@
|
|||||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
|
import { platform } from './utils';
|
||||||
let hasLocalStorage = false;
|
import { sendMetrics } from './api';
|
||||||
try {
|
|
||||||
hasLocalStorage = typeof localStorage !== 'undefined';
|
|
||||||
} catch (e) {
|
|
||||||
// when disabled, any mention of localStorage throws an error
|
|
||||||
}
|
|
||||||
|
|
||||||
const analytics = new testPilotGA({
|
|
||||||
an: 'Firefox Send',
|
|
||||||
ds: 'web',
|
|
||||||
tid: window.GOOGLE_ANALYTICS_ID
|
|
||||||
});
|
|
||||||
|
|
||||||
let appState = null;
|
let appState = null;
|
||||||
let experiment = null;
|
// let experiment = null;
|
||||||
|
const HOUR = 1000 * 60 * 60;
|
||||||
|
const events = [];
|
||||||
|
let session_id = Date.now();
|
||||||
|
const lang = document.querySelector('html').lang;
|
||||||
|
|
||||||
export default function initialize(state, emitter) {
|
export default function initialize(state, emitter) {
|
||||||
appState = state;
|
appState = state;
|
||||||
|
if (!appState.user.firstAction) {
|
||||||
|
appState.user.firstAction = appState.route === '/' ? 'upload' : 'download';
|
||||||
|
}
|
||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
// addExitHandlers();
|
// experiment = storage.enrolled[0];
|
||||||
experiment = storage.enrolled[0];
|
const query = appState.query;
|
||||||
sendEvent(category(), 'visit', {
|
addEvent('client_visit', {
|
||||||
cm5: storage.totalUploads,
|
entrypoint: appState.route === '/' ? 'upload' : 'download',
|
||||||
cm6: storage.files.length,
|
referrer: document.referrer,
|
||||||
cm7: storage.totalDownloads
|
utm_campaign: query.utm_campaign,
|
||||||
|
utm_content: query.utm_content,
|
||||||
|
utm_medium: query.utm_medium,
|
||||||
|
utm_source: query.utm_source,
|
||||||
|
utm_term: query.utm_term
|
||||||
});
|
});
|
||||||
//TODO restart handlers... somewhere
|
|
||||||
});
|
|
||||||
emitter.on('exit', evt => {
|
|
||||||
exitEvent(evt);
|
|
||||||
});
|
});
|
||||||
|
emitter.on('experiment', experimentEvent);
|
||||||
|
window.addEventListener('unload', submitEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
function category() {
|
function sizeOrder(n) {
|
||||||
switch (appState.route) {
|
return Math.floor(Math.log10(n));
|
||||||
case '/':
|
|
||||||
case '/share/:id':
|
|
||||||
return 'sender';
|
|
||||||
case '/download/:id/:key':
|
|
||||||
case '/download/:id':
|
|
||||||
case '/completed':
|
|
||||||
return 'recipient';
|
|
||||||
default:
|
|
||||||
return 'other';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendEvent() {
|
function submitEvents() {
|
||||||
const args = Array.from(arguments);
|
if (navigator.doNotTrack === '1') {
|
||||||
if (experiment && args[2]) {
|
return;
|
||||||
args[2].xid = experiment[0];
|
|
||||||
args[2].xvar = experiment[1];
|
|
||||||
}
|
}
|
||||||
return (
|
sendMetrics(
|
||||||
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
new Blob(
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
now: Date.now(),
|
||||||
|
session_id,
|
||||||
|
lang,
|
||||||
|
platform: platform(),
|
||||||
|
events
|
||||||
|
})
|
||||||
|
],
|
||||||
|
{ type: 'text/plain' } // see http://crbug.com/490015
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
events.splice(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlToMetric(url) {
|
async function addEvent(event_type, event_properties) {
|
||||||
switch (url) {
|
const user_id = await appState.user.metricId();
|
||||||
case 'https://www.mozilla.org/':
|
const device_id = await appState.user.deviceId();
|
||||||
return 'mozilla';
|
events.push({
|
||||||
case 'https://www.mozilla.org/about/legal':
|
device_id,
|
||||||
return 'legal';
|
event_properties,
|
||||||
case 'https://testpilot.firefox.com/about':
|
event_type,
|
||||||
return 'about';
|
time: Date.now(),
|
||||||
case 'https://testpilot.firefox.com/privacy':
|
user_id,
|
||||||
return 'privacy';
|
user_properties: {
|
||||||
case 'https://testpilot.firefox.com/terms':
|
anonymous: !appState.user.loggedIn,
|
||||||
return 'terms';
|
first_action: appState.user.firstAction,
|
||||||
case 'https://www.mozilla.org/privacy/websites/#cookies':
|
active_count: storage.files.length
|
||||||
return 'cookies';
|
|
||||||
case 'https://github.com/mozilla/send':
|
|
||||||
return 'github';
|
|
||||||
case 'https://twitter.com/FxTestPilot':
|
|
||||||
return 'twitter';
|
|
||||||
case 'https://www.mozilla.org/firefox/new/?scene=2':
|
|
||||||
return 'download-firefox';
|
|
||||||
case 'https://qsurvey.mozilla.com/s3/txp-firefox-send':
|
|
||||||
return 'survey';
|
|
||||||
case 'https://testpilot.firefox.com/':
|
|
||||||
case 'https://testpilot.firefox.com/experiments/send':
|
|
||||||
return 'testpilot';
|
|
||||||
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
|
||||||
return 'promo';
|
|
||||||
default:
|
|
||||||
return 'other';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setReferrer(state) {
|
|
||||||
if (category() === 'sender') {
|
|
||||||
if (state) {
|
|
||||||
storage.referrer = `${state}-upload`;
|
|
||||||
}
|
|
||||||
} else if (category() === 'recipient') {
|
|
||||||
if (state) {
|
|
||||||
storage.referrer = `${state}-download`;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (events.length === 25) {
|
||||||
|
submitEvents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function externalReferrer() {
|
function cancelledUpload(archive, duration) {
|
||||||
if (/^https:\/\/testpilot\.firefox\.com/.test(document.referrer)) {
|
return addEvent('client_upload', {
|
||||||
return 'testpilot';
|
download_limit: archive.dlimit,
|
||||||
}
|
duration: sizeOrder(duration),
|
||||||
return 'external';
|
file_count: archive.numFiles,
|
||||||
}
|
password_protected: !!archive.password,
|
||||||
|
size: sizeOrder(archive.size),
|
||||||
function takeReferrer() {
|
status: 'cancel',
|
||||||
const referrer = storage.referrer || externalReferrer();
|
time_limit: archive.timeLimit
|
||||||
storage.referrer = null;
|
|
||||||
return referrer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startedUpload(params) {
|
|
||||||
return sendEvent('sender', 'upload-started', {
|
|
||||||
cm1: params.size,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length + 1,
|
|
||||||
cm7: storage.totalDownloads,
|
|
||||||
cd1: params.type,
|
|
||||||
cd5: takeReferrer()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelledUpload(params) {
|
function completedUpload(archive, duration) {
|
||||||
setReferrer('cancelled');
|
return addEvent('client_upload', {
|
||||||
return sendEvent('sender', 'upload-stopped', {
|
download_limit: archive.dlimit,
|
||||||
cm1: params.size,
|
duration: sizeOrder(duration),
|
||||||
cm5: storage.totalUploads,
|
file_count: archive.numFiles,
|
||||||
cm6: storage.files.length,
|
password_protected: !!archive.password,
|
||||||
cm7: storage.totalDownloads,
|
size: sizeOrder(archive.size),
|
||||||
cd1: params.type,
|
status: 'ok',
|
||||||
cd2: 'cancelled'
|
time_limit: archive.timeLimit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedUpload(params) {
|
function stoppedUpload(archive) {
|
||||||
return sendEvent('sender', 'upload-stopped', {
|
return addEvent('client_upload', {
|
||||||
cm1: params.size,
|
download_limit: archive.dlimit,
|
||||||
cm2: params.time,
|
file_count: archive.numFiles,
|
||||||
cm3: params.speed,
|
password_protected: !!archive.password,
|
||||||
cm5: storage.totalUploads,
|
size: sizeOrder(archive.size),
|
||||||
cm6: storage.files.length,
|
status: 'error',
|
||||||
cm7: storage.totalDownloads,
|
time_limit: archive.timeLimit
|
||||||
cd1: params.type,
|
|
||||||
cd2: 'completed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addedPassword(params) {
|
|
||||||
return sendEvent('sender', 'password-added', {
|
|
||||||
cm1: params.size,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function startedDownload(params) {
|
|
||||||
return sendEvent('recipient', 'download-started', {
|
|
||||||
cm1: params.size,
|
|
||||||
cm4: params.ttl,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedDownload(params) {
|
function stoppedDownload(params) {
|
||||||
return sendEvent('recipient', 'download-stopped', {
|
return addEvent('client_download', {
|
||||||
cm1: params.size,
|
duration: sizeOrder(params.duration),
|
||||||
cm5: storage.totalUploads,
|
password_protected: params.password_protected,
|
||||||
cm6: storage.files.length,
|
size: sizeOrder(params.size),
|
||||||
cm7: storage.totalDownloads,
|
status: 'error'
|
||||||
cd2: 'errored',
|
|
||||||
cd6: params.err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelledDownload(params) {
|
|
||||||
setReferrer('cancelled');
|
|
||||||
return sendEvent('recipient', 'download-stopped', {
|
|
||||||
cm1: params.size,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads,
|
|
||||||
cd2: 'cancelled'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function stoppedUpload(params) {
|
|
||||||
return sendEvent('sender', 'upload-stopped', {
|
|
||||||
cm1: params.size,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads,
|
|
||||||
cd1: params.type,
|
|
||||||
cd2: 'errored',
|
|
||||||
cd6: params.err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function changedDownloadLimit(params) {
|
|
||||||
return sendEvent('sender', 'download-limit-changed', {
|
|
||||||
cm1: params.size,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads,
|
|
||||||
cm8: params.dlimit
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedDownload(params) {
|
function completedDownload(params) {
|
||||||
return sendEvent('recipient', 'download-stopped', {
|
return addEvent('client_download', {
|
||||||
cm1: params.size,
|
duration: sizeOrder(params.duration),
|
||||||
cm2: params.time,
|
password_protected: params.password_protected,
|
||||||
cm3: params.speed,
|
size: sizeOrder(params.size),
|
||||||
cm5: storage.totalUploads,
|
status: 'ok'
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads,
|
|
||||||
cd2: 'completed'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletedUpload(params) {
|
function deletedUpload(ownedFile) {
|
||||||
return sendEvent(category(), 'upload-deleted', {
|
return addEvent('client_delete', {
|
||||||
cm1: params.size,
|
age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR),
|
||||||
cm2: params.time,
|
downloaded: ownedFile.dtotal > 0,
|
||||||
cm3: params.speed,
|
status: 'ok'
|
||||||
cm4: params.ttl,
|
|
||||||
cm5: storage.totalUploads,
|
|
||||||
cm6: storage.files.length,
|
|
||||||
cm7: storage.totalDownloads,
|
|
||||||
cd1: params.type,
|
|
||||||
cd4: params.location
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsupported(params) {
|
function experimentEvent(params) {
|
||||||
return sendEvent(category(), 'unsupported', {
|
return addEvent('client_experiment', params);
|
||||||
cd6: params.err
|
}
|
||||||
|
|
||||||
|
function submittedSignup(params) {
|
||||||
|
return addEvent('client_login', {
|
||||||
|
status: 'ok',
|
||||||
|
trigger: params.trigger
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copiedLink(params) {
|
function canceledSignup(params) {
|
||||||
return sendEvent('sender', 'copied', {
|
return addEvent('client_login', {
|
||||||
cd4: params.location
|
status: 'cancel',
|
||||||
|
trigger: params.trigger
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitEvent(target) {
|
function loggedOut(params) {
|
||||||
return sendEvent(category(), 'exited', {
|
addEvent('client_logout', {
|
||||||
cd3: urlToMetric(target.currentTarget.href)
|
status: 'ok',
|
||||||
});
|
trigger: params.trigger
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function addExitHandlers() {
|
|
||||||
const links = Array.from(document.querySelectorAll('a'));
|
|
||||||
links.forEach(l => {
|
|
||||||
if (/^http/.test(l.getAttribute('href'))) {
|
|
||||||
l.addEventListener('click', exitEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function restart(state) {
|
|
||||||
setReferrer(state);
|
|
||||||
return sendEvent(category(), 'restarted', {
|
|
||||||
cd2: state
|
|
||||||
});
|
});
|
||||||
|
// flush events and start new anon session
|
||||||
|
submitEvents();
|
||||||
|
session_id = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
copiedLink,
|
|
||||||
startedUpload,
|
|
||||||
cancelledUpload,
|
cancelledUpload,
|
||||||
stoppedUpload,
|
stoppedUpload,
|
||||||
completedUpload,
|
completedUpload,
|
||||||
changedDownloadLimit,
|
|
||||||
deletedUpload,
|
deletedUpload,
|
||||||
startedDownload,
|
|
||||||
cancelledDownload,
|
|
||||||
stoppedDownload,
|
stoppedDownload,
|
||||||
completedDownload,
|
completedDownload,
|
||||||
addedPassword,
|
submittedSignup,
|
||||||
restart,
|
canceledSignup,
|
||||||
unsupported
|
loggedOut
|
||||||
};
|
};
|
||||||
|
|||||||
93
app/ownedFile.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64 } from './utils';
|
||||||
|
import { del, fileInfo, setParams, setPassword } from './api';
|
||||||
|
|
||||||
|
export default class OwnedFile {
|
||||||
|
constructor(obj) {
|
||||||
|
this.id = obj.id;
|
||||||
|
this.url = obj.url;
|
||||||
|
this.name = obj.name;
|
||||||
|
this.size = obj.size;
|
||||||
|
this.manifest = obj.manifest;
|
||||||
|
this.time = obj.time;
|
||||||
|
this.speed = obj.speed;
|
||||||
|
this.createdAt = obj.createdAt;
|
||||||
|
this.expiresAt = obj.expiresAt;
|
||||||
|
this.ownerToken = obj.ownerToken;
|
||||||
|
this.dlimit = obj.dlimit || 1;
|
||||||
|
this.dtotal = obj.dtotal || 0;
|
||||||
|
this.keychain = new Keychain(obj.secretKey, obj.nonce);
|
||||||
|
this._hasPassword = !!obj.hasPassword;
|
||||||
|
this.timeLimit = obj.timeLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPassword() {
|
||||||
|
return !!this._hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
get expired() {
|
||||||
|
return this.dlimit === this.dtotal || Date.now() > this.expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(password) {
|
||||||
|
try {
|
||||||
|
this.password = password;
|
||||||
|
this._hasPassword = true;
|
||||||
|
this.keychain.setPassword(password, this.url);
|
||||||
|
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
this.password = null;
|
||||||
|
this._hasPassword = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
del() {
|
||||||
|
return del(this.id, this.ownerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLimit(dlimit, user = {}) {
|
||||||
|
if (this.dlimit !== dlimit) {
|
||||||
|
this.dlimit = dlimit;
|
||||||
|
return setParams(this.id, this.ownerToken, user.bearerToken, { dlimit });
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDownloadCount() {
|
||||||
|
const oldTotal = this.dtotal;
|
||||||
|
const oldLimit = this.dlimit;
|
||||||
|
try {
|
||||||
|
const result = await fileInfo(this.id, this.ownerToken);
|
||||||
|
this.dtotal = result.dtotal;
|
||||||
|
this.dlimit = result.dlimit;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '404') {
|
||||||
|
this.dtotal = this.dlimit;
|
||||||
|
}
|
||||||
|
// ignore other errors
|
||||||
|
}
|
||||||
|
return oldTotal !== this.dtotal || oldLimit !== this.dlimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
name: this.name,
|
||||||
|
size: this.size,
|
||||||
|
manifest: this.manifest,
|
||||||
|
time: this.time,
|
||||||
|
speed: this.speed,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
expiresAt: this.expiresAt,
|
||||||
|
secretKey: arrayToB64(this.keychain.rawSecret),
|
||||||
|
ownerToken: this.ownerToken,
|
||||||
|
dlimit: this.dlimit,
|
||||||
|
dtotal: this.dtotal,
|
||||||
|
hasPassword: this.hasPassword,
|
||||||
|
timeLimit: this.timeLimit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/pasteManager.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
function getString(item) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
item.getAsString(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
window.addEventListener('paste', async event => {
|
||||||
|
if (state.route !== '/' || state.uploading) return;
|
||||||
|
if (['password', 'text'].includes(event.target.type)) return;
|
||||||
|
|
||||||
|
const items = Array.from(event.clipboardData.items);
|
||||||
|
const transferFiles = items.filter(item => item.kind === 'file');
|
||||||
|
const strings = items.filter(item => item.kind === 'string');
|
||||||
|
if (transferFiles.length) {
|
||||||
|
const promises = transferFiles.map(async (f, i) => {
|
||||||
|
const blob = f.getAsFile();
|
||||||
|
if (!blob) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const name = await getString(strings[i]);
|
||||||
|
const file = new File([blob], name, { type: blob.type });
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
const files = (await Promise.all(promises)).filter(f => !!f);
|
||||||
|
if (files.length) {
|
||||||
|
emitter.emit('addFiles', { files });
|
||||||
|
}
|
||||||
|
} else if (strings.length) {
|
||||||
|
strings[0].getAsString(s => {
|
||||||
|
const file = new File([s], 'pasted.txt', { type: 'text/plain' });
|
||||||
|
emitter.emit('addFiles', { files: [file] });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
9
app/readme.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Application Code
|
||||||
|
|
||||||
|
`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, crypto, and UI. All of it gets used in the browser, and some of it by the server for server side rendering.
|
||||||
|
|
||||||
|
The main entrypoint for the browser is [main.js](./main.js) and on the server [routes/index.js](./routes/index.js) gets imported by [/server/routes/pages.js](../server/routes/pages.js)
|
||||||
|
|
||||||
|
- `pages` contains display logic an markup for pages
|
||||||
|
- `routes` contains route definitions and logic
|
||||||
|
- `templates` contains ui elements smaller than pages
|
||||||
18
app/routes.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const choo = require('choo');
|
||||||
|
const download = require('./ui/download');
|
||||||
|
const body = require('./ui/body');
|
||||||
|
|
||||||
|
module.exports = function(app = choo()) {
|
||||||
|
app.route('/', body(require('./ui/home')));
|
||||||
|
app.route('/download/:id', body(download));
|
||||||
|
app.route('/download/:id/:key', body(download));
|
||||||
|
app.route('/unsupported/:reason', body(require('./ui/unsupported')));
|
||||||
|
app.route('/legal', body(require('./ui/legal')));
|
||||||
|
app.route('/error', body(require('./ui/error')));
|
||||||
|
app.route('/blank', body(require('./ui/blank')));
|
||||||
|
app.route('/oauth', function(state, emit) {
|
||||||
|
emit('authenticate', state.query.code, state.query.state);
|
||||||
|
});
|
||||||
|
app.route('*', body(require('./ui/notFound')));
|
||||||
|
return app;
|
||||||
|
};
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const preview = require('../templates/preview');
|
|
||||||
const download = require('../templates/download');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
if (state.transfer) {
|
|
||||||
const s = state.transfer.state;
|
|
||||||
if (s === 'downloading' || s === 'complete') {
|
|
||||||
return download(state, emit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return preview(state, emit);
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const welcome = require('../templates/welcome');
|
|
||||||
const upload = require('../templates/upload');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
if (state.transfer && state.transfer.iv) {
|
|
||||||
//TODO relying on 'iv' is gross
|
|
||||||
return upload(state, emit);
|
|
||||||
}
|
|
||||||
return welcome(state, emit);
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
const choo = require('choo');
|
|
||||||
const html = require('choo/html');
|
|
||||||
const download = require('./download');
|
|
||||||
const header = require('../templates/header');
|
|
||||||
const footer = require('../templates/footer');
|
|
||||||
const fxPromo = require('../templates/fxPromo');
|
|
||||||
|
|
||||||
const app = choo();
|
|
||||||
|
|
||||||
function body(template) {
|
|
||||||
return function(state, emit) {
|
|
||||||
const b = html`<body>
|
|
||||||
${state.promo ? fxPromo(state, emit) : ''}
|
|
||||||
${header(state)}
|
|
||||||
<div class="all">
|
|
||||||
<noscript>
|
|
||||||
<h2>Firefox Send requires JavaScript</h2>
|
|
||||||
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
|
||||||
<p>Please enable JavaScript and try again.</p>
|
|
||||||
</noscript>
|
|
||||||
${template(state, emit)}
|
|
||||||
</div>
|
|
||||||
${footer(state)}
|
|
||||||
</body>`;
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, b);
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
app.route('/', body(require('./home')));
|
|
||||||
app.route('/share/:id', body(require('../templates/share')));
|
|
||||||
app.route('/download/:id', body(download));
|
|
||||||
app.route('/download/:id/:key', body(download));
|
|
||||||
app.route('/completed', body(require('../templates/completed')));
|
|
||||||
app.route('/unsupported/:reason', body(require('../templates/unsupported')));
|
|
||||||
app.route('/legal', body(require('../templates/legal')));
|
|
||||||
app.route('/error', body(require('../templates/error')));
|
|
||||||
app.route('/blank', body(require('../templates/blank')));
|
|
||||||
app.route('*', body(require('../templates/notFound')));
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
161
app/serviceWorker.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import assets from '../common/assets';
|
||||||
|
import { version } from '../package.json';
|
||||||
|
import Keychain from './keychain';
|
||||||
|
import { downloadStream } from './api';
|
||||||
|
import { transformStream } from './streams';
|
||||||
|
import Zip from './zip';
|
||||||
|
import contentDisposition from 'content-disposition';
|
||||||
|
|
||||||
|
let noSave = false;
|
||||||
|
const map = new Map();
|
||||||
|
const IMAGES = /.*\.(png|svg|jpg)$/;
|
||||||
|
const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)$/;
|
||||||
|
const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(precache());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
async function decryptStream(id) {
|
||||||
|
const file = map.get(id);
|
||||||
|
if (!file) {
|
||||||
|
return new Response(null, { status: 400 });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let size = file.size;
|
||||||
|
let type = file.type;
|
||||||
|
const keychain = new Keychain(file.key, file.nonce);
|
||||||
|
if (file.requiresPassword) {
|
||||||
|
keychain.setPassword(file.password, file.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
file.download = downloadStream(id, keychain);
|
||||||
|
|
||||||
|
const body = await file.download.result;
|
||||||
|
|
||||||
|
const decrypted = keychain.decryptStream(body);
|
||||||
|
|
||||||
|
let zipStream = null;
|
||||||
|
if (file.type === 'send-archive') {
|
||||||
|
const zip = new Zip(file.manifest, decrypted);
|
||||||
|
zipStream = zip.stream;
|
||||||
|
type = 'application/zip';
|
||||||
|
size = zip.size;
|
||||||
|
}
|
||||||
|
const responseStream = transformStream(
|
||||||
|
zipStream || decrypted,
|
||||||
|
{
|
||||||
|
transform(chunk, controller) {
|
||||||
|
file.progress += chunk.length;
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function oncancel() {
|
||||||
|
// NOTE: cancel doesn't currently fire on chrome
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
|
||||||
|
file.download.cancel();
|
||||||
|
map.delete(id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Disposition': contentDisposition(file.filename),
|
||||||
|
'Content-Type': type,
|
||||||
|
'Content-Length': size
|
||||||
|
};
|
||||||
|
return new Response(responseStream, { headers });
|
||||||
|
} catch (e) {
|
||||||
|
if (noSave) {
|
||||||
|
return new Response(null, { status: e.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: {
|
||||||
|
Location: `/download/${id}/#${file.key}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function precache() {
|
||||||
|
const oldCaches = await caches.keys();
|
||||||
|
for (const c of oldCaches) {
|
||||||
|
if (c !== version) {
|
||||||
|
await caches.delete(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cache = await caches.open(version);
|
||||||
|
const images = assets.match(IMAGES);
|
||||||
|
await cache.addAll(images);
|
||||||
|
return self.skipWaiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cachedOrFetched(req) {
|
||||||
|
const cache = await caches.open(version);
|
||||||
|
const cached = await cache.match(req);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const fetched = await fetch(req);
|
||||||
|
if (fetched.ok && VERSIONED_ASSET.test(req.url)) {
|
||||||
|
cache.put(req, fetched.clone());
|
||||||
|
}
|
||||||
|
return fetched;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onfetch = event => {
|
||||||
|
const req = event.request;
|
||||||
|
if (req.method !== 'GET') return;
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const dlmatch = DOWNLOAD_URL.exec(url.pathname);
|
||||||
|
if (dlmatch) {
|
||||||
|
event.respondWith(decryptStream(dlmatch[1]));
|
||||||
|
} else if (VERSIONED_ASSET.test(url.pathname)) {
|
||||||
|
event.respondWith(cachedOrFetched(req));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onmessage = event => {
|
||||||
|
if (event.data.request === 'init') {
|
||||||
|
noSave = event.data.noSave;
|
||||||
|
const info = {
|
||||||
|
key: event.data.key,
|
||||||
|
nonce: event.data.nonce,
|
||||||
|
filename: event.data.filename,
|
||||||
|
requiresPassword: event.data.requiresPassword,
|
||||||
|
password: event.data.password,
|
||||||
|
url: event.data.url,
|
||||||
|
type: event.data.type,
|
||||||
|
manifest: event.data.manifest,
|
||||||
|
size: event.data.size,
|
||||||
|
progress: 0
|
||||||
|
};
|
||||||
|
map.set(event.data.id, info);
|
||||||
|
|
||||||
|
event.ports[0].postMessage('file info received');
|
||||||
|
} else if (event.data.request === 'progress') {
|
||||||
|
const file = map.get(event.data.id);
|
||||||
|
if (!file) {
|
||||||
|
event.ports[0].postMessage({ error: 'cancelled' });
|
||||||
|
} else {
|
||||||
|
if (file.progress === file.size) {
|
||||||
|
map.delete(event.data.id);
|
||||||
|
}
|
||||||
|
event.ports[0].postMessage({ progress: file.progress });
|
||||||
|
}
|
||||||
|
} else if (event.data.request === 'cancel') {
|
||||||
|
const file = map.get(event.data.id);
|
||||||
|
if (file) {
|
||||||
|
if (file.download) {
|
||||||
|
file.download.cancel();
|
||||||
|
}
|
||||||
|
map.delete(event.data.id);
|
||||||
|
}
|
||||||
|
event.ports[0].postMessage('download cancelled');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isFile } from './utils';
|
import { arrayToB64, isFile } from './utils';
|
||||||
|
import OwnedFile from './ownedFile';
|
||||||
|
|
||||||
class Mem {
|
class Mem {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -37,23 +38,33 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadFiles() {
|
loadFiles() {
|
||||||
const fs = [];
|
const fs = new Map();
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
for (let i = 0; i < this.engine.length; i++) {
|
||||||
const k = this.engine.key(i);
|
const k = this.engine.key(i);
|
||||||
if (isFile(k)) {
|
if (isFile(k)) {
|
||||||
try {
|
try {
|
||||||
const f = JSON.parse(this.engine.getItem(k));
|
const f = new OwnedFile(JSON.parse(this.engine.getItem(k)));
|
||||||
if (!f.id) {
|
if (!f.id) {
|
||||||
f.id = f.fileId;
|
f.id = f.fileId;
|
||||||
}
|
}
|
||||||
fs.push(f);
|
|
||||||
|
fs.set(f.id, f);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// obviously you're not a golfer
|
// obviously you're not a golfer
|
||||||
this.engine.removeItem(k);
|
this.engine.removeItem(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
return fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
let id = this.engine.getItem('device_id');
|
||||||
|
if (!id) {
|
||||||
|
id = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
|
||||||
|
this.engine.setItem('device_id', id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalDownloads() {
|
get totalDownloads() {
|
||||||
@@ -88,31 +99,89 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get files() {
|
get files() {
|
||||||
return this._files;
|
return Array.from(this._files.values()).sort(
|
||||||
|
(a, b) => a.createdAt - b.createdAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get user() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.engine.getItem('user'));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set user(info) {
|
||||||
|
return this.engine.setItem('user', JSON.stringify(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileById(id) {
|
getFileById(id) {
|
||||||
return this._files.find(f => f.id === id);
|
return this._files.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id) {
|
||||||
return this.engine.getItem(id);
|
return this.engine.getItem(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set(id, value) {
|
||||||
|
return this.engine.setItem(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
if (isFile(property)) {
|
if (isFile(property)) {
|
||||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
this._files.delete(property);
|
||||||
}
|
}
|
||||||
this.engine.removeItem(property);
|
this.engine.removeItem(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFile(file) {
|
addFile(file) {
|
||||||
this._files.push(file);
|
this._files.set(file.id, file);
|
||||||
|
this.writeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(file) {
|
||||||
this.engine.setItem(file.id, JSON.stringify(file));
|
this.engine.setItem(file.id, JSON.stringify(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFiles() {
|
writeFiles() {
|
||||||
this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
|
this._files.forEach(f => this.writeFile(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLocalFiles() {
|
||||||
|
this._files.forEach(f => this.engine.removeItem(f.id));
|
||||||
|
this._files = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async merge(files = []) {
|
||||||
|
let incoming = false;
|
||||||
|
let outgoing = false;
|
||||||
|
let downloadCount = false;
|
||||||
|
for (const f of files) {
|
||||||
|
if (!this.getFileById(f.id)) {
|
||||||
|
this.addFile(new OwnedFile(f));
|
||||||
|
incoming = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const workingFiles = this.files.slice();
|
||||||
|
for (const f of workingFiles) {
|
||||||
|
const cc = await f.updateDownloadCount();
|
||||||
|
if (cc) {
|
||||||
|
await this.writeFile(f);
|
||||||
|
}
|
||||||
|
downloadCount = downloadCount || cc;
|
||||||
|
outgoing = outgoing || f.expired;
|
||||||
|
if (f.expired) {
|
||||||
|
this.remove(f.id);
|
||||||
|
} else if (!files.find(x => x.id === f.id)) {
|
||||||
|
outgoing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
incoming,
|
||||||
|
outgoing,
|
||||||
|
downloadCount
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
app/streams.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* global ReadableStream TransformStream */
|
||||||
|
|
||||||
|
export function transformStream(readable, transformer, oncancel) {
|
||||||
|
try {
|
||||||
|
return readable.pipeThrough(new TransformStream(transformer));
|
||||||
|
} catch (e) {
|
||||||
|
const reader = readable.getReader();
|
||||||
|
return new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
if (transformer.start) {
|
||||||
|
return transformer.start(controller);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async pull(controller) {
|
||||||
|
let enqueued = false;
|
||||||
|
const wrappedController = {
|
||||||
|
enqueue(d) {
|
||||||
|
enqueued = true;
|
||||||
|
controller.enqueue(d);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while (!enqueued) {
|
||||||
|
const data = await reader.read();
|
||||||
|
if (data.done) {
|
||||||
|
if (transformer.flush) {
|
||||||
|
await transformer.flush(controller);
|
||||||
|
}
|
||||||
|
return controller.close();
|
||||||
|
}
|
||||||
|
await transformer.transform(data.value, wrappedController);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel(reason) {
|
||||||
|
readable.cancel(reason);
|
||||||
|
if (oncancel) {
|
||||||
|
oncancel(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlobStreamController {
|
||||||
|
constructor(blob, size) {
|
||||||
|
this.blob = blob;
|
||||||
|
this.index = 0;
|
||||||
|
this.chunkSize = size || 1024 * 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
pull(controller) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const bytesLeft = this.blob.size - this.index;
|
||||||
|
if (bytesLeft <= 0) {
|
||||||
|
controller.close();
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
const size = Math.min(this.chunkSize, bytesLeft);
|
||||||
|
const slice = this.blob.slice(this.index, this.index + size);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
controller.enqueue(new Uint8Array(reader.result));
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(slice);
|
||||||
|
this.index += size;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function blobStream(blob, size) {
|
||||||
|
return new ReadableStream(new BlobStreamController(blob, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConcatStreamController {
|
||||||
|
constructor(streams) {
|
||||||
|
this.streams = streams;
|
||||||
|
this.index = 0;
|
||||||
|
this.reader = null;
|
||||||
|
this.nextReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextReader() {
|
||||||
|
const next = this.streams[this.index++];
|
||||||
|
this.reader = next && next.getReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pull(controller) {
|
||||||
|
if (!this.reader) {
|
||||||
|
return controller.close();
|
||||||
|
}
|
||||||
|
const data = await this.reader.read();
|
||||||
|
if (data.done) {
|
||||||
|
this.nextReader();
|
||||||
|
return this.pull(controller);
|
||||||
|
}
|
||||||
|
controller.enqueue(data.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function concatStream(streams) {
|
||||||
|
return new ReadableStream(new ConcatStreamController(streams));
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function() {
|
|
||||||
const div = html`<div id="page-one"></div>`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download" class="fadeIn">
|
|
||||||
<div id="download-progress">
|
|
||||||
<div id="dl-title" class="title">${state.translate(
|
|
||||||
'downloadFinish'
|
|
||||||
)}</div>
|
|
||||||
<div class="description"></div>
|
|
||||||
${progress(1)}
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a class="send-new" data-state="completed" href="/" onclick=${
|
|
||||||
sendNew
|
|
||||||
}>${state.translate('sendYourFilesLink')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
async function sendNew(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
await fadeOut('download');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const transfer = state.transfer;
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download-progress" class="fadeIn">
|
|
||||||
<div id="dl-title" class="title">${state.translate(
|
|
||||||
'downloadingPageProgress',
|
|
||||||
{
|
|
||||||
filename: state.fileInfo.name,
|
|
||||||
size: bytes(state.fileInfo.size)
|
|
||||||
}
|
|
||||||
)}</div>
|
|
||||||
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
|
||||||
${progress(transfer.progressRatio)}
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text">${state.translate(
|
|
||||||
transfer.msg,
|
|
||||||
transfer.sizes
|
|
||||||
)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const fileInfo = state.fileInfo;
|
|
||||||
const label =
|
|
||||||
fileInfo.password === null
|
|
||||||
? html`
|
|
||||||
<label class="red"
|
|
||||||
for="unlock-input">${state.translate('passwordTryAgain')}</label>`
|
|
||||||
: html`
|
|
||||||
<label for="unlock-input">
|
|
||||||
${state.translate('unlockInputLabel')}
|
|
||||||
</label>`;
|
|
||||||
const div = html`
|
|
||||||
<div class="enterPassword">
|
|
||||||
${label}
|
|
||||||
<form id="unlock" onsubmit=${checkPassword} data-no-csrf>
|
|
||||||
<input id="unlock-input"
|
|
||||||
class="unlock-input input-no-btn"
|
|
||||||
maxlength="64"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
|
||||||
oninput=${inputChanged}
|
|
||||||
type="password"/>
|
|
||||||
<input type="submit"
|
|
||||||
id="unlock-btn"
|
|
||||||
class="btn btn-hidden"
|
|
||||||
value="${state.translate('unlockButtonLabel')}"/>
|
|
||||||
</form>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
function inputChanged() {
|
|
||||||
const input = document.getElementById('unlock-input');
|
|
||||||
const btn = document.getElementById('unlock-btn');
|
|
||||||
if (input.value.length > 0) {
|
|
||||||
btn.classList.remove('btn-hidden');
|
|
||||||
input.classList.remove('input-no-btn');
|
|
||||||
} else {
|
|
||||||
btn.classList.add('btn-hidden');
|
|
||||||
input.classList.add('input-no-btn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPassword(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const password = document.getElementById('unlock-input').value;
|
|
||||||
if (password.length > 0) {
|
|
||||||
document.getElementById('unlock-btn').disabled = true;
|
|
||||||
state.fileInfo.url = window.location.href;
|
|
||||||
state.fileInfo.password = password;
|
|
||||||
emit('preview');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
return html`
|
|
||||||
<div id="upload-error">
|
|
||||||
<div class="title">${state.translate('errorPageHeader')}</div>
|
|
||||||
<img id="upload-error-img" src="${assets.get('illustration_error.svg')}"/>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
function timeLeft(milliseconds) {
|
|
||||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
|
||||||
if (hours >= 1) {
|
|
||||||
return `${hours}h ${minutes % 60}m`;
|
|
||||||
} else if (hours === 0) {
|
|
||||||
return `${minutes}m ${seconds}s`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(file, state, emit) {
|
|
||||||
const ttl = file.expiresAt - Date.now();
|
|
||||||
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
|
||||||
const row = html`
|
|
||||||
<tr id="${file.id}">
|
|
||||||
<td class="overflow-col" title="${
|
|
||||||
file.name
|
|
||||||
}"><a class="link" href="/share/${file.id}">${file.name}</a></td>
|
|
||||||
<td class="center-col">
|
|
||||||
<img onclick=${copyClick} src="${assets.get(
|
|
||||||
'copy-16.svg'
|
|
||||||
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
|
|
||||||
<span class="text-copied" hidden="true">${state.translate(
|
|
||||||
'copiedUrl'
|
|
||||||
)}</span>
|
|
||||||
</td>
|
|
||||||
<td>${remaining}</td>
|
|
||||||
<td class="center-col">
|
|
||||||
<img onclick=${showPopup} src="${assets.get(
|
|
||||||
'close-16.svg'
|
|
||||||
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
|
||||||
<div class="popup">
|
|
||||||
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
|
||||||
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
|
||||||
<div class="popup-action">
|
|
||||||
<span class="popup-no" onclick=${cancel}>${state.translate(
|
|
||||||
'deletePopupCancel'
|
|
||||||
)}</span>
|
|
||||||
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
|
|
||||||
'deletePopupYes'
|
|
||||||
)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function copyClick(e) {
|
|
||||||
emit('copy', { url: file.url, location: 'upload-list' });
|
|
||||||
const icon = e.target;
|
|
||||||
const text = e.target.nextSibling;
|
|
||||||
icon.hidden = true;
|
|
||||||
text.hidden = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
icon.hidden = false;
|
|
||||||
text.hidden = true;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPopup() {
|
|
||||||
const tr = document.getElementById(file.id);
|
|
||||||
const popup = tr.querySelector('.popuptext');
|
|
||||||
popup.classList.add('show');
|
|
||||||
popup.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const tr = document.getElementById(file.id);
|
|
||||||
const popup = tr.querySelector('.popuptext');
|
|
||||||
popup.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
|
||||||
emit('delete', { file, location: 'upload-list' });
|
|
||||||
emit('render');
|
|
||||||
}
|
|
||||||
|
|
||||||
return row;
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const file = require('./file');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
let table = '';
|
|
||||||
if (state.storage.files.length) {
|
|
||||||
table = html`
|
|
||||||
<table id="uploaded-files">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
|
||||||
<th id="copy-file-list" class="center-col">${state.translate(
|
|
||||||
'copyFileList'
|
|
||||||
)}</th>
|
|
||||||
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
|
|
||||||
<th id="delete-file-list" class="center-col">${state.translate(
|
|
||||||
'deleteFileList'
|
|
||||||
)}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${state.storage.files.map(f => file(f, state, emit))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<div id="file-list">
|
|
||||||
${table}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
return html`<div class="footer">
|
|
||||||
<div class="legal-links">
|
|
||||||
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
|
|
||||||
'mozilla-logo.svg'
|
|
||||||
)}" alt="mozilla"/></a>
|
|
||||||
<a href="https://www.mozilla.org/about/legal">${state.translate(
|
|
||||||
'footerLinkLegal'
|
|
||||||
)}</a>
|
|
||||||
<a href="https://testpilot.firefox.com/about">${state.translate(
|
|
||||||
'footerLinkAbout'
|
|
||||||
)}</a>
|
|
||||||
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
|
|
||||||
<a href="/legal">${state.translate('footerLinkTerms')}</a>
|
|
||||||
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
|
|
||||||
'footerLinkCookies'
|
|
||||||
)}</a>
|
|
||||||
<a href="https://www.mozilla.org/about/legal/report-infringement/">${state.translate(
|
|
||||||
'reportIPInfringement'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
|
|
||||||
'github-icon.svg'
|
|
||||||
)}" alt="github"/></a>
|
|
||||||
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
|
|
||||||
'twitter-icon.svg'
|
|
||||||
)}" alt="twitter"/></a>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
// function replaceLinks(str, urls) {
|
|
||||||
// let i = -1;
|
|
||||||
// const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
|
||||||
// i++;
|
|
||||||
// return `<a class="link" href="${urls[i]}">${v}</a>`;
|
|
||||||
// });
|
|
||||||
// return [`<span>${s}</span>`];
|
|
||||||
// }
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
// function close() {
|
|
||||||
// document.querySelector('.banner').remove();
|
|
||||||
// }
|
|
||||||
|
|
||||||
function clicked(evt) {
|
|
||||||
emit('exit', evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = state.promo === 'blue' ? 'banner banner-blue' : 'banner';
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="${classes}">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src="${assets.get('firefox_logo-only.svg')}"
|
|
||||||
class="firefox-logo-small"
|
|
||||||
alt="Firefox"/>
|
|
||||||
<span>Send is brought to you by the all-new Firefox.
|
|
||||||
<a
|
|
||||||
class="link"
|
|
||||||
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com"
|
|
||||||
onclick=${clicked}
|
|
||||||
>Download Firefox now ≫</a></span>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
<img
|
|
||||||
src="${assets.get('close-16.svg')}"
|
|
||||||
class="icon-delete"
|
|
||||||
onclick=${close}>
|
|
||||||
*/
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
/*
|
|
||||||
The current weback config uses package.json to generate
|
|
||||||
version.json for /__version__ meaning `require` returns the
|
|
||||||
string 'version.json' in the frontend context but the json
|
|
||||||
on the server.
|
|
||||||
|
|
||||||
We want `version` to be constant at build time so this file
|
|
||||||
has a custom loader (/build/version_loader.js) just to replace
|
|
||||||
string with the value from package.json. 🤢
|
|
||||||
*/
|
|
||||||
const version = require('../../package.json').version || 'VERSION';
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
return html`<header class="header">
|
|
||||||
<div class="send-logo">
|
|
||||||
<a href="/">
|
|
||||||
<img src="${assets.get(
|
|
||||||
'send_logo.svg'
|
|
||||||
)}" alt="Send"/><h1 class="site-title">Send</h1>
|
|
||||||
</a>
|
|
||||||
<div class="site-subtitle">
|
|
||||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
|
||||||
<div>${state.translate('siteSubtitle')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
class="feedback"
|
|
||||||
target="_blank">${state.translate('siteFeedback')}</a>
|
|
||||||
</header>`;
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
function replaceLinks(str, urls) {
|
|
||||||
let i = -1;
|
|
||||||
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
|
||||||
i++;
|
|
||||||
return `<a href="${urls[i]}">${v}</a>`;
|
|
||||||
});
|
|
||||||
return [`<div class="description">${s}</div>`];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="legal">
|
|
||||||
<div class="title">${state.translate('legalHeader')}</div>
|
|
||||||
${html(
|
|
||||||
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
|
||||||
'https://testpilot.firefox.com/terms',
|
|
||||||
'https://testpilot.firefox.com/privacy',
|
|
||||||
'https://testpilot.firefox.com/experiments/send'
|
|
||||||
])
|
|
||||||
)}
|
|
||||||
${html(
|
|
||||||
replaceLinks(state.translate('legalNoticeMozilla'), [
|
|
||||||
'https://www.mozilla.org/privacy/websites/',
|
|
||||||
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
|
||||||
])
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<div class="title">${state.translate('expiredPageHeader')}</div>
|
|
||||||
<div class="share-window">
|
|
||||||
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
|
||||||
</div>
|
|
||||||
<div class="expired-description">${state.translate(
|
|
||||||
'uploadPageExplainer'
|
|
||||||
)}</div>
|
|
||||||
<a class="send-new" href="/" data-state="notfound">${state.translate(
|
|
||||||
'sendYourFilesLink'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const notFound = require('./notFound');
|
|
||||||
const downloadPassword = require('./downloadPassword');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
|
|
||||||
function getFileFromDOM() {
|
|
||||||
const el = document.getElementById('dl-file');
|
|
||||||
if (!el) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
nonce: el.getAttribute('data-nonce'),
|
|
||||||
pwd: !!+el.getAttribute('data-requires-password')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
state.fileInfo = state.fileInfo || getFileFromDOM();
|
|
||||||
if (!state.fileInfo) {
|
|
||||||
return notFound(state, emit);
|
|
||||||
}
|
|
||||||
state.fileInfo.id = state.params.id;
|
|
||||||
state.fileInfo.key = state.params.key;
|
|
||||||
const fileInfo = state.fileInfo;
|
|
||||||
const size = fileInfo.size
|
|
||||||
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
|
||||||
: '';
|
|
||||||
let action = html`
|
|
||||||
<div>
|
|
||||||
<img src="${assets.get('illustration_download.svg')}"
|
|
||||||
id="download-img"
|
|
||||||
alt="${state.translate('downloadAltText')}"/>
|
|
||||||
<div>
|
|
||||||
<button id="download-btn"
|
|
||||||
class="btn"
|
|
||||||
onclick=${download}>${state.translate('downloadButtonLabel')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
if (fileInfo.pwd && !fileInfo.password) {
|
|
||||||
action = downloadPassword(state, emit);
|
|
||||||
} else if (!state.transfer) {
|
|
||||||
emit('preview');
|
|
||||||
}
|
|
||||||
const title = fileInfo.name
|
|
||||||
? state.translate('downloadFileName', { filename: fileInfo.name })
|
|
||||||
: state.translate('downloadFileTitle');
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<div id="download-page-one">
|
|
||||||
<div class="title">
|
|
||||||
<span id="dl-file"
|
|
||||||
data-nonce="${fileInfo.nonce}"
|
|
||||||
data-requires-password="${fileInfo.pwd}">${title}</span>
|
|
||||||
<span id="dl-filesize">${' ' + size}</span>
|
|
||||||
</div>
|
|
||||||
<div class="description">${state.translate('downloadMessage')}</div>
|
|
||||||
${action}
|
|
||||||
</div>
|
|
||||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function download(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
emit('download', fileInfo);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
const radius = 73;
|
|
||||||
const oRadius = radius + 10;
|
|
||||||
const oDiameter = oRadius * 2;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
|
|
||||||
module.exports = function(progressRatio) {
|
|
||||||
const dashOffset = (1 - progressRatio) * circumference;
|
|
||||||
const percent = Math.floor(progressRatio * 100);
|
|
||||||
const div = html`
|
|
||||||
<div class="progress-bar">
|
|
||||||
<svg id="progress" width="${oDiameter}" height="${
|
|
||||||
oDiameter
|
|
||||||
}" viewPort="0 0 ${oDiameter} ${oDiameter}" version="1.1">
|
|
||||||
<circle r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent"/>
|
|
||||||
<circle id="bar" r="${radius}" cx="${oRadius}" cy="${
|
|
||||||
oRadius
|
|
||||||
}" fill="transparent" transform="rotate(-90 ${oRadius} ${
|
|
||||||
oRadius
|
|
||||||
})" stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"/>
|
|
||||||
<text class="percentage" text-anchor="middle" x="50%" y="98"><tspan class="percent-number">${
|
|
||||||
percent
|
|
||||||
}</tspan><tspan class="percent-sign">%</tspan></text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(selected, options, translate, changed) {
|
|
||||||
const id = `select-${Math.random()}`;
|
|
||||||
let x = selected;
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
const ul = document.getElementById(id);
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
ul.classList.remove('active');
|
|
||||||
body.removeEventListener('click', close);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const ul = document.getElementById(id);
|
|
||||||
if (ul.classList.contains('active')) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
ul.classList.add('active');
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
body.addEventListener('click', close);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function choose(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const target = event.target;
|
|
||||||
const value = +target.dataset.value;
|
|
||||||
target.parentNode.previousSibling.firstElementChild.textContent = translate(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
if (x !== value) {
|
|
||||||
x = value;
|
|
||||||
changed(value);
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<div class="selectbox">
|
|
||||||
<div onclick=${toggle}>
|
|
||||||
<span class="link">${translate(selected)}</span>
|
|
||||||
<svg width="32" height="32">
|
|
||||||
<polygon points="8 18 17 28 26 18" fill="#0094fb"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<ul id="${id}" class="selectOptions">
|
|
||||||
${options.map(
|
|
||||||
i =>
|
|
||||||
html`<li class="selectOption" onclick=${choose} data-value="${i}">${
|
|
||||||
i
|
|
||||||
}</li>`
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||