Compare commits
570 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82fe65ada2 | ||
|
|
ce79d7b745 | ||
|
|
755cc4f5ec | ||
|
|
fde4d311e3 | ||
|
|
b08f40aaa3 | ||
|
|
e12ade6b31 | ||
|
|
43fc80ef41 | ||
|
|
6ca96157f6 | ||
|
|
856181ea54 | ||
|
|
f84bd46cdc | ||
|
|
0b43924ee2 | ||
|
|
16d3fd3828 | ||
|
|
cf5defa6a9 | ||
|
|
3de760db12 | ||
|
|
1366f0b68e | ||
|
|
be498e0bd3 | ||
|
|
a4e13f032a | ||
|
|
fef3136b1b | ||
|
|
6b318c248f | ||
|
|
fd6a3a5579 | ||
|
|
2292267e39 | ||
|
|
dbfae53222 | ||
|
|
d39ed267f3 | ||
|
|
9d04ad704a | ||
|
|
11cbb4df23 | ||
|
|
232911f725 | ||
|
|
41a0c6c73f | ||
|
|
10e80edb1d | ||
|
|
af3848586c | ||
|
|
48807bf030 | ||
|
|
21a14cb225 | ||
|
|
b725d07744 | ||
|
|
94e707da8a | ||
|
|
4fb4041f13 | ||
|
|
545da556d2 | ||
|
|
9f3da3454f | ||
|
|
15105426b1 | ||
|
|
b2d2e27945 | ||
|
|
d93a4cf9f3 | ||
|
|
8c3ec5da3d | ||
|
|
d067b1d55a | ||
|
|
55070ace8b | ||
|
|
0f35961173 | ||
|
|
b63f68bf74 | ||
|
|
8c6d6db07e | ||
|
|
a74a70fd8c | ||
|
|
97ad674be2 | ||
|
|
81534d2c13 | ||
|
|
51b7036456 | ||
|
|
e40fdad251 | ||
|
|
e4b0e97ac0 | ||
|
|
003320ca34 | ||
|
|
780addc50b | ||
|
|
8892a5f43d | ||
|
|
75e52ebca7 | ||
|
|
e7b3bbcd0e | ||
|
|
b9d60639e2 | ||
|
|
2ce0ce65e1 | ||
|
|
a32bdcb06a | ||
|
|
f9f8dff8b4 | ||
|
|
5c5cd1e501 | ||
|
|
89bcd1c4a8 | ||
|
|
6b7b142961 | ||
|
|
565e47aef8 | ||
|
|
dd448cb3ed | ||
|
|
7a9a19e3b9 | ||
|
|
a6e94441aa | ||
|
|
68d66d8ab3 | ||
|
|
aa5cc7ff9f | ||
|
|
eb8ede8376 | ||
|
|
4dc92bae4e | ||
|
|
9e1a975b23 | ||
|
|
e5a26fa0be | ||
|
|
4035a3a1e5 | ||
|
|
0380d286cd | ||
|
|
91dc3aa5d2 | ||
|
|
4bbffcf35e | ||
|
|
bb73eeb7ea | ||
|
|
546f8a12e4 | ||
|
|
3a865d4aaa | ||
|
|
05feb6b1b0 | ||
|
|
efa6fe3ef1 | ||
|
|
e917927979 | ||
|
|
61689ed451 | ||
|
|
b74919c376 | ||
|
|
67cc4f9600 | ||
|
|
ec749de3fa | ||
|
|
b99cd7ed3f | ||
|
|
87b9c955ca | ||
|
|
00aee2771e | ||
|
|
291276494d | ||
|
|
fc3823f0af | ||
|
|
855ef73a9a | ||
|
|
517a8d2104 | ||
|
|
a042795ec1 | ||
|
|
e427d65bf5 | ||
|
|
5ce319456f | ||
|
|
03828bfdaa | ||
|
|
7c836d121c | ||
|
|
b2ee3c5f30 | ||
|
|
04205783b8 | ||
|
|
f27e71fbd8 | ||
|
|
fbca679334 | ||
|
|
5b594ba0b4 | ||
|
|
3d4b5b0c1e | ||
|
|
c7574de7dc | ||
|
|
c8a3bb5f81 | ||
|
|
118edef773 | ||
|
|
050a1aff83 | ||
|
|
9ad74e9cbf | ||
|
|
09f8decc59 | ||
|
|
0decdf156b | ||
|
|
9316655b8c | ||
|
|
1bcc4fd2d7 | ||
|
|
239a4da145 | ||
|
|
d8e66c9b6a | ||
|
|
72279f4995 | ||
|
|
48b21de011 | ||
|
|
f7dc86ab2b | ||
|
|
39bfe6d2cb | ||
|
|
d6f534c3c0 | ||
|
|
45067d0354 | ||
|
|
da1ff63f72 | ||
|
|
5d715c50de | ||
|
|
47072ae1fe | ||
|
|
72f301fa45 | ||
|
|
e9b89629a6 | ||
|
|
3ef5ef166f | ||
|
|
37ca7a706a | ||
|
|
734c65fbda | ||
|
|
145605d628 | ||
|
|
7ac432fbc5 | ||
|
|
0c92cec2ea | ||
|
|
f6a788b36f | ||
|
|
72dffcf46b | ||
|
|
3140cdd148 | ||
|
|
d87adbce63 | ||
|
|
ffdf2bc0cd | ||
|
|
be5d7a1c9f | ||
|
|
94288b5cef | ||
|
|
63abbf5949 | ||
|
|
49214281f7 | ||
|
|
9688dde1a4 | ||
|
|
fdcc31f049 | ||
|
|
55ed6100e0 | ||
|
|
7fb11ba912 | ||
|
|
f3d77fdcf2 | ||
|
|
6489ab6a56 | ||
|
|
bace117ada | ||
|
|
76175d61af | ||
|
|
87110095a0 | ||
|
|
50ba8bec5a | ||
|
|
1741b1c686 | ||
|
|
99097baf9d | ||
|
|
bac1cc8243 | ||
|
|
096489d486 | ||
|
|
9811a9a3e1 | ||
|
|
910cde4380 | ||
|
|
4f1ccf83c8 | ||
|
|
9501c1ce4b | ||
|
|
069f0e53e1 | ||
|
|
aedfba795e | ||
|
|
9f162c0703 | ||
|
|
7b96c46e39 | ||
|
|
3d48ea71b9 | ||
|
|
24ee984a2e | ||
|
|
4255cbe540 | ||
|
|
fe16f24c41 | ||
|
|
a6e1fc5c44 | ||
|
|
8434312728 | ||
|
|
46f641aaec | ||
|
|
c246d8d517 | ||
|
|
4ec1aeafaf | ||
|
|
27cfd04ea7 | ||
|
|
797cfcb98d | ||
|
|
96a9b52e6d | ||
|
|
3b7462070b | ||
|
|
f08dd5960b | ||
|
|
9972196f70 | ||
|
|
ebbf06787c | ||
|
|
1d2b0cb093 | ||
|
|
6f27c6e4aa | ||
|
|
7b6008c37e | ||
|
|
cf5405fbe4 | ||
|
|
b4ec7402fc | ||
|
|
ff9a107a29 | ||
|
|
417ad87bcc | ||
|
|
265f99f327 | ||
|
|
0f8c3caf18 | ||
|
|
3a7677b46d | ||
|
|
3fd64d9956 | ||
|
|
1d26f4b24f | ||
|
|
1a4cea7b4f | ||
|
|
bbb1c42641 | ||
|
|
7ce2320eda | ||
|
|
1729b30f89 | ||
|
|
25d6595d5e | ||
|
|
3b31b9d65b | ||
|
|
e9e64fed3f | ||
|
|
3b88ea9b1c | ||
|
|
623f76fa6c | ||
|
|
19efcb1a0d | ||
|
|
31c8916622 | ||
|
|
7a0b189a1d | ||
|
|
fa0d56d57a | ||
|
|
85670bbc6a | ||
|
|
fc9a85b6ad | ||
|
|
8760b132da | ||
|
|
b80ee8d778 | ||
|
|
c8e168aa3e | ||
|
|
106aef579f | ||
|
|
d7084829c3 | ||
|
|
243d72eed5 | ||
|
|
aa60e97d5d | ||
|
|
4ae12db99c | ||
|
|
98ad058e2e | ||
|
|
58cc381abf | ||
|
|
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 | ||
|
|
58840e2c00 | ||
|
|
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 |
@@ -1,12 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
.DS_Store
|
.DS_Store
|
||||||
static
|
|
||||||
test
|
|
||||||
scripts
|
|
||||||
docs
|
|
||||||
firefox
|
firefox
|
||||||
|
assets
|
||||||
|
docs
|
||||||
public
|
public
|
||||||
views
|
test
|
||||||
webpack
|
|
||||||
frontend
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
public
|
dist
|
||||||
test/frontend/bundle.js
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
|||||||
7
.gitignore
vendored
@@ -1,6 +1,3 @@
|
|||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
node_modules
|
node_modules
|
||||||
static/*
|
dist
|
||||||
!static/info.txt
|
.idea
|
||||||
test/frontend/bundle.js
|
|
||||||
|
|||||||
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
|
||||||
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)
|
||||||
14
CONTRIBUTORS
@@ -2,15 +2,19 @@ Abhinav Adduri
|
|||||||
Alexander Slovesnik
|
Alexander Slovesnik
|
||||||
Amin Mahmudian
|
Amin Mahmudian
|
||||||
Andreas Pettersson
|
Andreas Pettersson
|
||||||
|
Arash Mousavi
|
||||||
Balázs Meskó
|
Balázs Meskó
|
||||||
|
Belayet Hossain
|
||||||
Bjørn I
|
Bjørn I
|
||||||
Boopesh Mahendran
|
Boopesh Mahendran
|
||||||
Chuck Harmston
|
Chuck Harmston
|
||||||
|
Cláudio Esperança
|
||||||
Cynthia Pereira
|
Cynthia Pereira
|
||||||
Daniel Thorn
|
Daniel Thorn
|
||||||
Daniela Arcese
|
Daniela Arcese
|
||||||
Danny Coates
|
Danny Coates
|
||||||
Emin Mastizada
|
Emin Mastizada
|
||||||
|
Enol
|
||||||
Erica
|
Erica
|
||||||
Erica Wright
|
Erica Wright
|
||||||
Fjoerfoks
|
Fjoerfoks
|
||||||
@@ -18,9 +22,13 @@ Francesco Lodolo
|
|||||||
Francesco Lodolo [:flod]
|
Francesco Lodolo [:flod]
|
||||||
Gautam krishna.R
|
Gautam krishna.R
|
||||||
Håvar Henriksen
|
Håvar Henriksen
|
||||||
|
Jae Hyeon Park
|
||||||
|
Jakub Rychlý
|
||||||
|
Jamie
|
||||||
Jim Spentzos
|
Jim Spentzos
|
||||||
Johann-S
|
Johann-S
|
||||||
John Gruen
|
John Gruen
|
||||||
|
Jon Vadillo
|
||||||
Jordi Serratosa
|
Jordi Serratosa
|
||||||
Juraj Cigáň
|
Juraj Cigáň
|
||||||
Kohei Yoshino
|
Kohei Yoshino
|
||||||
@@ -48,12 +56,14 @@ Rok Žerdin
|
|||||||
Sahithi
|
Sahithi
|
||||||
Sairam Raavi
|
Sairam Raavi
|
||||||
Sandro
|
Sandro
|
||||||
|
Schieck :)
|
||||||
Selim Şumlu
|
Selim Şumlu
|
||||||
Slimane Amiri
|
Slimane Amiri
|
||||||
Théo Chevalier
|
Théo Chevalier
|
||||||
Tomáš Zelina
|
Tomáš Zelina
|
||||||
Ton
|
Ton
|
||||||
Tymur Faradzhev
|
Tymur Faradzhev
|
||||||
|
Varghese Thomas
|
||||||
Victor Bychek
|
Victor Bychek
|
||||||
Weihang Lo
|
Weihang Lo
|
||||||
Wil Clouser
|
Wil Clouser
|
||||||
@@ -69,10 +79,14 @@ erdem cobanoglu
|
|||||||
gautamkrishnar
|
gautamkrishnar
|
||||||
goofy
|
goofy
|
||||||
hi
|
hi
|
||||||
|
jesferman1993
|
||||||
|
josotrix
|
||||||
kenrick95
|
kenrick95
|
||||||
manxmensch
|
manxmensch
|
||||||
ravmn
|
ravmn
|
||||||
|
reza.habibi2008
|
||||||
siparon
|
siparon
|
||||||
|
skystar-p
|
||||||
xcffl
|
xcffl
|
||||||
Μιχάλης
|
Μιχάλης
|
||||||
Марко Костић (Marko Kostić)
|
Марко Костић (Marko Kostić)
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ RUN npm install --production && npm cache clean --force
|
|||||||
ENV PORT=1443
|
ENV PORT=1443
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "prod"]
|
||||||
|
|||||||
63
README.md
@@ -5,38 +5,79 @@
|
|||||||
|
|
||||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
**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
|
## What it does
|
||||||
|
|
||||||
A file sharing experiment which allows you to send encrypted files to other users.
|
A file sharing experiment which allows you to send encrypted files to other users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Node.js 8+](https://nodejs.org/)
|
- [Node.js 8.2+](https://nodejs.org/)
|
||||||
- [Redis server](https://redis.io/)
|
- [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
|
```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 |
|
| 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 format` | Formats the frontend and server code using **prettier**.
|
||||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
| `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 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
|
## Localization
|
||||||
|
|
||||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
Pull requests are always welcome! Feel free to check out the list of ["good first issues"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -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/>
|
| Stage | <https://send.stage.mozaws.net/>
|
||||||
| Development | <https://send.dev.mozaws.net/>
|
| Development | <https://send.dev.mozaws.net/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Mozilla Public License Version 2.0](LICENSE)
|
[Mozilla Public License Version 2.0](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
env:
|
env:
|
||||||
browser: true
|
browser: true
|
||||||
node: false
|
node: true
|
||||||
|
|
||||||
parserOptions:
|
parserOptions:
|
||||||
sourceType: module
|
sourceType: module
|
||||||
213
app/api.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
function post(obj) {
|
||||||
|
return {
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(obj)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNonce(header) {
|
||||||
|
header = header || '';
|
||||||
|
return header.split(' ')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, params, keychain) {
|
||||||
|
const result = {};
|
||||||
|
params = params || {};
|
||||||
|
const h = await keychain.authHeader();
|
||||||
|
params.headers = new Headers({ Authorization: h });
|
||||||
|
const response = await fetch(url, params);
|
||||||
|
result.response = response;
|
||||||
|
result.ok = response.ok;
|
||||||
|
const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
|
||||||
|
result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
|
||||||
|
keychain.nonce = nonce;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuthAndRetry(url, params, keychain) {
|
||||||
|
const result = await fetchWithAuth(url, params, keychain);
|
||||||
|
if (result.shouldRetry) {
|
||||||
|
return fetchWithAuth(url, params, keychain);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(id, owner_token) {
|
||||||
|
const response = await fetch(`/api/delete/${id}`, post({ owner_token }));
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setParams(id, owner_token, params) {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/params/${id}`,
|
||||||
|
post({
|
||||||
|
owner_token,
|
||||||
|
dlimit: params.dlimit
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileInfo(id, owner_token) {
|
||||||
|
const response = await fetch(`/api/info/${id}`, post({ owner_token }));
|
||||||
|
if (response.ok) {
|
||||||
|
const obj = await response.json();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function metadata(id, keychain) {
|
||||||
|
const result = await fetchWithAuthAndRetry(
|
||||||
|
`/api/metadata/${id}`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
keychain
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
const data = await result.response.json();
|
||||||
|
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
|
||||||
|
return {
|
||||||
|
size: data.size,
|
||||||
|
ttl: data.ttl,
|
||||||
|
iv: meta.iv,
|
||||||
|
name: meta.name,
|
||||||
|
type: meta.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(result.response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPassword(id, owner_token, keychain) {
|
||||||
|
const auth = await keychain.authKeyB64();
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/password/${id}`,
|
||||||
|
post({ owner_token, auth })
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadFile(encrypted, metadata, verifierB64, keychain) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const upload = {
|
||||||
|
onprogress: function() {},
|
||||||
|
cancel: function() {
|
||||||
|
xhr.abort();
|
||||||
|
},
|
||||||
|
result: new Promise(function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
|
return resolve({
|
||||||
|
url: responseObj.url,
|
||||||
|
id: responseObj.id,
|
||||||
|
ownerToken: responseObj.owner
|
||||||
|
});
|
||||||
|
}
|
||||||
|
reject(new Error(xhr.status));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const dataView = new DataView(encrypted);
|
||||||
|
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('data', blob);
|
||||||
|
xhr.upload.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
upload.onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.open('post', '/api/upload', true);
|
||||||
|
xhr.setRequestHeader('X-File-Metadata', arrayToB64(new Uint8Array(metadata)));
|
||||||
|
xhr.setRequestHeader('Authorization', `send-v1 ${verifierB64}`);
|
||||||
|
xhr.send(fd);
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(id, keychain) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const download = {
|
||||||
|
onprogress: function() {},
|
||||||
|
cancel: function() {
|
||||||
|
xhr.abort();
|
||||||
|
},
|
||||||
|
result: new Promise(async function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status === 404) {
|
||||||
|
return reject(new Error('notfound'));
|
||||||
|
}
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([xhr.response]);
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.readAsArrayBuffer(blob);
|
||||||
|
fileReader.onload = function() {
|
||||||
|
resolve(this.result);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
xhr.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable && event.target.status === 200) {
|
||||||
|
download.onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
xhr.open('get', `/api/download/${id}`);
|
||||||
|
xhr.setRequestHeader('Authorization', auth);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return download;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDownload(id, keychain, onprogress, tries = 1) {
|
||||||
|
const dl = download(id, keychain);
|
||||||
|
dl.onprogress = onprogress;
|
||||||
|
try {
|
||||||
|
const result = await dl.result;
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401' && --tries > 0) {
|
||||||
|
return tryDownload(id, keychain, onprogress, tries);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadFile(id, keychain) {
|
||||||
|
let cancelled = false;
|
||||||
|
function updateProgress(p) {
|
||||||
|
if (cancelled) {
|
||||||
|
// This is a bit of a hack
|
||||||
|
// We piggyback off of the progress event as a chance to cancel.
|
||||||
|
// Otherwise wiring the xhr abort up while allowing retries
|
||||||
|
// gets pretty nasty.
|
||||||
|
// 'this' here is the object returned by download(id, keychain)
|
||||||
|
return this.cancel();
|
||||||
|
}
|
||||||
|
dl.onprogress(p);
|
||||||
|
}
|
||||||
|
const dl = {
|
||||||
|
onprogress: function() {},
|
||||||
|
cancel: function() {
|
||||||
|
cancelled = true;
|
||||||
|
},
|
||||||
|
result: tryDownload(id, keychain, updateProgress, 2)
|
||||||
|
};
|
||||||
|
return dl;
|
||||||
|
}
|
||||||
36
app/dragManager.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
const { bytes } = require('./utils');
|
||||||
|
|
||||||
|
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.uploading) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.querySelector('.upload-window').classList.remove('ondrag');
|
||||||
|
const target = event.dataTransfer;
|
||||||
|
if (target.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.files.length > 1) {
|
||||||
|
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||||
|
}
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
window.alert(
|
||||||
|
state.translate('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emitter.emit('upload', { file, type: 'drop' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
82
app/experiments.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import hash from 'string-hash';
|
||||||
|
|
||||||
|
const experiments = {
|
||||||
|
S9wqVl2SQ4ab2yZtqDI3Dw: {
|
||||||
|
id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
|
||||||
|
run: function(variant, state, emitter) {
|
||||||
|
switch (variant) {
|
||||||
|
case 1:
|
||||||
|
state.promo = 'blue';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
state.promo = 'pink';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state.promo = 'grey';
|
||||||
|
}
|
||||||
|
emitter.emit('render');
|
||||||
|
},
|
||||||
|
eligible: function() {
|
||||||
|
return (
|
||||||
|
!/firefox/i.test(navigator.userAgent) &&
|
||||||
|
document.querySelector('html').lang === 'en-US'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
variant: function(state) {
|
||||||
|
const n = this.luckyNumber(state);
|
||||||
|
if (n < 0.33) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return n < 0.66 ? 1 : 2;
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
221
app/fileManager.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import FileSender from './fileSender';
|
||||||
|
import FileReceiver from './fileReceiver';
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
delay,
|
||||||
|
fadeOut,
|
||||||
|
openLinksInNewTab,
|
||||||
|
percent,
|
||||||
|
saveFile
|
||||||
|
} from './utils';
|
||||||
|
import * as metrics from './metrics';
|
||||||
|
|
||||||
|
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 oldLimit = file.dlimit;
|
||||||
|
const oldTotal = file.dtotal;
|
||||||
|
await file.updateDownloadCount();
|
||||||
|
if (file.dtotal === file.dlimit) {
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
rerender = true;
|
||||||
|
} else if (oldLimit !== file.dlimit || oldTotal !== file.dtotal) {
|
||||||
|
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 file.changeLimit(value);
|
||||||
|
state.storage.writeFile(file);
|
||||||
|
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 file.del();
|
||||||
|
} catch (e) {
|
||||||
|
state.raven.captureException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
state.uploading = true;
|
||||||
|
render();
|
||||||
|
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
await delay(200);
|
||||||
|
try {
|
||||||
|
metrics.startedUpload({ size, type });
|
||||||
|
const ownedFile = await sender.upload();
|
||||||
|
ownedFile.type = type;
|
||||||
|
state.storage.totalUploads += 1;
|
||||||
|
metrics.completedUpload(ownedFile);
|
||||||
|
|
||||||
|
state.storage.addFile(ownedFile);
|
||||||
|
|
||||||
|
document.getElementById('cancel-upload').hidden = 'hidden';
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('upload-progress');
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
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');
|
||||||
|
} finally {
|
||||||
|
state.uploading = false;
|
||||||
|
state.transfer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('password', async ({ password, file }) => {
|
||||||
|
try {
|
||||||
|
await file.setPassword(password);
|
||||||
|
state.storage.writeFile(file);
|
||||||
|
metrics.addedPassword({ size: file.size });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('getMetadata', async () => {
|
||||||
|
const file = state.fileInfo;
|
||||||
|
const receiver = new FileReceiver(file);
|
||||||
|
try {
|
||||||
|
await receiver.getMetadata();
|
||||||
|
receiver.on('progress', updateProgress);
|
||||||
|
receiver.on('decrypting', render);
|
||||||
|
state.transfer = receiver;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401') {
|
||||||
|
file.password = null;
|
||||||
|
if (!file.requiresPassword) {
|
||||||
|
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();
|
||||||
|
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.reset();
|
||||||
|
metrics.completedDownload({ size, time, speed });
|
||||||
|
emitter.emit('pushState', '/completed');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === '0') {
|
||||||
|
// download cancelled
|
||||||
|
state.transfer.reset();
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
console.error(err);
|
||||||
|
state.transfer = null;
|
||||||
|
const location = err.message === 'notfound' ? '/404' : '/error';
|
||||||
|
if (location === '/error') {
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedDownload({ size, err });
|
||||||
|
}
|
||||||
|
emitter.emit('pushState', location);
|
||||||
|
} finally {
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('copy', ({ url, location }) => {
|
||||||
|
copyToClipboard(url);
|
||||||
|
metrics.copiedLink({ location });
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for updates of the download counts
|
||||||
|
// TODO something for the share page: || state.route === '/share/:id'
|
||||||
|
if (state.route === '/') {
|
||||||
|
checkFiles();
|
||||||
|
}
|
||||||
|
}, 2 * 60 * 1000);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for rerendering the file list countdown timers
|
||||||
|
if (
|
||||||
|
state.route === '/' &&
|
||||||
|
state.storage.files.length > 0 &&
|
||||||
|
Date.now() - lastRender > 30000
|
||||||
|
) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
89
app/fileReceiver.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import Nanobus from 'nanobus';
|
||||||
|
import Keychain from './keychain';
|
||||||
|
import { bytes } from './utils';
|
||||||
|
import { metadata, downloadFile } from './api';
|
||||||
|
|
||||||
|
export default class FileReceiver extends Nanobus {
|
||||||
|
constructor(fileInfo) {
|
||||||
|
super('FileReceiver');
|
||||||
|
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
|
||||||
|
if (fileInfo.requiresPassword) {
|
||||||
|
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||||
|
}
|
||||||
|
this.fileInfo = fileInfo;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.fileDownload) {
|
||||||
|
this.fileDownload.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.fileDownload = null;
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.state = 'initialized';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata() {
|
||||||
|
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||||
|
if (meta) {
|
||||||
|
this.keychain.setIV(meta.iv);
|
||||||
|
this.fileInfo.name = meta.name;
|
||||||
|
this.fileInfo.type = meta.type;
|
||||||
|
this.fileInfo.iv = meta.iv;
|
||||||
|
this.fileInfo.size = meta.size;
|
||||||
|
this.state = 'ready';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state = 'invalid';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async download() {
|
||||||
|
this.state = 'downloading';
|
||||||
|
this.emit('progress', this.progress);
|
||||||
|
try {
|
||||||
|
const download = await downloadFile(this.fileInfo.id, this.keychain);
|
||||||
|
download.onprogress = p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress', p);
|
||||||
|
};
|
||||||
|
this.fileDownload = download;
|
||||||
|
const ciphertext = await download.result;
|
||||||
|
this.fileDownload = null;
|
||||||
|
this.msg = 'decryptingFile';
|
||||||
|
this.state = 'decrypting';
|
||||||
|
this.emit('decrypting');
|
||||||
|
const plaintext = await this.keychain.decryptFile(ciphertext);
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.msg = 'downloadFinish';
|
||||||
|
this.state = 'complete';
|
||||||
|
return {
|
||||||
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.fileInfo.name),
|
||||||
|
type: this.fileInfo.type
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
this.state = 'invalid';
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/fileSender.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
import Nanobus from 'nanobus';
|
||||||
|
import OwnedFile from './ownedFile';
|
||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64, bytes } from './utils';
|
||||||
|
import { uploadFile } from './api';
|
||||||
|
|
||||||
|
export default class FileSender extends Nanobus {
|
||||||
|
constructor(file) {
|
||||||
|
super('FileSender');
|
||||||
|
this.file = file;
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.msg = 'importingFile';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
|
this.keychain = new Keychain();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.uploadRequest) {
|
||||||
|
this.uploadRequest.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(this.file);
|
||||||
|
// TODO: progress?
|
||||||
|
reader.onload = function(event) {
|
||||||
|
const plaintext = new Uint8Array(this.result);
|
||||||
|
resolve(plaintext);
|
||||||
|
};
|
||||||
|
reader.onerror = function(err) {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload() {
|
||||||
|
const start = Date.now();
|
||||||
|
const plaintext = await this.readFile();
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.msg = 'encryptingFile';
|
||||||
|
this.emit('encrypting');
|
||||||
|
const encrypted = await this.keychain.encryptFile(plaintext);
|
||||||
|
const metadata = await this.keychain.encryptMetadata(this.file);
|
||||||
|
const authKeyB64 = await this.keychain.authKeyB64();
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.uploadRequest = uploadFile(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
authKeyB64,
|
||||||
|
this.keychain
|
||||||
|
);
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.uploadRequest.onprogress = p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress', p);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const result = await this.uploadRequest.result;
|
||||||
|
const time = Date.now() - start;
|
||||||
|
this.msg = 'notifyUploadDone';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.progress = [1, 1];
|
||||||
|
const secretKey = arrayToB64(this.keychain.rawSecret);
|
||||||
|
const ownedFile = new OwnedFile({
|
||||||
|
id: result.id,
|
||||||
|
url: `${result.url}#${secretKey}`,
|
||||||
|
name: this.file.name,
|
||||||
|
size: this.file.size,
|
||||||
|
time: time,
|
||||||
|
speed: this.file.size / (time / 1000),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + EXPIRE_SECONDS * 1000,
|
||||||
|
secretKey: secretKey,
|
||||||
|
nonce: this.keychain.nonce,
|
||||||
|
ownerToken: result.ownerToken
|
||||||
|
});
|
||||||
|
return ownedFile;
|
||||||
|
} catch (e) {
|
||||||
|
this.msg = 'errorPageHeader';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
209
app/keychain.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
export default class Keychain {
|
||||||
|
constructor(secretKeyB64, nonce, ivB64) {
|
||||||
|
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
|
||||||
|
if (ivB64) {
|
||||||
|
this.iv = b64ToArray(ivB64);
|
||||||
|
} else {
|
||||||
|
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
}
|
||||||
|
if (secretKeyB64) {
|
||||||
|
this.rawSecret = b64ToArray(secretKeyB64);
|
||||||
|
} else {
|
||||||
|
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
}
|
||||||
|
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.rawSecret,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('encryption'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('metadata'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: { name: 'SHA-256' }
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get nonce() {
|
||||||
|
return this._nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
set nonce(n) {
|
||||||
|
if (n && n !== this._nonce) {
|
||||||
|
this._nonce = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIV(ivB64) {
|
||||||
|
this.iv = b64ToArray(ivB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword(password, shareUrl) {
|
||||||
|
this.authKeyPromise = window.crypto.subtle
|
||||||
|
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
|
||||||
|
'deriveKey'
|
||||||
|
])
|
||||||
|
.then(passwordKey =>
|
||||||
|
window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: encoder.encode(shareUrl),
|
||||||
|
iterations: 100,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
passwordKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthKey(authKeyB64) {
|
||||||
|
this.authKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(authKeyB64),
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authKeyB64() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
||||||
|
return arrayToB64(new Uint8Array(rawAuth));
|
||||||
|
}
|
||||||
|
|
||||||
|
async authHeader() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const sig = await window.crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(this.nonce)
|
||||||
|
);
|
||||||
|
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptFile(plaintext) {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptMetadata(metadata) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
iv: arrayToB64(this.iv),
|
||||||
|
name: metadata.name,
|
||||||
|
type: metadata.type || 'application/octet-stream'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptFile(ciphertext) {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptMetadata(ciphertext) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/main.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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) => {
|
||||||
|
state.transfer = null;
|
||||||
|
state.fileInfo = null;
|
||||||
|
state.translate = locale.getTranslator();
|
||||||
|
state.storage = storage;
|
||||||
|
state.raven = Raven;
|
||||||
|
window.appState = state;
|
||||||
|
emitter.on('DOMContentLoaded', async function checkSupport() {
|
||||||
|
let unsupportedReason = null;
|
||||||
|
if (
|
||||||
|
// Firefox < 50
|
||||||
|
/firefox/i.test(navigator.userAgent) &&
|
||||||
|
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) < 50
|
||||||
|
) {
|
||||||
|
unsupportedReason = 'outdated';
|
||||||
|
}
|
||||||
|
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||||
|
unsupportedReason = 'edge';
|
||||||
|
}
|
||||||
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
|
if (!ok) {
|
||||||
|
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
||||||
|
? 'outdated'
|
||||||
|
: 'gcm';
|
||||||
|
}
|
||||||
|
if (unsupportedReason) {
|
||||||
|
setTimeout(() =>
|
||||||
|
emitter.emit('replaceState', `/unsupported/${unsupportedReason}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(metrics);
|
||||||
|
app.use(fileManager);
|
||||||
|
app.use(dragManager);
|
||||||
|
app.use(experiments);
|
||||||
|
|
||||||
|
app.mount('body');
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||||
import Storage from './storage';
|
import storage from './storage';
|
||||||
const storage = new Storage();
|
|
||||||
|
|
||||||
let hasLocalStorage = false;
|
let hasLocalStorage = false;
|
||||||
try {
|
try {
|
||||||
hasLocalStorage = !!localStorage;
|
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't care
|
// when disabled, any mention of localStorage throws an error
|
||||||
}
|
}
|
||||||
|
|
||||||
const analytics = new testPilotGA({
|
const analytics = new testPilotGA({
|
||||||
@@ -15,19 +14,47 @@ const analytics = new testPilotGA({
|
|||||||
tid: window.GOOGLE_ANALYTICS_ID
|
tid: window.GOOGLE_ANALYTICS_ID
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = location.pathname.includes('/download')
|
let appState = null;
|
||||||
? 'recipient'
|
let experiment = null;
|
||||||
: 'sender';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
export default function initialize(state, emitter) {
|
||||||
addExitHandlers();
|
appState = state;
|
||||||
addRestartHandlers();
|
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', exitEvent);
|
||||||
|
emitter.on('experiment', experimentEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
function sendEvent() {
|
||||||
|
const args = Array.from(arguments);
|
||||||
|
if (experiment && args[2]) {
|
||||||
|
args[2].xid = experiment[0];
|
||||||
|
args[2].xvar = experiment[1];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
hasLocalStorage &&
|
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||||
analytics.sendEvent.apply(analytics, arguments).catch(() => 0)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,17 +83,19 @@ function urlToMetric(url) {
|
|||||||
case 'https://testpilot.firefox.com/':
|
case 'https://testpilot.firefox.com/':
|
||||||
case 'https://testpilot.firefox.com/experiments/send':
|
case 'https://testpilot.firefox.com/experiments/send':
|
||||||
return 'testpilot';
|
return 'testpilot';
|
||||||
|
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
||||||
|
return 'promo';
|
||||||
default:
|
default:
|
||||||
return 'other';
|
return 'other';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setReferrer(state) {
|
function setReferrer(state) {
|
||||||
if (category === 'sender') {
|
if (category() === 'sender') {
|
||||||
if (state) {
|
if (state) {
|
||||||
storage.referrer = `${state}-upload`;
|
storage.referrer = `${state}-upload`;
|
||||||
}
|
}
|
||||||
} else if (category === 'recipient') {
|
} else if (category() === 'recipient') {
|
||||||
if (state) {
|
if (state) {
|
||||||
storage.referrer = `${state}-download`;
|
storage.referrer = `${state}-download`;
|
||||||
}
|
}
|
||||||
@@ -87,10 +116,10 @@ function takeReferrer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startedUpload(params) {
|
function startedUpload(params) {
|
||||||
return sendEvent(category, 'upload-started', {
|
return sendEvent('sender', 'upload-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles + 1,
|
cm6: storage.files.length + 1,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd5: takeReferrer()
|
cd5: takeReferrer()
|
||||||
@@ -99,10 +128,10 @@ function startedUpload(params) {
|
|||||||
|
|
||||||
function cancelledUpload(params) {
|
function cancelledUpload(params) {
|
||||||
setReferrer('cancelled');
|
setReferrer('cancelled');
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'cancelled'
|
cd2: 'cancelled'
|
||||||
@@ -110,33 +139,42 @@ function cancelledUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completedUpload(params) {
|
function completedUpload(params) {
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'completed'
|
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) {
|
function startedDownload(params) {
|
||||||
return sendEvent(category, 'download-started', {
|
return sendEvent('recipient', 'download-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm4: params.ttl,
|
cm4: params.ttl,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads
|
cm7: storage.totalDownloads
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedDownload(params) {
|
function stoppedDownload(params) {
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'errored',
|
cd2: 'errored',
|
||||||
cd6: params.err
|
cd6: params.err
|
||||||
@@ -145,20 +183,20 @@ function stoppedDownload(params) {
|
|||||||
|
|
||||||
function cancelledDownload(params) {
|
function cancelledDownload(params) {
|
||||||
setReferrer('cancelled');
|
setReferrer('cancelled');
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'cancelled'
|
cd2: 'cancelled'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedUpload(params) {
|
function stoppedUpload(params) {
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'errored',
|
cd2: 'errored',
|
||||||
@@ -166,26 +204,36 @@ function stoppedUpload(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changedDownloadLimit(params) {
|
||||||
|
return sendEvent('sender', 'download-limit-changed', {
|
||||||
|
cm1: params.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads,
|
||||||
|
cm8: params.dlimit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function completedDownload(params) {
|
function completedDownload(params) {
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'completed'
|
cd2: 'completed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletedUpload(params) {
|
function deletedUpload(params) {
|
||||||
return sendEvent(category, 'upload-deleted', {
|
return sendEvent(category(), 'upload-deleted', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm4: params.ttl,
|
cm4: params.ttl,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd4: params.location
|
cd4: params.location
|
||||||
@@ -193,23 +241,28 @@ function deletedUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unsupported(params) {
|
function unsupported(params) {
|
||||||
return sendEvent(category, 'unsupported', {
|
return sendEvent(category(), 'unsupported', {
|
||||||
cd6: params.err
|
cd6: params.err
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copiedLink(params) {
|
function copiedLink(params) {
|
||||||
return sendEvent(category, 'copied', {
|
return sendEvent('sender', 'copied', {
|
||||||
cd4: params.location
|
cd4: params.location
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitEvent(target) {
|
function exitEvent(target) {
|
||||||
return sendEvent(category, 'exited', {
|
return sendEvent(category(), 'exited', {
|
||||||
cd3: urlToMetric(target.currentTarget.href)
|
cd3: urlToMetric(target.currentTarget.href)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function experimentEvent(params) {
|
||||||
|
return sendEvent(category(), 'experiment', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function addExitHandlers() {
|
function addExitHandlers() {
|
||||||
const links = Array.from(document.querySelectorAll('a'));
|
const links = Array.from(document.querySelectorAll('a'));
|
||||||
links.forEach(l => {
|
links.forEach(l => {
|
||||||
@@ -219,31 +272,26 @@ function addExitHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartEvent(state) {
|
function restart(state) {
|
||||||
setReferrer(state);
|
setReferrer(state);
|
||||||
return sendEvent(category, 'restarted', {
|
return sendEvent(category(), 'restarted', {
|
||||||
cd2: state
|
cd2: state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRestartHandlers() {
|
|
||||||
const elements = Array.from(document.querySelectorAll('.send-new'));
|
|
||||||
elements.forEach(el => {
|
|
||||||
const state = el.getAttribute('data-state');
|
|
||||||
el.addEventListener('click', restartEvent.bind(null, state));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
copiedLink,
|
copiedLink,
|
||||||
startedUpload,
|
startedUpload,
|
||||||
cancelledUpload,
|
cancelledUpload,
|
||||||
stoppedUpload,
|
stoppedUpload,
|
||||||
completedUpload,
|
completedUpload,
|
||||||
|
changedDownloadLimit,
|
||||||
deletedUpload,
|
deletedUpload,
|
||||||
startedDownload,
|
startedDownload,
|
||||||
cancelledDownload,
|
cancelledDownload,
|
||||||
stoppedDownload,
|
stoppedDownload,
|
||||||
completedDownload,
|
completedDownload,
|
||||||
|
addedPassword,
|
||||||
|
restart,
|
||||||
unsupported
|
unsupported
|
||||||
};
|
};
|
||||||
77
app/ownedFile.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64 } from './utils';
|
||||||
|
import { del, fileInfo, setParams, setPassword } from './api';
|
||||||
|
|
||||||
|
export default class OwnedFile {
|
||||||
|
constructor(obj) {
|
||||||
|
this.id = obj.id;
|
||||||
|
this.url = obj.url;
|
||||||
|
this.name = obj.name;
|
||||||
|
this.size = obj.size;
|
||||||
|
this.type = obj.type;
|
||||||
|
this.time = obj.time;
|
||||||
|
this.speed = obj.speed;
|
||||||
|
this.createdAt = obj.createdAt;
|
||||||
|
this.expiresAt = obj.expiresAt;
|
||||||
|
this.ownerToken = obj.ownerToken;
|
||||||
|
this.dlimit = obj.dlimit || 1;
|
||||||
|
this.dtotal = obj.dtotal || 0;
|
||||||
|
this.keychain = new Keychain(obj.secretKey);
|
||||||
|
this._hasPassword = !!obj.hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(password) {
|
||||||
|
this.password = password;
|
||||||
|
this._hasPassword = true;
|
||||||
|
this.keychain.setPassword(password, this.url);
|
||||||
|
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
del() {
|
||||||
|
return del(this.id, this.ownerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLimit(dlimit) {
|
||||||
|
if (this.dlimit !== dlimit) {
|
||||||
|
this.dlimit = dlimit;
|
||||||
|
return setParams(this.id, this.ownerToken, { dlimit });
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPassword() {
|
||||||
|
return !!this._hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDownloadCount() {
|
||||||
|
try {
|
||||||
|
const result = await fileInfo(this.id, this.ownerToken);
|
||||||
|
this.dtotal = result.dtotal;
|
||||||
|
this.dlimit = result.dlimit;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '404') {
|
||||||
|
this.dtotal = this.dlimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
name: this.name,
|
||||||
|
size: this.size,
|
||||||
|
type: this.type,
|
||||||
|
time: this.time,
|
||||||
|
speed: this.speed,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
expiresAt: this.expiresAt,
|
||||||
|
secretKey: arrayToB64(this.keychain.rawSecret),
|
||||||
|
ownerToken: this.ownerToken,
|
||||||
|
dlimit: this.dlimit,
|
||||||
|
dtotal: this.dtotal,
|
||||||
|
hasPassword: this.hasPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
6
app/pages/blank.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
const div = html`<div></div>`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
34
app/pages/completed.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../templates/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;
|
||||||
|
};
|
||||||
46
app/pages/download.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../templates/progress');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
const cancelBtn = html`
|
||||||
|
<button
|
||||||
|
id="cancel-upload"
|
||||||
|
title="${state.translate('deletePopupCancel')}"
|
||||||
|
onclick=${cancel}>
|
||||||
|
${state.translate('deletePopupCancel')}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download">
|
||||||
|
<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>
|
||||||
|
${transfer.state === 'downloading' ? cancelBtn : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel-upload');
|
||||||
|
btn.remove();
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
10
app/pages/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>`;
|
||||||
|
};
|
||||||
34
app/pages/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/pages/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;
|
||||||
|
};
|
||||||
44
app/pages/preview.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, pageAction) {
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
|
||||||
|
const size = fileInfo.size
|
||||||
|
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const title = fileInfo.name
|
||||||
|
? state.translate('downloadFileName', { filename: fileInfo.name })
|
||||||
|
: state.translate('downloadFileTitle');
|
||||||
|
|
||||||
|
const info = html`
|
||||||
|
<div id="dl-file"
|
||||||
|
data-nonce="${fileInfo.nonce}"
|
||||||
|
data-requires-password="${fileInfo.requiresPassword}"></div>`;
|
||||||
|
if (!pageAction) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download">
|
||||||
|
<div id="download-page-one">
|
||||||
|
<div class="title">
|
||||||
|
<span>${title}</span>
|
||||||
|
<span id="dl-filesize">${' ' + size}</span>
|
||||||
|
</div>
|
||||||
|
<div class="description">${state.translate('downloadMessage')}</div>
|
||||||
|
<img
|
||||||
|
src="${assets.get('illustration_download.svg')}"
|
||||||
|
id="download-img"
|
||||||
|
title="${state.translate('downloadAltText')}"/>
|
||||||
|
${pageAction}
|
||||||
|
</div>
|
||||||
|
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||||
|
</div>
|
||||||
|
${info}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
126
app/pages/share.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const notFound = require('./notFound');
|
||||||
|
const uploadPasswordSet = require('../templates/uploadPasswordSet');
|
||||||
|
const uploadPasswordUnset = require('../templates/uploadPasswordUnset');
|
||||||
|
const selectbox = require('../templates/selectbox');
|
||||||
|
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||||
|
|
||||||
|
function expireInfo(file, translate, emit) {
|
||||||
|
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
|
||||||
|
const el = html([
|
||||||
|
`<div>${translate('expireInfo', {
|
||||||
|
downloadCount: '<select></select>',
|
||||||
|
timespan: translate('timespanHours', { num: hours })
|
||||||
|
})}</div>`
|
||||||
|
]);
|
||||||
|
const select = el.querySelector('select');
|
||||||
|
const options = [1, 2, 3, 4, 5, 20].filter(i => i > (file.dtotal || 0));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordSection = file.hasPassword
|
||||||
|
? uploadPasswordSet(state, emit)
|
||||||
|
: uploadPasswordUnset(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=${showPopup}>${state.translate('deleteFileButton')}
|
||||||
|
</button>
|
||||||
|
<div id="deletePopup" 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>
|
||||||
|
<a class="send-new"
|
||||||
|
data-state="completed"
|
||||||
|
href="/"
|
||||||
|
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const popupText = document.querySelector('.popuptext');
|
||||||
|
popupText.classList.add('show');
|
||||||
|
popupText.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const popupText = document.querySelector('.popuptext');
|
||||||
|
popupText.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
52
app/pages/unsupported.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
41
app/pages/upload.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../templates/progress');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
|
||||||
|
const div = html`
|
||||||
|
<div id="download">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel-upload');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = state.translate('uploadCancelNotification');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
82
app/pages/welcome.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const fileList = require('../templates/fileList');
|
||||||
|
const { bytes, 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;
|
||||||
|
}
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
window.alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fadeOut('page-one');
|
||||||
|
emit('upload', { file, type: 'click' });
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
60
app/routes/download.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const preview = require('../pages/preview');
|
||||||
|
const download = require('../pages/download');
|
||||||
|
const notFound = require('../pages/notFound');
|
||||||
|
const downloadPassword = require('../templates/downloadPassword');
|
||||||
|
const downloadButton = require('../templates/downloadButton');
|
||||||
|
|
||||||
|
function hasFileInfo() {
|
||||||
|
return !!document.getElementById('dl-file');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileInfoFromDOM() {
|
||||||
|
const el = document.getElementById('dl-file');
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nonce: el.getAttribute('data-nonce'),
|
||||||
|
requiresPassword: !!+el.getAttribute('data-requires-password')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFileInfo(state) {
|
||||||
|
const metadata = getFileInfoFromDOM();
|
||||||
|
return {
|
||||||
|
id: state.params.id,
|
||||||
|
secretKey: state.params.key,
|
||||||
|
nonce: metadata.nonce,
|
||||||
|
requiresPassword: metadata.requiresPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (!state.fileInfo) {
|
||||||
|
// This is a fresh page load
|
||||||
|
// We need to parse the file info from the server's html
|
||||||
|
if (!hasFileInfo()) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
state.fileInfo = createFileInfo(state);
|
||||||
|
|
||||||
|
if (!state.fileInfo.requiresPassword) {
|
||||||
|
emit('getMetadata');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageAction = null; //default state: we don't have file metadata
|
||||||
|
if (state.transfer) {
|
||||||
|
const s = state.transfer.state;
|
||||||
|
if (['downloading', 'decrypting', 'complete'].indexOf(s) > -1) {
|
||||||
|
// Downloading is in progress
|
||||||
|
return download(state, emit);
|
||||||
|
}
|
||||||
|
// we have file metadata
|
||||||
|
pageAction = downloadButton(state, emit);
|
||||||
|
} else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
|
||||||
|
// we're waiting on the user for a valid password
|
||||||
|
pageAction = downloadPassword(state, emit);
|
||||||
|
}
|
||||||
|
return preview(state, pageAction);
|
||||||
|
};
|
||||||
9
app/routes/home.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const welcome = require('../pages/welcome');
|
||||||
|
const upload = require('../pages/upload');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.uploading) {
|
||||||
|
return upload(state, emit);
|
||||||
|
}
|
||||||
|
return welcome(state, emit);
|
||||||
|
};
|
||||||
54
app/routes/index.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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 banner(state, emit) {
|
||||||
|
if (state.promo && !state.route.startsWith('/unsupported/')) {
|
||||||
|
return fxPromo(state, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function body(template) {
|
||||||
|
return function(state, emit) {
|
||||||
|
const b = html`<body>
|
||||||
|
${banner(state, emit)}
|
||||||
|
${header(state)}
|
||||||
|
<div class="all">
|
||||||
|
<noscript>
|
||||||
|
<h2>${state.translate('javascriptRequired')}</h2>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">
|
||||||
|
${state.translate('whyJavascript')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>${state.translate('enableJavascript')}</p>
|
||||||
|
</noscript>
|
||||||
|
${template(state, emit)}
|
||||||
|
</div>
|
||||||
|
${footer(state)}
|
||||||
|
</body>`;
|
||||||
|
if (state.layout) {
|
||||||
|
// server side only
|
||||||
|
return state.layout(state, b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.route('/', body(require('./home')));
|
||||||
|
app.route('/share/:id', body(require('../pages/share')));
|
||||||
|
app.route('/download/:id', body(download));
|
||||||
|
app.route('/download/:id/:key', body(download));
|
||||||
|
app.route('/completed', body(require('../pages/completed')));
|
||||||
|
app.route('/unsupported/:reason', body(require('../pages/unsupported')));
|
||||||
|
app.route('/legal', body(require('../pages/legal')));
|
||||||
|
app.route('/error', body(require('../pages/error')));
|
||||||
|
app.route('/blank', body(require('../pages/blank')));
|
||||||
|
app.route('*', body(require('../pages/notFound')));
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isFile } from './utils';
|
import { isFile } from './utils';
|
||||||
|
import OwnedFile from './ownedFile';
|
||||||
|
|
||||||
class Mem {
|
class Mem {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -26,13 +27,34 @@ class Mem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Storage {
|
class Storage {
|
||||||
constructor() {
|
constructor() {
|
||||||
try {
|
try {
|
||||||
this.engine = localStorage || new Mem();
|
this.engine = localStorage || new Mem();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.engine = new Mem();
|
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 = new OwnedFile(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() {
|
get totalDownloads() {
|
||||||
@@ -53,51 +75,50 @@ export default class Storage {
|
|||||||
set referrer(str) {
|
set referrer(str) {
|
||||||
this.engine.setItem('referrer', str);
|
this.engine.setItem('referrer', str);
|
||||||
}
|
}
|
||||||
|
get enrolled() {
|
||||||
get files() {
|
return JSON.parse(this.engine.getItem('experiments') || '[]');
|
||||||
const fs = [];
|
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
|
||||||
const k = this.engine.key(i);
|
|
||||||
if (isFile(k)) {
|
|
||||||
try {
|
|
||||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
|
||||||
} catch (err) {
|
|
||||||
// obviously you're not a golfer
|
|
||||||
this.engine.removeItem(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fs.sort((file1, file2) => {
|
|
||||||
const creationDate1 = new Date(file1.creationDate);
|
|
||||||
const creationDate2 = new Date(file2.creationDate);
|
|
||||||
return creationDate1 - creationDate2;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get numFiles() {
|
enroll(id, variant) {
|
||||||
let length = 0;
|
const enrolled = this.enrolled;
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
// eslint-disable-next-line no-unused-vars
|
||||||
const k = this.engine.key(i);
|
if (!enrolled.find(([i, v]) => i === id)) {
|
||||||
if (isFile(k)) {
|
enrolled.push([id, variant]);
|
||||||
length += 1;
|
this.engine.setItem('experiments', JSON.stringify(enrolled));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return length;
|
}
|
||||||
|
|
||||||
|
get files() {
|
||||||
|
return this._files;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileById(id) {
|
getFileById(id) {
|
||||||
try {
|
return this._files.find(f => f.id === id);
|
||||||
return JSON.parse(this.engine.getItem(id));
|
}
|
||||||
} catch (e) {
|
|
||||||
return null;
|
get(id) {
|
||||||
}
|
return this.engine.getItem(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
|
if (isFile(property)) {
|
||||||
|
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||||
|
}
|
||||||
this.engine.removeItem(property);
|
this.engine.removeItem(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFile(id, file) {
|
addFile(file) {
|
||||||
this.engine.setItem(id, JSON.stringify(file));
|
this._files.push(file);
|
||||||
|
this.writeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(file) {
|
||||||
|
this.engine.setItem(file.id, JSON.stringify(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFiles() {
|
||||||
|
this._files.forEach(f => this.writeFile(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new Storage();
|
||||||
16
app/templates/downloadButton.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
function download(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('download', state.fileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<button id="download-btn"
|
||||||
|
class="btn"
|
||||||
|
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
59
app/templates/downloadPassword.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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"
|
||||||
|
autofocus />
|
||||||
|
<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('getMetadata');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
99
app/templates/file.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
function timeLeft(milliseconds, state) {
|
||||||
|
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours >= 1) {
|
||||||
|
return state.translate('expiresHoursMinutes', {
|
||||||
|
hours,
|
||||||
|
minutes: minutes % 60
|
||||||
|
});
|
||||||
|
} else if (hours === 0) {
|
||||||
|
if (minutes === 0) {
|
||||||
|
return state.translate('expiresMinutes', { minutes: '< 1' });
|
||||||
|
}
|
||||||
|
return state.translate('expiresMinutes', { minutes });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const ttl = file.expiresAt - Date.now();
|
||||||
|
const remainingTime =
|
||||||
|
timeLeft(ttl, state) || state.translate('linkExpiredAlt');
|
||||||
|
const downloadLimit = file.dlimit || 1;
|
||||||
|
const totalDownloads = file.dtotal || 0;
|
||||||
|
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 class="overflow-col">${remainingTime}</td>
|
||||||
|
<td class="center-col">${totalDownloads} / ${downloadLimit}</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;
|
||||||
|
};
|
||||||
37
app/templates/fileList.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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-time-file-list" >
|
||||||
|
${state.translate('timeFileList')}
|
||||||
|
</th>
|
||||||
|
<th id="expiry-downloads-file-list" >
|
||||||
|
${state.translate('downloadsFileList')}
|
||||||
|
</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>
|
||||||
|
`;
|
||||||
|
};
|
||||||
43
app/templates/footer.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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>`;
|
||||||
|
};
|
||||||
33
app/templates/fxPromo.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
function clicked() {
|
||||||
|
emit('experiment', { cd3: 'promo' });
|
||||||
|
}
|
||||||
|
let classes = 'banner';
|
||||||
|
switch (state.promo) {
|
||||||
|
case 'blue':
|
||||||
|
classes = 'banner banner-blue';
|
||||||
|
break;
|
||||||
|
case 'pink':
|
||||||
|
classes = 'banner banner-pink';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
};
|
||||||
59
app/templates/header.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
function browserName() {
|
||||||
|
try {
|
||||||
|
if (/firefox/i.test(navigator.userAgent)) {
|
||||||
|
return 'firefox';
|
||||||
|
}
|
||||||
|
if (/edge/i.test(navigator.userAgent)) {
|
||||||
|
return 'edge';
|
||||||
|
}
|
||||||
|
if (/trident/i.test(navigator.userAgent)) {
|
||||||
|
return 'ie';
|
||||||
|
}
|
||||||
|
if (/chrome/i.test(navigator.userAgent)) {
|
||||||
|
return 'chrome';
|
||||||
|
}
|
||||||
|
if (/safari/i.test(navigator.userAgent)) {
|
||||||
|
return 'safari';
|
||||||
|
}
|
||||||
|
return 'other';
|
||||||
|
} catch (e) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = browserName();
|
||||||
|
|
||||||
|
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}&browser=${browser}"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="feedback"
|
||||||
|
target="_blank">${state.translate('siteFeedback')}</a>
|
||||||
|
</header>`;
|
||||||
|
};
|
||||||
41
app/templates/progress.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
54
app/templates/selectbox.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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>`;
|
||||||
|
};
|
||||||
80
app/templates/uploadPasswordSet.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
|
||||||
|
return html`<div class="selectPassword">
|
||||||
|
${passwordSpan(file.password)}
|
||||||
|
<button
|
||||||
|
id="resetButton"
|
||||||
|
onclick=${toggleResetInput}
|
||||||
|
>${state.translate('changePasswordButton')}</button>
|
||||||
|
<form
|
||||||
|
id='reset-form'
|
||||||
|
class="setPassword hidden"
|
||||||
|
onsubmit=${resetPassword}
|
||||||
|
data-no-csrf>
|
||||||
|
<input id="unlock-reset-input"
|
||||||
|
class="unlock-input input-no-btn"
|
||||||
|
maxlength="32"
|
||||||
|
autocomplete="off"
|
||||||
|
type="password"
|
||||||
|
oninput=${inputChanged}
|
||||||
|
placeholder="${state.translate('unlockInputPlaceholder')}">
|
||||||
|
<input type="submit"
|
||||||
|
id="unlock-reset-btn"
|
||||||
|
class="btn btn-hidden"
|
||||||
|
value="${state.translate('changePasswordButton')}"/>
|
||||||
|
</form>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function passwordSpan(password) {
|
||||||
|
password = password || '●●●●●';
|
||||||
|
const span = html([
|
||||||
|
`<span>${state.translate('passwordResult', {
|
||||||
|
password:
|
||||||
|
'<pre class="passwordOriginal"></pre><pre class="passwordMask"></pre>'
|
||||||
|
})}</span>`
|
||||||
|
]);
|
||||||
|
const og = span.querySelector('.passwordOriginal');
|
||||||
|
const masked = span.querySelector('.passwordMask');
|
||||||
|
og.textContent = password;
|
||||||
|
masked.textContent = password.replace(/./g, '●');
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
const resetInput = document.getElementById('unlock-reset-input');
|
||||||
|
const resetBtn = document.getElementById('unlock-reset-btn');
|
||||||
|
if (resetInput.value.length > 0) {
|
||||||
|
resetBtn.classList.remove('btn-hidden');
|
||||||
|
resetInput.classList.remove('input-no-btn');
|
||||||
|
} else {
|
||||||
|
resetBtn.classList.add('btn-hidden');
|
||||||
|
resetInput.classList.add('input-no-btn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const password = document.querySelector('#unlock-reset-input').value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
document.getElementById('copy').classList.remove('wait-password');
|
||||||
|
document.getElementById('copy-btn').disabled = false;
|
||||||
|
emit('password', { password, file });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleResetInput(event) {
|
||||||
|
const form = event.target.parentElement.querySelector('form');
|
||||||
|
const input = document.getElementById('unlock-reset-input');
|
||||||
|
if (form.style.visibility === 'hidden' || form.style.visibility === '') {
|
||||||
|
form.style.visibility = 'visible';
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
form.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
inputChanged();
|
||||||
|
}
|
||||||
|
};
|
||||||
70
app/templates/uploadPasswordUnset.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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="32"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||||
|
type="password"
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
181
app/utils.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fadeOut,
|
||||||
|
delay,
|
||||||
|
allowedCopy,
|
||||||
|
bytes,
|
||||||
|
percent,
|
||||||
|
copyToClipboard,
|
||||||
|
arrayToB64,
|
||||||
|
b64ToArray,
|
||||||
|
canHasSend,
|
||||||
|
isFile,
|
||||||
|
saveFile,
|
||||||
|
openLinksInNewTab
|
||||||
|
};
|
||||||
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 |
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,6 +1,6 @@
|
|||||||
/*** index.html ***/
|
/*** index.html ***/
|
||||||
html {
|
html {
|
||||||
background: url('../../public/resources/send_bg.svg');
|
background: url('./send_bg.svg');
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
@@ -8,7 +8,6 @@ html {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center top;
|
background-position: center top;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 1440px;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ body {
|
|||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
background-color: #0297f8;
|
background-color: #0297f8;
|
||||||
background-image: url('../../public/resources/feedback.svg');
|
background-image: url('./feedback.svg');
|
||||||
background-position: 2px 4px;
|
background-position: 2px 4px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 18px;
|
background-size: 18px;
|
||||||
@@ -130,18 +129,27 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
max-width: 630px;
|
max-width: 650px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 96%;
|
width: 96%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea,
|
textarea,
|
||||||
button {
|
button {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -154,13 +162,43 @@ a {
|
|||||||
|
|
||||||
/** page-one **/
|
/** 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 {
|
.title {
|
||||||
font-size: 33px;
|
font-size: 33px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
font-family: 'SF Pro Display', sans-serif;
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,8 +213,8 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.upload-window {
|
.upload-window {
|
||||||
border: 1px dashed rgba(0, 148, 251, 0.5);
|
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||||
margin: 0 auto;
|
margin: 0 auto 10px;
|
||||||
height: 255px;
|
height: 255px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -189,8 +227,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.upload-window.ondrag {
|
.upload-window.ondrag {
|
||||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||||
margin: 0 auto;
|
|
||||||
height: 251px;
|
height: 251px;
|
||||||
transform: scale(1.04);
|
transform: scale(1.04);
|
||||||
border-radius: 4.2px;
|
border-radius: 4.2px;
|
||||||
@@ -201,6 +238,10 @@ a {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-window.ondrag * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: #0094fb;
|
color: #0094fb;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -214,16 +255,16 @@ a {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: #737373;
|
color: #737373;
|
||||||
margin: 20px 0 10px;
|
margin: 20px 0 10px;
|
||||||
font-family: 'SF Pro Display', sans-serif;
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#browse {
|
.browse {
|
||||||
background: #0297f8;
|
background: #0297f8;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 15px;
|
font-size: 20px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
height: 44px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -231,12 +272,22 @@ a {
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#browse:hover {
|
.browse:hover {
|
||||||
background-color: #0287e8;
|
background-color: #0287e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
input[type='file'] {
|
||||||
display: none;
|
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 {
|
#file-size-msg {
|
||||||
@@ -291,12 +342,27 @@ tbody {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#expiry-file-list {
|
#expiry-time-file-list {
|
||||||
width: 21%;
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#expiry-downloads-file-list {
|
||||||
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#delete-file-list {
|
#delete-file-list {
|
||||||
width: 12%;
|
width: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-col {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-col {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-delete,
|
.icon-delete,
|
||||||
@@ -305,7 +371,7 @@ tbody {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-copy[disabled="disabled"] {
|
.icon-copy[disabled='disabled'] {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
@@ -344,7 +410,7 @@ tbody {
|
|||||||
|
|
||||||
/* Popup arrow */
|
/* Popup arrow */
|
||||||
.popup .popuptext::after {
|
.popup .popuptext::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -11px;
|
bottom: -11px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
@@ -356,6 +422,8 @@ tbody {
|
|||||||
border-radius: 0 0 5px;
|
border-radius: 0 0 5px;
|
||||||
border-right: 1px solid #d7d7db;
|
border-right: 1px solid #d7d7db;
|
||||||
border-bottom: 1px solid #d7d7db;
|
border-bottom: 1px solid #d7d7db;
|
||||||
|
border-left: 1px solid #fff;
|
||||||
|
border-top: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup .show {
|
.popup .show {
|
||||||
@@ -431,12 +499,8 @@ tbody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.percentage {
|
.percentage {
|
||||||
position: absolute;
|
|
||||||
letter-spacing: -0.78px;
|
letter-spacing: -0.78px;
|
||||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||||
top: 58px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -449,7 +513,8 @@ tbody {
|
|||||||
|
|
||||||
.percent-sign {
|
.percent-sign {
|
||||||
font-size: 28.8px;
|
font-size: 28.8px;
|
||||||
color: rgb(104, 104, 104);
|
stroke: none;
|
||||||
|
fill: #686868;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
@@ -471,17 +536,26 @@ tbody {
|
|||||||
|
|
||||||
#cancel-upload {
|
#cancel-upload {
|
||||||
color: #d70022;
|
color: #d70022;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cancel-upload:disabled {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/** share-link **/
|
/** share-link **/
|
||||||
#share-window {
|
#share-window {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#share-window-r > div {
|
#share-window-r > div {
|
||||||
@@ -492,7 +566,12 @@ tbody {
|
|||||||
#copy {
|
#copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
width: 640px;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy.wait-password #link,
|
||||||
|
#copy.wait-password #copy-btn {
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#copy-text {
|
#copy-text {
|
||||||
@@ -509,9 +588,9 @@ tbody {
|
|||||||
height: 56px;
|
height: 56px;
|
||||||
border: 1px solid #0297f8;
|
border: 1px solid #0297f8;
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 6px 0 0 6px;
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
color: #737373;
|
color: #737373;
|
||||||
font-family: 'SF Pro Display', sans-serif;
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@@ -532,24 +611,27 @@ tbody {
|
|||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
height: 60px;
|
padding-bottom: 4px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#copy-btn:hover {
|
#copy-btn:not(:disabled):hover {
|
||||||
background-color: #0287e8;
|
background-color: #0287e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#copy-btn:disabled {
|
#copy-btn.success {
|
||||||
background: #05a700;
|
background: #05a700;
|
||||||
border: 1px solid #05a700;
|
border: 1px solid #05a700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy-btn:disabled {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
opacity: 0.3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#delete-file {
|
#delete-file {
|
||||||
|
align-self: center;
|
||||||
width: 176px;
|
width: 176px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -562,10 +644,35 @@ tbody {
|
|||||||
color: #313131;
|
color: #313131;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#deletePopup {
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
#delete-file:hover {
|
#delete-file:hover {
|
||||||
background: #efeff1;
|
background: #efeff1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resetButton {
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #313131;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resetButton:hover {
|
||||||
|
background: #efeff1;
|
||||||
|
}
|
||||||
|
|
||||||
.send-new {
|
.send-new {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -581,6 +688,37 @@ tbody {
|
|||||||
color: #0287e8;
|
color: #0287e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectPassword {
|
||||||
|
padding: 10px 0;
|
||||||
|
align-self: left;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordOriginal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectPassword :hover .passwordOriginal {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectPassword :hover .passwordMask {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setPassword {
|
||||||
|
align-self: left;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 80%;
|
||||||
|
padding: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* upload-error */
|
/* upload-error */
|
||||||
#upload-error {
|
#upload-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -619,16 +757,20 @@ tbody {
|
|||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.firefox-logo-small {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
#dl-firefox,
|
#dl-firefox,
|
||||||
#update-firefox {
|
#update-firefox {
|
||||||
margin-bottom: 181px;
|
margin-bottom: 181px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #12bc00;
|
background: #98e02b;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
||||||
font-family: 'Fira Sans';
|
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
@@ -645,7 +787,7 @@ tbody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.unsupported-button-text > span {
|
.unsupported-button-text > span {
|
||||||
font-family: 'Fira Sans';
|
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
letter-spacing: -0.69px;
|
letter-spacing: -0.69px;
|
||||||
@@ -676,7 +818,7 @@ tbody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#download {
|
#download {
|
||||||
margin: 0 auto;
|
margin: 0 auto 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -709,6 +851,68 @@ tbody {
|
|||||||
height: 196px;
|
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,
|
||||||
|
#unlock-reset-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,
|
||||||
|
#unlock-reset-btn:hover {
|
||||||
|
background-color: #0287e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-no-btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* footer */
|
/* footer */
|
||||||
.footer {
|
.footer {
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -731,7 +935,7 @@ tbody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legal-links {
|
.legal-links {
|
||||||
width: 81vw;
|
max-width: 81vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -773,6 +977,139 @@ tbody {
|
|||||||
margin-bottom: -5px;
|
margin-bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#addPasswordWrapper {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPassword {
|
||||||
|
position: absolute;
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPasswordWrapper:hover label::before {
|
||||||
|
border: 1px solid #0297f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPasswordWrapper label {
|
||||||
|
line-height: 23px;
|
||||||
|
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: linear-gradient(-180deg, #45a1ff 0%, #00feff 94%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-blue a {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-blue a:hover {
|
||||||
|
color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-pink {
|
||||||
|
background: linear-gradient(-180deg, #ff9400 0%, #ff1ad9 94%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-pink a {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-pink 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) {
|
@media (max-device-width: 992px), (max-width: 992px) {
|
||||||
.popup .popuptext {
|
.popup .popuptext {
|
||||||
left: auto;
|
left: auto;
|
||||||
@@ -843,22 +1180,42 @@ tbody {
|
|||||||
padding: 5px 5px 5px 20px;
|
padding: 5px 5px 5px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#copy {
|
#copy,
|
||||||
|
.setPassword,
|
||||||
|
#unlock {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#link {
|
.selectPassword {
|
||||||
|
align-self: center;
|
||||||
|
min-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPasswordWrapper label::before {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#link,
|
||||||
|
#unlock-input,
|
||||||
|
#unlock-reset-input {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
padding: 15px 10px;
|
padding: 15px 10px;
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#copy-btn {
|
#copy-btn,
|
||||||
|
#unlock-btn,
|
||||||
|
#unlock-reset-btn {
|
||||||
border-radius: 0 0 6px 6px;
|
border-radius: 0 0 6px 6px;
|
||||||
flex: 0 1 65px;
|
flex: 0 1 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#copy-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
@@ -868,4 +1225,12 @@ tbody {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 17px 5px 0;
|
padding: 17px 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup .popuptext::after {
|
||||||
|
left: 125px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#deletePopup {
|
||||||
|
left: 83px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 86 KiB 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>
|
||||||
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);
|
||||||
|
};
|
||||||
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;
|
||||||
@@ -8,6 +8,5 @@ services:
|
|||||||
- "1443:1443"
|
- "1443:1443"
|
||||||
environment:
|
environment:
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
- NODE_ENV=production
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
|||||||
@@ -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:
|
## Environment variables:
|
||||||
|
|
||||||
| Name | Description
|
| Name | Description
|
||||||
|
|||||||
@@ -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.
|
- `cm5` - the number of files the user has ever uploaded.
|
||||||
- `cm6` - the number of unexpired files the user has uploaded.
|
- `cm6` - the number of unexpired files the user has uploaded.
|
||||||
- `cm7` - the number of files the user has ever downloaded.
|
- `cm7` - the number of files the user has ever downloaded.
|
||||||
|
- `cm8` - the number of downloads permitted by the uploader.
|
||||||
|
|
||||||
### Custom Dimensions
|
### Custom Dimensions
|
||||||
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
- `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`
|
- `cd2`
|
||||||
- `cd6`
|
- `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`
|
#### `download-started`
|
||||||
Triggered whenever a user begins downloading a file. Includes:
|
Triggered whenever a user begins downloading a file. Includes:
|
||||||
|
|
||||||
|
|||||||
19
docs/takedowns.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Take-down process
|
||||||
|
|
||||||
|
In cases of a DMCA notice, or other abuse yet to be determined, a file has to be removed from the service.
|
||||||
|
|
||||||
|
Files can be delisted and made inaccessible by removing their record from Redis.
|
||||||
|
|
||||||
|
Send share links contain the `id` of the file, for example `https://send.firefox.com/download/3d9d2bb9a1`
|
||||||
|
|
||||||
|
From a host with access to the Redis server run a `DEL` command with the file id.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
redis-cli DEL 3d9d2bb9a1
|
||||||
|
```
|
||||||
|
|
||||||
|
Other redis-cli parameters like `-h` may also be required. See [redis-cli docs](https://redis.io/topics/rediscli) for more info.
|
||||||
|
|
||||||
|
The encrypted file resides on S3 as the same `id` under the bucket that the app was configured with as `S3_BUCKET`. The file can be managed if it has not already expired with the [AWS cli](https://docs.aws.amazon.com/cli/latest/reference/s3/index.html) or AWS web console.
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Raven from 'raven-js';
|
|
||||||
import { unsupported } from './metrics';
|
|
||||||
|
|
||||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
|
||||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
|
||||||
if (
|
|
||||||
ua.indexOf('firefox') > -1 &&
|
|
||||||
parseInt(ua.match(/firefox\/*([^\n\r]*)\./)[1], 10) <= 49
|
|
||||||
) {
|
|
||||||
unsupported({
|
|
||||||
err: new Error('Firefox is outdated.')
|
|
||||||
}).then(() => {
|
|
||||||
location.replace('/unsupported/outdated');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Raven };
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { Raven } from './common';
|
|
||||||
import FileReceiver from './fileReceiver';
|
|
||||||
import { bytes, notify, gcmCompliant } from './utils';
|
|
||||||
import Storage from './storage';
|
|
||||||
import * as links from './links';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
import * as progress from './progress';
|
|
||||||
|
|
||||||
const storage = new Storage();
|
|
||||||
function onUnload(size) {
|
|
||||||
metrics.cancelledDownload({ size });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download() {
|
|
||||||
const downloadBtn = document.getElementById('download-btn');
|
|
||||||
const downloadPanel = document.getElementById('download-page-one');
|
|
||||||
const progressPanel = document.getElementById('download-progress');
|
|
||||||
const file = document.getElementById('dl-file');
|
|
||||||
const size = Number(file.getAttribute('data-size'));
|
|
||||||
const ttl = Number(file.getAttribute('data-ttl'));
|
|
||||||
const unloadHandler = onUnload.bind(null, size);
|
|
||||||
const startTime = Date.now();
|
|
||||||
const fileReceiver = new FileReceiver(
|
|
||||||
'/assets' + location.pathname.slice(0, -1),
|
|
||||||
location.hash.slice(1)
|
|
||||||
);
|
|
||||||
|
|
||||||
downloadBtn.disabled = true;
|
|
||||||
downloadPanel.hidden = true;
|
|
||||||
progressPanel.hidden = false;
|
|
||||||
metrics.startedDownload({ size, ttl });
|
|
||||||
links.setOpenInNewTab(true);
|
|
||||||
window.addEventListener('unload', unloadHandler);
|
|
||||||
|
|
||||||
fileReceiver.on('progress', data => {
|
|
||||||
progress.setProgress({ complete: data[0], total: data[1] });
|
|
||||||
});
|
|
||||||
|
|
||||||
let downloadEnd;
|
|
||||||
fileReceiver.on('decrypting', () => {
|
|
||||||
downloadEnd = Date.now();
|
|
||||||
window.removeEventListener('unload', unloadHandler);
|
|
||||||
fileReceiver.removeAllListeners('progress');
|
|
||||||
document.l10n.formatValue('decryptingFile').then(progress.setText);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const file = await fileReceiver.download();
|
|
||||||
const endTime = Date.now();
|
|
||||||
const time = endTime - startTime;
|
|
||||||
const downloadTime = endTime - downloadEnd;
|
|
||||||
const speed = size / (downloadTime / 1000);
|
|
||||||
|
|
||||||
links.setOpenInNewTab(false);
|
|
||||||
storage.totalDownloads += 1;
|
|
||||||
metrics.completedDownload({ size, time, speed });
|
|
||||||
progress.setText(' ');
|
|
||||||
document.l10n
|
|
||||||
.formatValues('downloadNotification', 'downloadFinish')
|
|
||||||
.then(translated => {
|
|
||||||
notify(translated[0]);
|
|
||||||
document.getElementById('dl-title').textContent = translated[1];
|
|
||||||
document.querySelector('#download-progress .description').textContent =
|
|
||||||
' ';
|
|
||||||
});
|
|
||||||
const dataView = new DataView(file.plaintext);
|
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = downloadUrl;
|
|
||||||
if (window.navigator.msSaveBlob) {
|
|
||||||
window.navigator.msSaveBlob(blob, file.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
a.download = file.name;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
} catch (err) {
|
|
||||||
metrics.stoppedDownload({ size, err });
|
|
||||||
|
|
||||||
if (err.message === 'notfound') {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
progressPanel.hidden = true;
|
|
||||||
downloadPanel.hidden = true;
|
|
||||||
document.getElementById('upload-error').hidden = false;
|
|
||||||
}
|
|
||||||
Raven.captureException(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const file = document.getElementById('dl-file');
|
|
||||||
const filename = file.getAttribute('data-filename');
|
|
||||||
const b = Number(file.getAttribute('data-size'));
|
|
||||||
const size = bytes(b);
|
|
||||||
document.l10n.formatValue('downloadFileSize', { size }).then(str => {
|
|
||||||
document.getElementById('dl-filesize').textContent = str;
|
|
||||||
});
|
|
||||||
document.l10n
|
|
||||||
.formatValue('downloadingPageProgress', { filename, size })
|
|
||||||
.then(str => {
|
|
||||||
document.getElementById('dl-title').textContent = str;
|
|
||||||
});
|
|
||||||
|
|
||||||
gcmCompliant()
|
|
||||||
.then(() => {
|
|
||||||
document
|
|
||||||
.getElementById('download-btn')
|
|
||||||
.addEventListener('click', download);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
metrics.unsupported({ err }).then(() => {
|
|
||||||
location.replace('/unsupported/gcm');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import FileSender from './fileSender';
|
|
||||||
import Storage from './storage';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
import { allowedCopy, copyToClipboard, ONE_DAY_IN_MS } from './utils';
|
|
||||||
import bel from 'bel';
|
|
||||||
import copyImg from '../../public/resources/copy-16.svg';
|
|
||||||
import closeImg from '../../public/resources/close-16.svg';
|
|
||||||
|
|
||||||
const HOUR = 1000 * 60 * 60;
|
|
||||||
const storage = new Storage();
|
|
||||||
let fileList = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
fileList = document.getElementById('file-list');
|
|
||||||
toggleHeader();
|
|
||||||
Promise.all(
|
|
||||||
storage.files.map(file => {
|
|
||||||
const id = file.fileId;
|
|
||||||
return checkExistence(id).then(exists => {
|
|
||||||
if (exists) {
|
|
||||||
addFile(storage.getFileById(id));
|
|
||||||
} else {
|
|
||||||
storage.remove(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
.then(toggleHeader);
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleHeader() {
|
|
||||||
fileList.hidden = storage.files.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 'Expired';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFile(file) {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file.creationDate = new Date(file.creationDate);
|
|
||||||
const url = `${file.url}#${file.secretKey}`;
|
|
||||||
const future = new Date();
|
|
||||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
|
||||||
const countdown = future.getTime() - Date.now();
|
|
||||||
|
|
||||||
const row = bel`
|
|
||||||
<tr>
|
|
||||||
<td>${file.name}</td>
|
|
||||||
<td>
|
|
||||||
<span class="icon-docs" data-l10n-id="copyUrlHover"></span>
|
|
||||||
<img onclick=${copyClick} src="${copyImg}" class="icon-copy" data-l10n-id="copyUrlHover">
|
|
||||||
<span data-l10n-id="copiedUrl" class="text-copied" hidden="true"></span>
|
|
||||||
</td>
|
|
||||||
<td>${timeLeft(countdown)}</td>
|
|
||||||
<td>
|
|
||||||
<span class="icon-cancel-1" data-l10n-id="deleteButtonHover" title="Delete"></span>
|
|
||||||
<img onclick=${showPopup} src="${closeImg}" class="icon-delete" data-l10n-id="deleteButtonHover" title="Delete">
|
|
||||||
<div class="popup">
|
|
||||||
<div class="popuptext" onclick=${stopProp} onblur=${cancel} tabindex="-1">
|
|
||||||
<div class="popup-message" data-l10n-id="deletePopupText"></div>
|
|
||||||
<div class="popup-action">
|
|
||||||
<span class="popup-no" onclick=${cancel} data-l10n-id="deletePopupCancel"></span>
|
|
||||||
<span class="popup-yes" onclick=${deleteFile} data-l10n-id="deletePopupYes"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
const popup = row.querySelector('.popuptext');
|
|
||||||
const timeCol = row.querySelectorAll('td')[2];
|
|
||||||
if (!allowedCopy()) {
|
|
||||||
row.querySelector('.icon-copy').disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.querySelector('tbody').appendChild(row);
|
|
||||||
toggleHeader();
|
|
||||||
poll();
|
|
||||||
|
|
||||||
function copyClick(e) {
|
|
||||||
metrics.copiedLink({ location: 'upload-list' });
|
|
||||||
copyToClipboard(url);
|
|
||||||
const icon = e.target;
|
|
||||||
const text = e.target.nextSibling;
|
|
||||||
icon.hidden = true;
|
|
||||||
text.hidden = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
icon.hidden = false;
|
|
||||||
text.hidden = true;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function poll() {
|
|
||||||
const countdown = future.getTime() - Date.now();
|
|
||||||
if (countdown <= 0) {
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
row.parentNode.removeChild(row);
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
timeCol.textContent = timeLeft(countdown);
|
|
||||||
setTimeout(poll, countdown >= HOUR ? 60000 : 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
|
||||||
FileSender.delete(file.fileId, file.deleteToken);
|
|
||||||
const ttl = ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
|
||||||
metrics.deletedUpload({
|
|
||||||
size: file.size,
|
|
||||||
time: file.totalTime,
|
|
||||||
speed: file.uploadSpeed,
|
|
||||||
type: file.typeOfUpload,
|
|
||||||
location: 'upload-list',
|
|
||||||
ttl
|
|
||||||
});
|
|
||||||
row.parentNode.removeChild(row);
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPopup() {
|
|
||||||
popup.classList.add('show');
|
|
||||||
popup.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
popup.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopProp(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkExistence(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
|
||||||
resolve(xhr.status === 200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = reject;
|
|
||||||
xhr.ontimeout = reject;
|
|
||||||
xhr.open('get', '/exists/' + id);
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { addFile };
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import EventEmitter from 'events';
|
|
||||||
import { hexToArray } from './utils';
|
|
||||||
|
|
||||||
export default class FileReceiver extends EventEmitter {
|
|
||||||
constructor(url, k) {
|
|
||||||
super();
|
|
||||||
this.key = window.crypto.subtle.importKey(
|
|
||||||
'jwk',
|
|
||||||
{
|
|
||||||
k,
|
|
||||||
kty: 'oct',
|
|
||||||
alg: 'A128GCM',
|
|
||||||
ext: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AES-GCM'
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadFile() {
|
|
||||||
return 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 meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
resolve({
|
|
||||||
data: this.result,
|
|
||||||
name: meta.filename,
|
|
||||||
type: meta.mimeType,
|
|
||||||
iv: meta.id
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('get', this.url);
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async download() {
|
|
||||||
const key = await this.key;
|
|
||||||
const file = await this.downloadFile();
|
|
||||||
this.emit('decrypting');
|
|
||||||
const plaintext = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: hexToArray(file.iv),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
file.data
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
plaintext,
|
|
||||||
name: decodeURIComponent(file.name),
|
|
||||||
type: file.type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import EventEmitter from 'events';
|
|
||||||
import { arrayToHex } from './utils';
|
|
||||||
|
|
||||||
export default class FileSender extends EventEmitter {
|
|
||||||
constructor(file) {
|
|
||||||
super();
|
|
||||||
this.file = file;
|
|
||||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
this.uploadXHR = new XMLHttpRequest();
|
|
||||||
this.key = window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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, keydata) {
|
|
||||||
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 = this.uploadXHR;
|
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', e => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
this.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({
|
|
||||||
id: fileId,
|
|
||||||
filename: encodeURIComponent(file.name)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
xhr.send(fd);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload() {
|
|
||||||
this.emit('loading');
|
|
||||||
const key = await this.key;
|
|
||||||
const plaintext = await this.readFile();
|
|
||||||
this.emit('encrypting');
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: this.iv,
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
plaintext
|
|
||||||
);
|
|
||||||
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
|
||||||
return this.uploadFile(encrypted, keydata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
let links = [];
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
links = Array.from(document.querySelectorAll('a:not([target])'));
|
|
||||||
});
|
|
||||||
|
|
||||||
function setOpenInNewTab(bool) {
|
|
||||||
if (bool === false) {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.removeAttribute('target');
|
|
||||||
l.removeAttribute('rel');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.setAttribute('target', '_blank');
|
|
||||||
l.setAttribute('rel', 'noopener noreferrer');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { setOpenInNewTab };
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { bytes, percent } from './utils';
|
|
||||||
|
|
||||||
let percentText = null;
|
|
||||||
let text = null;
|
|
||||||
let title = null;
|
|
||||||
let bar = null;
|
|
||||||
let updateTitle = false;
|
|
||||||
|
|
||||||
const radius = 73;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
percentText = document.querySelector('.percent-number');
|
|
||||||
text = document.querySelector('.progress-text');
|
|
||||||
bar = document.getElementById('bar');
|
|
||||||
title = document.querySelector('title');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('blur', function() {
|
|
||||||
updateTitle = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('focus', function() {
|
|
||||||
updateTitle = false;
|
|
||||||
return title && (title.textContent = 'Firefox Send');
|
|
||||||
});
|
|
||||||
|
|
||||||
function setProgress(params) {
|
|
||||||
const ratio = params.complete / params.total;
|
|
||||||
bar.setAttribute('stroke-dashoffset', (1 - ratio) * circumference);
|
|
||||||
percentText.textContent = Math.floor(ratio * 100);
|
|
||||||
if (updateTitle) {
|
|
||||||
title.textContent = percent(ratio);
|
|
||||||
}
|
|
||||||
document.l10n
|
|
||||||
.formatValue('fileSizeProgress', {
|
|
||||||
partialSize: bytes(params.complete),
|
|
||||||
totalSize: bytes(params.total)
|
|
||||||
})
|
|
||||||
.then(setText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setText(str) {
|
|
||||||
text.textContent = str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { setProgress, setText };
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
|
||||||
import { Raven } from './common';
|
|
||||||
import FileSender from './fileSender';
|
|
||||||
import {
|
|
||||||
allowedCopy,
|
|
||||||
bytes,
|
|
||||||
copyToClipboard,
|
|
||||||
notify,
|
|
||||||
gcmCompliant,
|
|
||||||
ONE_DAY_IN_MS
|
|
||||||
} from './utils';
|
|
||||||
import Storage from './storage';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
import * as progress from './progress';
|
|
||||||
import * as fileList from './fileList';
|
|
||||||
import checkImg from '../../public/resources/check-16.svg';
|
|
||||||
|
|
||||||
const storage = new Storage();
|
|
||||||
|
|
||||||
async function upload(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const pageOne = document.getElementById('page-one');
|
|
||||||
const link = document.getElementById('link');
|
|
||||||
const uploadWindow = document.querySelector('.upload-window');
|
|
||||||
const uploadError = document.getElementById('upload-error');
|
|
||||||
const uploadProgress = document.getElementById('upload-progress');
|
|
||||||
const clickOrDrop = event.type === 'drop' ? 'drop' : 'click';
|
|
||||||
|
|
||||||
// don't allow upload if not on upload page
|
|
||||||
if (pageOne.hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.totalUploads += 1;
|
|
||||||
|
|
||||||
let file = '';
|
|
||||||
if (clickOrDrop === 'drop') {
|
|
||||||
if (!event.dataTransfer.files[0]) {
|
|
||||||
uploadWindow.classList.remove('ondrag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.dataTransfer.files.length > 1 ||
|
|
||||||
event.dataTransfer.files[0].size === 0
|
|
||||||
) {
|
|
||||||
uploadWindow.classList.remove('ondrag');
|
|
||||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
|
||||||
alert(str);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file = event.dataTransfer.files[0];
|
|
||||||
} else {
|
|
||||||
file = event.target.files[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > MAXFILESIZE) {
|
|
||||||
return document.l10n
|
|
||||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
|
||||||
.then(alert);
|
|
||||||
}
|
|
||||||
|
|
||||||
pageOne.hidden = true;
|
|
||||||
uploadError.hidden = true;
|
|
||||||
uploadProgress.hidden = false;
|
|
||||||
document.l10n
|
|
||||||
.formatValue('uploadingPageProgress', {
|
|
||||||
size: bytes(file.size),
|
|
||||||
filename: file.name
|
|
||||||
})
|
|
||||||
.then(str => {
|
|
||||||
document.getElementById('upload-filename').textContent = str;
|
|
||||||
});
|
|
||||||
document.l10n.formatValue('importingFile').then(progress.setText);
|
|
||||||
//don't allow drag and drop when not on page-one
|
|
||||||
document.body.removeEventListener('drop', upload);
|
|
||||||
|
|
||||||
const fileSender = new FileSender(file);
|
|
||||||
document.getElementById('cancel-upload').addEventListener('click', () => {
|
|
||||||
fileSender.cancel();
|
|
||||||
metrics.cancelledUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop
|
|
||||||
});
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
let uploadStart;
|
|
||||||
fileSender.on('progress', data => {
|
|
||||||
uploadStart = uploadStart || Date.now();
|
|
||||||
progress.setProgress({
|
|
||||||
complete: data[0],
|
|
||||||
total: data[1]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
fileSender.on('encrypting', () => {
|
|
||||||
document.l10n.formatValue('encryptingFile').then(progress.setText);
|
|
||||||
});
|
|
||||||
|
|
||||||
let t;
|
|
||||||
const startTime = Date.now();
|
|
||||||
metrics.startedUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop
|
|
||||||
});
|
|
||||||
// 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 time = endTime - startTime;
|
|
||||||
const uploadTime = endTime - uploadStart;
|
|
||||||
const speed = file.size / (uploadTime / 1000);
|
|
||||||
const expiration = EXPIRE_SECONDS * 1000;
|
|
||||||
|
|
||||||
link.setAttribute('value', `${info.url}#${info.secretKey}`);
|
|
||||||
|
|
||||||
const copyText = document.getElementById('copy-text');
|
|
||||||
copyText.setAttribute(
|
|
||||||
'data-l10n-args',
|
|
||||||
JSON.stringify({ filename: file.name })
|
|
||||||
);
|
|
||||||
copyText.setAttribute('data-l10n-id', 'copyUrlFormLabelWithName');
|
|
||||||
|
|
||||||
metrics.completedUpload({
|
|
||||||
size: file.size,
|
|
||||||
time,
|
|
||||||
speed,
|
|
||||||
type: clickOrDrop
|
|
||||||
});
|
|
||||||
|
|
||||||
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: time,
|
|
||||||
typeOfUpload: clickOrDrop,
|
|
||||||
uploadSpeed: speed
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('delete-file').addEventListener('click', () => {
|
|
||||||
FileSender.delete(fileData.fileId, fileData.deleteToken).then(() => {
|
|
||||||
const ttl =
|
|
||||||
ONE_DAY_IN_MS - (Date.now() - fileData.creationDate.getTime());
|
|
||||||
metrics
|
|
||||||
.deletedUpload({
|
|
||||||
size: fileData.size,
|
|
||||||
time: fileData.totalTime,
|
|
||||||
speed: fileData.uploadSpeed,
|
|
||||||
type: fileData.typeOfUpload,
|
|
||||||
location: 'success-screen',
|
|
||||||
ttl
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
storage.remove(fileData.fileId);
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
storage.addFile(info.fileId, fileData);
|
|
||||||
|
|
||||||
pageOne.hidden = true;
|
|
||||||
uploadProgress.hidden = true;
|
|
||||||
uploadError.hidden = true;
|
|
||||||
document.getElementById('share-link').hidden = false;
|
|
||||||
|
|
||||||
fileList.addFile(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);
|
|
||||||
pageOne.hidden = true;
|
|
||||||
uploadProgress.hidden = true;
|
|
||||||
uploadError.hidden = false;
|
|
||||||
window.clearTimeout(t);
|
|
||||||
|
|
||||||
metrics.stoppedUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop,
|
|
||||||
err
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
gcmCompliant()
|
|
||||||
.then(function() {
|
|
||||||
const pageOne = document.getElementById('page-one');
|
|
||||||
const copyBtn = document.getElementById('copy-btn');
|
|
||||||
const link = document.getElementById('link');
|
|
||||||
const uploadWindow = document.querySelector('.upload-window');
|
|
||||||
|
|
||||||
pageOne.hidden = false;
|
|
||||||
document.getElementById('file-upload').addEventListener('change', upload);
|
|
||||||
|
|
||||||
document.body.addEventListener('dragover', allowDrop);
|
|
||||||
document.body.addEventListener('drop', upload);
|
|
||||||
|
|
||||||
// reset copy button
|
|
||||||
copyBtn.disabled = !allowedCopy();
|
|
||||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
|
||||||
|
|
||||||
link.disabled = false;
|
|
||||||
|
|
||||||
// copy link to clipboard
|
|
||||||
copyBtn.addEventListener('click', () => {
|
|
||||||
if (allowedCopy() && copyToClipboard(link.getAttribute('value'))) {
|
|
||||||
metrics.copiedLink({ location: 'success-screen' });
|
|
||||||
|
|
||||||
//disable button for 3s
|
|
||||||
copyBtn.disabled = true;
|
|
||||||
link.disabled = true;
|
|
||||||
copyBtn.innerHTML = `<img src="${checkImg}" class="icon-check"></img>`;
|
|
||||||
setTimeout(() => {
|
|
||||||
copyBtn.disabled = !allowedCopy();
|
|
||||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
|
||||||
link.disabled = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadWindow.addEventListener('dragover', () =>
|
|
||||||
uploadWindow.classList.add('ondrag')
|
|
||||||
);
|
|
||||||
uploadWindow.addEventListener('dragleave', () =>
|
|
||||||
uploadWindow.classList.remove('ondrag')
|
|
||||||
);
|
|
||||||
|
|
||||||
// on file upload by browse or drag & drop
|
|
||||||
|
|
||||||
function allowDrop(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
metrics.unsupported({ err }).then(() => {
|
|
||||||
location.replace('/unsupported/gcm');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,150 +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 loadShim();
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return loadShim();
|
|
||||||
}
|
|
||||||
function loadShim() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const shim = document.createElement('script');
|
|
||||||
shim.src = '/cryptofill.js';
|
|
||||||
shim.addEventListener('load', resolve);
|
|
||||||
shim.addEventListener('error', reject);
|
|
||||||
document.head.appendChild(shim);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
|
||||||
);
|
|
||||||
|
|
||||||
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
|
||||||
function bytes(num) {
|
|
||||||
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
|
||||||
const n = Number(num / Math.pow(1000, exponent));
|
|
||||||
const nStr = LOCALIZE_NUMBERS
|
|
||||||
? n.toLocaleString(navigator.languages, {
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 1
|
|
||||||
})
|
|
||||||
: n.toFixed(1);
|
|
||||||
return `${nStr}${UNITS[exponent]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function percent(ratio) {
|
|
||||||
return LOCALIZE_NUMBERS
|
|
||||||
? ratio.toLocaleString(navigator.languages, { style: 'percent' })
|
|
||||||
: `${Math.floor(ratio * 100)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function allowedCopy() {
|
|
||||||
const support = !!document.queryCommandSupported;
|
|
||||||
return support ? document.queryCommandSupported('copy') : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
|
||||||
|
|
||||||
export {
|
|
||||||
allowedCopy,
|
|
||||||
bytes,
|
|
||||||
percent,
|
|
||||||
copyToClipboard,
|
|
||||||
arrayToHex,
|
|
||||||
hexToArray,
|
|
||||||
notify,
|
|
||||||
gcmCompliant,
|
|
||||||
isFile,
|
|
||||||
ONE_DAY_IN_MS
|
|
||||||
};
|
|
||||||
8184
package-lock.json
generated
208
package.json
@@ -1,87 +1,132 @@
|
|||||||
{
|
{
|
||||||
"name": "firefox-send",
|
"name": "firefox-send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "1.1.2",
|
"version": "2.3.1",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"dependencies": {
|
"repository": "mozilla/send",
|
||||||
"aws-sdk": "^2.98.0",
|
"homepage": "https://github.com/mozilla/send/",
|
||||||
"body-parser": "^1.17.2",
|
"license": "MPL-2.0",
|
||||||
"connect-busboy": "0.0.2",
|
"private": true,
|
||||||
"convict": "^3.0.0",
|
"scripts": {
|
||||||
"express": "^4.15.3",
|
"precommit": "lint-staged",
|
||||||
"express-handlebars": "^3.0.0",
|
"clean": "rimraf dist",
|
||||||
"helmet": "^3.8.0",
|
"build": "npm run clean && webpack -p",
|
||||||
"mozlog": "^2.1.1",
|
"lint": "npm-run-all lint:*",
|
||||||
"raven": "^2.1.0",
|
"lint:css": "stylelint 'assets/*.css'",
|
||||||
"redis": "^2.8.0"
|
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"lint-staged": {
|
||||||
"asmcrypto.js": "0.0.11",
|
"*.js": [
|
||||||
"autoprefixer": "^7.1.2",
|
"prettier --single-quote --write",
|
||||||
"babel-core": "^6.25.0",
|
"eslint",
|
||||||
"babel-loader": "^7.1.1",
|
"git add"
|
||||||
"babel-polyfill": "^6.23.0",
|
],
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"*.css": [
|
||||||
"babel-preset-stage-2": "^6.24.1",
|
"prettier --single-quote --write",
|
||||||
"bel": "^5.0.3",
|
"stylelint",
|
||||||
"browserify": "^14.4.0",
|
"git add"
|
||||||
"copy-webpack-plugin": "^4.0.1",
|
]
|
||||||
"cross-env": "^5.0.5",
|
|
||||||
"css-loader": "^0.28.4",
|
|
||||||
"css-mqpacker": "^6.0.1",
|
|
||||||
"cssnano": "^3.10.0",
|
|
||||||
"eslint": "^4.3.0",
|
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
|
||||||
"eslint-plugin-node": "^5.1.1",
|
|
||||||
"eslint-plugin-security": "^1.4.0",
|
|
||||||
"extract-loader": "^1.0.0",
|
|
||||||
"file-loader": "^0.11.2",
|
|
||||||
"git-rev-sync": "^1.9.1",
|
|
||||||
"html-loader": "^0.5.1",
|
|
||||||
"html-webpack-plugin": "^2.30.1",
|
|
||||||
"husky": "^0.14.3",
|
|
||||||
"l20n": "^5.0.0",
|
|
||||||
"lint-staged": "^4.0.3",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"mocha": "^3.4.2",
|
|
||||||
"npm-run-all": "^4.0.2",
|
|
||||||
"postcss-cli": "^4.1.0",
|
|
||||||
"postcss-loader": "^2.0.6",
|
|
||||||
"prettier": "^1.5.3",
|
|
||||||
"proxyquire": "^1.8.0",
|
|
||||||
"raven-js": "^3.17.0",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"selenium-webdriver": "^3.5.0",
|
|
||||||
"sinon": "^2.3.8",
|
|
||||||
"stylelint": "^8.0.0",
|
|
||||||
"stylelint-config-standard": "^17.0.0",
|
|
||||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
|
||||||
"supertest": "^3.0.0",
|
|
||||||
"testpilot-ga": "^0.3.0",
|
|
||||||
"webcrypto-liner": "^0.1.25",
|
|
||||||
"webpack": "^3.5.4",
|
|
||||||
"webpack-dev-middleware": "^1.12.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.2.0"
|
"node": ">=8.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/mozilla/send/",
|
"devDependencies": {
|
||||||
"license": "MPL-2.0",
|
"autoprefixer": "^7.2.5",
|
||||||
"repository": "mozilla/send",
|
"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.1",
|
||||||
|
"cross-env": "^5.1.3",
|
||||||
|
"css-loader": "^0.28.9",
|
||||||
|
"css-mqpacker": "^6.0.2",
|
||||||
|
"cssnano": "^3.10.0",
|
||||||
|
"eslint": "^4.16.0",
|
||||||
|
"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.2",
|
||||||
|
"file-loader": "^1.1.6",
|
||||||
|
"fluent-intl-polyfill": "^0.1.0",
|
||||||
|
"git-rev-sync": "^1.9.1",
|
||||||
|
"github-changes": "^1.1.2",
|
||||||
|
"html-loader": "^0.5.5",
|
||||||
|
"husky": "^0.14.3",
|
||||||
|
"lint-staged": "^4.3.0",
|
||||||
|
"mocha": "^3.5.3",
|
||||||
|
"nanobus": "^4.3.2",
|
||||||
|
"npm-run-all": "^4.1.2",
|
||||||
|
"postcss-loader": "^2.0.10",
|
||||||
|
"prettier": "^1.10.2",
|
||||||
|
"proxyquire": "^1.8.0",
|
||||||
|
"raven-js": "^3.22.1",
|
||||||
|
"redis-mock": "^0.20.0",
|
||||||
|
"require-from-string": "^2.0.1",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"selenium-webdriver": "^3.6.0",
|
||||||
|
"sinon": "^4.2.2",
|
||||||
|
"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.188.0",
|
||||||
|
"body-parser": "^1.18.2",
|
||||||
|
"choo": "^6.7.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.10.0",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"mozlog": "^2.2.0",
|
||||||
|
"raven": "^2.4.0",
|
||||||
|
"redis": "^2.8.0"
|
||||||
|
},
|
||||||
"availableLanguages": [
|
"availableLanguages": [
|
||||||
|
"en-US",
|
||||||
"ast",
|
"ast",
|
||||||
"az",
|
"az",
|
||||||
|
"bs",
|
||||||
"ca",
|
"ca",
|
||||||
|
"cak",
|
||||||
"cs",
|
"cs",
|
||||||
"cy",
|
"cy",
|
||||||
"de",
|
"de",
|
||||||
"dsb",
|
"dsb",
|
||||||
"el",
|
"el",
|
||||||
"en-US",
|
|
||||||
"es-AR",
|
"es-AR",
|
||||||
"es-CL",
|
"es-CL",
|
||||||
"es-ES",
|
"es-ES",
|
||||||
"es-MX",
|
"es-MX",
|
||||||
|
"et",
|
||||||
|
"fa",
|
||||||
"fr",
|
"fr",
|
||||||
"fy-NL",
|
"fy-NL",
|
||||||
"hsb",
|
"hsb",
|
||||||
@@ -89,6 +134,7 @@
|
|||||||
"id",
|
"id",
|
||||||
"it",
|
"it",
|
||||||
"ja",
|
"ja",
|
||||||
|
"ka",
|
||||||
"kab",
|
"kab",
|
||||||
"ko",
|
"ko",
|
||||||
"ms",
|
"ms",
|
||||||
@@ -102,45 +148,11 @@
|
|||||||
"sl",
|
"sl",
|
||||||
"sr",
|
"sr",
|
||||||
"sv-SE",
|
"sv-SE",
|
||||||
|
"tl",
|
||||||
"tr",
|
"tr",
|
||||||
"uk",
|
"uk",
|
||||||
"vi",
|
"vi",
|
||||||
"zh-CN",
|
"zh-CN",
|
||||||
"zh-TW"
|
"zh-TW"
|
||||||
],
|
]
|
||||||
"scripts": {
|
|
||||||
"precommit": "lint-staged",
|
|
||||||
"clean": "rimraf dist",
|
|
||||||
"build": "npm-run-all build:*",
|
|
||||||
"build:js": "webpack -p",
|
|
||||||
"build:version": "node scripts/version",
|
|
||||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
|
||||||
"dev": "npm run clean && npm run build && npm start",
|
|
||||||
"format": "prettier '{,frontend/src/,scripts/,server/,test/**/!(bundle)}*.{js,css}' --single-quote --write",
|
|
||||||
"get-prod-locales": "node scripts/get-prod-locales",
|
|
||||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
|
||||||
"lint": "npm-run-all lint:*",
|
|
||||||
"lint:css": "stylelint 'frontend/src/*.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",
|
|
||||||
"start": "node server/server",
|
|
||||||
"test": "cross-env NODE_ENV=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"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.js": [
|
|
||||||
"prettier --single-quote --write",
|
|
||||||
"eslint",
|
|
||||||
"git add"
|
|
||||||
],
|
|
||||||
"*.css": [
|
|
||||||
"prettier --single-quote --write",
|
|
||||||
"stylelint",
|
|
||||||
"git add"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||