Compare commits
679 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8c3caf18 | ||
|
|
3a7677b46d | ||
|
|
3fd64d9956 | ||
|
|
1a4cea7b4f | ||
|
|
bbb1c42641 | ||
|
|
7ce2320eda | ||
|
|
1729b30f89 | ||
|
|
25d6595d5e | ||
|
|
3b31b9d65b | ||
|
|
e9e64fed3f | ||
|
|
3b88ea9b1c | ||
|
|
623f76fa6c | ||
|
|
19efcb1a0d | ||
|
|
31c8916622 | ||
|
|
7a0b189a1d | ||
|
|
fa0d56d57a | ||
|
|
85670bbc6a | ||
|
|
fc9a85b6ad | ||
|
|
8760b132da | ||
|
|
d7084829c3 | ||
|
|
243d72eed5 | ||
|
|
aa60e97d5d | ||
|
|
4ae12db99c | ||
|
|
98ad058e2e | ||
|
|
58cc381abf | ||
|
|
d039c38f00 | ||
|
|
81f3347981 | ||
|
|
b96d2949f7 | ||
|
|
a2e745a349 | ||
|
|
0fe02b18ce | ||
|
|
655eb8c253 | ||
|
|
d9356f8171 | ||
|
|
be7d23163c | ||
|
|
7ac2e57484 | ||
|
|
727ea1283a | ||
|
|
3314057059 | ||
|
|
df1b00fa2c | ||
|
|
6434329f61 | ||
|
|
454853dc93 | ||
|
|
b663bf94e4 | ||
|
|
081cf081c5 | ||
|
|
f86b164c62 | ||
|
|
92b79e8272 | ||
|
|
335086c9d7 | ||
|
|
60806a3954 | ||
|
|
e0e96e7b9b | ||
|
|
cda35eb127 | ||
|
|
bf1ef345a2 | ||
|
|
e54ae7a51f | ||
|
|
a5a2d654ae | ||
|
|
b6340d8657 | ||
|
|
cdb87e2ac2 | ||
|
|
a0baed4e14 | ||
|
|
573a2d4fb1 | ||
|
|
df3ae9e22d | ||
|
|
52bffa024e | ||
|
|
459499d5f5 | ||
|
|
7cc94f6829 | ||
|
|
53d5b08559 | ||
|
|
c517b074f1 | ||
|
|
503ba43ebc | ||
|
|
b0755f4bc5 | ||
|
|
89ece1ec4c | ||
|
|
9fcaefbd99 | ||
|
|
81f33c7bbd | ||
|
|
0ce1bf2360 | ||
|
|
bb10c1aa9d | ||
|
|
b2c1daa6c7 | ||
|
|
da4338ed69 | ||
|
|
1849d712b1 | ||
|
|
28a7e0f717 | ||
|
|
ddbfc79cae | ||
|
|
ec8900d38f | ||
|
|
8aadd5d775 | ||
|
|
268f9306d9 | ||
|
|
866552254c | ||
|
|
42f87424c6 | ||
|
|
4899fb27ec | ||
|
|
c2c8d2bdaa | ||
|
|
daa373967f | ||
|
|
dccc9bfbcd | ||
|
|
f51a873752 | ||
|
|
bad5efc5e8 | ||
|
|
edf8384348 | ||
|
|
65c2f0e191 | ||
|
|
427a09296c | ||
|
|
ba11d62003 | ||
|
|
900dbe781e | ||
|
|
ee19aa8f2e | ||
|
|
e9cff66852 | ||
|
|
f63894bea7 | ||
|
|
35a285d71a | ||
|
|
ba80b01150 | ||
|
|
c9be359db4 | ||
|
|
46a3933dbb | ||
|
|
7b4060f9e1 | ||
|
|
beb3a6e67b | ||
|
|
b393c35284 | ||
|
|
ad371553ba | ||
|
|
0107fb9486 | ||
|
|
7cf70317e8 | ||
|
|
7d8aaac5d2 | ||
|
|
2b5f7d126f | ||
|
|
3cbc71ca1b | ||
|
|
9c4ac38a6c | ||
|
|
9ae64a97ba | ||
|
|
2807b1cad5 | ||
|
|
157e832c95 | ||
|
|
d78fcd3721 | ||
|
|
ac7ab79aef | ||
|
|
00fb353465 | ||
|
|
f0ec5a9496 | ||
|
|
7a31082da1 | ||
|
|
b54f4575ee | ||
|
|
490a1e88eb | ||
|
|
2f8a3c9904 | ||
|
|
e7fdf76120 | ||
|
|
d0d41b743a | ||
|
|
a2995411d6 | ||
|
|
3246c4a621 | ||
|
|
48faf929a4 | ||
|
|
b7f922a999 | ||
|
|
bfcdf9340d | ||
|
|
4ed515f5a3 | ||
|
|
84b2737ffb | ||
|
|
deabca5a94 | ||
|
|
e9a49e23e8 | ||
|
|
cfdef23365 | ||
|
|
7c4b6a9de4 | ||
|
|
bb8866f73f | ||
|
|
33c42648ef | ||
|
|
7feddd2eee | ||
|
|
1344b84cf5 | ||
|
|
ef03b750c5 | ||
|
|
0c07d78a37 | ||
|
|
b5885e446c | ||
|
|
0fa3d4481a | ||
|
|
49e7c2e05b | ||
|
|
2dc7d046ef | ||
|
|
3e0bd41efd | ||
|
|
929eaca2d8 | ||
|
|
60517c5ab6 | ||
|
|
97a8a2b305 | ||
|
|
8fc54bdbe2 | ||
|
|
6ff251b24a | ||
|
|
7237800a91 | ||
|
|
b4cc8e92c7 | ||
|
|
2e233da16d | ||
|
|
b703f78db9 | ||
|
|
13e792cf4d | ||
|
|
552c2d74f3 | ||
|
|
593f23b021 | ||
|
|
1baf83bac3 | ||
|
|
d43ca3190e | ||
|
|
c4c7860876 | ||
|
|
0decdeb37c | ||
|
|
47505dcc31 | ||
|
|
e71d6e792b | ||
|
|
29796cfec8 | ||
|
|
4bdf255c3a | ||
|
|
11efe8b8d1 | ||
|
|
3dd2d09584 | ||
|
|
ed4e8e8f25 | ||
|
|
181a74df88 | ||
|
|
166b2f3a52 | ||
|
|
bd5cdc52f9 | ||
|
|
5d0318c102 | ||
|
|
17adc644fb | ||
|
|
f48159dc0b | ||
|
|
360697c034 | ||
|
|
45dd833b27 | ||
|
|
98491eed01 | ||
|
|
fdafc1c59e | ||
|
|
a32638ed4c | ||
|
|
39080a6046 | ||
|
|
a124d36714 | ||
|
|
7f9e619643 | ||
|
|
db12f2f5c8 | ||
|
|
415e0b70f3 | ||
|
|
0e4b1e5ec7 | ||
|
|
054a97371c | ||
|
|
2b25b6a6ea | ||
|
|
780ed3120e | ||
|
|
bdd3cbd4c7 | ||
|
|
0c8013038f | ||
|
|
c020d59c56 | ||
|
|
a5c336494b | ||
|
|
aaa4655f45 | ||
|
|
ed18ec0bc5 | ||
|
|
1de43f31b6 | ||
|
|
1a04c86edd | ||
|
|
2bbdcae82e | ||
|
|
9b0aa5d601 | ||
|
|
cdc261e30a | ||
|
|
09133a66b0 | ||
|
|
64f1a31533 | ||
|
|
9a92a50a5f | ||
|
|
2afd93e82f | ||
|
|
cb62cc1e9d | ||
|
|
2cd3fc5af9 | ||
|
|
bac710a17f | ||
|
|
fb8b0f78ca | ||
|
|
e18ce15753 | ||
|
|
ed8ce9e3ca | ||
|
|
af5ef04115 | ||
|
|
9530a3df52 | ||
|
|
2794ac653f | ||
|
|
43eb758d73 | ||
|
|
d0364cd101 | ||
|
|
6a008bf312 | ||
|
|
dfb271410c | ||
|
|
789d67209c | ||
|
|
a31f6b75d9 | ||
|
|
f814427a7d | ||
|
|
b0307e92d4 | ||
|
|
7fa3d69aa1 | ||
|
|
58555a6b85 | ||
|
|
52113395db | ||
|
|
8dd1309c21 | ||
|
|
202e428412 | ||
|
|
6e7ed3cea3 | ||
|
|
41cb49141b | ||
|
|
82a8283b6e | ||
|
|
a5d28adc44 | ||
|
|
82e206bccf | ||
|
|
1e4d6646c6 | ||
|
|
acbf9fc32f | ||
|
|
046f227003 | ||
|
|
50ac9e32be | ||
|
|
3459dcaa15 | ||
|
|
9410defab6 | ||
|
|
b5a26e11f8 | ||
|
|
c51481628d | ||
|
|
24fa51a12c | ||
|
|
7328520d05 | ||
|
|
409d206f1e | ||
|
|
b0b393f3d9 | ||
|
|
c4499088c8 | ||
|
|
4cccd6ac5c | ||
|
|
188b28fce3 | ||
|
|
24adda6c7d | ||
|
|
4b49302fbe | ||
|
|
402ab350de | ||
|
|
47b68770af | ||
|
|
60aa16a327 | ||
|
|
5702a4806b | ||
|
|
74c4bdb660 | ||
|
|
203a2cf7fb | ||
|
|
1faa2733b3 | ||
|
|
ffa432a876 | ||
|
|
009fd29265 | ||
|
|
8f05c2324e | ||
|
|
e1ab515883 | ||
|
|
a68a7a60a7 | ||
|
|
b4dc274646 | ||
|
|
b31892bdc6 | ||
|
|
f388a1348d | ||
|
|
52dacbddf9 | ||
|
|
cc52f60aa1 | ||
|
|
1af818b691 | ||
|
|
d76f7758e7 | ||
|
|
48ab2f2400 | ||
|
|
0aa844eebc | ||
|
|
481b02ccf2 | ||
|
|
67e6ef6fda | ||
|
|
7d19f86d7a | ||
|
|
2d27d8a47c | ||
|
|
10fac130ef | ||
|
|
c6b632543d | ||
|
|
32eb8157eb | ||
|
|
3628f22114 | ||
|
|
177fa37041 | ||
|
|
717f6576ea | ||
|
|
f1b2ffa0fa | ||
|
|
ac73c23c73 | ||
|
|
ac40308b1c | ||
|
|
6fb81aa78c | ||
|
|
92430c78c2 | ||
|
|
cb7ddaa295 | ||
|
|
786d079632 | ||
|
|
4af25a505a | ||
|
|
3218803aae | ||
|
|
2311d5bcef | ||
|
|
e56d92334f | ||
|
|
bc24a069da | ||
|
|
837747f8f7 | ||
|
|
a8c32ae49c | ||
|
|
32c5b414de | ||
|
|
12c81a22e8 | ||
|
|
0c5d0d4bb2 | ||
|
|
234f9c624d | ||
|
|
da669b44ff | ||
|
|
3c39f5f085 | ||
|
|
6de91b5872 | ||
|
|
ff9a0979f6 | ||
|
|
e1e8af2489 | ||
|
|
1eb000f615 | ||
|
|
e20fd97e59 | ||
|
|
d10ceacd67 | ||
|
|
208c28ee01 | ||
|
|
cdd1bb3c29 | ||
|
|
3d9c4fa320 | ||
|
|
9c4d18ef3b | ||
|
|
ce9ff3959f | ||
|
|
51aef4e1e5 | ||
|
|
90247059d0 | ||
|
|
c97abb46ed | ||
|
|
b8f5e371c7 | ||
|
|
401311a05f | ||
|
|
652b8e4e15 | ||
|
|
99b7e7c0f1 | ||
|
|
81442bb6f2 | ||
|
|
137f474b69 | ||
|
|
8e14d3f8f7 | ||
|
|
07b7bc003a | ||
|
|
df691c1516 | ||
|
|
17e61bb09d | ||
|
|
14e21988b2 | ||
|
|
205de5a633 | ||
|
|
eabdc903b9 | ||
|
|
0628e71ec9 | ||
|
|
ebbcb38c7a | ||
|
|
5c1f535291 | ||
|
|
2c8e488611 | ||
|
|
6f3eac659c | ||
|
|
86bca790bc | ||
|
|
895d196876 | ||
|
|
3d8b38ffe4 | ||
|
|
fddc415c86 | ||
|
|
7a8e9b5de1 | ||
|
|
bbaeb44b26 | ||
|
|
a95f659474 | ||
|
|
4d311b134f | ||
|
|
5f90de577f | ||
|
|
3c32ce946a | ||
|
|
5ca89d0e0d | ||
|
|
781d1f5b0a | ||
|
|
ef9bfae319 | ||
|
|
7abbc60e17 | ||
|
|
15244e1a64 | ||
|
|
4b39d61ff4 | ||
|
|
74718d6361 | ||
|
|
ced640c24a | ||
|
|
cdaa92c86d | ||
|
|
57012f0660 | ||
|
|
6fde6e0a79 | ||
|
|
182bde30fa | ||
|
|
59e2267513 | ||
|
|
01a064ef7f | ||
|
|
9759338e6a | ||
|
|
5ac4560157 | ||
|
|
8e60ca1ac9 | ||
|
|
131a8b5564 | ||
|
|
663023a204 | ||
|
|
2b5c9dfb35 | ||
|
|
a9a34fdd0a | ||
|
|
1655094ce3 | ||
|
|
9ae7e3df11 | ||
|
|
0a31e2d521 | ||
|
|
b6849661a6 | ||
|
|
53e822964e | ||
|
|
b2f76d2df9 | ||
|
|
574a3ce894 | ||
|
|
c68f796891 | ||
|
|
c592f84d7d | ||
|
|
31faaf147e | ||
|
|
09c20f5933 | ||
|
|
fa4ab7bd5c | ||
|
|
de4a24a7f8 | ||
|
|
8f1c404724 | ||
|
|
1b975e7ba7 | ||
|
|
1d7473c489 | ||
|
|
a7d3992ba1 | ||
|
|
d81e9a76db | ||
|
|
0624e59776 | ||
|
|
546064a7ee | ||
|
|
9d49cc876c | ||
|
|
0bf8481fd0 | ||
|
|
ae0758ac14 | ||
|
|
e9405f49ee | ||
|
|
a1d0eef8a5 | ||
|
|
254b806fb4 | ||
|
|
a7aee1450f | ||
|
|
fa5573a5ff | ||
|
|
9714bb0a0a | ||
|
|
ad82d30dd9 | ||
|
|
757ac14d1a | ||
|
|
3e066258c4 | ||
|
|
127f73b4fe | ||
|
|
ae5009e1e3 | ||
|
|
0ab8ddc894 | ||
|
|
b429841534 | ||
|
|
279f6df6f4 | ||
|
|
634e6b2834 | ||
|
|
856b2cdc60 | ||
|
|
41351f877c | ||
|
|
afbb89fbe8 | ||
|
|
b40b45273d | ||
|
|
f1fb877c7f | ||
|
|
9d7ad06b1a | ||
|
|
c6a4b089d9 | ||
|
|
24917f8aa5 | ||
|
|
eada94b262 | ||
|
|
43fa551a64 | ||
|
|
e1137db946 | ||
|
|
c1878649b3 | ||
|
|
30b86b14ed | ||
|
|
e91b341f8a | ||
|
|
70148232c6 | ||
|
|
56e3d5766c | ||
|
|
350e31ae4a | ||
|
|
0794bcc458 | ||
|
|
a6aee8ad62 | ||
|
|
8305d13dab | ||
|
|
441a520765 | ||
|
|
ba84e59f39 | ||
|
|
22a316ab58 | ||
|
|
6b9502d252 | ||
|
|
cdc3a5340d | ||
|
|
f03f7a0286 | ||
|
|
d8a5789701 | ||
|
|
8d26e0e742 | ||
|
|
e142d76cb4 | ||
|
|
c488c1d724 | ||
|
|
bed57af6c5 | ||
|
|
7500bd8326 | ||
|
|
0250924961 | ||
|
|
70813556ad | ||
|
|
2bb9af1943 | ||
|
|
55bd44a8f5 | ||
|
|
d83900f272 | ||
|
|
fa4f9299b2 | ||
|
|
0f7b19c385 | ||
|
|
a9c1dd0180 | ||
|
|
c468e2f34e | ||
|
|
718f42897f | ||
|
|
fb468bd1bc | ||
|
|
dafe00cabb | ||
|
|
98aebb7f70 | ||
|
|
a990d78bc0 | ||
|
|
9b4069be3e | ||
|
|
ff3bc0dd62 | ||
|
|
b39b131928 | ||
|
|
2646fb9b3c | ||
|
|
c2b84650e2 | ||
|
|
fecf938ae7 | ||
|
|
8abf631430 | ||
|
|
d69c535dda | ||
|
|
082ca6c57b | ||
|
|
b263231068 | ||
|
|
947a6d9992 | ||
|
|
1ad7edf5a9 | ||
|
|
0c26204ea1 | ||
|
|
1e3bbee7f1 | ||
|
|
ec80e8e622 | ||
|
|
61e2c0d85b | ||
|
|
80db74fc3a | ||
|
|
30936eb2fa | ||
|
|
31e29d58b9 | ||
|
|
702134b3b1 | ||
|
|
11ae7f857c | ||
|
|
8827556974 | ||
|
|
21b7f16b1e | ||
|
|
314ab237ec | ||
|
|
0fa0416c3f | ||
|
|
09faedf059 | ||
|
|
16aa7983ed | ||
|
|
493bf8dc89 | ||
|
|
46432b9649 | ||
|
|
193664a8e8 | ||
|
|
626e578acb | ||
|
|
51bffe11a8 | ||
|
|
08e2c6c112 | ||
|
|
c38d91db98 | ||
|
|
c13839a522 | ||
|
|
4894d5162f | ||
|
|
77b6fb138f | ||
|
|
9dab74891d | ||
|
|
393d2a0052 | ||
|
|
44ac783f6a | ||
|
|
7ea8712538 | ||
|
|
fb92a793e4 | ||
|
|
87eaba6337 | ||
|
|
7e13f2ab32 | ||
|
|
3214d293ca | ||
|
|
1437116cf3 | ||
|
|
87a8cfba40 | ||
|
|
f929bb2aec | ||
|
|
740001ddde | ||
|
|
aaec16cf52 | ||
|
|
24af3207e9 | ||
|
|
5da43f0da7 | ||
|
|
dffb26349b | ||
|
|
38746078ed | ||
|
|
c4751842fe | ||
|
|
08ec9d98b8 | ||
|
|
5200928d5c | ||
|
|
4c3e37f4b5 | ||
|
|
60eda64c8d | ||
|
|
4db7fc6db3 | ||
|
|
5844a9a03c | ||
|
|
9829e46270 | ||
|
|
9a150ddb22 | ||
|
|
e7b90ea1b9 | ||
|
|
07bcfa2d36 | ||
|
|
dce134744d | ||
|
|
288bc50484 | ||
|
|
09d6c64a9b | ||
|
|
d7a542f2c2 | ||
|
|
aca82dd880 | ||
|
|
aa72a4c72c | ||
|
|
a70dcfc905 | ||
|
|
49731ab8ba | ||
|
|
3d9e01f8e5 | ||
|
|
441fe86186 | ||
|
|
f9283f5f6a | ||
|
|
c8e0b696a6 | ||
|
|
cf5c64a3f8 | ||
|
|
097ff19c5f | ||
|
|
8767b9b6b0 | ||
|
|
4ac7ac2b24 | ||
|
|
4cdce6c841 | ||
|
|
763414c848 | ||
|
|
9172af48fd | ||
|
|
14a4297db8 | ||
|
|
a78475aff4 | ||
|
|
d2b57039bf | ||
|
|
dd5be246d4 | ||
|
|
779005845e | ||
|
|
f3499326bd | ||
|
|
fe10b131fc | ||
|
|
9b6b7aa998 | ||
|
|
e9d5d8ec11 | ||
|
|
73e39ad453 | ||
|
|
c91d24cd86 | ||
|
|
1fc83aa902 | ||
|
|
5ac013ca40 | ||
|
|
6d17b86d28 | ||
|
|
fcea981127 | ||
|
|
bdd7044808 | ||
|
|
96bea84a73 | ||
|
|
9aef7df82c | ||
|
|
b0d36529a1 | ||
|
|
2a7099a7a2 | ||
|
|
4c069949f2 | ||
|
|
699c97a9c0 | ||
|
|
3c640061f0 | ||
|
|
bbd74efc2c | ||
|
|
2dfbee090a | ||
|
|
a03b1b7a9a | ||
|
|
594b584500 | ||
|
|
d74d339e4b | ||
|
|
859554ce21 | ||
|
|
703325f223 | ||
|
|
9a416e3e78 | ||
|
|
8821403a9b | ||
|
|
4a841bf563 | ||
|
|
4d6995536a | ||
|
|
6ef4e86029 | ||
|
|
2408c766ce | ||
|
|
882e13bac6 | ||
|
|
e48c2cf75b | ||
|
|
c8f7e60259 | ||
|
|
432a39d313 | ||
|
|
59ed64698d | ||
|
|
6edfe5146c | ||
|
|
44ea0756ea | ||
|
|
02fc4d74db | ||
|
|
0f77b6d86b | ||
|
|
ae2eb14fda | ||
|
|
455c4f5472 | ||
|
|
7335232680 | ||
|
|
2f9372e8e0 | ||
|
|
80db158ee3 | ||
|
|
6c18f4fb2c | ||
|
|
f28444f4c6 | ||
|
|
93ac742a5e | ||
|
|
6dcbc19315 | ||
|
|
65df0fa9cf | ||
|
|
387e88907c | ||
|
|
9253695f8d | ||
|
|
67635b9151 | ||
|
|
e0abfb5cf7 | ||
|
|
f1de6a14da | ||
|
|
38052c1b85 | ||
|
|
5847faf4e2 | ||
|
|
0c1c8178f5 | ||
|
|
86fbd2a4b5 | ||
|
|
8cfb45944c | ||
|
|
0289409c9c | ||
|
|
b93a33eba4 | ||
|
|
fb41a40128 | ||
|
|
e18fda91fe | ||
|
|
b077635427 | ||
|
|
4d4098b7c9 | ||
|
|
9e4838d121 | ||
|
|
dd439bcdfb | ||
|
|
d5e936b8fc | ||
|
|
376e1efa4c | ||
|
|
09cc047ade | ||
|
|
9b6507bc1b | ||
|
|
27d2927b14 | ||
|
|
21cb8f6608 | ||
|
|
41cb49c99f | ||
|
|
504d475f5d | ||
|
|
926ce972f5 | ||
|
|
2bc7f270f8 | ||
|
|
b03022b924 | ||
|
|
66863ceb1a | ||
|
|
69e91eb4ed | ||
|
|
167bf8a607 | ||
|
|
5e10fd2db6 | ||
|
|
a451ae435e | ||
|
|
125c4232ae | ||
|
|
7f5e33613d | ||
|
|
5576bc0652 | ||
|
|
ebd3a45a2c | ||
|
|
193f54547f | ||
|
|
d344531bda | ||
|
|
4ac07800c4 | ||
|
|
e9fa6ad401 | ||
|
|
95e3ca7a1c | ||
|
|
705289d34b | ||
|
|
4282c93b2b | ||
|
|
434f8b56cc | ||
|
|
5c793dcde8 | ||
|
|
e36f685bd2 | ||
|
|
5a7d05744c | ||
|
|
81999f2fc4 | ||
|
|
c419d29c96 | ||
|
|
8cdfb47f84 | ||
|
|
5e192d0f4a | ||
|
|
1460537169 | ||
|
|
f888ea74b0 | ||
|
|
ea6c78f49a | ||
|
|
3f316fb8b0 | ||
|
|
ee444186e9 | ||
|
|
2b7f06dda2 | ||
|
|
c19b273d4f | ||
|
|
596ad871df | ||
|
|
655f685883 | ||
|
|
bbe111a95e | ||
|
|
b063cf35b8 | ||
|
|
030d65d7af | ||
|
|
aa113fd903 | ||
|
|
caeba94e04 | ||
|
|
a7de951115 | ||
|
|
e17d1f7235 | ||
|
|
51ba253f95 | ||
|
|
fef26e083a | ||
|
|
de826afb9b | ||
|
|
5944b85e67 | ||
|
|
d208a82089 | ||
|
|
a64eced8be | ||
|
|
ada45323e1 | ||
|
|
ced8c24f47 | ||
|
|
5b9e9d5146 | ||
|
|
280a4f65e7 | ||
|
|
d2dd9f4b4d | ||
|
|
2897a39131 | ||
|
|
626b9068a9 | ||
|
|
c3bb876a2c | ||
|
|
48912dd4d4 | ||
|
|
f1c894d14f | ||
|
|
60d61fa52c | ||
|
|
fe313f1001 | ||
|
|
68705f60db | ||
|
|
92303988c0 | ||
|
|
49d578217c | ||
|
|
1abb3b7ebe | ||
|
|
4f3c2498a6 | ||
|
|
33babe6f67 | ||
|
|
a51ee89939 | ||
|
|
bca79489c0 | ||
|
|
ddfbb06e1a | ||
|
|
cd2c944d41 | ||
|
|
5f66496519 | ||
|
|
318964251d | ||
|
|
5effeb16d1 |
@@ -1,8 +1,8 @@
|
||||
node_modules
|
||||
.git
|
||||
.DS_Store
|
||||
static
|
||||
test
|
||||
scripts
|
||||
docs
|
||||
firefox
|
||||
assets
|
||||
docs
|
||||
public
|
||||
test
|
||||
|
||||
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,html,yml,json,handlebars}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.toml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -1,3 +1,3 @@
|
||||
public
|
||||
test/frontend/bundle.js
|
||||
dist
|
||||
assets
|
||||
firefox
|
||||
|
||||
@@ -21,6 +21,7 @@ rules:
|
||||
|
||||
eol-last: [error, always]
|
||||
eqeqeq: error
|
||||
no-alert: warn
|
||||
no-console: warn
|
||||
no-path-concat: error
|
||||
no-unused-vars: [error, {argsIgnorePattern: "^_|err|event|next|reject"}]
|
||||
|
||||
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
public/locales/* linguist-documentation
|
||||
docs/* linguist-documentation
|
||||
9
.gitignore
vendored
@@ -1,9 +1,2 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
public/upload.js
|
||||
public/download.js
|
||||
public/version.json
|
||||
public/l20n.min.js
|
||||
static/*
|
||||
!static/info.txt
|
||||
test/frontend/bundle.js
|
||||
dist
|
||||
|
||||
3
.nsprc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exceptions": ["https://nodesecurity.io/advisories/534"]
|
||||
}
|
||||
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
assets/*.js
|
||||
@@ -1,6 +1,11 @@
|
||||
extends: stylelint-config-standard
|
||||
|
||||
plugins:
|
||||
- stylelint-no-unsupported-browser-features
|
||||
|
||||
rules:
|
||||
plugin/no-unsupported-browser-features: [true, {severity: warning}]
|
||||
|
||||
color-hex-case: lower
|
||||
declaration-colon-newline-after: null
|
||||
selector-list-comma-newline-after: null
|
||||
|
||||
218
CHANGELOG.md
Normal file
@@ -0,0 +1,218 @@
|
||||
## Change Log
|
||||
|
||||
### v1.1.1 (2017/08/17 01:29 +00:00)
|
||||
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
||||
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
||||
- [#515](https://github.com/mozilla/send/pull/515) removed jquery from upload.js (@dannycoates)
|
||||
- [#514](https://github.com/mozilla/send/pull/514) use async and removed jquery from download.js (@dannycoates)
|
||||
- [#513](https://github.com/mozilla/send/pull/513) use svg for progress (@dannycoates)
|
||||
- [#510](https://github.com/mozilla/send/pull/510) added precommit hook for format (@dannycoates)
|
||||
- [#502](https://github.com/mozilla/send/pull/502) extracted filelist into its own file (@dannycoates)
|
||||
- [#428](https://github.com/mozilla/send/pull/428) add twitter and open graph cards (@dannycoates, @johngruen)
|
||||
- [#506](https://github.com/mozilla/send/pull/506) 404 page (@varghesethomase)
|
||||
- [#508](https://github.com/mozilla/send/pull/508) fixes 478 (@abhinadduri)
|
||||
- [#504](https://github.com/mozilla/send/pull/504) fix japanese browse button (@johngruen)
|
||||
- [#503](https://github.com/mozilla/send/pull/503) Added editorconfig (@skystar-p)
|
||||
- [#499](https://github.com/mozilla/send/pull/499) use import/export in the frontend code (@dannycoates)
|
||||
- [#500](https://github.com/mozilla/send/pull/500) fixed build:css on windows (@dannycoates)
|
||||
- [#481](https://github.com/mozilla/send/pull/481) Cater for mobile and desktop (@pdehaan, @hubdotcom)
|
||||
- [#493](https://github.com/mozilla/send/pull/493) added webpack-dev-middleware (@dannycoates)
|
||||
- [#491](https://github.com/mozilla/send/pull/491) added missing exit event cases (@dannycoates)
|
||||
- [#492](https://github.com/mozilla/send/pull/492) make the site mostly work when cookies (localStorage) are disabled (@dannycoates)
|
||||
- [#490](https://github.com/mozilla/send/pull/490) set the mime type in the download blob (@dannycoates)
|
||||
- [#485](https://github.com/mozilla/send/pull/485) added progress to tab title when not in focus (@dannycoates)
|
||||
- [#474](https://github.com/mozilla/send/pull/474) Fixing bug #438 by adding role attribute to anchor tags and alt attribute images (@varghesethomase)
|
||||
- [#480](https://github.com/mozilla/send/pull/480) Increase font weight to 500 on <button>s and <label>s (@pdehaan)
|
||||
- [#419](https://github.com/mozilla/send/pull/419) Add autoprefixer and cssnano support (@pdehaan)
|
||||
|
||||
### v1.1.0 (2017/08/08 03:59 +00:00)
|
||||
- [#473](https://github.com/mozilla/send/pull/473) Sort contributors alphabetically to prevent churn (@pdehaan)
|
||||
- [#472](https://github.com/mozilla/send/pull/472) removed references to checksums in frontend tests (@abhinadduri)
|
||||
- [#470](https://github.com/mozilla/send/pull/470) removed the file sha256 hash (@dannycoates)
|
||||
- [#469](https://github.com/mozilla/send/pull/469) Increase mimimum node version to 8.2.0 (@ehuggett)
|
||||
- [#468](https://github.com/mozilla/send/pull/468) attach delete-file handler only after upload (@dannycoates)
|
||||
- [#466](https://github.com/mozilla/send/pull/466) added webpack (@dannycoates)
|
||||
- [#427](https://github.com/mozilla/send/pull/427) Extended system font list fixes:#408 (@gautamkrishnar)
|
||||
- [#448](https://github.com/mozilla/send/pull/448) Migrate width attribute to CSS (Fixes #436) (@nskins)
|
||||
- [#457](https://github.com/mozilla/send/pull/457) factored out progress into progress.js (@dannycoates)
|
||||
- [#452](https://github.com/mozilla/send/pull/452) refactored metrics (@dannycoates)
|
||||
- [#455](https://github.com/mozilla/send/pull/455) Add a few missing strings from es-CL and tr locales (@pdehaan)
|
||||
- [#444](https://github.com/mozilla/send/pull/444) Chain jQuery calls, do not use events alias and store selectors (@Johann-S)
|
||||
- [#416](https://github.com/mozilla/send/pull/416) WIP: use webcrypto-liner to support Safari 10 (@dannycoates)
|
||||
- [#451](https://github.com/mozilla/send/pull/451) Add rel noopener noreferrer to target='_blank' anchor elements (Fixes #439) (@boopeshmahendran)
|
||||
- [#449](https://github.com/mozilla/send/pull/449) Add X-UA-Compatible meta tag (@kenrick95)
|
||||
- [#433](https://github.com/mozilla/send/pull/433) Prevent download button from being clicked multiple times (@pdehaan)
|
||||
- [#432](https://github.com/mozilla/send/pull/432) Add contributors script (@pdehaan)
|
||||
- [#409](https://github.com/mozilla/send/pull/409) Handle copy clipboard disabled (@Johann-S)
|
||||
|
||||
### v1.0.4 (2017/08/03 23:05 +00:00)
|
||||
- [#418](https://github.com/mozilla/send/pull/418) _blank all footer links (@dannycoates)
|
||||
- [#386](https://github.com/mozilla/send/pull/386) fix percentage view on mobile layout (@ariestiyansyah)
|
||||
- [#414](https://github.com/mozilla/send/pull/414) Add link to FAQ in unsupported view (@pdehaan)
|
||||
- [#415](https://github.com/mozilla/send/pull/415) Only include Fira CSS on /unsupported/* route (@pdehaan)
|
||||
- [#412](https://github.com/mozilla/send/pull/412) throw key errors before download begins (@dannycoates)
|
||||
- [#404](https://github.com/mozilla/send/pull/404) Use async function instead of promise (#325) (@weihanglo)
|
||||
- [#406](https://github.com/mozilla/send/pull/406) Add noscript tag (@pdehaan)
|
||||
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||
|
||||
### v1.0.3 (2017/08/02 23:59 +00:00)
|
||||
- [#402](https://github.com/mozilla/send/pull/402) filter the hash from error reports (@dannycoates)
|
||||
- [#400](https://github.com/mozilla/send/pull/400) fix link that breaks download by opening in new tab (@johngruen)
|
||||
- [#369](https://github.com/mozilla/send/pull/369) Add ESLint no-alert shame rule (@pdehaan)
|
||||
- [#396](https://github.com/mozilla/send/pull/396) add babel-polyfill (@dannycoates)
|
||||
- [#394](https://github.com/mozilla/send/pull/394) catch JSON.parse errors of storage metadata (@dannycoates)
|
||||
- [#367](https://github.com/mozilla/send/pull/367) Generate production locales using 'compare-locales' (@pdehaan)
|
||||
- [#392](https://github.com/mozilla/send/pull/392) Adjust hover behavior on send-logo (#382)
|
||||
Fixes: #382. (@weihanglo)
|
||||
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||
- [#380](https://github.com/mozilla/send/pull/380) Add Pontoon URL to README (@pdehaan)
|
||||
|
||||
### v1.0.2 (2017/07/31 18:58 +00:00)
|
||||
- [#365](https://github.com/mozilla/send/pull/365) revert the IE fix to fix footer on chrome (@dannycoates)
|
||||
|
||||
### v1.0.1 (2017/07/31 17:28 +00:00)
|
||||
- [#353](https://github.com/mozilla/send/pull/353) redirect ie to /unsupported (@abhinadduri, @dannycoates)
|
||||
- [#360](https://github.com/mozilla/send/pull/360) Fix some linting nits (@pdehaan)
|
||||
- [#362](https://github.com/mozilla/send/pull/362) Adjusts category of unsupported event (fixes #350). (@chuckharmston)
|
||||
- [#355](https://github.com/mozilla/send/pull/355) Make order of uploaded files in list consistent (@pdehaan)
|
||||
- [#356](https://github.com/mozilla/send/pull/356) Get rid of console.log statements (@pdehaan)
|
||||
- [#358](https://github.com/mozilla/send/pull/358) Fix some missing .title attributes in dev-only locales (@pdehaan)
|
||||
- [#354](https://github.com/mozilla/send/pull/354) Remove /en-US/ from cookies link in footer (@pdehaan)
|
||||
- [#339](https://github.com/mozilla/send/pull/339) Show error page on firefox v49 and below (@ericawright, @abhinadduri)
|
||||
- [#346](https://github.com/mozilla/send/pull/346) Add docs/CODEOWNERS file (@pdehaan)
|
||||
- [#345](https://github.com/mozilla/send/pull/345) wrap long file names (@dnarcese)
|
||||
- [#344](https://github.com/mozilla/send/pull/344) don't wrap file list headers (@dnarcese)
|
||||
- [#327](https://github.com/mozilla/send/pull/327) Modify popup delete dialog (@youwenliang)
|
||||
- [#341](https://github.com/mozilla/send/pull/341) center percentage text on all browser versions (@dnarcese)
|
||||
- [#340](https://github.com/mozilla/send/pull/340) Remove duplicate entities in localized FTL files (@flodolo)
|
||||
- [#337](https://github.com/mozilla/send/pull/337) support v 50 and 51 by not allowing const in loops (@ericawright)
|
||||
- [#338](https://github.com/mozilla/send/pull/338) Remove duplicated strings in en-US, fix nn-NO file (@flodolo)
|
||||
- [#336](https://github.com/mozilla/send/pull/336) German(de): Fixed missing value for deleteFileButton (#336) (@flodolo)
|
||||
- [#334](https://github.com/mozilla/send/pull/334) fix functionality on firefox 50 and 51 (@dnarcese)
|
||||
|
||||
### v1.0.0 (2017/07/26 19:08 +00:00)
|
||||
- [#323](https://github.com/mozilla/send/pull/323) disable upload/download notifications (@dannycoates)
|
||||
- [#322](https://github.com/mozilla/send/pull/322) fix feedback button jump (@dnarcese)
|
||||
- [#320](https://github.com/mozilla/send/pull/320) fix German footer (@dnarcese)
|
||||
|
||||
### v0.2.2 (2017/07/26 04:50 +00:00)
|
||||
- [#314](https://github.com/mozilla/send/pull/314) added L10N_DEV environment variable for making all languages available (@dannycoates)
|
||||
- [#313](https://github.com/mozilla/send/pull/313) removing timeout limit for front end tests (@abhinadduri)
|
||||
- [#311](https://github.com/mozilla/send/pull/311) expired ids should reject instead of returning null (@dannycoates)
|
||||
- [#302](https://github.com/mozilla/send/pull/302) UX Refine WIP (@youwenliang)
|
||||
- [#310](https://github.com/mozilla/send/pull/310) if the download card is pressed, the expired card shows up properly (@abhinadduri)
|
||||
- [#269](https://github.com/mozilla/send/pull/269) refactored ftl file (@abhinadduri)
|
||||
- [#291](https://github.com/mozilla/send/pull/291) added legal page (@dannycoates)
|
||||
- [#307](https://github.com/mozilla/send/pull/307) don't show error page on upload cancel (@dnarcese)
|
||||
- [#299](https://github.com/mozilla/send/pull/299) use CIRCLE_TAG as version.json version if present (@dannycoates)
|
||||
|
||||
### v0.2.1 (2017/07/24 23:34 +00:00)
|
||||
- [#296](https://github.com/mozilla/send/pull/296) restyle delete popup (@dnarcese)
|
||||
- [#295](https://github.com/mozilla/send/pull/295) renamed environment variables to remove P2P_ prefix (@dannycoates)
|
||||
- [#294](https://github.com/mozilla/send/pull/294) dealing with invalid drag and drops (@abhinadduri)
|
||||
- [#297](https://github.com/mozilla/send/pull/297) added environment variable for expire time (@dannycoates)
|
||||
- [#292](https://github.com/mozilla/send/pull/292) Fixes289 (@abhinadduri)
|
||||
- [#288](https://github.com/mozilla/send/pull/288) fix: Don`t allow upload when not on the upload page. (@ericawright)
|
||||
- [#285](https://github.com/mozilla/send/pull/285) added messages for processing phases (@dannycoates)
|
||||
- [#267](https://github.com/mozilla/send/pull/267) make site responsive and add feedback link (@johngruen)
|
||||
- [#286](https://github.com/mozilla/send/pull/286) Update download progress bar color (@pdehaan)
|
||||
- [#281](https://github.com/mozilla/send/pull/281) Stop ESLint from linting the /public/ directory (@pdehaan)
|
||||
- [#280](https://github.com/mozilla/send/pull/280) created /unsupported page and added gcmCompliant to /download page (@dannycoates)
|
||||
- [#279](https://github.com/mozilla/send/pull/279) create separate js bundles for upload/download pages (@dannycoates)
|
||||
- [#268](https://github.com/mozilla/send/pull/268) Testpilot ga (@abhinadduri)
|
||||
|
||||
### v0.2.0 (2017/07/21 19:27 +00:00)
|
||||
- [#266](https://github.com/mozilla/send/pull/266) abort uploads over maxfilesize (@dannycoates)
|
||||
- [#264](https://github.com/mozilla/send/pull/264) Remove duplicate custom metric. (@chuckharmston)
|
||||
- [#259](https://github.com/mozilla/send/pull/259) add alert when uploading multiple files (@dnarcese)
|
||||
- [#262](https://github.com/mozilla/send/pull/262) sync download progress bar with percentage (@dnarcese)
|
||||
- [#258](https://github.com/mozilla/send/pull/258) better sync percent with progress bar (@dnarcese)
|
||||
- [#257](https://github.com/mozilla/send/pull/257) add a dynamic js script for page config (@dannycoates)
|
||||
- [#256](https://github.com/mozilla/send/pull/256) add file size limit message (@dnarcese)
|
||||
- [#253](https://github.com/mozilla/send/pull/253) Add favicon.ico version of the Send logo (@pdehaan)
|
||||
- [#254](https://github.com/mozilla/send/pull/254) Add nsp check to circle ci (@pdehaan)
|
||||
- [#245](https://github.com/mozilla/send/pull/245) Localization (@abhinadduri)
|
||||
- [#252](https://github.com/mozilla/send/pull/252) only allow drag and drop on upload page (@dnarcese)
|
||||
- [#250](https://github.com/mozilla/send/pull/250) make footer not overlap (@dnarcese)
|
||||
- [#251](https://github.com/mozilla/send/pull/251) minify all images (@ericawright)
|
||||
- [#249](https://github.com/mozilla/send/pull/249) change how the file upload box expands (@dnarcese)
|
||||
- [#246](https://github.com/mozilla/send/pull/246) remove P2P references. Fixes #224 (@clouserw)
|
||||
- [#242](https://github.com/mozilla/send/pull/242) Make only icons clickable in file list (@dnarcese)
|
||||
- [#236](https://github.com/mozilla/send/pull/236) add FAQ. Fixes #186 (@clouserw)
|
||||
- [#235](https://github.com/mozilla/send/pull/235) allow send another file link to open in new tab (@dnarcese)
|
||||
- [#234](https://github.com/mozilla/send/pull/234) fix download svg (@dnarcese)
|
||||
- [#232](https://github.com/mozilla/send/pull/232) escape filename in the ui (@dannycoates)
|
||||
- [#226](https://github.com/mozilla/send/pull/226) added functionality to cancel uploads (@abhinadduri)
|
||||
- [#231](https://github.com/mozilla/send/pull/231) move head and html tags to main template (@dnarcese)
|
||||
- [#228](https://github.com/mozilla/send/pull/228) add send logo (@dnarcese)
|
||||
- [#229](https://github.com/mozilla/send/pull/229) change learn more and github links (@dnarcese)
|
||||
- [#201](https://github.com/mozilla/send/pull/201) Adds metrics documentation (closes #5). (@chuckharmston)
|
||||
- [#223](https://github.com/mozilla/send/pull/223) change size of send another file links (@dnarcese)
|
||||
- [#222](https://github.com/mozilla/send/pull/222) add footer (@dnarcese)
|
||||
- [#197](https://github.com/mozilla/send/pull/197) fixes issues 195 and 192 (@abhinadduri)
|
||||
- [#204](https://github.com/mozilla/send/pull/204) added HSTS header (@dannycoates)
|
||||
- [#193](https://github.com/mozilla/send/pull/193) Frontend tests (@abhinadduri)
|
||||
- [#191](https://github.com/mozilla/send/pull/191) New ui! (@dnarcese)
|
||||
|
||||
### v0.1.4 (2017/07/12 18:21 +00:00)
|
||||
- [#189](https://github.com/mozilla/send/pull/189) Add CSP directives (@dannycoates)
|
||||
- [#188](https://github.com/mozilla/send/pull/188) fixes delete button error (@abhinadduri)
|
||||
- [#185](https://github.com/mozilla/send/pull/185) added loading, hashing, and encrypting events for uploader; decryptin… (@abhinadduri)
|
||||
- [#183](https://github.com/mozilla/send/pull/183) rename to 'Send' (@dannycoates)
|
||||
- [#184](https://github.com/mozilla/send/pull/184) Server tests (@abhinadduri)
|
||||
- [#178](https://github.com/mozilla/send/pull/178) fixed issues in branch title (@abhinadduri)
|
||||
- [#177](https://github.com/mozilla/send/pull/177) Gcm compliance (@abhinadduri)
|
||||
- [#106](https://github.com/mozilla/send/pull/106) Gcm (@abhinadduri, @dannycoates)
|
||||
- [#168](https://github.com/mozilla/send/pull/168) Show error page if upload fails (@dnarcese)
|
||||
- [#148](https://github.com/mozilla/send/pull/148) WIP: Add basic contribute.json (@pdehaan)
|
||||
- [#162](https://github.com/mozilla/send/pull/162) Fix dev server URL in README.md file (@pdehaan)
|
||||
- [#167](https://github.com/mozilla/send/pull/167) build docker image with new name (@relud)
|
||||
- [#164](https://github.com/mozilla/send/pull/164) Add word wraps to table (@dnarcese)
|
||||
- [#149](https://github.com/mozilla/send/pull/149) Add robots.txt (@pdehaan)
|
||||
- [#161](https://github.com/mozilla/send/pull/161) Hide table header on empty list (@dnarcese)
|
||||
- [#154](https://github.com/mozilla/send/pull/154) Remove expired uploads (@dnarcese)
|
||||
- [#146](https://github.com/mozilla/send/pull/146) Update README with some more details (@pdehaan)
|
||||
|
||||
### v0.1.2 (2017/06/24 03:38 +00:00)
|
||||
- [#138](https://github.com/mozilla/send/pull/138) remove notLocalHost (@dannycoates)
|
||||
|
||||
### v0.1.0 (2017/06/24 01:24 +00:00)
|
||||
- [#137](https://github.com/mozilla/send/pull/137) refactored docker build (@dannycoates)
|
||||
- [#132](https://github.com/mozilla/send/pull/132) Add /__version__ route (@pdehaan)
|
||||
- [#135](https://github.com/mozilla/send/pull/135) make dockerfile more dockerflowy (@dannycoates)
|
||||
- [#134](https://github.com/mozilla/send/pull/134) Load previous uploads (@dannycoates, @dnarcese)
|
||||
- [#131](https://github.com/mozilla/send/pull/131) added __heartbeat__ (@dannycoates)
|
||||
- [#133](https://github.com/mozilla/send/pull/133) Add LICENSE file (@pdehaan)
|
||||
- [#130](https://github.com/mozilla/send/pull/130) added sentry to server code (@abhinadduri)
|
||||
- [#124](https://github.com/mozilla/send/pull/124) Remove unused [dev]dependencies (@pdehaan)
|
||||
- [#119](https://github.com/mozilla/send/pull/119) Move cross-env to a dep (@pdehaan)
|
||||
- [#123](https://github.com/mozilla/send/pull/123) removed bitly integration (@abhinadduri)
|
||||
- [#122](https://github.com/mozilla/send/pull/122) fix docker build (@dannycoates)
|
||||
- [#121](https://github.com/mozilla/send/pull/121) added docker service to circle.yml (@dannycoates)
|
||||
- [#120](https://github.com/mozilla/send/pull/120) added sentry (@abhinadduri)
|
||||
- [#118](https://github.com/mozilla/send/pull/118) change docker image name and add builds for tags (@relud)
|
||||
- [#116](https://github.com/mozilla/send/pull/116) add /__lbheartbeat__ endpoint (@relud)
|
||||
- [#79](https://github.com/mozilla/send/pull/79) Optimize/minimize bundle.js for production (@pdehaan)
|
||||
- [#104](https://github.com/mozilla/send/pull/104) Fix a bunch of ESLint and HTMLLint errors (@pdehaan)
|
||||
- [#105](https://github.com/mozilla/send/pull/105) Progress bars (@dnarcese)
|
||||
- [#111](https://github.com/mozilla/send/pull/111) added in anonmyized ip google analytics (@abhinadduri)
|
||||
- [#110](https://github.com/mozilla/send/pull/110) added notifications (@abhinadduri)
|
||||
- [#103](https://github.com/mozilla/send/pull/103) added Dockerfile (@dannycoates)
|
||||
- [#100](https://github.com/mozilla/send/pull/100) Added Helmet Middleware (@abhinadduri)
|
||||
- [#99](https://github.com/mozilla/send/pull/99) Testing (@abhinadduri)
|
||||
- [#77](https://github.com/mozilla/send/pull/77) Fix the linter errors (@pdehaan)
|
||||
- [#54](https://github.com/mozilla/send/pull/54) Adding basic ESLint config (@pdehaan)
|
||||
- [#71](https://github.com/mozilla/send/pull/71) Drag & drop (@dnarcese)
|
||||
- [#72](https://github.com/mozilla/send/pull/72) Logging (@abhinadduri, @dannycoates)
|
||||
- [#45](https://github.com/mozilla/send/pull/45) S3 integration (@abhinadduri, @dannycoates)
|
||||
- [#46](https://github.com/mozilla/send/pull/46) Download page and share link UI (@dnarcese)
|
||||
- [#41](https://github.com/mozilla/send/pull/41) Added upload page and file list UI (@dnarcese)
|
||||
- [#40](https://github.com/mozilla/send/pull/40) Tweak the package.json file (@pdehaan)
|
||||
- [#43](https://github.com/mozilla/send/pull/43) added return (@abhinadduri)
|
||||
- [#42](https://github.com/mozilla/send/pull/42) changed to handle 404 during download, also removing progress listene… (@abhinadduri)
|
||||
- [#39](https://github.com/mozilla/send/pull/39) Refactor riff (@abhinadduri, @dannycoates)
|
||||
- [#36](https://github.com/mozilla/send/pull/36) added prettier for js formatting (@dannycoates)
|
||||
- [#28](https://github.com/mozilla/send/pull/28) Added a UI for the uploader end, made stylistic changes, implemented deleting (@abhinadduri)
|
||||
- [#25](https://github.com/mozilla/send/pull/25) Changed naming for some pages, no longer stores files by name on server (@abhinadduri)
|
||||
92
CONTRIBUTORS
Normal file
@@ -0,0 +1,92 @@
|
||||
Abhinav Adduri
|
||||
Alexander Slovesnik
|
||||
Amin Mahmudian
|
||||
Andreas Pettersson
|
||||
Arash Mousavi
|
||||
Balázs Meskó
|
||||
Belayet Hossain
|
||||
Bjørn I
|
||||
Boopesh Mahendran
|
||||
Chuck Harmston
|
||||
Cláudio Esperança
|
||||
Cynthia Pereira
|
||||
Daniel Thorn
|
||||
Daniela Arcese
|
||||
Danny Coates
|
||||
Emin Mastizada
|
||||
Enol
|
||||
Erica
|
||||
Erica Wright
|
||||
Fjoerfoks
|
||||
Francesco Lodolo
|
||||
Francesco Lodolo [:flod]
|
||||
Gautam krishna.R
|
||||
Håvar Henriksen
|
||||
Jae Hyeon Park
|
||||
Jakub Rychlý
|
||||
Jamie
|
||||
Jim Spentzos
|
||||
Johann-S
|
||||
John Gruen
|
||||
Jon Vadillo
|
||||
Jordi Serratosa
|
||||
Juraj Cigáň
|
||||
Kohei Yoshino
|
||||
Lan Glad
|
||||
Luna Jernberg
|
||||
Marcelo Poli
|
||||
Marco Aurélio
|
||||
Mark Liang
|
||||
Matjaž Horvat
|
||||
Maykon Chagas
|
||||
Michael Köhler
|
||||
Michael Wolf
|
||||
Michal Stanke
|
||||
Michal Vašíček
|
||||
Moḥend Belqasem
|
||||
Nicholas Skinsacos
|
||||
Peter deHaan
|
||||
Pierre Neter
|
||||
Pin-guang Chen
|
||||
Rhoslyn Prys
|
||||
Rizky Ariestiyansyah
|
||||
Roberto Alvarado
|
||||
Rodrigo
|
||||
Rok Žerdin
|
||||
Sahithi
|
||||
Sairam Raavi
|
||||
Sandro
|
||||
Schieck :)
|
||||
Selim Şumlu
|
||||
Slimane Amiri
|
||||
Théo Chevalier
|
||||
Tomáš Zelina
|
||||
Ton
|
||||
Tymur Faradzhev
|
||||
Varghese Thomas
|
||||
Victor Bychek
|
||||
Weihang Lo
|
||||
Wil Clouser
|
||||
YFdyh000
|
||||
You-Wen Liang (Mark)
|
||||
alex_mayorga
|
||||
ariestiyansyah
|
||||
avelper
|
||||
dgadelha
|
||||
ehuggett
|
||||
eljuno
|
||||
erdem cobanoglu
|
||||
gautamkrishnar
|
||||
goofy
|
||||
hi
|
||||
jesferman1993
|
||||
josotrix
|
||||
kenrick95
|
||||
manxmensch
|
||||
ravmn
|
||||
reza.habibi2008
|
||||
siparon
|
||||
skystar-p
|
||||
xcffl
|
||||
Μιχάλης
|
||||
Марко Костић (Marko Kostić)
|
||||
@@ -12,4 +12,4 @@ RUN npm install --production && npm cache clean --force
|
||||
ENV PORT=1443
|
||||
EXPOSE $PORT
|
||||
|
||||
CMD ["npm", "start"]
|
||||
CMD ["npm", "run", "prod"]
|
||||
|
||||
61
README.md
@@ -5,39 +5,80 @@
|
||||
|
||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [What it does](#what-it-does)
|
||||
* [Requirements](#requirements)
|
||||
* [Development](#development)
|
||||
* [Commands](#commands)
|
||||
* [Configuration](#configuration)
|
||||
* [Localization](#localization)
|
||||
* [Contributing](#contributing)
|
||||
* [Testing](#testing)
|
||||
* [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
A file sharing experiment which allows you to send encrypted files to other users.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Node.js 8+](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/)
|
||||
- [Node.js 8.2+](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/) (optional for development)
|
||||
- [AWS S3](https://aws.amazon.com/s3/) or compatible service. (optional)
|
||||
|
||||
**NOTE:** To run the project, make sure you have a Redis server running locally:
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
To start an ephemeral development server run:
|
||||
|
||||
```sh
|
||||
$ redis-server /usr/local/etc/redis.conf
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## How to use it
|
||||
Then browse to http://localhost:8080
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|------------------|-------------|
|
||||
| `npm run dev` | Builds and starts the web server locally for development.
|
||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||
| `npm start` | Starts the Express web server.
|
||||
| `npm test` | Runs the suite of mocha tests.
|
||||
| `npm start` | Runs the server in development configuration.
|
||||
| `npm run build` | Builds the production assets.
|
||||
| `npm run prod` | Runs the server in production configuration.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
The server is configured with environment variables. See [server/config.js](server/config.js) for all options and [docs/docker.md](docs/docker.md) for examples.
|
||||
|
||||
---
|
||||
|
||||
## Localization
|
||||
|
||||
_Coming soon_ (see [#57](https://github.com/mozilla/send/issues/57))
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
| ENVIRONMENT | URL
|
||||
@@ -46,6 +87,10 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
|
||||
| Stage | <https://send.stage.mozaws.net/>
|
||||
| Development | <https://send.dev.mozaws.net/>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License Version 2.0](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
9
app/.eslintrc.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
env:
|
||||
browser: true
|
||||
node: true
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
|
||||
rules:
|
||||
node/no-unsupported-features: off
|
||||
24
app/dragManager.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function(state, emitter) {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.body.addEventListener('dragover', event => {
|
||||
if (state.route === '/') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('drop', event => {
|
||||
if (state.route === '/' && !state.transfer) {
|
||||
event.preventDefault();
|
||||
document.querySelector('.upload-window').classList.remove('ondrag');
|
||||
const target = event.dataTransfer;
|
||||
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' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
69
app/experiments.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import hash from 'string-hash';
|
||||
|
||||
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
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function luckyNumber(str) {
|
||||
return hash(str) / 0xffffffff;
|
||||
}
|
||||
|
||||
function checkExperiments(state, emitter) {
|
||||
const all = Object.keys(experiments);
|
||||
const id = all.find(id => experiments[id].eligible(state));
|
||||
if (id) {
|
||||
const variant = experiments[id].variant(state);
|
||||
state.storage.enroll(id, variant);
|
||||
experiments[id].run(variant, state, emitter);
|
||||
}
|
||||
}
|
||||
|
||||
export default function initialize(state, emitter) {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
const xp = experiments[state.query.x];
|
||||
if (xp) {
|
||||
xp.run(+state.query.v, state, emitter);
|
||||
}
|
||||
});
|
||||
|
||||
if (!state.storage.get('testpilot_ga__cid')) {
|
||||
// first ever visit. check again after cid is assigned.
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
checkExperiments(state, emitter);
|
||||
});
|
||||
} else {
|
||||
const enrolled = state.storage.enrolled.filter(([id, variant]) => {
|
||||
const xp = experiments[id];
|
||||
if (xp) {
|
||||
xp.run(variant, state, emitter);
|
||||
}
|
||||
return !!xp;
|
||||
});
|
||||
// single experiment per session for now
|
||||
if (enrolled.length === 0) {
|
||||
checkExperiments(state, emitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
256
app/fileManager.js
Normal file
@@ -0,0 +1,256 @@
|
||||
/* 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);
|
||||
}
|
||||
271
app/fileReceiver.js
Normal file
@@ -0,0 +1,271 @@
|
||||
import Nanobus from 'nanobus';
|
||||
import { arrayToB64, b64ToArray, bytes } from './utils';
|
||||
|
||||
export default class FileReceiver extends Nanobus {
|
||||
constructor(url, file) {
|
||||
super('FileReceiver');
|
||||
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
b64ToArray(file.key),
|
||||
'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 => {
|
||||
const encoder = new TextEncoder();
|
||||
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() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
|
||||
get sizes() {
|
||||
return {
|
||||
partialSize: bytes(this.progress[0]),
|
||||
totalSize: bytes(this.progress[1])
|
||||
};
|
||||
}
|
||||
|
||||
cancel() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
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) {
|
||||
const authHeader = await this.getAuthHeader(nonce);
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.progress = [event.loaded, event.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = event => {
|
||||
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) {
|
||||
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.emit('progress', this.progress);
|
||||
try {
|
||||
const encryptKey = await this.encryptKeyPromise;
|
||||
let ciphertext = null;
|
||||
try {
|
||||
ciphertext = await this.downloadFile(nonce);
|
||||
} catch (e) {
|
||||
if (e.message === '401' && nonce !== e.nonce) {
|
||||
ciphertext = await this.downloadFile(e.nonce);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.msg = 'decryptingFile';
|
||||
this.emit('decrypting');
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: b64ToArray(this.file.iv),
|
||||
tagLength: 128
|
||||
},
|
||||
encryptKey,
|
||||
ciphertext
|
||||
);
|
||||
this.msg = 'downloadFinish';
|
||||
this.state = 'complete';
|
||||
return {
|
||||
plaintext,
|
||||
name: decodeURIComponent(this.file.name),
|
||||
type: this.file.type
|
||||
};
|
||||
} catch (e) {
|
||||
this.state = 'invalid';
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
322
app/fileSender.js
Normal file
@@ -0,0 +1,322 @@
|
||||
import Nanobus from 'nanobus';
|
||||
import { arrayToB64, b64ToArray, bytes } from './utils';
|
||||
|
||||
async function getAuthHeader(authKey, nonce) {
|
||||
const sig = await window.crypto.subtle.sign(
|
||||
{
|
||||
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 {
|
||||
constructor(file) {
|
||||
super('FileSender');
|
||||
this.file = file;
|
||||
this.msg = 'importingFile';
|
||||
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() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
|
||||
get sizes() {
|
||||
return {
|
||||
partialSize: bytes(this.progress[0]),
|
||||
totalSize: bytes(this.progress[1])
|
||||
};
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
if (this.msg === 'fileSizeProgress') {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
}
|
||||
|
||||
readFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
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) {
|
||||
throw new Error(0);
|
||||
}
|
||||
this.msg = 'encryptingFile';
|
||||
this.emit('encrypting');
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
tagLength: 128
|
||||
},
|
||||
encryptKey,
|
||||
plaintext
|
||||
);
|
||||
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) {
|
||||
throw new Error(0);
|
||||
}
|
||||
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
|
||||
}
|
||||
|
||||
static async setPassword(password, file) {
|
||||
const encoder = new TextEncoder();
|
||||
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 {
|
||||
await sendPassword(file, authKey, rawAuth);
|
||||
} catch (e) {
|
||||
if (e.message === '401' && file.nonce !== e.nonce) {
|
||||
await sendPassword(file, authKey, rawAuth);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/main.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'fluent-intl-polyfill';
|
||||
import app from './routes';
|
||||
import locale from '../common/locales';
|
||||
import fileManager from './fileManager';
|
||||
import dragManager from './dragManager';
|
||||
import { canHasSend } from './utils';
|
||||
import assets from '../common/assets';
|
||||
import storage from './storage';
|
||||
import metrics from './metrics';
|
||||
import experiments from './experiments';
|
||||
import Raven from 'raven-js';
|
||||
|
||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||
}
|
||||
|
||||
app.use((state, emitter) => {
|
||||
// init state
|
||||
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);
|
||||
app.use(fileManager);
|
||||
app.use(dragManager);
|
||||
app.use(experiments);
|
||||
|
||||
app.mount('body');
|
||||
294
app/metrics.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||
import storage from './storage';
|
||||
|
||||
let hasLocalStorage = false;
|
||||
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 experiment = null;
|
||||
|
||||
export default function initialize(state, emitter) {
|
||||
appState = state;
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
// addExitHandlers();
|
||||
experiment = storage.enrolled[0];
|
||||
sendEvent(category(), 'visit', {
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
//TODO restart handlers... somewhere
|
||||
});
|
||||
emitter.on('exit', evt => {
|
||||
exitEvent(evt);
|
||||
});
|
||||
}
|
||||
|
||||
function category() {
|
||||
switch (appState.route) {
|
||||
case '/':
|
||||
case '/share/:id':
|
||||
return 'sender';
|
||||
case '/download/:id/:key':
|
||||
case '/download/:id':
|
||||
case '/completed':
|
||||
return 'recipient';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
function sendEvent() {
|
||||
const args = Array.from(arguments);
|
||||
if (experiment && args[2]) {
|
||||
args[2].xid = experiment[0];
|
||||
args[2].xvar = experiment[1];
|
||||
}
|
||||
return (
|
||||
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||
);
|
||||
}
|
||||
|
||||
function urlToMetric(url) {
|
||||
switch (url) {
|
||||
case 'https://www.mozilla.org/':
|
||||
return 'mozilla';
|
||||
case 'https://www.mozilla.org/about/legal':
|
||||
return 'legal';
|
||||
case 'https://testpilot.firefox.com/about':
|
||||
return 'about';
|
||||
case 'https://testpilot.firefox.com/privacy':
|
||||
return 'privacy';
|
||||
case 'https://testpilot.firefox.com/terms':
|
||||
return 'terms';
|
||||
case 'https://www.mozilla.org/privacy/websites/#cookies':
|
||||
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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function externalReferrer() {
|
||||
if (/^https:\/\/testpilot\.firefox\.com/.test(document.referrer)) {
|
||||
return 'testpilot';
|
||||
}
|
||||
return 'external';
|
||||
}
|
||||
|
||||
function takeReferrer() {
|
||||
const referrer = storage.referrer || externalReferrer();
|
||||
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) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
}
|
||||
|
||||
function completedUpload(params) {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
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) {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
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) {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'completed'
|
||||
});
|
||||
}
|
||||
|
||||
function deletedUpload(params) {
|
||||
return sendEvent(category(), 'upload-deleted', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm4: params.ttl,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd4: params.location
|
||||
});
|
||||
}
|
||||
|
||||
function unsupported(params) {
|
||||
return sendEvent(category(), 'unsupported', {
|
||||
cd6: params.err
|
||||
});
|
||||
}
|
||||
|
||||
function copiedLink(params) {
|
||||
return sendEvent('sender', 'copied', {
|
||||
cd4: params.location
|
||||
});
|
||||
}
|
||||
|
||||
function exitEvent(target) {
|
||||
return sendEvent(category(), 'exited', {
|
||||
cd3: urlToMetric(target.currentTarget.href)
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
copiedLink,
|
||||
startedUpload,
|
||||
cancelledUpload,
|
||||
stoppedUpload,
|
||||
completedUpload,
|
||||
changedDownloadLimit,
|
||||
deletedUpload,
|
||||
startedDownload,
|
||||
cancelledDownload,
|
||||
stoppedDownload,
|
||||
completedDownload,
|
||||
addedPassword,
|
||||
restart,
|
||||
unsupported
|
||||
};
|
||||
12
app/routes/download.js
Normal file
@@ -0,0 +1,12 @@
|
||||
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);
|
||||
};
|
||||
10
app/routes/home.js
Normal file
@@ -0,0 +1,10 @@
|
||||
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);
|
||||
};
|
||||
43
app/routes/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
119
app/storage.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { isFile } from './utils';
|
||||
|
||||
class Mem {
|
||||
constructor() {
|
||||
this.items = new Map();
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.items.get(key);
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
return this.items.set(key, value);
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
return this.items.delete(key);
|
||||
}
|
||||
|
||||
key(i) {
|
||||
return this.items.keys()[i];
|
||||
}
|
||||
}
|
||||
|
||||
class Storage {
|
||||
constructor() {
|
||||
try {
|
||||
this.engine = localStorage || new Mem();
|
||||
} catch (e) {
|
||||
this.engine = new Mem();
|
||||
}
|
||||
this._files = this.loadFiles();
|
||||
}
|
||||
|
||||
loadFiles() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
try {
|
||||
const f = JSON.parse(this.engine.getItem(k));
|
||||
if (!f.id) {
|
||||
f.id = f.fileId;
|
||||
}
|
||||
fs.push(f);
|
||||
} catch (err) {
|
||||
// obviously you're not a golfer
|
||||
this.engine.removeItem(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
||||
}
|
||||
|
||||
get totalDownloads() {
|
||||
return Number(this.engine.getItem('totalDownloads'));
|
||||
}
|
||||
set totalDownloads(n) {
|
||||
this.engine.setItem('totalDownloads', n);
|
||||
}
|
||||
get totalUploads() {
|
||||
return Number(this.engine.getItem('totalUploads'));
|
||||
}
|
||||
set totalUploads(n) {
|
||||
this.engine.setItem('totalUploads', n);
|
||||
}
|
||||
get referrer() {
|
||||
return this.engine.getItem('referrer');
|
||||
}
|
||||
set referrer(str) {
|
||||
this.engine.setItem('referrer', str);
|
||||
}
|
||||
get enrolled() {
|
||||
return JSON.parse(this.engine.getItem('experiments') || '[]');
|
||||
}
|
||||
|
||||
enroll(id, variant) {
|
||||
const enrolled = this.enrolled;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
if (!enrolled.find(([i, v]) => i === id)) {
|
||||
enrolled.push([id, variant]);
|
||||
this.engine.setItem('experiments', JSON.stringify(enrolled));
|
||||
}
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this._files;
|
||||
}
|
||||
|
||||
getFileById(id) {
|
||||
return this._files.find(f => f.id === id);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.engine.getItem(id);
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
if (isFile(property)) {
|
||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||
}
|
||||
this.engine.removeItem(property);
|
||||
}
|
||||
|
||||
addFile(file) {
|
||||
this._files.push(file);
|
||||
this.engine.setItem(file.id, JSON.stringify(file));
|
||||
}
|
||||
|
||||
writeFiles() {
|
||||
this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
|
||||
}
|
||||
}
|
||||
|
||||
export default new Storage();
|
||||
6
app/templates/blank.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function() {
|
||||
const div = html`<div id="page-one"></div>`;
|
||||
return div;
|
||||
};
|
||||
33
app/templates/completed.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
};
|
||||
30
app/templates/download.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
};
|
||||
56
app/templates/downloadPassword.js
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
};
|
||||
10
app/templates/error.js
Normal file
@@ -0,0 +1,10 @@
|
||||
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>`;
|
||||
};
|
||||
86
app/templates/file.js
Normal file
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
};
|
||||
32
app/templates/fileList.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
`;
|
||||
};
|
||||
34
app/templates/footer.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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>`;
|
||||
};
|
||||
46
app/templates/fxPromo.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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}>
|
||||
*/
|
||||
33
app/templates/header.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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>`;
|
||||
};
|
||||
34
app/templates/legal.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
};
|
||||
21
app/templates/notFound.js
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
};
|
||||
72
app/templates/preview.js
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
};
|
||||
29
app/templates/progress.js
Normal file
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
};
|
||||
56
app/templates/selectbox.js
Normal file
@@ -0,0 +1,56 @@
|
||||
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>`;
|
||||
};
|
||||
109
app/templates/share.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const uploadPassword = require('./uploadPassword');
|
||||
const selectbox = require('./selectbox');
|
||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||
|
||||
function passwordComplete(state, password) {
|
||||
const el = html([
|
||||
`<div class="selectPassword">${state.translate('passwordResult', {
|
||||
password: '<pre></pre>'
|
||||
})}</div>`
|
||||
]);
|
||||
el.lastElementChild.textContent = password;
|
||||
return el;
|
||||
}
|
||||
|
||||
function expireInfo(file, translate, emit) {
|
||||
const el = html([
|
||||
`<div>${translate('expireInfo', {
|
||||
downloadCount: '<select></select>',
|
||||
timespan: translate('timespanHours', { num: 24 })
|
||||
})}</div>`
|
||||
]);
|
||||
const select = el.querySelector('select');
|
||||
const options = [1, 2, 3, 4, 5, 20];
|
||||
const t = num => translate('downloadCount', { num });
|
||||
const changed = value => emit('changeLimit', { file, value });
|
||||
select.parentNode.replaceChild(
|
||||
selectbox(file.dlimit || 1, options, t, changed),
|
||||
select
|
||||
);
|
||||
return el;
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
if (!file) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
|
||||
file.password = file.password || '';
|
||||
|
||||
const passwordSection = file.password
|
||||
? passwordComplete(state, file.password)
|
||||
: uploadPassword(state, emit);
|
||||
const div = html`
|
||||
<div id="share-link" class="fadeIn">
|
||||
<div class="title">${expireInfo(file, state.translate, emit)}</div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text">
|
||||
${state.translate('copyUrlFormLabelWithName', {
|
||||
filename: file.name
|
||||
})}</div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||
<button id="copy-btn"
|
||||
class="btn"
|
||||
title="${state.translate('copyUrlFormButton')}"
|
||||
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||
</div>
|
||||
${passwordSection}
|
||||
<button id="delete-file"
|
||||
class="btn"
|
||||
title="${state.translate('deleteFileButton')}"
|
||||
onclick=${deleteFile}>${state.translate('deleteFileButton')}</button>
|
||||
<a class="send-new"
|
||||
data-state="completed"
|
||||
href="/"
|
||||
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
if (allowedCopy()) {
|
||||
emit('copy', { url: file.url, location: 'success-screen' });
|
||||
const input = document.getElementById('link');
|
||||
input.disabled = true;
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
copyBtn.disabled = true;
|
||||
copyBtn.classList.add('success');
|
||||
copyBtn.replaceChild(
|
||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||
copyBtn.firstChild
|
||||
);
|
||||
await delay(2000);
|
||||
input.disabled = false;
|
||||
if (!copyBtn.parentNode.classList.contains('wait-password')) {
|
||||
copyBtn.disabled = false;
|
||||
}
|
||||
copyBtn.classList.remove('success');
|
||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
emit('delete', { file, location: 'success-screen' });
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
46
app/templates/unsupported.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const msg =
|
||||
state.params.reason === 'outdated'
|
||||
? html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate(
|
||||
'notSupportedOutdatedDetail'
|
||||
)}</div>
|
||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">${state.translate(
|
||||
'updateFirefox'
|
||||
)}</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`
|
||||
: html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
||||
'notSupportedLink'
|
||||
)}</a></div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">Firefox<br>
|
||||
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`;
|
||||
const div = html`<div id="page-one">${msg}</div>`;
|
||||
return div;
|
||||
};
|
||||
38
app/templates/upload.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const transfer = state.transfer;
|
||||
|
||||
const div = html`
|
||||
<div id="upload-progress" class="fadeIn">
|
||||
<div class="title" id="upload-filename">${state.translate(
|
||||
'uploadingPageProgress',
|
||||
{
|
||||
filename: transfer.file.name,
|
||||
size: bytes(transfer.file.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
<button id="cancel-upload" title="${state.translate(
|
||||
'uploadingPageCancel'
|
||||
)}" onclick=${cancel}>${state.translate('uploadingPageCancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function cancel() {
|
||||
const btn = document.getElementById('cancel-upload');
|
||||
btn.disabled = true;
|
||||
btn.textContent = state.translate('uploadCancelNotification');
|
||||
emit('cancel');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
65
app/templates/uploadPassword.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
const div = html`
|
||||
<div class="selectPassword">
|
||||
<div id="addPasswordWrapper">
|
||||
<input id="addPassword" type="checkbox" autocomplete="off" onchange=${
|
||||
togglePasswordInput
|
||||
}/>
|
||||
<label for="addPassword">
|
||||
${state.translate('requirePasswordCheckbox')}</label>
|
||||
</div>
|
||||
<form class="setPassword hidden" onsubmit=${setPassword} data-no-csrf>
|
||||
<input id="unlock-input"
|
||||
class="unlock-input input-no-btn"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
oninput=${inputChanged}/>
|
||||
<input type="submit"
|
||||
id="unlock-btn"
|
||||
class="btn btn-hidden"
|
||||
value="${state.translate('addPasswordButton')}"/>
|
||||
</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 togglePasswordInput(e) {
|
||||
const unlockInput = document.getElementById('unlock-input');
|
||||
const boxChecked = e.target.checked;
|
||||
document
|
||||
.querySelector('.setPassword')
|
||||
.classList.toggle('hidden', !boxChecked);
|
||||
if (boxChecked) {
|
||||
unlockInput.focus();
|
||||
} else {
|
||||
unlockInput.value = '';
|
||||
}
|
||||
inputChanged();
|
||||
}
|
||||
|
||||
function setPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.getElementById('unlock-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('copy').classList.remove('wait-password');
|
||||
document.getElementById('copy-btn').disabled = false;
|
||||
emit('password', { password, file });
|
||||
}
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
71
app/templates/welcome.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const fileList = require('./fileList');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="page-one" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||
<div class="description">
|
||||
<div>${state.translate('uploadPageExplainer')}</div>
|
||||
<a href="https://testpilot.firefox.com/experiments/send"
|
||||
class="link">${state.translate('uploadPageLearnMore')}</a>
|
||||
</div>
|
||||
<div class="upload-window"
|
||||
ondragover=${dragover}
|
||||
ondragleave=${dragleave}>
|
||||
<div id="upload-img">
|
||||
<img src="${assets.get('upload.svg')}"
|
||||
title="${state.translate('uploadSvgAlt')}"/>
|
||||
</div>
|
||||
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
||||
<span id="file-size-msg">
|
||||
<em>${state.translate('uploadPageSizeMessage')}</em>
|
||||
</span>
|
||||
<input id="file-upload"
|
||||
type="file"
|
||||
name="fileUploaded"
|
||||
onfocus=${onfocus}
|
||||
onblur=${onblur}
|
||||
onchange=${upload} />
|
||||
<label for="file-upload"
|
||||
id="browse"
|
||||
class="btn browse"
|
||||
title="${state.translate('uploadPageBrowseButton1')}">
|
||||
${state.translate('uploadPageBrowseButton1')}</label>
|
||||
</div>
|
||||
${fileList(state, emit)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
function dragover(event) {
|
||||
const div = document.querySelector('.upload-window');
|
||||
div.classList.add('ondrag');
|
||||
}
|
||||
|
||||
function dragleave(event) {
|
||||
const div = document.querySelector('.upload-window');
|
||||
div.classList.remove('ondrag');
|
||||
}
|
||||
|
||||
function onfocus(event) {
|
||||
event.target.classList.add('has-focus');
|
||||
}
|
||||
|
||||
function onblur(event) {
|
||||
event.target.classList.remove('has-focus');
|
||||
}
|
||||
|
||||
async function upload(event) {
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const file = target.files[0];
|
||||
if (file.size === 0) {
|
||||
return;
|
||||
}
|
||||
await fadeOut('page-one');
|
||||
emit('upload', { file, type: 'click' });
|
||||
}
|
||||
return div;
|
||||
};
|
||||
166
app/utils.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const b64 = require('base64-js');
|
||||
|
||||
function arrayToB64(array) {
|
||||
return b64
|
||||
.fromByteArray(array)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
function b64ToArray(str) {
|
||||
str = (str + '==='.slice((str.length + 3) % 4))
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
return b64.toByteArray(str);
|
||||
}
|
||||
|
||||
function notify(str) {
|
||||
return str;
|
||||
/* TODO: enable once we have an opt-in ui element
|
||||
if (!('Notification' in window)) {
|
||||
return;
|
||||
} else if (Notification.permission === 'granted') {
|
||||
new Notification(str);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission(function(permission) {
|
||||
if (permission === 'granted') new Notification(str);
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function loadShim(polyfill) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shim = document.createElement('script');
|
||||
shim.src = polyfill;
|
||||
shim.addEventListener('load', () => resolve(true));
|
||||
shim.addEventListener('error', () => resolve(false));
|
||||
document.head.appendChild(shim);
|
||||
});
|
||||
}
|
||||
|
||||
async function canHasSend(polyfill) {
|
||||
try {
|
||||
const key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return loadShim(polyfill);
|
||||
}
|
||||
}
|
||||
|
||||
function isFile(id) {
|
||||
return /^[0-9a-fA-F]{10}$/.test(id);
|
||||
}
|
||||
|
||||
function copyToClipboard(str) {
|
||||
const aux = document.createElement('input');
|
||||
aux.setAttribute('value', str);
|
||||
aux.contentEditable = true;
|
||||
aux.readOnly = true;
|
||||
document.body.appendChild(aux);
|
||||
if (navigator.userAgent.match(/iphone|ipad|ipod/i)) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(aux);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
aux.setSelectionRange(0, str.length);
|
||||
} else {
|
||||
aux.select();
|
||||
}
|
||||
const result = document.execCommand('copy');
|
||||
document.body.removeChild(aux);
|
||||
return result;
|
||||
}
|
||||
|
||||
const LOCALIZE_NUMBERS = !!(
|
||||
typeof Intl === 'object' &&
|
||||
Intl &&
|
||||
typeof Intl.NumberFormat === 'function' &&
|
||||
typeof navigator === 'object'
|
||||
);
|
||||
|
||||
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||
function bytes(num) {
|
||||
if (num < 1) {
|
||||
return '0B';
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
||||
const n = Number(num / Math.pow(1000, exponent));
|
||||
let nStr = n.toFixed(1);
|
||||
if (LOCALIZE_NUMBERS) {
|
||||
try {
|
||||
const locale = document.querySelector('html').lang;
|
||||
nStr = n.toLocaleString(locale, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
});
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return `${nStr}${UNITS[exponent]}`;
|
||||
}
|
||||
|
||||
function percent(ratio) {
|
||||
if (LOCALIZE_NUMBERS) {
|
||||
try {
|
||||
const locale = document.querySelector('html').lang;
|
||||
return ratio.toLocaleString(locale, { style: 'percent' });
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return `${Math.floor(ratio * 100)}%`;
|
||||
}
|
||||
|
||||
function allowedCopy() {
|
||||
const support = !!document.queryCommandSupported;
|
||||
return support ? document.queryCommandSupported('copy') : false;
|
||||
}
|
||||
|
||||
function delay(delay = 100) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
function fadeOut(id) {
|
||||
const classes = document.getElementById(id).classList;
|
||||
classes.remove('fadeIn');
|
||||
classes.add('fadeOut');
|
||||
return delay(300);
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
|
||||
module.exports = {
|
||||
fadeOut,
|
||||
delay,
|
||||
allowedCopy,
|
||||
bytes,
|
||||
percent,
|
||||
copyToClipboard,
|
||||
arrayToB64,
|
||||
b64ToArray,
|
||||
notify,
|
||||
canHasSend,
|
||||
isFile,
|
||||
ONE_DAY_IN_MS
|
||||
};
|
||||
1
assets/check-16-blue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#0A84FF " d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/></svg>
|
||||
|
After Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
22
assets/cryptofill.js
Normal file
BIN
assets/favicon-120.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/favicon-128.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/favicon-144.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/favicon-152.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/favicon-167.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/favicon-180.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/favicon-195.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/favicon-196.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/favicon-228.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/favicon-96.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
1
assets/firefox_logo-only.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -1,22 +1,19 @@
|
||||
/*** index.html ***/
|
||||
html {
|
||||
background: url('resources/send_bg.svg');
|
||||
font-family: -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'SF Pro Text',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
background: url('./send_bg.svg');
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||
font-weight: 200;
|
||||
background-size: 110%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
height: 100%;
|
||||
max-width: 1440px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
@@ -24,6 +21,16 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#progress circle {
|
||||
stroke: #eee;
|
||||
stroke-width: 0.75em;
|
||||
}
|
||||
|
||||
#progress #bar {
|
||||
transition: stroke-dashoffset 300ms linear;
|
||||
stroke: #3b9dff;
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
@@ -39,6 +46,14 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.send-logo h1 {
|
||||
transition: color 50ms;
|
||||
}
|
||||
|
||||
.send-logo h1:hover {
|
||||
color: #0297f8;
|
||||
}
|
||||
|
||||
.send-logo > a {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -67,13 +82,13 @@ body {
|
||||
transition: color 50ms;
|
||||
}
|
||||
|
||||
.send-logo:hover a {
|
||||
.site-subtitle a:hover {
|
||||
color: #0297f8;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
background-color: #0297f8;
|
||||
background-image: url('resources/feedback.svg');
|
||||
background-image: url('./feedback.svg');
|
||||
background-position: 2px 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px;
|
||||
@@ -114,33 +129,74 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
max-width: 630px;
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
pre,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/** page-one **/
|
||||
|
||||
.fadeOut {
|
||||
opacity: 0;
|
||||
animation: fadeout 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
opacity: 1;
|
||||
animation: fadein 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 33px;
|
||||
line-height: 40px;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
max-width: 520px;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -155,8 +211,8 @@ a {
|
||||
}
|
||||
|
||||
.upload-window {
|
||||
border: 1px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto 10px;
|
||||
height: 255px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
@@ -169,8 +225,7 @@ a {
|
||||
}
|
||||
|
||||
.upload-window.ondrag {
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||
height: 251px;
|
||||
transform: scale(1.04);
|
||||
border-radius: 4.2px;
|
||||
@@ -181,6 +236,10 @@ a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-window.ondrag * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0094fb;
|
||||
text-decoration: none;
|
||||
@@ -194,28 +253,39 @@ a {
|
||||
font-size: 22px;
|
||||
color: #737373;
|
||||
margin: 20px 0 10px;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
}
|
||||
|
||||
#browse {
|
||||
.browse {
|
||||
background: #0297f8;
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
width: 240px;
|
||||
height: 44px;
|
||||
min-width: 240px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#browse:hover {
|
||||
.browse:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
input[type='file'] {
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
input[type='file'].has-focus + #browse,
|
||||
input[type='file']:focus + #browse {
|
||||
background-color: #0287e8;
|
||||
outline: 1px dotted #000;
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
}
|
||||
|
||||
#file-size-msg {
|
||||
@@ -262,12 +332,48 @@ tbody {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
#uploaded-file {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
#copy-file-list {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#expiry-file-list {
|
||||
width: 21%;
|
||||
}
|
||||
|
||||
#delete-file-list {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.overflow-col {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.center-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-delete,
|
||||
.icon-copy,
|
||||
.icon-check {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-copy[disabled='disabled'] {
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.text-copied {
|
||||
color: #0a8dff;
|
||||
}
|
||||
|
||||
/* Popup container */
|
||||
.popup {
|
||||
position: absolute;
|
||||
@@ -298,7 +404,7 @@ tbody {
|
||||
|
||||
/* Popup arrow */
|
||||
.popup .popuptext::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -11px;
|
||||
left: 20px;
|
||||
@@ -385,12 +491,8 @@ tbody {
|
||||
}
|
||||
|
||||
.percentage {
|
||||
position: absolute;
|
||||
letter-spacing: -0.78px;
|
||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||
top: 53px;
|
||||
left: 246.75px;
|
||||
width: 98.5px;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@@ -403,7 +505,8 @@ tbody {
|
||||
|
||||
.percent-sign {
|
||||
font-size: 28.8px;
|
||||
color: rgb(104, 104, 104);
|
||||
stroke: none;
|
||||
fill: #686868;
|
||||
}
|
||||
|
||||
.upload {
|
||||
@@ -425,10 +528,18 @@ tbody {
|
||||
|
||||
#cancel-upload {
|
||||
color: #d70022;
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#cancel-upload:disabled {
|
||||
text-decoration: none;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/** share-link **/
|
||||
#share-window {
|
||||
margin: 0 auto;
|
||||
@@ -436,6 +547,8 @@ tbody {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
#share-window-r > div {
|
||||
@@ -446,7 +559,12 @@ tbody {
|
||||
#copy {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 640px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#copy.wait-password #link,
|
||||
#copy.wait-password #copy-btn {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
@@ -463,9 +581,9 @@ tbody {
|
||||
height: 56px;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 6px 0 0 6px;
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: #737373;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
letter-spacing: 0;
|
||||
line-height: 23px;
|
||||
font-weight: 300;
|
||||
@@ -486,19 +604,22 @@ tbody {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
height: 60px;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#copy-btn:hover {
|
||||
#copy-btn:not(:disabled):hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
#copy-btn:disabled {
|
||||
#copy-btn.success {
|
||||
background: #05a700;
|
||||
border: 1px solid #05a700;
|
||||
}
|
||||
|
||||
#copy-btn:disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
@@ -534,6 +655,25 @@ tbody {
|
||||
color: #0287e8;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectPassword {
|
||||
padding: 10px 0;
|
||||
align-self: left;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.setPassword {
|
||||
align-self: left;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 80%;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
/* upload-error */
|
||||
#upload-error {
|
||||
display: flex;
|
||||
@@ -572,11 +712,15 @@ tbody {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.firefox-logo-small {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
#dl-firefox,
|
||||
#update-firefox {
|
||||
margin-bottom: 181px;
|
||||
height: 80px;
|
||||
background: #12bc00;
|
||||
background: #98e02b;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
@@ -616,7 +760,6 @@ tbody {
|
||||
background: #0297f8;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 5px;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -630,7 +773,7 @@ tbody {
|
||||
}
|
||||
|
||||
#download {
|
||||
margin: 0 auto;
|
||||
margin: 0 auto 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -663,6 +806,66 @@ tbody {
|
||||
height: 196px;
|
||||
}
|
||||
|
||||
.enterPassword {
|
||||
text-align: left;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#unlock {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.unlock-input {
|
||||
flex: 1;
|
||||
height: 46px;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 6px 0 0 6px;
|
||||
font-size: 20px;
|
||||
color: #737373;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
letter-spacing: 0;
|
||||
line-height: 23px;
|
||||
font-weight: 300;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#unlock-btn {
|
||||
flex: 0 1 165px;
|
||||
background: #0297f8;
|
||||
border-radius: 0 6px 6px 0;
|
||||
border: 1px solid #0297f8;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
|
||||
/* Force flat button look */
|
||||
-webkit-appearance: none;
|
||||
font-size: 15px;
|
||||
padding-bottom: 3px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#unlock-btn:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.btn-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.input-no-btn {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
.footer {
|
||||
right: 0;
|
||||
@@ -685,7 +888,7 @@ tbody {
|
||||
}
|
||||
|
||||
.legal-links {
|
||||
width: 81vw;
|
||||
max-width: 81vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
@@ -727,6 +930,120 @@ tbody {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
#addPasswordWrapper {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
#addPassword {
|
||||
position: absolute;
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
#addPasswordWrapper label {
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
#addPassword:checked + label {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#addPasswordWrapper label::before {
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
margin-left: 5px;
|
||||
float: left;
|
||||
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#addPassword:checked + label::before {
|
||||
background-image: url('./check-16-blue.svg');
|
||||
background-position: 2px 1px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
padding: 0 15px;
|
||||
height: 48px;
|
||||
background-color: #efeff1;
|
||||
color: #4a4a4f;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.banner > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.banner > div > span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.banner-blue {
|
||||
background-color: #0098f7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.banner-blue a {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.banner-blue a:hover {
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.selectbox {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectSelected {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectOptions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectOptions.active {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
margin: 40px 0;
|
||||
background-color: white;
|
||||
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 2px 4px rgba(12, 12, 13, 0.3);
|
||||
}
|
||||
|
||||
.selectOption {
|
||||
color: #737373;
|
||||
font-size: 12pt;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
padding: 0 60px;
|
||||
border-bottom: 1px solid rgba(12, 12, 13, 0.3);
|
||||
}
|
||||
|
||||
.selectOption:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
@media (max-device-width: 992px), (max-width: 992px) {
|
||||
.popup .popuptext {
|
||||
left: auto;
|
||||
@@ -797,22 +1114,40 @@ tbody {
|
||||
padding: 5px 5px 5px 20px;
|
||||
}
|
||||
|
||||
#copy {
|
||||
#copy,
|
||||
.setPassword,
|
||||
#unlock {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#link {
|
||||
.selectPassword {
|
||||
align-self: center;
|
||||
min-width: 95%;
|
||||
}
|
||||
|
||||
#addPasswordWrapper label::before {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#link,
|
||||
#unlock-input {
|
||||
font-size: 22px;
|
||||
padding: 15px 10px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
#copy-btn {
|
||||
#copy-btn,
|
||||
#unlock-btn {
|
||||
border-radius: 0 0 6px 6px;
|
||||
flex: 0 1 65px;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 14px;
|
||||
padding: 0 5px;
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
assets/send-fb.jpg
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
assets/send-twitter.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
1
assets/send_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd" transform="translate(-0.231,0.11948695)"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
10
browserconfig.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src=“favicon-76.png”/>
|
||||
<square150x150logo src="favicon-228.png"/>
|
||||
<TileColor>#0297F8</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
5
browserslist
Normal file
@@ -0,0 +1,5 @@
|
||||
last 2 chrome versions
|
||||
last 2 firefox versions
|
||||
firefox esr
|
||||
ie >= 9
|
||||
safari >= 9
|
||||
57
build/fluent_loader.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const { MessageContext } = require('fluent');
|
||||
const fs = require('fs');
|
||||
|
||||
function toJSON(map) {
|
||||
return JSON.stringify(Array.from(map));
|
||||
}
|
||||
|
||||
function merge(m1, m2) {
|
||||
const result = new Map(m1);
|
||||
for (const [k, v] of m2) {
|
||||
result.set(k, v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = function(source) {
|
||||
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||
const result = localeExp.exec(this.resourcePath);
|
||||
const locale = result && result[1];
|
||||
if (!locale) {
|
||||
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||
}
|
||||
// load default language and "merge" contexts
|
||||
// TODO: make this configurable
|
||||
const en_ftl = fs.readFileSync(
|
||||
require.resolve('../public/locales/en-US/send.ftl'),
|
||||
'utf8'
|
||||
);
|
||||
const en = new MessageContext('en-US');
|
||||
en.addMessages(en_ftl);
|
||||
// pre-parse the ftl
|
||||
const context = new MessageContext(locale);
|
||||
context.addMessages(source);
|
||||
|
||||
const merged = merge(en._messages, context._messages);
|
||||
return `
|
||||
module.exports = \`
|
||||
if (typeof window === 'undefined') {
|
||||
var fluent = require('fluent');
|
||||
}
|
||||
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||
ctx._messages = new Map(${toJSON(merged)});
|
||||
function translate(id, data) {
|
||||
var msg = ctx.getMessage(id);
|
||||
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||
msg = msg.attrs.title || msg.attrs.alt
|
||||
}
|
||||
return ctx.format(msg, data);
|
||||
}
|
||||
if (typeof window === 'undefined') {
|
||||
module.exports = translate;
|
||||
}
|
||||
else {
|
||||
window.translate = translate;
|
||||
}
|
||||
\``;
|
||||
};
|
||||
19
build/generate_asset_map.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(f) {
|
||||
return `"${f}": require('../assets/${f}')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
|
||||
const code = `module.exports = {
|
||||
"package.json": require('../package.json'),
|
||||
${files.map(kv).join(',\n')}
|
||||
};`;
|
||||
return {
|
||||
code,
|
||||
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
||||
22
build/generate_l10n_map.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(d) {
|
||||
return `"${d}": require('../public/locales/${d}/send.ftl')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||
const code = `
|
||||
module.exports = {
|
||||
translate: function (id, data) { return window.translate(id, data) },
|
||||
${dirs.map(kv).join(',\n')}
|
||||
};`;
|
||||
return {
|
||||
code,
|
||||
dependencies: dirs.map(d =>
|
||||
require.resolve(`../public/locales/${d}/send.ftl`)
|
||||
),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
||||
11
build/package_json_loader.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const commit = require('git-rev-sync').short();
|
||||
|
||||
module.exports = function(source) {
|
||||
const pkg = JSON.parse(source);
|
||||
const version = {
|
||||
commit,
|
||||
source: pkg.homepage,
|
||||
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
||||
};
|
||||
return `module.exports = '${JSON.stringify(version)}'`;
|
||||
};
|
||||
5
build/version_loader.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const version = require('../package.json').version;
|
||||
|
||||
module.exports = function(source) {
|
||||
return source.replace('VERSION', version);
|
||||
};
|
||||
@@ -16,7 +16,6 @@ deployment:
|
||||
latest:
|
||||
branch: master
|
||||
commands:
|
||||
- npm run build
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- docker build -t mozilla/send:latest .
|
||||
- docker push mozilla/send:latest
|
||||
@@ -24,13 +23,13 @@ deployment:
|
||||
tag: /.*/
|
||||
owner: mozilla
|
||||
commands:
|
||||
- npm run build
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
||||
- docker push mozilla/send:$CIRCLE_TAG
|
||||
|
||||
test:
|
||||
override:
|
||||
- npm run build
|
||||
- npm run lint
|
||||
- npm test
|
||||
- nsp check
|
||||
|
||||
32
common/assets.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const genmap = require('../build/generate_asset_map');
|
||||
const isServer = typeof genmap === 'function';
|
||||
const prefix = isServer ? '/' : '';
|
||||
let manifest = {};
|
||||
try {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
manifest = require('../dist/manifest.json');
|
||||
} catch (e) {
|
||||
// use middleware
|
||||
}
|
||||
|
||||
const assets = isServer ? manifest : genmap;
|
||||
|
||||
function getAsset(name) {
|
||||
return prefix + assets[name];
|
||||
}
|
||||
|
||||
const instance = {
|
||||
get: getAsset,
|
||||
setMiddleware: function(middleware) {
|
||||
if (middleware) {
|
||||
instance.get = function getAssetWithMiddleware(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl('/manifest.json')
|
||||
);
|
||||
return prefix + JSON.parse(f)[name];
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = instance;
|
||||
51
common/locales.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const gen = require('../build/generate_l10n_map');
|
||||
|
||||
const isServer = typeof gen === 'function';
|
||||
const prefix = isServer ? '/' : '';
|
||||
let manifest = {};
|
||||
try {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
manifest = require('../dist/manifest.json');
|
||||
} catch (e) {
|
||||
// use middleware
|
||||
}
|
||||
|
||||
const locales = isServer ? manifest : gen;
|
||||
|
||||
function getLocale(name) {
|
||||
return prefix + locales[`public/locales/${name}/send.ftl`];
|
||||
}
|
||||
|
||||
function serverTranslator(name) {
|
||||
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
||||
}
|
||||
|
||||
function browserTranslator() {
|
||||
return locales.translate;
|
||||
}
|
||||
|
||||
const translator = isServer ? serverTranslator : browserTranslator;
|
||||
|
||||
const instance = {
|
||||
get: getLocale,
|
||||
getTranslator: translator,
|
||||
setMiddleware: function(middleware) {
|
||||
if (middleware) {
|
||||
const _eval = require('require-from-string');
|
||||
instance.get = function getLocaleWithMiddleware(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl('/manifest.json')
|
||||
);
|
||||
return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
|
||||
};
|
||||
instance.getTranslator = function(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl(instance.get(name))
|
||||
);
|
||||
return _eval(f.toString());
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = instance;
|
||||
@@ -1,3 +1,14 @@
|
||||
## Setup
|
||||
|
||||
Before building the Docker image, you must build the production assets:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then you can run either `docker build` or `docker-compose up`.
|
||||
|
||||
|
||||
## Environment variables:
|
||||
|
||||
| Name | Description
|
||||
|
||||
11
docs/faq.md
@@ -17,6 +17,17 @@ Many browsers support this standard and should work fine, but some have not
|
||||
implemented it yet (mobile browsers lag behind on this, in
|
||||
particular).
|
||||
|
||||
## Why does Firefox Send require JavaScript?
|
||||
|
||||
Firefox Send uses JavaScript to:
|
||||
|
||||
- Encrypt and decrypt files locally on the client instead of the server.
|
||||
- Render the user interface.
|
||||
- Manage translations on the website into [various different languages](https://github.com/mozilla/send#localization).
|
||||
- Collect data to help us improve Send in accordance with our [Terms & Privacy](https://send.firefox.com/legal).
|
||||
|
||||
Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/mozilla/send/).
|
||||
|
||||
## How long are files available for?
|
||||
|
||||
Files are available to be downloaded for 24 hours, after which they are removed
|
||||
|
||||
@@ -27,6 +27,7 @@ Data will be collected with Google Analytics and follow [Test Pilot standards](h
|
||||
- `cm5` - the number of files the user has ever uploaded.
|
||||
- `cm6` - the number of unexpired files the user has uploaded.
|
||||
- `cm7` - the number of files the user has ever downloaded.
|
||||
- `cm8` - the number of downloads permitted by the uploader.
|
||||
|
||||
### Custom Dimensions
|
||||
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
||||
@@ -67,6 +68,25 @@ Triggered whenever a user stops uploading a file. Includes:
|
||||
- `cd2`
|
||||
- `cd6`
|
||||
|
||||
#### `download-limit-changed`
|
||||
Triggered whenever the sender changes the download limit. Includes:
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `download-limit-changed`
|
||||
- `cm1`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
- `cm8`
|
||||
|
||||
#### `password-added`
|
||||
Triggered whenever a password is added to a file. Includes:
|
||||
|
||||
- `cm1`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
|
||||
#### `download-started`
|
||||
Triggered whenever a user begins downloading a file. Includes:
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
env:
|
||||
browser: true
|
||||
jquery: true
|
||||
@@ -1,34 +0,0 @@
|
||||
window.Raven = require('raven-js');
|
||||
window.Raven.config(window.dsn).install();
|
||||
window.dsn = undefined;
|
||||
|
||||
const testPilotGA = require('testpilot-ga');
|
||||
const { gcmCompliant, sendEvent } = require('./utils');
|
||||
window.analytics = new testPilotGA({
|
||||
an: 'Firefox Send',
|
||||
ds: 'web',
|
||||
tid: window.trackerId
|
||||
});
|
||||
|
||||
const isSender = !location.pathname.includes('/download');
|
||||
const ec = isSender ? 'sender' : 'recipient';
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
gcmCompliant().catch(err => {
|
||||
sendEvent(ec, 'unsupported', {
|
||||
cd6: err
|
||||
}).then(() => {
|
||||
location.replace('/unsupported/gcm');
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
ua.indexOf('firefox') > -1 &&
|
||||
parseInt(ua.match(/firefox\/*([^\n\r]*)\./)[1], 10) <= 49
|
||||
) {
|
||||
sendEvent(ec, 'unsupported', {
|
||||
cd6: new Error('Firefox is outdated.')
|
||||
}).then(() => {
|
||||
location.replace('/unsupported/outdated');
|
||||
});
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
require('./common');
|
||||
const FileReceiver = require('./fileReceiver');
|
||||
const { notify, findMetric, sendEvent } = require('./utils');
|
||||
const bytes = require('bytes');
|
||||
const Storage = require('./storage');
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
const $ = require('jquery');
|
||||
require('jquery-circle-progress');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
$(document).ready(function() {
|
||||
//link back to homepage
|
||||
$('.send-new').attr('href', window.location.origin);
|
||||
|
||||
$('.send-new').click(function(target) {
|
||||
target.preventDefault();
|
||||
sendEvent('recipient', 'restarted', {
|
||||
cd2: 'completed'
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
|
||||
target.preventDefault();
|
||||
const metric = findMetric(target.currentTarget.href);
|
||||
// record exited event by recipient
|
||||
sendEvent('recipient', 'exited', {
|
||||
cd3: metric
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
const filename = $('#dl-filename').text();
|
||||
const bytelength = Number($('#dl-bytelength').text());
|
||||
const timeToExpiry = Number($('#dl-ttl').text());
|
||||
|
||||
//initiate progress bar
|
||||
$('#dl-progress').circleProgress({
|
||||
value: 0.0,
|
||||
startAngle: -Math.PI / 2,
|
||||
fill: '#3B9DFF',
|
||||
size: 158,
|
||||
animation: { duration: 300 }
|
||||
});
|
||||
$('#download-btn').click(download);
|
||||
function download() {
|
||||
storage.totalDownloads += 1;
|
||||
|
||||
const fileReceiver = new FileReceiver();
|
||||
const unexpiredFiles = storage.numFiles;
|
||||
|
||||
fileReceiver.on('progress', progress => {
|
||||
window.onunload = function() {
|
||||
storage.referrer = 'cancelled-download';
|
||||
// record download-stopped (cancelled by tab close or reload)
|
||||
sendEvent('recipient', 'download-stopped', {
|
||||
cm1: bytelength,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
};
|
||||
|
||||
$('#download-page-one').attr('hidden', true);
|
||||
$('#download-progress').removeAttr('hidden');
|
||||
const percent = progress[0] / progress[1];
|
||||
// update progress bar
|
||||
$('#dl-progress').circleProgress('value', percent);
|
||||
$('.percent-number').text(`${Math.floor(percent * 100)}`);
|
||||
$('.progress-text').text(
|
||||
`${filename} (${bytes(progress[0], {
|
||||
decimalPlaces: 1,
|
||||
fixedDecimals: true
|
||||
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
|
||||
);
|
||||
});
|
||||
|
||||
let downloadEnd;
|
||||
fileReceiver.on('decrypting', isStillDecrypting => {
|
||||
// The file is being decrypted
|
||||
if (isStillDecrypting) {
|
||||
fileReceiver.removeAllListeners('progress');
|
||||
window.onunload = null;
|
||||
document.l10n.formatValue('decryptingFile').then(decryptingFile => {
|
||||
$('.progress-text').text(decryptingFile);
|
||||
});
|
||||
} else {
|
||||
downloadEnd = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
fileReceiver.on('hashing', isStillHashing => {
|
||||
// The file is being hashed to make sure a malicious user hasn't tampered with it
|
||||
if (isStillHashing) {
|
||||
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
|
||||
$('.progress-text').text(verifyingFile);
|
||||
});
|
||||
} else {
|
||||
$('.progress-text').text(' ');
|
||||
document.l10n
|
||||
.formatValues('downloadNotification', 'downloadFinish')
|
||||
.then(translated => {
|
||||
notify(translated[0]);
|
||||
$('.title').text(translated[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// record download-started by recipient
|
||||
sendEvent('recipient', 'download-started', {
|
||||
cm1: bytelength,
|
||||
cm4: timeToExpiry,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
|
||||
fileReceiver
|
||||
.download()
|
||||
.catch(err => {
|
||||
// record download-stopped (errored) by recipient
|
||||
sendEvent('recipient', 'download-stopped', {
|
||||
cm1: bytelength,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'errored',
|
||||
cd6: err
|
||||
});
|
||||
|
||||
if (err.message === 'notfound') {
|
||||
location.reload();
|
||||
} else {
|
||||
document.l10n.formatValue('errorPageHeader').then(translated => {
|
||||
$('.title').text(translated);
|
||||
});
|
||||
$('#download-btn').attr('hidden', true);
|
||||
$('#expired-img').removeAttr('hidden');
|
||||
}
|
||||
return;
|
||||
})
|
||||
.then(([decrypted, fname]) => {
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const downloadTime = endTime - downloadEnd;
|
||||
const downloadSpeed = bytelength / (downloadTime / 1000);
|
||||
|
||||
storage.referrer = 'completed-download';
|
||||
// record download-stopped (completed) by recipient
|
||||
sendEvent('recipient', 'download-stopped', {
|
||||
cm1: bytelength,
|
||||
cm2: totalTime,
|
||||
cm3: downloadSpeed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'completed'
|
||||
});
|
||||
|
||||
const dataView = new DataView(decrypted);
|
||||
const blob = new Blob([dataView]);
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
if (window.navigator.msSaveBlob) {
|
||||
// if we are in microsoft edge or IE
|
||||
window.navigator.msSaveBlob(blob, fname);
|
||||
return;
|
||||
}
|
||||
a.download = fname;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
const EventEmitter = require('events');
|
||||
const { hexToArray } = require('./utils');
|
||||
|
||||
class FileReceiver extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
download() {
|
||||
return Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.emit('progress', [event.loaded, event.total]);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (xhr.status === 404) {
|
||||
reject(new Error('notfound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([this.response]);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function() {
|
||||
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
||||
resolve({
|
||||
data: this.result,
|
||||
aad: meta.aad,
|
||||
filename: meta.filename,
|
||||
iv: meta.id
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
};
|
||||
|
||||
xhr.open('get', '/assets' + location.pathname.slice(0, -1), true);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
}),
|
||||
window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
{
|
||||
kty: 'oct',
|
||||
k: location.hash.slice(1),
|
||||
alg: 'A128GCM',
|
||||
ext: true
|
||||
},
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
])
|
||||
.then(([fdata, key]) => {
|
||||
this.emit('decrypting', true);
|
||||
return Promise.all([
|
||||
window.crypto.subtle
|
||||
.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(fdata.iv),
|
||||
additionalData: hexToArray(fdata.aad),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
fdata.data
|
||||
)
|
||||
.then(decrypted => {
|
||||
this.emit('decrypting', false);
|
||||
return Promise.resolve(decrypted);
|
||||
}),
|
||||
fdata.filename,
|
||||
hexToArray(fdata.aad)
|
||||
]);
|
||||
})
|
||||
.then(([decrypted, fname, proposedHash]) => {
|
||||
this.emit('hashing', true);
|
||||
return window.crypto.subtle
|
||||
.digest('SHA-256', decrypted)
|
||||
.then(calculatedHash => {
|
||||
this.emit('hashing', false);
|
||||
const integrity =
|
||||
new Uint8Array(calculatedHash).toString() ===
|
||||
proposedHash.toString();
|
||||
if (!integrity) {
|
||||
this.emit('unsafe', true);
|
||||
return Promise.reject();
|
||||
} else {
|
||||
this.emit('safe', true);
|
||||
return Promise.all([decrypted, decodeURIComponent(fname)]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileReceiver;
|
||||
@@ -1,144 +0,0 @@
|
||||
const EventEmitter = require('events');
|
||||
const { arrayToHex } = require('./utils');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
class FileSender extends EventEmitter {
|
||||
constructor(file) {
|
||||
super();
|
||||
this.file = file;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
}
|
||||
|
||||
static delete(fileId, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!fileId || !token) {
|
||||
return reject();
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('post', '/delete/' + fileId, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({ delete_token: token }));
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
|
||||
upload() {
|
||||
const self = this;
|
||||
self.emit('loading', true);
|
||||
return Promise.all([
|
||||
window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
),
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(this.file);
|
||||
reader.onload = function(event) {
|
||||
self.emit('loading', false);
|
||||
self.emit('hashing', true);
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
window.crypto.subtle.digest('SHA-256', plaintext).then(hash => {
|
||||
self.emit('hashing', false);
|
||||
self.emit('encrypting', true);
|
||||
resolve({ plaintext: plaintext, hash: new Uint8Array(hash) });
|
||||
});
|
||||
};
|
||||
reader.onerror = function(err) {
|
||||
reject(err);
|
||||
};
|
||||
})
|
||||
])
|
||||
.then(([secretKey, file]) => {
|
||||
return Promise.all([
|
||||
window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
additionalData: file.hash,
|
||||
tagLength: 128
|
||||
},
|
||||
secretKey,
|
||||
file.plaintext
|
||||
)
|
||||
.then(encrypted => {
|
||||
self.emit('encrypting', false);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(encrypted);
|
||||
});
|
||||
}),
|
||||
window.crypto.subtle.exportKey('jwk', secretKey),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(file.hash);
|
||||
})
|
||||
]);
|
||||
})
|
||||
.then(([encrypted, keydata, hash]) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.file;
|
||||
const fileId = arrayToHex(this.iv);
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const fd = new FormData();
|
||||
fd.append('data', blob, file.name);
|
||||
|
||||
const xhr = self.uploadXHR;
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
self.emit('progress', [e.loaded, e.total]);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
return resolve({
|
||||
url: responseObj.url,
|
||||
fileId: responseObj.id,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.delete
|
||||
});
|
||||
}
|
||||
reject(xhr.status);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('post', '/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
JSON.stringify({
|
||||
aad: arrayToHex(hash),
|
||||
id: fileId,
|
||||
filename: encodeURIComponent(file.name)
|
||||
})
|
||||
);
|
||||
xhr.send(fd);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileSender;
|
||||
@@ -1,70 +0,0 @@
|
||||
const { isFile } = require('./utils');
|
||||
|
||||
class Storage {
|
||||
constructor(engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
get totalDownloads() {
|
||||
return Number(this.engine.getItem('totalDownloads'));
|
||||
}
|
||||
set totalDownloads(n) {
|
||||
this.engine.setItem('totalDownloads', n);
|
||||
}
|
||||
get totalUploads() {
|
||||
return Number(this.engine.getItem('totalUploads'));
|
||||
}
|
||||
set totalUploads(n) {
|
||||
this.engine.setItem('totalUploads', n);
|
||||
}
|
||||
get referrer() {
|
||||
return this.engine.getItem('referrer');
|
||||
}
|
||||
set referrer(str) {
|
||||
this.engine.setItem('referrer', str);
|
||||
}
|
||||
|
||||
get files() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
fs.push(JSON.parse(this.engine.getItem(k))); // parse or whatever else
|
||||
}
|
||||
}
|
||||
return fs.sort((file1, file2) => {
|
||||
const creationDate1 = new Date(file1.creationDate);
|
||||
const creationDate2 = new Date(file2.creationDate);
|
||||
return creationDate1 - creationDate2;
|
||||
});
|
||||
}
|
||||
|
||||
get numFiles() {
|
||||
let length = 0;
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
length += 1;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
getFileById(id) {
|
||||
return this.engine.getItem(id);
|
||||
}
|
||||
|
||||
has(property) {
|
||||
return this.engine.hasOwnProperty(property);
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
this.engine.removeItem(property);
|
||||
}
|
||||
|
||||
addFile(id, file) {
|
||||
this.engine.setItem(id, JSON.stringify(file));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Storage;
|
||||
@@ -1,550 +0,0 @@
|
||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
||||
require('./common');
|
||||
const FileSender = require('./fileSender');
|
||||
const {
|
||||
notify,
|
||||
findMetric,
|
||||
sendEvent,
|
||||
ONE_DAY_IN_MS
|
||||
} = require('./utils');
|
||||
const bytes = require('bytes');
|
||||
const Storage = require('./storage');
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
const $ = require('jquery');
|
||||
require('jquery-circle-progress');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
if (storage.has('referrer')) {
|
||||
window.referrer = storage.referrer;
|
||||
storage.remove('referrer');
|
||||
} else {
|
||||
window.referrer = 'external';
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#file-upload').change(onUpload);
|
||||
|
||||
$('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
|
||||
target.preventDefault();
|
||||
const metric = findMetric(target.currentTarget.href);
|
||||
// record exited event by recipient
|
||||
sendEvent('sender', 'exited', {
|
||||
cd3: metric
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('#send-new-completed').click(function(target) {
|
||||
target.preventDefault();
|
||||
// record restarted event
|
||||
sendEvent('sender', 'restarted', {
|
||||
cd2: 'completed'
|
||||
}).then(() => {
|
||||
storage.referrer = 'completed-upload';
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('#send-new-error').click(function(target) {
|
||||
target.preventDefault();
|
||||
// record restarted event
|
||||
sendEvent('sender', 'restarted', {
|
||||
cd2: 'errored'
|
||||
}).then(() => {
|
||||
storage.referrer = 'errored-upload';
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('body').on('dragover', allowDrop).on('drop', onUpload);
|
||||
// reset copy button
|
||||
const $copyBtn = $('#copy-btn');
|
||||
$copyBtn.attr('disabled', false);
|
||||
$('#link').attr('disabled', false);
|
||||
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
|
||||
|
||||
const files = storage.files;
|
||||
if (files.length === 0) {
|
||||
toggleHeader();
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let index in files) {
|
||||
const id = files[index].fileId;
|
||||
//check if file still exists before adding to list
|
||||
checkExistence(id, files[index], true);
|
||||
}
|
||||
}
|
||||
|
||||
// copy link to clipboard
|
||||
$copyBtn.click(() => {
|
||||
// record copied event from success screen
|
||||
sendEvent('sender', 'copied', {
|
||||
cd4: 'success-screen'
|
||||
});
|
||||
const aux = document.createElement('input');
|
||||
aux.setAttribute('value', $('#link').attr('value'));
|
||||
document.body.appendChild(aux);
|
||||
aux.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(aux);
|
||||
//disable button for 3s
|
||||
$copyBtn.attr('disabled', true);
|
||||
$('#link').attr('disabled', true);
|
||||
$copyBtn.html(
|
||||
'<img src="/resources/check-16.svg" class="icon-check"></img>'
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
$copyBtn.attr('disabled', false);
|
||||
$('#link').attr('disabled', false);
|
||||
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
$('.upload-window').on('dragover', () => {
|
||||
$('.upload-window').addClass('ondrag');
|
||||
});
|
||||
$('.upload-window').on('dragleave', () => {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
});
|
||||
//initiate progress bar
|
||||
$('#ul-progress').circleProgress({
|
||||
value: 0.0,
|
||||
startAngle: -Math.PI / 2,
|
||||
fill: '#3B9DFF',
|
||||
size: 158,
|
||||
animation: { duration: 300 }
|
||||
});
|
||||
|
||||
//link back to homepage
|
||||
$('.send-new').attr('href', window.location);
|
||||
|
||||
// on file upload by browse or drag & drop
|
||||
function onUpload(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// don't allow upload if not on upload page
|
||||
if ($('#page-one').attr('hidden')) {
|
||||
return;
|
||||
}
|
||||
|
||||
storage.totalUploads += 1;
|
||||
|
||||
let file = '';
|
||||
if (event.type === 'drop') {
|
||||
if (!event.originalEvent.dataTransfer.files[0]) {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.originalEvent.dataTransfer.files.length > 1 ||
|
||||
event.originalEvent.dataTransfer.files[0].size === 0
|
||||
) {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
||||
alert(str);
|
||||
});
|
||||
return;
|
||||
}
|
||||
file = event.originalEvent.dataTransfer.files[0];
|
||||
} else {
|
||||
file = event.target.files[0];
|
||||
}
|
||||
|
||||
if (file.size > MAXFILESIZE) {
|
||||
return document.l10n
|
||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||
.then(alert);
|
||||
}
|
||||
|
||||
$('#page-one').attr('hidden', true);
|
||||
$('#upload-error').attr('hidden', true);
|
||||
$('#upload-progress').removeAttr('hidden');
|
||||
document.l10n.formatValue('importingFile').then(importingFile => {
|
||||
$('.progress-text').text(importingFile);
|
||||
});
|
||||
//don't allow drag and drop when not on page-one
|
||||
$('body').off('drop', onUpload);
|
||||
|
||||
const fileSender = new FileSender(file);
|
||||
$('#cancel-upload').click(() => {
|
||||
fileSender.cancel();
|
||||
storage.referrer = 'cancelled-upload';
|
||||
|
||||
// record upload-stopped (cancelled) by sender
|
||||
sendEvent('sender', 'upload-stopped', {
|
||||
cm1: file.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
location.reload();
|
||||
});
|
||||
|
||||
fileSender.on('progress', progress => {
|
||||
const percent = progress[0] / progress[1];
|
||||
// update progress bar
|
||||
$('#ul-progress').circleProgress('value', percent);
|
||||
$('#ul-progress').circleProgress().on('circle-animation-end', function() {
|
||||
$('.percent-number').text(`${Math.floor(percent * 100)}`);
|
||||
});
|
||||
$('.progress-text').text(
|
||||
`${file.name} (${bytes(progress[0], {
|
||||
decimalPlaces: 1,
|
||||
fixedDecimals: true
|
||||
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
|
||||
);
|
||||
});
|
||||
|
||||
fileSender.on('hashing', isStillHashing => {
|
||||
// The file is being hashed
|
||||
if (isStillHashing) {
|
||||
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
|
||||
$('.progress-text').text(verifyingFile);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let uploadStart;
|
||||
fileSender.on('encrypting', isStillEncrypting => {
|
||||
// The file is being encrypted
|
||||
if (isStillEncrypting) {
|
||||
document.l10n.formatValue('encryptingFile').then(encryptingFile => {
|
||||
$('.progress-text').text(encryptingFile);
|
||||
});
|
||||
} else {
|
||||
uploadStart = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
let t;
|
||||
const startTime = Date.now();
|
||||
const unexpiredFiles = storage.numFiles + 1;
|
||||
|
||||
// record upload-started event by sender
|
||||
sendEvent('sender', 'upload-started', {
|
||||
cm1: file.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd5: window.referrer
|
||||
});
|
||||
|
||||
// For large files we need to give the ui a tick to breathe and update
|
||||
// before we kick off the FileSender
|
||||
setTimeout(() => {
|
||||
fileSender
|
||||
.upload()
|
||||
.then(info => {
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const uploadTime = endTime - uploadStart;
|
||||
const uploadSpeed = file.size / (uploadTime / 1000);
|
||||
const expiration = EXPIRE_SECONDS * 1000;
|
||||
|
||||
// record upload-stopped (completed) by sender
|
||||
sendEvent('sender', 'upload-stopped', {
|
||||
cm1: file.size,
|
||||
cm2: totalTime,
|
||||
cm3: uploadSpeed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd2: 'completed'
|
||||
});
|
||||
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken,
|
||||
creationDate: new Date(),
|
||||
expiry: expiration,
|
||||
totalTime: totalTime,
|
||||
typeOfUpload: event.type === 'drop' ? 'drop' : 'click',
|
||||
uploadSpeed: uploadSpeed
|
||||
};
|
||||
|
||||
storage.addFile(info.fileId, fileData);
|
||||
$('#upload-filename').attr(
|
||||
'data-l10n-id',
|
||||
'uploadSuccessConfirmHeader'
|
||||
);
|
||||
t = window.setTimeout(() => {
|
||||
$('#page-one').attr('hidden', true);
|
||||
$('#upload-progress').attr('hidden', true);
|
||||
$('#upload-error').attr('hidden', true);
|
||||
$('#share-link').removeAttr('hidden');
|
||||
}, 1000);
|
||||
|
||||
populateFileList(fileData);
|
||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
||||
notify(str);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
// err is 0 when coming from a cancel upload event
|
||||
if (err === 0) {
|
||||
return;
|
||||
}
|
||||
// only show error page when the error is anything other than user cancelling the upload
|
||||
Raven.captureException(err);
|
||||
$('#page-one').attr('hidden', true);
|
||||
$('#upload-progress').attr('hidden', true);
|
||||
$('#upload-error').removeAttr('hidden');
|
||||
window.clearTimeout(t);
|
||||
|
||||
// record upload-stopped (errored) by sender
|
||||
sendEvent('sender', 'upload-stopped', {
|
||||
cm1: file.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd2: 'errored',
|
||||
cd6: err
|
||||
});
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function checkExistence(id, file, populate) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
if (populate) {
|
||||
populateFileList(file);
|
||||
}
|
||||
} else if (xhr.status === 404) {
|
||||
storage.remove(id);
|
||||
if (storage.numFiles === 0) {
|
||||
toggleHeader();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open('get', '/exists/' + id, true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
//update file table with current files in storage
|
||||
function populateFileList(file) {
|
||||
const row = document.createElement('tr');
|
||||
const name = document.createElement('td');
|
||||
const link = document.createElement('td');
|
||||
const $copyIcon = $('<img>', {
|
||||
src: '/resources/copy-16.svg',
|
||||
class: 'icon-copy',
|
||||
'data-l10n-id': 'copyUrlHover'
|
||||
});
|
||||
const expiry = document.createElement('td');
|
||||
const del = document.createElement('td');
|
||||
const $delIcon = $('<img>', {
|
||||
src: '/resources/close-16.svg',
|
||||
class: 'icon-delete',
|
||||
'data-l10n-id': 'deleteButtonHover'
|
||||
});
|
||||
const popupDiv = document.createElement('div');
|
||||
const $popupText = $('<div>', { class: 'popuptext' });
|
||||
const cellText = document.createTextNode(file.name);
|
||||
|
||||
const url = file.url.trim() + `#${file.secretKey}`.trim();
|
||||
|
||||
$('#link').attr('value', url);
|
||||
$('#copy-text').attr('data-l10n-args', '{"filename": "' + file.name + '"}');
|
||||
$('#copy-text').attr('data-l10n-id', 'copyUrlFormLabelWithName');
|
||||
$popupText.attr('tabindex', '-1');
|
||||
|
||||
name.appendChild(cellText);
|
||||
|
||||
// create delete button
|
||||
|
||||
const delSpan = document.createElement('span');
|
||||
$(delSpan).addClass('icon-cancel-1');
|
||||
$(delSpan).attr('data-l10n-id', 'deleteButtonHover');
|
||||
del.appendChild(delSpan);
|
||||
|
||||
const linkSpan = document.createElement('span');
|
||||
$(linkSpan).addClass('icon-docs');
|
||||
$(linkSpan).attr('data-l10n-id', 'copyUrlHover');
|
||||
link.appendChild(linkSpan);
|
||||
|
||||
link.style.color = '#0A8DFF';
|
||||
|
||||
//copy link to clipboard when icon clicked
|
||||
$copyIcon.click(function() {
|
||||
// record copied event from upload list
|
||||
sendEvent('sender', 'copied', {
|
||||
cd4: 'upload-list'
|
||||
});
|
||||
const aux = document.createElement('input');
|
||||
aux.setAttribute('value', url);
|
||||
document.body.appendChild(aux);
|
||||
aux.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(aux);
|
||||
document.l10n.formatValue('copiedUrl').then(translated => {
|
||||
link.innerHTML = translated;
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
const linkImg = document.createElement('img');
|
||||
$(linkImg).addClass('icon-copy');
|
||||
$(linkImg).attr('data-l10n-id', 'copyUrlHover');
|
||||
$(linkImg).attr('src', '/resources/copy-16.svg');
|
||||
$(link).html(linkImg);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
file.creationDate = new Date(file.creationDate);
|
||||
|
||||
const future = new Date();
|
||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||
|
||||
let countdown = 0;
|
||||
countdown = future.getTime() - Date.now();
|
||||
let minutes = Math.floor(countdown / 1000 / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let seconds = Math.floor(countdown / 1000 % 60);
|
||||
|
||||
poll();
|
||||
|
||||
function poll() {
|
||||
countdown = future.getTime() - Date.now();
|
||||
minutes = Math.floor(countdown / 1000 / 60);
|
||||
hours = Math.floor(minutes / 60);
|
||||
seconds = Math.floor(countdown / 1000 % 60);
|
||||
let t;
|
||||
|
||||
if (hours >= 1) {
|
||||
expiry.innerHTML = hours + 'h ' + minutes % 60 + 'm';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 60000);
|
||||
} else if (hours === 0) {
|
||||
expiry.innerHTML = minutes + 'm ' + seconds + 's';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 1000);
|
||||
}
|
||||
//remove from list when expired
|
||||
if (countdown <= 0) {
|
||||
storage.remove(file.fileId);
|
||||
$(expiry).parents('tr').remove();
|
||||
window.clearTimeout(t);
|
||||
toggleHeader();
|
||||
}
|
||||
}
|
||||
|
||||
// create popup
|
||||
popupDiv.classList.add('popup');
|
||||
const $popupMessage = $('<div>', { class: 'popup-message' });
|
||||
$popupMessage.attr('data-l10n-id', 'deletePopupText');
|
||||
const $popupAction = $('<div>', { class: 'popup-action' });
|
||||
const $popupNvmSpan = $('<span>', { class: 'popup-no' });
|
||||
$popupNvmSpan.attr('data-l10n-id', 'deletePopupCancel');
|
||||
const $popupDelSpan = $('<span>', { class: 'popup-yes' });
|
||||
$popupDelSpan.attr('data-l10n-id', 'deletePopupYes');
|
||||
|
||||
$popupText.html([$popupMessage, $popupAction]);
|
||||
$popupAction.html([$popupNvmSpan, $popupDelSpan]);
|
||||
|
||||
// add data cells to table row
|
||||
row.appendChild(name);
|
||||
$(link).append($copyIcon);
|
||||
row.appendChild(link);
|
||||
row.appendChild(expiry);
|
||||
$(popupDiv).append($popupText);
|
||||
$(del).append($delIcon);
|
||||
del.appendChild(popupDiv);
|
||||
row.appendChild(del);
|
||||
$('tbody').append(row); //add row to table
|
||||
|
||||
const unexpiredFiles = storage.numFiles;
|
||||
|
||||
// delete file
|
||||
$popupText.find('.popup-yes').click(e => {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
$(e.target).parents('tr').remove();
|
||||
const timeToExpiry =
|
||||
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||
// record upload-deleted from file list
|
||||
sendEvent('sender', 'upload-deleted', {
|
||||
cm1: file.size,
|
||||
cm2: file.totalTime,
|
||||
cm3: file.uploadSpeed,
|
||||
cm4: timeToExpiry,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: file.typeOfUpload,
|
||||
cd4: 'upload-list'
|
||||
}).then(() => {
|
||||
storage.remove(file.fileId);
|
||||
});
|
||||
toggleHeader();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('delete-file').onclick = () => {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
const timeToExpiry =
|
||||
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||
// record upload-deleted from success screen
|
||||
sendEvent('sender', 'upload-deleted', {
|
||||
cm1: file.size,
|
||||
cm2: file.totalTime,
|
||||
cm3: file.uploadSpeed,
|
||||
cm4: timeToExpiry,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: file.typeOfUpload,
|
||||
cd4: 'success-screen'
|
||||
}).then(() => {
|
||||
storage.remove(file.fileId);
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
};
|
||||
// show popup
|
||||
$delIcon.click(function() {
|
||||
$popupText.addClass('show');
|
||||
$popupText.focus();
|
||||
});
|
||||
// hide popup
|
||||
$popupText.find('.popup-no').click(function(e) {
|
||||
e.stopPropagation();
|
||||
$popupText.removeClass('show');
|
||||
});
|
||||
$popupText.click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
//close when popup loses focus
|
||||
$popupText.blur(() => {
|
||||
$popupText.removeClass('show');
|
||||
});
|
||||
|
||||
toggleHeader();
|
||||
}
|
||||
function toggleHeader() {
|
||||
//hide table header if empty list
|
||||
if (document.querySelector('tbody').childNodes.length === 1) {
|
||||
$('#file-list').attr('hidden', true);
|
||||
} else {
|
||||
$('#file-list').removeAttr('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
function arrayToHex(iv) {
|
||||
let hexStr = '';
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let i in iv) {
|
||||
if (iv[i] < 16) {
|
||||
hexStr += '0' + iv[i].toString(16);
|
||||
} else {
|
||||
hexStr += iv[i].toString(16);
|
||||
}
|
||||
}
|
||||
return hexStr;
|
||||
}
|
||||
|
||||
function hexToArray(str) {
|
||||
const iv = new Uint8Array(str.length / 2);
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16);
|
||||
}
|
||||
|
||||
return iv;
|
||||
}
|
||||
|
||||
function notify(str) {
|
||||
return str;
|
||||
/* TODO: enable once we have an opt-in ui element
|
||||
if (!('Notification' in window)) {
|
||||
return;
|
||||
} else if (Notification.permission === 'granted') {
|
||||
new Notification(str);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission(function(permission) {
|
||||
if (permission === 'granted') new Notification(str);
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function gcmCompliant() {
|
||||
try {
|
||||
return window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(key => {
|
||||
return window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
)
|
||||
.then(() => {
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject();
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
function findMetric(href) {
|
||||
switch (href) {
|
||||
case 'https://www.mozilla.org/':
|
||||
return 'mozilla';
|
||||
case 'https://www.mozilla.org/about/legal':
|
||||
return 'legal';
|
||||
case 'https://testpilot.firefox.com/about':
|
||||
return 'about';
|
||||
case 'https://testpilot.firefox.com/privacy':
|
||||
return 'privacy';
|
||||
case 'https://testpilot.firefox.com/terms':
|
||||
return 'terms';
|
||||
case 'https://www.mozilla.org/privacy/websites/#cookies':
|
||||
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';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
function isFile(id) {
|
||||
return ![
|
||||
'referrer',
|
||||
'totalDownloads',
|
||||
'totalUploads',
|
||||
'testpilot_ga__cid'
|
||||
].includes(id);
|
||||
}
|
||||
|
||||
function sendEvent() {
|
||||
return window.analytics.sendEvent
|
||||
.apply(window.analytics, arguments)
|
||||
.catch(() => 0);
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
|
||||
module.exports = {
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
notify,
|
||||
gcmCompliant,
|
||||
findMetric,
|
||||
isFile,
|
||||
sendEvent,
|
||||
ONE_DAY_IN_MS
|
||||
};
|
||||
11665
package-lock.json
generated
218
package.json
@@ -1,94 +1,158 @@
|
||||
{
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "1.0.2",
|
||||
"version": "2.2.1",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.89.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"bytes": "^2.5.0",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^3.0.0",
|
||||
"express": "^4.15.3",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"helmet": "^3.8.0",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.1.0",
|
||||
"redis": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^14.4.0",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"jquery": "^3.2.1",
|
||||
"jquery-circle-progress": "^1.2.2",
|
||||
"l20n": "^5.0.0",
|
||||
"mocha": "^3.4.2",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"prettier": "^1.5.3",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.17.0",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^2.3.8",
|
||||
"stylelint": "^8.0.0",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"uglifyify": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"repository": "mozilla/send",
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"repository": "mozilla/send",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && webpack -p",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'assets/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"lint-locales": "node scripts/lint-locales",
|
||||
"lint-locales:dev": "npm run lint-locales",
|
||||
"lint-locales:prod": "npm run lint-locales -- --production",
|
||||
"format": "prettier '**/*.js' 'assets/*.css' --single-quote --write",
|
||||
"get-prod-locales": "node scripts/get-prod-locales",
|
||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
||||
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
||||
"release": "npm-run-all contributors changelog",
|
||||
"test": "mocha test/unit",
|
||||
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
||||
"prod": "node server/prod.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"prettier --single-quote --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
],
|
||||
"*.css": [
|
||||
"prettier --single-quote --write",
|
||||
"stylelint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.2.3",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-yo-yoify": "^1.0.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"base64-js": "^1.2.1",
|
||||
"copy-webpack-plugin": "^4.3.0",
|
||||
"cross-env": "^5.1.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"css-mqpacker": "^6.0.1",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"expose-loader": "^0.7.4",
|
||||
"extract-loader": "^1.0.1",
|
||||
"file-loader": "^1.1.6",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"github-changes": "^1.1.1",
|
||||
"html-loader": "^0.5.1",
|
||||
"husky": "^0.14.3",
|
||||
"lint-staged": "^4.3.0",
|
||||
"mocha": "^3.5.3",
|
||||
"nanobus": "^4.3.1",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"postcss-loader": "^2.0.9",
|
||||
"prettier": "^1.9.2",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.21.0",
|
||||
"redis-mock": "^0.20.0",
|
||||
"require-from-string": "^2.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"selenium-webdriver": "^3.6.0",
|
||||
"sinon": "^4.1.3",
|
||||
"string-hash": "^1.1.3",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.0.1",
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"val-loader": "^1.1.0",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-dev-server": "2.9.1",
|
||||
"webpack-manifest-plugin": "^1.3.2",
|
||||
"webpack-unassert-loader": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.171.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"choo": "^6.6.0",
|
||||
"cldr-core": "^32.0.0",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^4.0.1",
|
||||
"express": "^4.16.2",
|
||||
"fluent": "^0.4.1",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"helmet": "^3.9.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mozlog": "^2.2.0",
|
||||
"raven": "^2.3.0",
|
||||
"redis": "^2.8.0"
|
||||
},
|
||||
"availableLanguages": [
|
||||
"en-US",
|
||||
"zh-TW",
|
||||
"zh-CN",
|
||||
"ast",
|
||||
"az",
|
||||
"bs",
|
||||
"ca",
|
||||
"cak",
|
||||
"cs",
|
||||
"fr",
|
||||
"fy-NL",
|
||||
"cy",
|
||||
"de",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"kab",
|
||||
"ms",
|
||||
"nb-NO",
|
||||
"nn-NO",
|
||||
"pt-PT",
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"sk",
|
||||
"sl",
|
||||
"dsb",
|
||||
"hsb",
|
||||
"el",
|
||||
"es-AR",
|
||||
"es-CL",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"et",
|
||||
"fa",
|
||||
"fr",
|
||||
"fy-NL",
|
||||
"hsb",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ka",
|
||||
"kab",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb-NO",
|
||||
"nl",
|
||||
"nn-NO",
|
||||
"pt-BR",
|
||||
"pt-PT",
|
||||
"ru",
|
||||
"sk",
|
||||
"sl",
|
||||
"sr",
|
||||
"sv-SE",
|
||||
"tl",
|
||||
"tr",
|
||||
"cy"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm-run-all build:*",
|
||||
"build:upload": "browserify frontend/src/upload.js -g uglifyify -o public/upload.js",
|
||||
"build:download": "browserify frontend/src/download.js -g uglifyify -o public/download.js",
|
||||
"build:version": "node scripts/version",
|
||||
"build:l10n": "cp node_modules/l20n/dist/web/l20n.min.js public",
|
||||
"dev": "npm run build && npm start",
|
||||
"format": "prettier '{frontend/src/,scripts/,server/,test/**/!(bundle)}*.js' 'public/*.css' --single-quote --write",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'public/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"start": "node server/server",
|
||||
"test": "npm-run-all test:*",
|
||||
"test:unit": "mocha test/unit",
|
||||
"test:server": "mocha test/server",
|
||||
"test:browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d && node test/frontend/driver.js"
|
||||
}
|
||||
"uk",
|
||||
"vi",
|
||||
"zh-CN",
|
||||
"zh-TW"
|
||||
]
|
||||
}
|
||||
|
||||
15
postcss.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const cssnano = require('cssnano');
|
||||
const mqpacker = require('css-mqpacker');
|
||||
|
||||
const config = require('./server/config');
|
||||
|
||||
const options = {
|
||||
plugins: [autoprefixer, mqpacker, cssnano]
|
||||
};
|
||||
|
||||
if (config.env === 'development') {
|
||||
options.map = { inline: true };
|
||||
}
|
||||
|
||||
module.exports = options;
|
||||