Compare commits
384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2807b1cad5 | ||
|
|
157e832c95 | ||
|
|
d78fcd3721 | ||
|
|
ac7ab79aef | ||
|
|
00fb353465 | ||
|
|
f0ec5a9496 | ||
|
|
7a31082da1 | ||
|
|
b54f4575ee | ||
|
|
490a1e88eb | ||
|
|
2f8a3c9904 | ||
|
|
e7fdf76120 | ||
|
|
d0d41b743a | ||
|
|
a2995411d6 | ||
|
|
3246c4a621 | ||
|
|
48faf929a4 | ||
|
|
b7f922a999 | ||
|
|
bfcdf9340d | ||
|
|
4ed515f5a3 | ||
|
|
84b2737ffb | ||
|
|
deabca5a94 | ||
|
|
e9a49e23e8 | ||
|
|
cfdef23365 | ||
|
|
7c4b6a9de4 | ||
|
|
bb8866f73f | ||
|
|
33c42648ef | ||
|
|
7feddd2eee | ||
|
|
1344b84cf5 | ||
|
|
ef03b750c5 | ||
|
|
0c07d78a37 | ||
|
|
b5885e446c | ||
|
|
0fa3d4481a | ||
|
|
49e7c2e05b | ||
|
|
2dc7d046ef | ||
|
|
3e0bd41efd | ||
|
|
929eaca2d8 | ||
|
|
60517c5ab6 | ||
|
|
97a8a2b305 | ||
|
|
8fc54bdbe2 | ||
|
|
6ff251b24a | ||
|
|
7237800a91 | ||
|
|
b4cc8e92c7 | ||
|
|
2e233da16d | ||
|
|
b703f78db9 | ||
|
|
13e792cf4d | ||
|
|
552c2d74f3 | ||
|
|
593f23b021 | ||
|
|
1baf83bac3 | ||
|
|
d43ca3190e | ||
|
|
c4c7860876 | ||
|
|
0decdeb37c | ||
|
|
47505dcc31 | ||
|
|
e71d6e792b | ||
|
|
29796cfec8 | ||
|
|
4bdf255c3a | ||
|
|
11efe8b8d1 | ||
|
|
3dd2d09584 | ||
|
|
ed4e8e8f25 | ||
|
|
181a74df88 | ||
|
|
166b2f3a52 | ||
|
|
bd5cdc52f9 | ||
|
|
5d0318c102 | ||
|
|
17adc644fb | ||
|
|
f48159dc0b | ||
|
|
360697c034 | ||
|
|
45dd833b27 | ||
|
|
98491eed01 | ||
|
|
fdafc1c59e | ||
|
|
a32638ed4c | ||
|
|
39080a6046 | ||
|
|
a124d36714 | ||
|
|
7f9e619643 | ||
|
|
db12f2f5c8 | ||
|
|
415e0b70f3 | ||
|
|
0e4b1e5ec7 | ||
|
|
054a97371c | ||
|
|
2b25b6a6ea | ||
|
|
780ed3120e | ||
|
|
bdd3cbd4c7 | ||
|
|
0c8013038f | ||
|
|
c020d59c56 | ||
|
|
a5c336494b | ||
|
|
aaa4655f45 | ||
|
|
ed18ec0bc5 | ||
|
|
1de43f31b6 | ||
|
|
1a04c86edd | ||
|
|
2bbdcae82e | ||
|
|
9b0aa5d601 | ||
|
|
cdc261e30a | ||
|
|
09133a66b0 | ||
|
|
64f1a31533 | ||
|
|
9a92a50a5f | ||
|
|
2afd93e82f | ||
|
|
cb62cc1e9d | ||
|
|
2cd3fc5af9 | ||
|
|
bac710a17f | ||
|
|
fb8b0f78ca | ||
|
|
e18ce15753 | ||
|
|
ed8ce9e3ca | ||
|
|
af5ef04115 | ||
|
|
9530a3df52 | ||
|
|
2794ac653f | ||
|
|
43eb758d73 | ||
|
|
d0364cd101 | ||
|
|
6a008bf312 | ||
|
|
dfb271410c | ||
|
|
789d67209c | ||
|
|
a31f6b75d9 | ||
|
|
f814427a7d | ||
|
|
b0307e92d4 | ||
|
|
7fa3d69aa1 | ||
|
|
58555a6b85 | ||
|
|
52113395db | ||
|
|
8dd1309c21 | ||
|
|
202e428412 | ||
|
|
6e7ed3cea3 | ||
|
|
41cb49141b | ||
|
|
82a8283b6e | ||
|
|
a5d28adc44 | ||
|
|
82e206bccf | ||
|
|
1e4d6646c6 | ||
|
|
acbf9fc32f | ||
|
|
046f227003 | ||
|
|
50ac9e32be | ||
|
|
3459dcaa15 | ||
|
|
9410defab6 | ||
|
|
b5a26e11f8 | ||
|
|
c51481628d | ||
|
|
24fa51a12c | ||
|
|
7328520d05 | ||
|
|
409d206f1e | ||
|
|
b0b393f3d9 | ||
|
|
c4499088c8 | ||
|
|
4cccd6ac5c | ||
|
|
188b28fce3 | ||
|
|
24adda6c7d | ||
|
|
4b49302fbe | ||
|
|
402ab350de | ||
|
|
47b68770af | ||
|
|
60aa16a327 | ||
|
|
5702a4806b | ||
|
|
74c4bdb660 | ||
|
|
203a2cf7fb | ||
|
|
1faa2733b3 | ||
|
|
ffa432a876 | ||
|
|
009fd29265 | ||
|
|
8f05c2324e | ||
|
|
e1ab515883 | ||
|
|
a68a7a60a7 | ||
|
|
b4dc274646 | ||
|
|
b31892bdc6 | ||
|
|
f388a1348d | ||
|
|
52dacbddf9 | ||
|
|
cc52f60aa1 | ||
|
|
1af818b691 | ||
|
|
d76f7758e7 | ||
|
|
48ab2f2400 | ||
|
|
0aa844eebc | ||
|
|
481b02ccf2 | ||
|
|
67e6ef6fda | ||
|
|
7d19f86d7a | ||
|
|
2d27d8a47c | ||
|
|
10fac130ef | ||
|
|
c6b632543d | ||
|
|
32eb8157eb | ||
|
|
3628f22114 | ||
|
|
177fa37041 | ||
|
|
717f6576ea | ||
|
|
f1b2ffa0fa | ||
|
|
ac73c23c73 | ||
|
|
ac40308b1c | ||
|
|
6fb81aa78c | ||
|
|
92430c78c2 | ||
|
|
cb7ddaa295 | ||
|
|
786d079632 | ||
|
|
4af25a505a | ||
|
|
3218803aae | ||
|
|
2311d5bcef | ||
|
|
e56d92334f | ||
|
|
bc24a069da | ||
|
|
837747f8f7 | ||
|
|
a8c32ae49c | ||
|
|
32c5b414de | ||
|
|
12c81a22e8 | ||
|
|
0c5d0d4bb2 | ||
|
|
234f9c624d | ||
|
|
da669b44ff | ||
|
|
3c39f5f085 | ||
|
|
6de91b5872 | ||
|
|
ff9a0979f6 | ||
|
|
e1e8af2489 | ||
|
|
1eb000f615 | ||
|
|
e20fd97e59 | ||
|
|
d10ceacd67 | ||
|
|
208c28ee01 | ||
|
|
cdd1bb3c29 | ||
|
|
3d9c4fa320 | ||
|
|
9c4d18ef3b | ||
|
|
ce9ff3959f | ||
|
|
51aef4e1e5 | ||
|
|
90247059d0 | ||
|
|
c97abb46ed | ||
|
|
b8f5e371c7 | ||
|
|
401311a05f | ||
|
|
652b8e4e15 | ||
|
|
99b7e7c0f1 | ||
|
|
81442bb6f2 | ||
|
|
137f474b69 | ||
|
|
8e14d3f8f7 | ||
|
|
07b7bc003a | ||
|
|
df691c1516 | ||
|
|
17e61bb09d | ||
|
|
14e21988b2 | ||
|
|
205de5a633 | ||
|
|
eabdc903b9 | ||
|
|
0628e71ec9 | ||
|
|
ebbcb38c7a | ||
|
|
5c1f535291 | ||
|
|
2c8e488611 | ||
|
|
6f3eac659c | ||
|
|
86bca790bc | ||
|
|
895d196876 | ||
|
|
3d8b38ffe4 | ||
|
|
fddc415c86 | ||
|
|
7a8e9b5de1 | ||
|
|
bbaeb44b26 | ||
|
|
a95f659474 | ||
|
|
4d311b134f | ||
|
|
5f90de577f | ||
|
|
3c32ce946a | ||
|
|
5ca89d0e0d | ||
|
|
781d1f5b0a | ||
|
|
ef9bfae319 | ||
|
|
7abbc60e17 | ||
|
|
15244e1a64 | ||
|
|
4b39d61ff4 | ||
|
|
74718d6361 | ||
|
|
ced640c24a | ||
|
|
cdaa92c86d | ||
|
|
57012f0660 | ||
|
|
6fde6e0a79 | ||
|
|
182bde30fa | ||
|
|
59e2267513 | ||
|
|
01a064ef7f | ||
|
|
9759338e6a | ||
|
|
5ac4560157 | ||
|
|
8e60ca1ac9 | ||
|
|
131a8b5564 | ||
|
|
663023a204 | ||
|
|
2b5c9dfb35 | ||
|
|
a9a34fdd0a | ||
|
|
1655094ce3 | ||
|
|
9ae7e3df11 | ||
|
|
0a31e2d521 | ||
|
|
b6849661a6 | ||
|
|
53e822964e | ||
|
|
b2f76d2df9 | ||
|
|
574a3ce894 | ||
|
|
c68f796891 | ||
|
|
c592f84d7d | ||
|
|
31faaf147e | ||
|
|
09c20f5933 | ||
|
|
fa4ab7bd5c | ||
|
|
de4a24a7f8 | ||
|
|
8f1c404724 | ||
|
|
1b975e7ba7 | ||
|
|
1d7473c489 | ||
|
|
a7d3992ba1 | ||
|
|
d81e9a76db | ||
|
|
0624e59776 | ||
|
|
546064a7ee | ||
|
|
9d49cc876c | ||
|
|
0bf8481fd0 | ||
|
|
ae0758ac14 | ||
|
|
e9405f49ee | ||
|
|
a1d0eef8a5 | ||
|
|
254b806fb4 | ||
|
|
a7aee1450f | ||
|
|
fa5573a5ff | ||
|
|
9714bb0a0a | ||
|
|
ad82d30dd9 | ||
|
|
757ac14d1a | ||
|
|
3e066258c4 | ||
|
|
127f73b4fe | ||
|
|
ae5009e1e3 | ||
|
|
0ab8ddc894 | ||
|
|
b429841534 | ||
|
|
279f6df6f4 | ||
|
|
634e6b2834 | ||
|
|
856b2cdc60 | ||
|
|
41351f877c | ||
|
|
afbb89fbe8 | ||
|
|
b40b45273d | ||
|
|
f1fb877c7f | ||
|
|
9d7ad06b1a | ||
|
|
c6a4b089d9 | ||
|
|
24917f8aa5 | ||
|
|
eada94b262 | ||
|
|
43fa551a64 | ||
|
|
e1137db946 | ||
|
|
c1878649b3 | ||
|
|
30b86b14ed | ||
|
|
e91b341f8a | ||
|
|
70148232c6 | ||
|
|
56e3d5766c | ||
|
|
350e31ae4a | ||
|
|
0794bcc458 | ||
|
|
a6aee8ad62 | ||
|
|
8305d13dab | ||
|
|
441a520765 | ||
|
|
ba84e59f39 | ||
|
|
22a316ab58 | ||
|
|
6b9502d252 | ||
|
|
cdc3a5340d | ||
|
|
f03f7a0286 | ||
|
|
d8a5789701 | ||
|
|
8d26e0e742 | ||
|
|
e142d76cb4 | ||
|
|
c488c1d724 | ||
|
|
bed57af6c5 | ||
|
|
7500bd8326 | ||
|
|
0250924961 | ||
|
|
70813556ad | ||
|
|
2bb9af1943 | ||
|
|
55bd44a8f5 | ||
|
|
d83900f272 | ||
|
|
fa4f9299b2 | ||
|
|
0f7b19c385 | ||
|
|
a9c1dd0180 | ||
|
|
c468e2f34e | ||
|
|
718f42897f | ||
|
|
fb468bd1bc | ||
|
|
dafe00cabb | ||
|
|
98aebb7f70 | ||
|
|
a990d78bc0 | ||
|
|
9b4069be3e | ||
|
|
ff3bc0dd62 | ||
|
|
b39b131928 | ||
|
|
2646fb9b3c | ||
|
|
c2b84650e2 | ||
|
|
fecf938ae7 | ||
|
|
8abf631430 | ||
|
|
d69c535dda | ||
|
|
082ca6c57b | ||
|
|
b263231068 | ||
|
|
947a6d9992 | ||
|
|
1ad7edf5a9 | ||
|
|
0c26204ea1 | ||
|
|
1e3bbee7f1 | ||
|
|
ec80e8e622 | ||
|
|
61e2c0d85b | ||
|
|
80db74fc3a | ||
|
|
30936eb2fa | ||
|
|
31e29d58b9 | ||
|
|
702134b3b1 | ||
|
|
11ae7f857c | ||
|
|
8827556974 | ||
|
|
21b7f16b1e | ||
|
|
314ab237ec | ||
|
|
0fa0416c3f | ||
|
|
09faedf059 | ||
|
|
16aa7983ed | ||
|
|
493bf8dc89 | ||
|
|
46432b9649 | ||
|
|
193664a8e8 | ||
|
|
626e578acb | ||
|
|
51bffe11a8 | ||
|
|
08e2c6c112 | ||
|
|
c38d91db98 | ||
|
|
c13839a522 | ||
|
|
4894d5162f | ||
|
|
77b6fb138f | ||
|
|
9dab74891d | ||
|
|
393d2a0052 | ||
|
|
44ac783f6a | ||
|
|
7ea8712538 | ||
|
|
fb92a793e4 | ||
|
|
87eaba6337 | ||
|
|
7e13f2ab32 | ||
|
|
3214d293ca | ||
|
|
1437116cf3 | ||
|
|
740001ddde | ||
|
|
24af3207e9 | ||
|
|
38746078ed | ||
|
|
fcea981127 |
@@ -1,8 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
.DS_Store
|
.DS_Store
|
||||||
static
|
|
||||||
test
|
|
||||||
scripts
|
|
||||||
docs
|
|
||||||
firefox
|
firefox
|
||||||
|
assets
|
||||||
|
docs
|
||||||
|
public
|
||||||
|
test
|
||||||
|
|||||||
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{js,html,yml,json,handlebars}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.toml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
public
|
dist
|
||||||
test/frontend/bundle.js
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
|||||||
10
.gitignore
vendored
@@ -1,10 +1,2 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
node_modules
|
||||||
public/upload.js
|
dist
|
||||||
public/download.js
|
|
||||||
public/version.json
|
|
||||||
public/l20n.min.js
|
|
||||||
public/polyfill.min.js
|
|
||||||
static/*
|
|
||||||
!static/info.txt
|
|
||||||
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
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
extends: stylelint-config-standard
|
extends: stylelint-config-standard
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- stylelint-no-unsupported-browser-features
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
plugin/no-unsupported-browser-features: [true, {severity: warning}]
|
||||||
|
|
||||||
color-hex-case: lower
|
color-hex-case: lower
|
||||||
declaration-colon-newline-after: null
|
declaration-colon-newline-after: null
|
||||||
selector-list-comma-newline-after: null
|
selector-list-comma-newline-after: null
|
||||||
|
|||||||
218
CHANGELOG.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
## Change Log
|
||||||
|
|
||||||
|
### v1.1.1 (2017/08/17 01:29 +00:00)
|
||||||
|
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
||||||
|
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
||||||
|
- [#515](https://github.com/mozilla/send/pull/515) removed jquery from upload.js (@dannycoates)
|
||||||
|
- [#514](https://github.com/mozilla/send/pull/514) use async and removed jquery from download.js (@dannycoates)
|
||||||
|
- [#513](https://github.com/mozilla/send/pull/513) use svg for progress (@dannycoates)
|
||||||
|
- [#510](https://github.com/mozilla/send/pull/510) added precommit hook for format (@dannycoates)
|
||||||
|
- [#502](https://github.com/mozilla/send/pull/502) extracted filelist into its own file (@dannycoates)
|
||||||
|
- [#428](https://github.com/mozilla/send/pull/428) add twitter and open graph cards (@dannycoates, @johngruen)
|
||||||
|
- [#506](https://github.com/mozilla/send/pull/506) 404 page (@varghesethomase)
|
||||||
|
- [#508](https://github.com/mozilla/send/pull/508) fixes 478 (@abhinadduri)
|
||||||
|
- [#504](https://github.com/mozilla/send/pull/504) fix japanese browse button (@johngruen)
|
||||||
|
- [#503](https://github.com/mozilla/send/pull/503) Added editorconfig (@skystar-p)
|
||||||
|
- [#499](https://github.com/mozilla/send/pull/499) use import/export in the frontend code (@dannycoates)
|
||||||
|
- [#500](https://github.com/mozilla/send/pull/500) fixed build:css on windows (@dannycoates)
|
||||||
|
- [#481](https://github.com/mozilla/send/pull/481) Cater for mobile and desktop (@pdehaan, @hubdotcom)
|
||||||
|
- [#493](https://github.com/mozilla/send/pull/493) added webpack-dev-middleware (@dannycoates)
|
||||||
|
- [#491](https://github.com/mozilla/send/pull/491) added missing exit event cases (@dannycoates)
|
||||||
|
- [#492](https://github.com/mozilla/send/pull/492) make the site mostly work when cookies (localStorage) are disabled (@dannycoates)
|
||||||
|
- [#490](https://github.com/mozilla/send/pull/490) set the mime type in the download blob (@dannycoates)
|
||||||
|
- [#485](https://github.com/mozilla/send/pull/485) added progress to tab title when not in focus (@dannycoates)
|
||||||
|
- [#474](https://github.com/mozilla/send/pull/474) Fixing bug #438 by adding role attribute to anchor tags and alt attribute images (@varghesethomase)
|
||||||
|
- [#480](https://github.com/mozilla/send/pull/480) Increase font weight to 500 on <button>s and <label>s (@pdehaan)
|
||||||
|
- [#419](https://github.com/mozilla/send/pull/419) Add autoprefixer and cssnano support (@pdehaan)
|
||||||
|
|
||||||
|
### v1.1.0 (2017/08/08 03:59 +00:00)
|
||||||
|
- [#473](https://github.com/mozilla/send/pull/473) Sort contributors alphabetically to prevent churn (@pdehaan)
|
||||||
|
- [#472](https://github.com/mozilla/send/pull/472) removed references to checksums in frontend tests (@abhinadduri)
|
||||||
|
- [#470](https://github.com/mozilla/send/pull/470) removed the file sha256 hash (@dannycoates)
|
||||||
|
- [#469](https://github.com/mozilla/send/pull/469) Increase mimimum node version to 8.2.0 (@ehuggett)
|
||||||
|
- [#468](https://github.com/mozilla/send/pull/468) attach delete-file handler only after upload (@dannycoates)
|
||||||
|
- [#466](https://github.com/mozilla/send/pull/466) added webpack (@dannycoates)
|
||||||
|
- [#427](https://github.com/mozilla/send/pull/427) Extended system font list fixes:#408 (@gautamkrishnar)
|
||||||
|
- [#448](https://github.com/mozilla/send/pull/448) Migrate width attribute to CSS (Fixes #436) (@nskins)
|
||||||
|
- [#457](https://github.com/mozilla/send/pull/457) factored out progress into progress.js (@dannycoates)
|
||||||
|
- [#452](https://github.com/mozilla/send/pull/452) refactored metrics (@dannycoates)
|
||||||
|
- [#455](https://github.com/mozilla/send/pull/455) Add a few missing strings from es-CL and tr locales (@pdehaan)
|
||||||
|
- [#444](https://github.com/mozilla/send/pull/444) Chain jQuery calls, do not use events alias and store selectors (@Johann-S)
|
||||||
|
- [#416](https://github.com/mozilla/send/pull/416) WIP: use webcrypto-liner to support Safari 10 (@dannycoates)
|
||||||
|
- [#451](https://github.com/mozilla/send/pull/451) Add rel noopener noreferrer to target='_blank' anchor elements (Fixes #439) (@boopeshmahendran)
|
||||||
|
- [#449](https://github.com/mozilla/send/pull/449) Add X-UA-Compatible meta tag (@kenrick95)
|
||||||
|
- [#433](https://github.com/mozilla/send/pull/433) Prevent download button from being clicked multiple times (@pdehaan)
|
||||||
|
- [#432](https://github.com/mozilla/send/pull/432) Add contributors script (@pdehaan)
|
||||||
|
- [#409](https://github.com/mozilla/send/pull/409) Handle copy clipboard disabled (@Johann-S)
|
||||||
|
|
||||||
|
### v1.0.4 (2017/08/03 23:05 +00:00)
|
||||||
|
- [#418](https://github.com/mozilla/send/pull/418) _blank all footer links (@dannycoates)
|
||||||
|
- [#386](https://github.com/mozilla/send/pull/386) fix percentage view on mobile layout (@ariestiyansyah)
|
||||||
|
- [#414](https://github.com/mozilla/send/pull/414) Add link to FAQ in unsupported view (@pdehaan)
|
||||||
|
- [#415](https://github.com/mozilla/send/pull/415) Only include Fira CSS on /unsupported/* route (@pdehaan)
|
||||||
|
- [#412](https://github.com/mozilla/send/pull/412) throw key errors before download begins (@dannycoates)
|
||||||
|
- [#404](https://github.com/mozilla/send/pull/404) Use async function instead of promise (#325) (@weihanglo)
|
||||||
|
- [#406](https://github.com/mozilla/send/pull/406) Add noscript tag (@pdehaan)
|
||||||
|
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||||
|
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||||
|
|
||||||
|
### v1.0.3 (2017/08/02 23:59 +00:00)
|
||||||
|
- [#402](https://github.com/mozilla/send/pull/402) filter the hash from error reports (@dannycoates)
|
||||||
|
- [#400](https://github.com/mozilla/send/pull/400) fix link that breaks download by opening in new tab (@johngruen)
|
||||||
|
- [#369](https://github.com/mozilla/send/pull/369) Add ESLint no-alert shame rule (@pdehaan)
|
||||||
|
- [#396](https://github.com/mozilla/send/pull/396) add babel-polyfill (@dannycoates)
|
||||||
|
- [#394](https://github.com/mozilla/send/pull/394) catch JSON.parse errors of storage metadata (@dannycoates)
|
||||||
|
- [#367](https://github.com/mozilla/send/pull/367) Generate production locales using 'compare-locales' (@pdehaan)
|
||||||
|
- [#392](https://github.com/mozilla/send/pull/392) Adjust hover behavior on send-logo (#382)
|
||||||
|
Fixes: #382. (@weihanglo)
|
||||||
|
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||||
|
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||||
|
- [#380](https://github.com/mozilla/send/pull/380) Add Pontoon URL to README (@pdehaan)
|
||||||
|
|
||||||
|
### v1.0.2 (2017/07/31 18:58 +00:00)
|
||||||
|
- [#365](https://github.com/mozilla/send/pull/365) revert the IE fix to fix footer on chrome (@dannycoates)
|
||||||
|
|
||||||
|
### v1.0.1 (2017/07/31 17:28 +00:00)
|
||||||
|
- [#353](https://github.com/mozilla/send/pull/353) redirect ie to /unsupported (@abhinadduri, @dannycoates)
|
||||||
|
- [#360](https://github.com/mozilla/send/pull/360) Fix some linting nits (@pdehaan)
|
||||||
|
- [#362](https://github.com/mozilla/send/pull/362) Adjusts category of unsupported event (fixes #350). (@chuckharmston)
|
||||||
|
- [#355](https://github.com/mozilla/send/pull/355) Make order of uploaded files in list consistent (@pdehaan)
|
||||||
|
- [#356](https://github.com/mozilla/send/pull/356) Get rid of console.log statements (@pdehaan)
|
||||||
|
- [#358](https://github.com/mozilla/send/pull/358) Fix some missing .title attributes in dev-only locales (@pdehaan)
|
||||||
|
- [#354](https://github.com/mozilla/send/pull/354) Remove /en-US/ from cookies link in footer (@pdehaan)
|
||||||
|
- [#339](https://github.com/mozilla/send/pull/339) Show error page on firefox v49 and below (@ericawright, @abhinadduri)
|
||||||
|
- [#346](https://github.com/mozilla/send/pull/346) Add docs/CODEOWNERS file (@pdehaan)
|
||||||
|
- [#345](https://github.com/mozilla/send/pull/345) wrap long file names (@dnarcese)
|
||||||
|
- [#344](https://github.com/mozilla/send/pull/344) don't wrap file list headers (@dnarcese)
|
||||||
|
- [#327](https://github.com/mozilla/send/pull/327) Modify popup delete dialog (@youwenliang)
|
||||||
|
- [#341](https://github.com/mozilla/send/pull/341) center percentage text on all browser versions (@dnarcese)
|
||||||
|
- [#340](https://github.com/mozilla/send/pull/340) Remove duplicate entities in localized FTL files (@flodolo)
|
||||||
|
- [#337](https://github.com/mozilla/send/pull/337) support v 50 and 51 by not allowing const in loops (@ericawright)
|
||||||
|
- [#338](https://github.com/mozilla/send/pull/338) Remove duplicated strings in en-US, fix nn-NO file (@flodolo)
|
||||||
|
- [#336](https://github.com/mozilla/send/pull/336) German(de): Fixed missing value for deleteFileButton (#336) (@flodolo)
|
||||||
|
- [#334](https://github.com/mozilla/send/pull/334) fix functionality on firefox 50 and 51 (@dnarcese)
|
||||||
|
|
||||||
|
### v1.0.0 (2017/07/26 19:08 +00:00)
|
||||||
|
- [#323](https://github.com/mozilla/send/pull/323) disable upload/download notifications (@dannycoates)
|
||||||
|
- [#322](https://github.com/mozilla/send/pull/322) fix feedback button jump (@dnarcese)
|
||||||
|
- [#320](https://github.com/mozilla/send/pull/320) fix German footer (@dnarcese)
|
||||||
|
|
||||||
|
### v0.2.2 (2017/07/26 04:50 +00:00)
|
||||||
|
- [#314](https://github.com/mozilla/send/pull/314) added L10N_DEV environment variable for making all languages available (@dannycoates)
|
||||||
|
- [#313](https://github.com/mozilla/send/pull/313) removing timeout limit for front end tests (@abhinadduri)
|
||||||
|
- [#311](https://github.com/mozilla/send/pull/311) expired ids should reject instead of returning null (@dannycoates)
|
||||||
|
- [#302](https://github.com/mozilla/send/pull/302) UX Refine WIP (@youwenliang)
|
||||||
|
- [#310](https://github.com/mozilla/send/pull/310) if the download card is pressed, the expired card shows up properly (@abhinadduri)
|
||||||
|
- [#269](https://github.com/mozilla/send/pull/269) refactored ftl file (@abhinadduri)
|
||||||
|
- [#291](https://github.com/mozilla/send/pull/291) added legal page (@dannycoates)
|
||||||
|
- [#307](https://github.com/mozilla/send/pull/307) don't show error page on upload cancel (@dnarcese)
|
||||||
|
- [#299](https://github.com/mozilla/send/pull/299) use CIRCLE_TAG as version.json version if present (@dannycoates)
|
||||||
|
|
||||||
|
### v0.2.1 (2017/07/24 23:34 +00:00)
|
||||||
|
- [#296](https://github.com/mozilla/send/pull/296) restyle delete popup (@dnarcese)
|
||||||
|
- [#295](https://github.com/mozilla/send/pull/295) renamed environment variables to remove P2P_ prefix (@dannycoates)
|
||||||
|
- [#294](https://github.com/mozilla/send/pull/294) dealing with invalid drag and drops (@abhinadduri)
|
||||||
|
- [#297](https://github.com/mozilla/send/pull/297) added environment variable for expire time (@dannycoates)
|
||||||
|
- [#292](https://github.com/mozilla/send/pull/292) Fixes289 (@abhinadduri)
|
||||||
|
- [#288](https://github.com/mozilla/send/pull/288) fix: Don`t allow upload when not on the upload page. (@ericawright)
|
||||||
|
- [#285](https://github.com/mozilla/send/pull/285) added messages for processing phases (@dannycoates)
|
||||||
|
- [#267](https://github.com/mozilla/send/pull/267) make site responsive and add feedback link (@johngruen)
|
||||||
|
- [#286](https://github.com/mozilla/send/pull/286) Update download progress bar color (@pdehaan)
|
||||||
|
- [#281](https://github.com/mozilla/send/pull/281) Stop ESLint from linting the /public/ directory (@pdehaan)
|
||||||
|
- [#280](https://github.com/mozilla/send/pull/280) created /unsupported page and added gcmCompliant to /download page (@dannycoates)
|
||||||
|
- [#279](https://github.com/mozilla/send/pull/279) create separate js bundles for upload/download pages (@dannycoates)
|
||||||
|
- [#268](https://github.com/mozilla/send/pull/268) Testpilot ga (@abhinadduri)
|
||||||
|
|
||||||
|
### v0.2.0 (2017/07/21 19:27 +00:00)
|
||||||
|
- [#266](https://github.com/mozilla/send/pull/266) abort uploads over maxfilesize (@dannycoates)
|
||||||
|
- [#264](https://github.com/mozilla/send/pull/264) Remove duplicate custom metric. (@chuckharmston)
|
||||||
|
- [#259](https://github.com/mozilla/send/pull/259) add alert when uploading multiple files (@dnarcese)
|
||||||
|
- [#262](https://github.com/mozilla/send/pull/262) sync download progress bar with percentage (@dnarcese)
|
||||||
|
- [#258](https://github.com/mozilla/send/pull/258) better sync percent with progress bar (@dnarcese)
|
||||||
|
- [#257](https://github.com/mozilla/send/pull/257) add a dynamic js script for page config (@dannycoates)
|
||||||
|
- [#256](https://github.com/mozilla/send/pull/256) add file size limit message (@dnarcese)
|
||||||
|
- [#253](https://github.com/mozilla/send/pull/253) Add favicon.ico version of the Send logo (@pdehaan)
|
||||||
|
- [#254](https://github.com/mozilla/send/pull/254) Add nsp check to circle ci (@pdehaan)
|
||||||
|
- [#245](https://github.com/mozilla/send/pull/245) Localization (@abhinadduri)
|
||||||
|
- [#252](https://github.com/mozilla/send/pull/252) only allow drag and drop on upload page (@dnarcese)
|
||||||
|
- [#250](https://github.com/mozilla/send/pull/250) make footer not overlap (@dnarcese)
|
||||||
|
- [#251](https://github.com/mozilla/send/pull/251) minify all images (@ericawright)
|
||||||
|
- [#249](https://github.com/mozilla/send/pull/249) change how the file upload box expands (@dnarcese)
|
||||||
|
- [#246](https://github.com/mozilla/send/pull/246) remove P2P references. Fixes #224 (@clouserw)
|
||||||
|
- [#242](https://github.com/mozilla/send/pull/242) Make only icons clickable in file list (@dnarcese)
|
||||||
|
- [#236](https://github.com/mozilla/send/pull/236) add FAQ. Fixes #186 (@clouserw)
|
||||||
|
- [#235](https://github.com/mozilla/send/pull/235) allow send another file link to open in new tab (@dnarcese)
|
||||||
|
- [#234](https://github.com/mozilla/send/pull/234) fix download svg (@dnarcese)
|
||||||
|
- [#232](https://github.com/mozilla/send/pull/232) escape filename in the ui (@dannycoates)
|
||||||
|
- [#226](https://github.com/mozilla/send/pull/226) added functionality to cancel uploads (@abhinadduri)
|
||||||
|
- [#231](https://github.com/mozilla/send/pull/231) move head and html tags to main template (@dnarcese)
|
||||||
|
- [#228](https://github.com/mozilla/send/pull/228) add send logo (@dnarcese)
|
||||||
|
- [#229](https://github.com/mozilla/send/pull/229) change learn more and github links (@dnarcese)
|
||||||
|
- [#201](https://github.com/mozilla/send/pull/201) Adds metrics documentation (closes #5). (@chuckharmston)
|
||||||
|
- [#223](https://github.com/mozilla/send/pull/223) change size of send another file links (@dnarcese)
|
||||||
|
- [#222](https://github.com/mozilla/send/pull/222) add footer (@dnarcese)
|
||||||
|
- [#197](https://github.com/mozilla/send/pull/197) fixes issues 195 and 192 (@abhinadduri)
|
||||||
|
- [#204](https://github.com/mozilla/send/pull/204) added HSTS header (@dannycoates)
|
||||||
|
- [#193](https://github.com/mozilla/send/pull/193) Frontend tests (@abhinadduri)
|
||||||
|
- [#191](https://github.com/mozilla/send/pull/191) New ui! (@dnarcese)
|
||||||
|
|
||||||
|
### v0.1.4 (2017/07/12 18:21 +00:00)
|
||||||
|
- [#189](https://github.com/mozilla/send/pull/189) Add CSP directives (@dannycoates)
|
||||||
|
- [#188](https://github.com/mozilla/send/pull/188) fixes delete button error (@abhinadduri)
|
||||||
|
- [#185](https://github.com/mozilla/send/pull/185) added loading, hashing, and encrypting events for uploader; decryptin… (@abhinadduri)
|
||||||
|
- [#183](https://github.com/mozilla/send/pull/183) rename to 'Send' (@dannycoates)
|
||||||
|
- [#184](https://github.com/mozilla/send/pull/184) Server tests (@abhinadduri)
|
||||||
|
- [#178](https://github.com/mozilla/send/pull/178) fixed issues in branch title (@abhinadduri)
|
||||||
|
- [#177](https://github.com/mozilla/send/pull/177) Gcm compliance (@abhinadduri)
|
||||||
|
- [#106](https://github.com/mozilla/send/pull/106) Gcm (@abhinadduri, @dannycoates)
|
||||||
|
- [#168](https://github.com/mozilla/send/pull/168) Show error page if upload fails (@dnarcese)
|
||||||
|
- [#148](https://github.com/mozilla/send/pull/148) WIP: Add basic contribute.json (@pdehaan)
|
||||||
|
- [#162](https://github.com/mozilla/send/pull/162) Fix dev server URL in README.md file (@pdehaan)
|
||||||
|
- [#167](https://github.com/mozilla/send/pull/167) build docker image with new name (@relud)
|
||||||
|
- [#164](https://github.com/mozilla/send/pull/164) Add word wraps to table (@dnarcese)
|
||||||
|
- [#149](https://github.com/mozilla/send/pull/149) Add robots.txt (@pdehaan)
|
||||||
|
- [#161](https://github.com/mozilla/send/pull/161) Hide table header on empty list (@dnarcese)
|
||||||
|
- [#154](https://github.com/mozilla/send/pull/154) Remove expired uploads (@dnarcese)
|
||||||
|
- [#146](https://github.com/mozilla/send/pull/146) Update README with some more details (@pdehaan)
|
||||||
|
|
||||||
|
### v0.1.2 (2017/06/24 03:38 +00:00)
|
||||||
|
- [#138](https://github.com/mozilla/send/pull/138) remove notLocalHost (@dannycoates)
|
||||||
|
|
||||||
|
### v0.1.0 (2017/06/24 01:24 +00:00)
|
||||||
|
- [#137](https://github.com/mozilla/send/pull/137) refactored docker build (@dannycoates)
|
||||||
|
- [#132](https://github.com/mozilla/send/pull/132) Add /__version__ route (@pdehaan)
|
||||||
|
- [#135](https://github.com/mozilla/send/pull/135) make dockerfile more dockerflowy (@dannycoates)
|
||||||
|
- [#134](https://github.com/mozilla/send/pull/134) Load previous uploads (@dannycoates, @dnarcese)
|
||||||
|
- [#131](https://github.com/mozilla/send/pull/131) added __heartbeat__ (@dannycoates)
|
||||||
|
- [#133](https://github.com/mozilla/send/pull/133) Add LICENSE file (@pdehaan)
|
||||||
|
- [#130](https://github.com/mozilla/send/pull/130) added sentry to server code (@abhinadduri)
|
||||||
|
- [#124](https://github.com/mozilla/send/pull/124) Remove unused [dev]dependencies (@pdehaan)
|
||||||
|
- [#119](https://github.com/mozilla/send/pull/119) Move cross-env to a dep (@pdehaan)
|
||||||
|
- [#123](https://github.com/mozilla/send/pull/123) removed bitly integration (@abhinadduri)
|
||||||
|
- [#122](https://github.com/mozilla/send/pull/122) fix docker build (@dannycoates)
|
||||||
|
- [#121](https://github.com/mozilla/send/pull/121) added docker service to circle.yml (@dannycoates)
|
||||||
|
- [#120](https://github.com/mozilla/send/pull/120) added sentry (@abhinadduri)
|
||||||
|
- [#118](https://github.com/mozilla/send/pull/118) change docker image name and add builds for tags (@relud)
|
||||||
|
- [#116](https://github.com/mozilla/send/pull/116) add /__lbheartbeat__ endpoint (@relud)
|
||||||
|
- [#79](https://github.com/mozilla/send/pull/79) Optimize/minimize bundle.js for production (@pdehaan)
|
||||||
|
- [#104](https://github.com/mozilla/send/pull/104) Fix a bunch of ESLint and HTMLLint errors (@pdehaan)
|
||||||
|
- [#105](https://github.com/mozilla/send/pull/105) Progress bars (@dnarcese)
|
||||||
|
- [#111](https://github.com/mozilla/send/pull/111) added in anonmyized ip google analytics (@abhinadduri)
|
||||||
|
- [#110](https://github.com/mozilla/send/pull/110) added notifications (@abhinadduri)
|
||||||
|
- [#103](https://github.com/mozilla/send/pull/103) added Dockerfile (@dannycoates)
|
||||||
|
- [#100](https://github.com/mozilla/send/pull/100) Added Helmet Middleware (@abhinadduri)
|
||||||
|
- [#99](https://github.com/mozilla/send/pull/99) Testing (@abhinadduri)
|
||||||
|
- [#77](https://github.com/mozilla/send/pull/77) Fix the linter errors (@pdehaan)
|
||||||
|
- [#54](https://github.com/mozilla/send/pull/54) Adding basic ESLint config (@pdehaan)
|
||||||
|
- [#71](https://github.com/mozilla/send/pull/71) Drag & drop (@dnarcese)
|
||||||
|
- [#72](https://github.com/mozilla/send/pull/72) Logging (@abhinadduri, @dannycoates)
|
||||||
|
- [#45](https://github.com/mozilla/send/pull/45) S3 integration (@abhinadduri, @dannycoates)
|
||||||
|
- [#46](https://github.com/mozilla/send/pull/46) Download page and share link UI (@dnarcese)
|
||||||
|
- [#41](https://github.com/mozilla/send/pull/41) Added upload page and file list UI (@dnarcese)
|
||||||
|
- [#40](https://github.com/mozilla/send/pull/40) Tweak the package.json file (@pdehaan)
|
||||||
|
- [#43](https://github.com/mozilla/send/pull/43) added return (@abhinadduri)
|
||||||
|
- [#42](https://github.com/mozilla/send/pull/42) changed to handle 404 during download, also removing progress listene… (@abhinadduri)
|
||||||
|
- [#39](https://github.com/mozilla/send/pull/39) Refactor riff (@abhinadduri, @dannycoates)
|
||||||
|
- [#36](https://github.com/mozilla/send/pull/36) added prettier for js formatting (@dannycoates)
|
||||||
|
- [#28](https://github.com/mozilla/send/pull/28) Added a UI for the uploader end, made stylistic changes, implemented deleting (@abhinadduri)
|
||||||
|
- [#25](https://github.com/mozilla/send/pull/25) Changed naming for some pages, no longer stores files by name on server (@abhinadduri)
|
||||||
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"]
|
||||||
|
|||||||
59
README.md
@@ -5,39 +5,80 @@
|
|||||||
|
|
||||||
**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 bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
| ENVIRONMENT | URL
|
| ENVIRONMENT | URL
|
||||||
@@ -46,6 +87,10 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
|
|||||||
| Stage | <https://send.stage.mozaws.net/>
|
| 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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
9
app/.eslintrc.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
node: true
|
||||||
|
|
||||||
|
parserOptions:
|
||||||
|
sourceType: module
|
||||||
|
|
||||||
|
rules:
|
||||||
|
node/no-unsupported-features: off
|
||||||
24
app/dragManager.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default function(state, emitter) {
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.body.addEventListener('dragover', event => {
|
||||||
|
if (state.route === '/') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.body.addEventListener('drop', event => {
|
||||||
|
if (state.route === '/' && !state.transfer) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.querySelector('.upload-window').classList.remove('ondrag');
|
||||||
|
const target = event.dataTransfer;
|
||||||
|
if (target.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.files.length > 1 || target.files[0].size === 0) {
|
||||||
|
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||||
|
}
|
||||||
|
const file = target.files[0];
|
||||||
|
emitter.emit('upload', { file, type: 'drop' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
69
app/experiments.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import hash from 'string-hash';
|
||||||
|
|
||||||
|
const experiments = {
|
||||||
|
'SyI-hI7gT9agiH-f3f0BYg': {
|
||||||
|
id: 'SyI-hI7gT9agiH-f3f0BYg',
|
||||||
|
run: function(variant, state, emitter) {
|
||||||
|
state.promo = variant === 1 ? 'body' : 'header';
|
||||||
|
emitter.emit('render');
|
||||||
|
},
|
||||||
|
eligible: function() {
|
||||||
|
return (
|
||||||
|
!/firefox/i.test(navigator.userAgent) &&
|
||||||
|
document.querySelector('html').lang === 'en-US'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
variant: function(state) {
|
||||||
|
return this.luckyNumber(state) > 0.5 ? 1 : 0;
|
||||||
|
},
|
||||||
|
luckyNumber: function(state) {
|
||||||
|
return luckyNumber(
|
||||||
|
`${this.id}:${state.storage.get('testpilot_ga__cid')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Returns a number between 0 and 1
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function luckyNumber(str) {
|
||||||
|
return hash(str) / 0xffffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkExperiments(state, emitter) {
|
||||||
|
const all = Object.keys(experiments);
|
||||||
|
const id = all.find(id => experiments[id].eligible(state));
|
||||||
|
if (id) {
|
||||||
|
const variant = experiments[id].variant(state);
|
||||||
|
state.storage.enroll(id, variant);
|
||||||
|
experiments[id].run(variant, state, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function initialize(state, emitter) {
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
const xp = experiments[state.query.x];
|
||||||
|
if (xp) {
|
||||||
|
xp.run(+state.query.v, state, emitter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state.storage.get('testpilot_ga__cid')) {
|
||||||
|
// first ever visit. check again after cid is assigned.
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
checkExperiments(state, emitter);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const enrolled = state.storage.enrolled.filter(([id, variant]) => {
|
||||||
|
const xp = experiments[id];
|
||||||
|
if (xp) {
|
||||||
|
xp.run(variant, state, emitter);
|
||||||
|
}
|
||||||
|
return !!xp;
|
||||||
|
});
|
||||||
|
// single experiment per session for now
|
||||||
|
if (enrolled.length === 0) {
|
||||||
|
checkExperiments(state, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
249
app/fileManager.js
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
import FileSender from './fileSender';
|
||||||
|
import FileReceiver from './fileReceiver';
|
||||||
|
import { copyToClipboard, delay, fadeOut, percent } from './utils';
|
||||||
|
import * as metrics from './metrics';
|
||||||
|
|
||||||
|
function saveFile(file) {
|
||||||
|
const dataView = new DataView(file.plaintext);
|
||||||
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
if (window.navigator.msSaveBlob) {
|
||||||
|
return window.navigator.msSaveBlob(blob, file.name);
|
||||||
|
}
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLinksInNewTab(links, should = true) {
|
||||||
|
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||||
|
if (should) {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.setAttribute('target', '_blank');
|
||||||
|
l.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.removeAttribute('target');
|
||||||
|
l.removeAttribute('rel');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exists(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
||||||
|
resolve(xhr.status === 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => resolve(false);
|
||||||
|
xhr.ontimeout = () => resolve(false);
|
||||||
|
xhr.open('get', '/api/exists/' + id);
|
||||||
|
xhr.timeout = 2000;
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
let lastRender = 0;
|
||||||
|
let updateTitle = false;
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
emitter.emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFiles() {
|
||||||
|
const files = state.storage.files;
|
||||||
|
let rerender = false;
|
||||||
|
for (const file of files) {
|
||||||
|
const ok = await exists(file.id);
|
||||||
|
if (!ok) {
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
rerender = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rerender) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
if (updateTitle) {
|
||||||
|
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.addEventListener('blur', () => (updateTitle = true));
|
||||||
|
document.addEventListener('focus', () => {
|
||||||
|
updateTitle = false;
|
||||||
|
emitter.emit('DOMTitleChange', 'Firefox Send');
|
||||||
|
});
|
||||||
|
checkFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('navigate', checkFiles);
|
||||||
|
|
||||||
|
emitter.on('render', () => {
|
||||||
|
lastRender = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('delete', async ({ file, location }) => {
|
||||||
|
try {
|
||||||
|
metrics.deletedUpload({
|
||||||
|
size: file.size,
|
||||||
|
time: file.time,
|
||||||
|
speed: file.speed,
|
||||||
|
type: file.type,
|
||||||
|
ttl: file.expiresAt - Date.now(),
|
||||||
|
location
|
||||||
|
});
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
await FileSender.delete(file.id, file.deleteToken);
|
||||||
|
} catch (e) {
|
||||||
|
state.raven.captureException(e);
|
||||||
|
}
|
||||||
|
state.fileInfo = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('cancel', () => {
|
||||||
|
state.transfer.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('upload', async ({ file, type }) => {
|
||||||
|
const size = file.size;
|
||||||
|
const sender = new FileSender(file);
|
||||||
|
sender.on('progress', updateProgress);
|
||||||
|
sender.on('encrypting', render);
|
||||||
|
state.transfer = sender;
|
||||||
|
render();
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
await delay(200);
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
metrics.startedUpload({ size, type });
|
||||||
|
const info = await sender.upload();
|
||||||
|
const time = Date.now() - start;
|
||||||
|
const speed = size / (time / 1000);
|
||||||
|
metrics.completedUpload({ size, time, speed, type });
|
||||||
|
document.getElementById('cancel-upload').hidden = 'hidden';
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('upload-progress');
|
||||||
|
info.name = file.name;
|
||||||
|
info.size = size;
|
||||||
|
info.type = type;
|
||||||
|
info.time = time;
|
||||||
|
info.speed = speed;
|
||||||
|
info.createdAt = Date.now();
|
||||||
|
info.url = `${info.url}#${info.secretKey}`;
|
||||||
|
info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
|
||||||
|
state.fileInfo = info;
|
||||||
|
state.storage.addFile(state.fileInfo);
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
state.transfer = null;
|
||||||
|
state.storage.totalUploads += 1;
|
||||||
|
emitter.emit('pushState', `/share/${info.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
state.transfer = null;
|
||||||
|
if (err.message === '0') {
|
||||||
|
//cancelled. do nothing
|
||||||
|
metrics.cancelledUpload({ size, type });
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedUpload({ size, type, err });
|
||||||
|
emitter.emit('pushState', '/error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('password', async ({ password, file }) => {
|
||||||
|
try {
|
||||||
|
await FileSender.setPassword(password, file);
|
||||||
|
metrics.addedPassword({ size: file.size });
|
||||||
|
file.password = password;
|
||||||
|
state.storage.writeFiles();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('preview', async () => {
|
||||||
|
const file = state.fileInfo;
|
||||||
|
const url = `/api/download/${file.id}`;
|
||||||
|
const receiver = new FileReceiver(url, file);
|
||||||
|
receiver.on('progress', updateProgress);
|
||||||
|
receiver.on('decrypting', render);
|
||||||
|
state.transfer = receiver;
|
||||||
|
try {
|
||||||
|
await receiver.getMetadata(file.nonce);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401') {
|
||||||
|
file.password = null;
|
||||||
|
if (!file.pwd) {
|
||||||
|
return emitter.emit('pushState', '/404');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('download', async file => {
|
||||||
|
state.transfer.on('progress', render);
|
||||||
|
state.transfer.on('decrypting', render);
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
const size = file.size;
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||||
|
const f = await state.transfer.download(file.nonce);
|
||||||
|
const time = Date.now() - start;
|
||||||
|
const speed = size / (time / 1000);
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('download-progress');
|
||||||
|
saveFile(f);
|
||||||
|
state.storage.totalDownloads += 1;
|
||||||
|
state.transfer = null;
|
||||||
|
metrics.completedDownload({ size, time, speed });
|
||||||
|
emitter.emit('pushState', '/completed');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
// TODO cancelled download
|
||||||
|
const location = err.message === 'notfound' ? '/404' : '/error';
|
||||||
|
if (location === '/error') {
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedDownload({ size, err });
|
||||||
|
}
|
||||||
|
emitter.emit('pushState', location);
|
||||||
|
} finally {
|
||||||
|
state.transfer = null;
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('copy', ({ url, location }) => {
|
||||||
|
copyToClipboard(url);
|
||||||
|
metrics.copiedLink({ location });
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for rerendering the file list countdown timers
|
||||||
|
if (
|
||||||
|
state.route === '/' &&
|
||||||
|
state.storage.files.length > 0 &&
|
||||||
|
Date.now() - lastRender > 30000
|
||||||
|
) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
251
app/fileReceiver.js
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import Nanobus from 'nanobus';
|
||||||
|
import { arrayToB64, b64ToArray, bytes } from './utils';
|
||||||
|
|
||||||
|
export default class FileReceiver extends Nanobus {
|
||||||
|
constructor(url, file) {
|
||||||
|
super('FileReceiver');
|
||||||
|
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(file.key),
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('encryption'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
sk,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (file.pwd) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
this.authKeyPromise = window.crypto.subtle
|
||||||
|
.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(file.password),
|
||||||
|
{ name: 'PBKDF2' },
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
)
|
||||||
|
.then(pwdKey =>
|
||||||
|
window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: encoder.encode(file.url),
|
||||||
|
iterations: 100,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
pwdKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.authKeyPromise = this.secretKeyPromise.then(sk => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
sk,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: { name: 'SHA-256' }
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.metaKeyPromise = this.secretKeyPromise.then(sk => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('metadata'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
sk,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.file = file;
|
||||||
|
this.url = url;
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.state = 'initialized';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressRatio() {
|
||||||
|
return this.progress[0] / this.progress[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get sizes() {
|
||||||
|
return {
|
||||||
|
partialSize: bytes(this.progress[0]),
|
||||||
|
totalSize: bytes(this.progress[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMetadata(sig) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
|
||||||
|
this.file.nonce = nonce;
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
return resolve(xhr.response);
|
||||||
|
}
|
||||||
|
reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new Error(0));
|
||||||
|
xhr.ontimeout = () => reject(new Error(0));
|
||||||
|
xhr.open('get', `/api/metadata/${this.file.id}`);
|
||||||
|
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
xhr.timeout = 2000;
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(nonce) {
|
||||||
|
try {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const sig = await window.crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(nonce)
|
||||||
|
);
|
||||||
|
const data = await this.fetchMetadata(new Uint8Array(sig));
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const json = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
b64ToArray(data.metadata)
|
||||||
|
);
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const meta = JSON.parse(decoder.decode(json));
|
||||||
|
this.file.name = meta.name;
|
||||||
|
this.file.type = meta.type;
|
||||||
|
this.file.iv = meta.iv;
|
||||||
|
this.file.size = data.size;
|
||||||
|
this.file.ttl = data.ttl;
|
||||||
|
this.state = 'ready';
|
||||||
|
} catch (e) {
|
||||||
|
this.state = 'invalid';
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(sig) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.onprogress = event => {
|
||||||
|
if (event.lengthComputable && event.target.status !== 404) {
|
||||||
|
this.progress = [event.loaded, event.total];
|
||||||
|
this.emit('progress', this.progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = event => {
|
||||||
|
if (xhr.status === 404) {
|
||||||
|
reject(new Error('notfound'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([xhr.response]);
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = function() {
|
||||||
|
resolve(this.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsArrayBuffer(blob);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.open('get', this.url);
|
||||||
|
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(nonce) {
|
||||||
|
this.state = 'downloading';
|
||||||
|
this.emit('progress', this.progress);
|
||||||
|
try {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const sig = await window.crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(nonce)
|
||||||
|
);
|
||||||
|
const ciphertext = await this.downloadFile(new Uint8Array(sig));
|
||||||
|
this.msg = 'decryptingFile';
|
||||||
|
this.emit('decrypting');
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: b64ToArray(this.file.iv),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
this.msg = 'downloadFinish';
|
||||||
|
this.state = 'complete';
|
||||||
|
return {
|
||||||
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.file.name),
|
||||||
|
type: this.file.type
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
this.state = 'invalid';
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
app/fileSender.js
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import Nanobus from 'nanobus';
|
||||||
|
import { arrayToB64, b64ToArray, bytes } from './utils';
|
||||||
|
|
||||||
|
export default class FileSender extends Nanobus {
|
||||||
|
constructor(file) {
|
||||||
|
super('FileSender');
|
||||||
|
this.file = file;
|
||||||
|
this.msg = 'importingFile';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
|
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
this.uploadXHR = new XMLHttpRequest();
|
||||||
|
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
this.secretKey = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.rawSecret,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static delete(id, token) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!id || !token) {
|
||||||
|
return reject();
|
||||||
|
}
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', `/api/delete/${id}`);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(JSON.stringify({ delete_token: token }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressRatio() {
|
||||||
|
return this.progress[0] / this.progress[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get sizes() {
|
||||||
|
return {
|
||||||
|
partialSize: bytes(this.progress[0]),
|
||||||
|
totalSize: bytes(this.progress[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.cancelled = true;
|
||||||
|
if (this.msg === 'fileSizeProgress') {
|
||||||
|
this.uploadXHR.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(this.file);
|
||||||
|
reader.onload = function(event) {
|
||||||
|
const plaintext = new Uint8Array(this.result);
|
||||||
|
resolve(plaintext);
|
||||||
|
};
|
||||||
|
reader.onerror = function(err) {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile(encrypted, metadata, rawAuth) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const dataView = new DataView(encrypted);
|
||||||
|
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('data', blob);
|
||||||
|
|
||||||
|
const xhr = this.uploadXHR;
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', e => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
this.progress = [e.loaded, e.total];
|
||||||
|
this.emit('progress', this.progress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const nonce = xhr
|
||||||
|
.getResponseHeader('WWW-Authenticate')
|
||||||
|
.split(' ')[1];
|
||||||
|
this.progress = [1, 1];
|
||||||
|
this.msg = 'notifyUploadDone';
|
||||||
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
|
return resolve({
|
||||||
|
url: responseObj.url,
|
||||||
|
id: responseObj.id,
|
||||||
|
secretKey: arrayToB64(this.rawSecret),
|
||||||
|
deleteToken: responseObj.delete,
|
||||||
|
nonce
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.msg = 'errorPageHeader';
|
||||||
|
reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.open('post', '/api/upload', true);
|
||||||
|
xhr.setRequestHeader(
|
||||||
|
'X-File-Metadata',
|
||||||
|
arrayToB64(new Uint8Array(metadata))
|
||||||
|
);
|
||||||
|
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(rawAuth)}`);
|
||||||
|
xhr.send(fd);
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload() {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const secretKey = await this.secretKey;
|
||||||
|
const encryptKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('encryption'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
const authKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
const metaKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('metadata'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
const plaintext = await this.readFile();
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.msg = 'encryptingFile';
|
||||||
|
this.emit('encrypting');
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
const metadata = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
iv: arrayToB64(this.iv),
|
||||||
|
name: this.file.name,
|
||||||
|
type: this.file.type || 'application/octet-stream'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setPassword(password, file) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const secretKey = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(file.secretKey),
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const authKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
const sig = await window.crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(file.nonce)
|
||||||
|
);
|
||||||
|
const pwdKey = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(password),
|
||||||
|
{ name: 'PBKDF2' },
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const newAuthKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: encoder.encode(file.url),
|
||||||
|
iterations: 100,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
pwdKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
return resolve(xhr.response);
|
||||||
|
}
|
||||||
|
if (xhr.status === 401) {
|
||||||
|
const nonce = xhr
|
||||||
|
.getResponseHeader('WWW-Authenticate')
|
||||||
|
.split(' ')[1];
|
||||||
|
file.nonce = nonce;
|
||||||
|
}
|
||||||
|
reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new Error(0));
|
||||||
|
xhr.ontimeout = () => reject(new Error(0));
|
||||||
|
xhr.open('post', `/api/password/${file.id}`);
|
||||||
|
xhr.setRequestHeader(
|
||||||
|
'Authorization',
|
||||||
|
`send-v1 ${arrayToB64(new Uint8Array(sig))}`
|
||||||
|
);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
xhr.timeout = 2000;
|
||||||
|
xhr.send(JSON.stringify({ auth: arrayToB64(new Uint8Array(rawAuth)) }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/main.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import app from './routes';
|
||||||
|
import locale from '../common/locales';
|
||||||
|
import fileManager from './fileManager';
|
||||||
|
import dragManager from './dragManager';
|
||||||
|
import { canHasSend } from './utils';
|
||||||
|
import assets from '../common/assets';
|
||||||
|
import storage from './storage';
|
||||||
|
import metrics from './metrics';
|
||||||
|
import experiments from './experiments';
|
||||||
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
|
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use((state, emitter) => {
|
||||||
|
// init state
|
||||||
|
state.transfer = null;
|
||||||
|
state.fileInfo = null;
|
||||||
|
state.translate = locale.getTranslator();
|
||||||
|
state.storage = storage;
|
||||||
|
state.raven = Raven;
|
||||||
|
emitter.on('DOMContentLoaded', async () => {
|
||||||
|
let reason = null;
|
||||||
|
if (
|
||||||
|
/firefox/i.test(navigator.userAgent) &&
|
||||||
|
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) <=
|
||||||
|
49
|
||||||
|
) {
|
||||||
|
reason = 'outdated';
|
||||||
|
}
|
||||||
|
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||||
|
reason = 'edge';
|
||||||
|
}
|
||||||
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
|
if (!ok) {
|
||||||
|
reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
||||||
|
}
|
||||||
|
if (reason) {
|
||||||
|
setTimeout(() => emitter.emit('replaceState', `/unsupported/${reason}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(metrics);
|
||||||
|
app.use(fileManager);
|
||||||
|
app.use(dragManager);
|
||||||
|
app.use(experiments);
|
||||||
|
|
||||||
|
app.mount('body');
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
const testPilotGA = require('testpilot-ga/src/TestPilotGA');
|
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||||
const Storage = require('./storage');
|
import storage from './storage';
|
||||||
const storage = new Storage(localStorage);
|
|
||||||
|
let hasLocalStorage = false;
|
||||||
|
try {
|
||||||
|
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||||
|
} catch (e) {
|
||||||
|
// when disabled, any mention of localStorage throws an error
|
||||||
|
}
|
||||||
|
|
||||||
const analytics = new testPilotGA({
|
const analytics = new testPilotGA({
|
||||||
an: 'Firefox Send',
|
an: 'Firefox Send',
|
||||||
@@ -8,17 +14,49 @@ 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', evt => {
|
||||||
|
exitEvent(evt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function category() {
|
||||||
|
switch (appState.route) {
|
||||||
|
case '/':
|
||||||
|
case '/share/:id':
|
||||||
|
return 'sender';
|
||||||
|
case '/download/:id/:key':
|
||||||
|
case '/download/:id':
|
||||||
|
case '/completed':
|
||||||
|
return 'recipient';
|
||||||
|
default:
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendEvent() {
|
function sendEvent() {
|
||||||
return analytics.sendEvent.apply(analytics, arguments).catch(() => 0);
|
const args = Array.from(arguments);
|
||||||
|
if (experiment && args[2]) {
|
||||||
|
args[2].xid = experiment[0];
|
||||||
|
args[2].xvar = experiment[1];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlToMetric(url) {
|
function urlToMetric(url) {
|
||||||
@@ -41,17 +79,24 @@ function urlToMetric(url) {
|
|||||||
return 'twitter';
|
return 'twitter';
|
||||||
case 'https://www.mozilla.org/firefox/new/?scene=2':
|
case 'https://www.mozilla.org/firefox/new/?scene=2':
|
||||||
return 'download-firefox';
|
return 'download-firefox';
|
||||||
|
case 'https://qsurvey.mozilla.com/s3/txp-firefox-send':
|
||||||
|
return 'survey';
|
||||||
|
case 'https://testpilot.firefox.com/':
|
||||||
|
case 'https://testpilot.firefox.com/experiments/send':
|
||||||
|
return 'testpilot';
|
||||||
|
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
||||||
|
return 'promo';
|
||||||
default:
|
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`;
|
||||||
}
|
}
|
||||||
@@ -72,10 +117,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()
|
||||||
@@ -84,10 +129,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'
|
||||||
@@ -95,33 +140,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
|
||||||
@@ -130,20 +184,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',
|
||||||
@@ -152,25 +206,25 @@ function stoppedUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -178,48 +232,41 @@ 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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
if (/^http/.test(l.href)) {
|
if (/^http/.test(l.getAttribute('href'))) {
|
||||||
l.addEventListener('click', exitEvent);
|
l.addEventListener('click', exitEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartEvent(state) {
|
function restart(state) {
|
||||||
setReferrer(state);
|
setReferrer(state);
|
||||||
return sendEvent(category, 'restarted', {
|
return sendEvent(category(), 'restarted', {
|
||||||
cd2: state
|
cd2: state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRestartHandlers() {
|
export {
|
||||||
const elements = Array.from(document.querySelectorAll('.send-new'));
|
|
||||||
elements.forEach(el => {
|
|
||||||
const state = el.getAttribute('data-state');
|
|
||||||
el.addEventListener('click', restartEvent.bind(null, state));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
copiedLink,
|
copiedLink,
|
||||||
startedUpload,
|
startedUpload,
|
||||||
cancelledUpload,
|
cancelledUpload,
|
||||||
@@ -230,5 +277,7 @@ module.exports = {
|
|||||||
cancelledDownload,
|
cancelledDownload,
|
||||||
stoppedDownload,
|
stoppedDownload,
|
||||||
completedDownload,
|
completedDownload,
|
||||||
|
addedPassword,
|
||||||
|
restart,
|
||||||
unsupported
|
unsupported
|
||||||
};
|
};
|
||||||
12
app/routes/download.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const preview = require('../templates/preview');
|
||||||
|
const download = require('../templates/download');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.transfer) {
|
||||||
|
const s = state.transfer.state;
|
||||||
|
if (s === 'downloading' || s === 'complete') {
|
||||||
|
return download(state, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return preview(state, emit);
|
||||||
|
};
|
||||||
10
app/routes/home.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const welcome = require('../templates/welcome');
|
||||||
|
const upload = require('../templates/upload');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.transfer && state.transfer.iv) {
|
||||||
|
//TODO relying on 'iv' is gross
|
||||||
|
return upload(state, emit);
|
||||||
|
}
|
||||||
|
return welcome(state, emit);
|
||||||
|
};
|
||||||
43
app/routes/index.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const choo = require('choo');
|
||||||
|
const html = require('choo/html');
|
||||||
|
const download = require('./download');
|
||||||
|
const header = require('../templates/header');
|
||||||
|
const footer = require('../templates/footer');
|
||||||
|
const fxPromo = require('../templates/fxPromo');
|
||||||
|
|
||||||
|
const app = choo();
|
||||||
|
|
||||||
|
function body(template) {
|
||||||
|
return function(state, emit) {
|
||||||
|
const b = html`<body>
|
||||||
|
${state.promo === 'header' ? fxPromo(state, emit) : ''}
|
||||||
|
${header(state)}
|
||||||
|
<div class="all">
|
||||||
|
<noscript>
|
||||||
|
<h2>Firefox Send requires JavaScript</h2>
|
||||||
|
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
||||||
|
<p>Please enable JavaScript and try again.</p>
|
||||||
|
</noscript>
|
||||||
|
${template(state, emit)}
|
||||||
|
</div>
|
||||||
|
${footer(state)}
|
||||||
|
</body>`;
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.route('/', body(require('./home')));
|
||||||
|
app.route('/share/:id', body(require('../templates/share')));
|
||||||
|
app.route('/download/:id', body(download));
|
||||||
|
app.route('/download/:id/:key', body(download));
|
||||||
|
app.route('/completed', body(require('../templates/completed')));
|
||||||
|
app.route('/unsupported/:reason', body(require('../templates/unsupported')));
|
||||||
|
app.route('/legal', body(require('../templates/legal')));
|
||||||
|
app.route('/error', body(require('../templates/error')));
|
||||||
|
app.route('/blank', body(require('../templates/blank')));
|
||||||
|
app.route('*', body(require('../templates/notFound')));
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
119
app/storage.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { isFile } from './utils';
|
||||||
|
|
||||||
|
class Mem {
|
||||||
|
constructor() {
|
||||||
|
this.items = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.items.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key) {
|
||||||
|
return this.items.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key, value) {
|
||||||
|
return this.items.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key) {
|
||||||
|
return this.items.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
key(i) {
|
||||||
|
return this.items.keys()[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
constructor() {
|
||||||
|
try {
|
||||||
|
this.engine = localStorage || new Mem();
|
||||||
|
} catch (e) {
|
||||||
|
this.engine = new Mem();
|
||||||
|
}
|
||||||
|
this._files = this.loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiles() {
|
||||||
|
const fs = [];
|
||||||
|
for (let i = 0; i < this.engine.length; i++) {
|
||||||
|
const k = this.engine.key(i);
|
||||||
|
if (isFile(k)) {
|
||||||
|
try {
|
||||||
|
const f = JSON.parse(this.engine.getItem(k));
|
||||||
|
if (!f.id) {
|
||||||
|
f.id = f.fileId;
|
||||||
|
}
|
||||||
|
fs.push(f);
|
||||||
|
} catch (err) {
|
||||||
|
// obviously you're not a golfer
|
||||||
|
this.engine.removeItem(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalDownloads() {
|
||||||
|
return Number(this.engine.getItem('totalDownloads'));
|
||||||
|
}
|
||||||
|
set totalDownloads(n) {
|
||||||
|
this.engine.setItem('totalDownloads', n);
|
||||||
|
}
|
||||||
|
get totalUploads() {
|
||||||
|
return Number(this.engine.getItem('totalUploads'));
|
||||||
|
}
|
||||||
|
set totalUploads(n) {
|
||||||
|
this.engine.setItem('totalUploads', n);
|
||||||
|
}
|
||||||
|
get referrer() {
|
||||||
|
return this.engine.getItem('referrer');
|
||||||
|
}
|
||||||
|
set referrer(str) {
|
||||||
|
this.engine.setItem('referrer', str);
|
||||||
|
}
|
||||||
|
get enrolled() {
|
||||||
|
return JSON.parse(this.engine.getItem('experiments') || '[]');
|
||||||
|
}
|
||||||
|
|
||||||
|
enroll(id, variant) {
|
||||||
|
const enrolled = this.enrolled;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
if (!enrolled.find(([i, v]) => i === id)) {
|
||||||
|
enrolled.push([id, variant]);
|
||||||
|
this.engine.setItem('experiments', JSON.stringify(enrolled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get files() {
|
||||||
|
return this._files;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileById(id) {
|
||||||
|
return this._files.find(f => f.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return this.engine.getItem(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(property) {
|
||||||
|
if (isFile(property)) {
|
||||||
|
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||||
|
}
|
||||||
|
this.engine.removeItem(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(file) {
|
||||||
|
this._files.push(file);
|
||||||
|
this.engine.setItem(file.id, JSON.stringify(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFiles() {
|
||||||
|
this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Storage();
|
||||||
6
app/templates/blank.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
const div = html`<div id="page-one"></div>`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
35
app/templates/completed.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('./progress');
|
||||||
|
const { fadeOut } = require('../utils');
|
||||||
|
const fxPromo = require('./fxPromo');
|
||||||
|
|
||||||
|
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>
|
||||||
|
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('download');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
32
app/templates/download.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('./progress');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
const fxPromo = require('./fxPromo');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download-progress" class="fadeIn">
|
||||||
|
<div id="dl-title" class="title">${state.translate(
|
||||||
|
'downloadingPageProgress',
|
||||||
|
{
|
||||||
|
filename: state.fileInfo.name,
|
||||||
|
size: bytes(state.fileInfo.size)
|
||||||
|
}
|
||||||
|
)}</div>
|
||||||
|
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
||||||
|
${progress(transfer.progressRatio)}
|
||||||
|
<div class="upload">
|
||||||
|
<div class="progress-text">${state.translate(
|
||||||
|
transfer.msg,
|
||||||
|
transfer.sizes
|
||||||
|
)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
56
app/templates/downloadPassword.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
const label =
|
||||||
|
fileInfo.password === null
|
||||||
|
? html`
|
||||||
|
<label class="red"
|
||||||
|
for="unlock-input">${state.translate('passwordTryAgain')}</label>`
|
||||||
|
: html`
|
||||||
|
<label for="unlock-input">
|
||||||
|
${state.translate('unlockInputLabel')}
|
||||||
|
</label>`;
|
||||||
|
const div = html`
|
||||||
|
<div class="enterPassword">
|
||||||
|
${label}
|
||||||
|
<form id="unlock" onsubmit=${checkPassword} data-no-csrf>
|
||||||
|
<input id="unlock-input"
|
||||||
|
class="unlock-input input-no-btn"
|
||||||
|
maxlength="64"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||||
|
oninput=${inputChanged}
|
||||||
|
type="password"/>
|
||||||
|
<input type="submit"
|
||||||
|
id="unlock-btn"
|
||||||
|
class="btn btn-hidden"
|
||||||
|
value="${state.translate('unlockButtonLabel')}"/>
|
||||||
|
</form>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
const input = document.getElementById('unlock-input');
|
||||||
|
const btn = document.getElementById('unlock-btn');
|
||||||
|
if (input.value.length > 0) {
|
||||||
|
btn.classList.remove('btn-hidden');
|
||||||
|
input.classList.remove('input-no-btn');
|
||||||
|
} else {
|
||||||
|
btn.classList.add('btn-hidden');
|
||||||
|
input.classList.add('input-no-btn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const password = document.getElementById('unlock-input').value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
document.getElementById('unlock-btn').disabled = true;
|
||||||
|
state.fileInfo.url = window.location.href;
|
||||||
|
state.fileInfo.password = password;
|
||||||
|
emit('preview');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
10
app/templates/error.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div id="upload-error">
|
||||||
|
<div class="title">${state.translate('errorPageHeader')}</div>
|
||||||
|
<img id="upload-error-img" src="${assets.get('illustration_error.svg')}"/>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
84
app/templates/file.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
function timeLeft(milliseconds) {
|
||||||
|
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||||
|
if (hours >= 1) {
|
||||||
|
return `${hours}h ${minutes % 60}m`;
|
||||||
|
} else if (hours === 0) {
|
||||||
|
return `${minutes}m ${seconds}s`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const ttl = file.expiresAt - Date.now();
|
||||||
|
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
||||||
|
const row = html`
|
||||||
|
<tr id="${file.id}">
|
||||||
|
<td class="overflow-col" title="${file.name}">${file.name}</td>
|
||||||
|
<td class="center-col">
|
||||||
|
<img onclick=${copyClick} src="${assets.get(
|
||||||
|
'copy-16.svg'
|
||||||
|
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
|
||||||
|
<span class="text-copied" hidden="true">${state.translate(
|
||||||
|
'copiedUrl'
|
||||||
|
)}</span>
|
||||||
|
</td>
|
||||||
|
<td>${remaining}</td>
|
||||||
|
<td class="center-col">
|
||||||
|
<img onclick=${showPopup} src="${assets.get(
|
||||||
|
'close-16.svg'
|
||||||
|
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
||||||
|
<div class="popup">
|
||||||
|
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
||||||
|
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
||||||
|
<div class="popup-action">
|
||||||
|
<span class="popup-no" onclick=${cancel}>${state.translate(
|
||||||
|
'deletePopupCancel'
|
||||||
|
)}</span>
|
||||||
|
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
|
||||||
|
'deletePopupYes'
|
||||||
|
)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function copyClick(e) {
|
||||||
|
emit('copy', { url: file.url, location: 'upload-list' });
|
||||||
|
const icon = e.target;
|
||||||
|
const text = e.target.nextSibling;
|
||||||
|
icon.hidden = true;
|
||||||
|
text.hidden = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
icon.hidden = false;
|
||||||
|
text.hidden = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const tr = document.getElementById(file.id);
|
||||||
|
const popup = tr.querySelector('.popuptext');
|
||||||
|
popup.classList.add('show');
|
||||||
|
popup.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const tr = document.getElementById(file.id);
|
||||||
|
const popup = tr.querySelector('.popuptext');
|
||||||
|
popup.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'upload-list' });
|
||||||
|
emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
};
|
||||||
32
app/templates/fileList.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const file = require('./file');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
let table = '';
|
||||||
|
if (state.storage.files.length) {
|
||||||
|
table = html`
|
||||||
|
<table id="uploaded-files">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
||||||
|
<th id="copy-file-list" class="center-col">${state.translate(
|
||||||
|
'copyFileList'
|
||||||
|
)}</th>
|
||||||
|
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
|
||||||
|
<th id="delete-file-list" class="center-col">${state.translate(
|
||||||
|
'deleteFileList'
|
||||||
|
)}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${state.storage.files.map(f => file(f, state, emit))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div id="file-list">
|
||||||
|
${table}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
31
app/templates/footer.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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>
|
||||||
|
</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>`;
|
||||||
|
};
|
||||||
44
app/templates/fxPromo.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
// function replaceLinks(str, urls) {
|
||||||
|
// let i = -1;
|
||||||
|
// const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
||||||
|
// i++;
|
||||||
|
// return `<a class="link" href="${urls[i]}">${v}</a>`;
|
||||||
|
// });
|
||||||
|
// return [`<span>${s}</span>`];
|
||||||
|
// }
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
// function close() {
|
||||||
|
// document.querySelector('.banner').remove();
|
||||||
|
// }
|
||||||
|
|
||||||
|
function clicked(evt) {
|
||||||
|
emit('exit', evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="banner">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="${assets.get('firefox_logo-only.svg')}"
|
||||||
|
class="firefox-logo-small"
|
||||||
|
alt="Firefox"/>
|
||||||
|
<span>Send is brought to you by the all-new Firefox.
|
||||||
|
<a
|
||||||
|
class="link"
|
||||||
|
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com"
|
||||||
|
onclick=${clicked}
|
||||||
|
>Download Firefox now ≫</a></span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
<img
|
||||||
|
src="${assets.get('close-16.svg')}"
|
||||||
|
class="icon-delete"
|
||||||
|
onclick=${close}>
|
||||||
|
*/
|
||||||
21
app/templates/header.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
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" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
|
||||||
|
'siteFeedback'
|
||||||
|
)}</a>
|
||||||
|
</header>`;
|
||||||
|
};
|
||||||
34
app/templates/legal.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
function replaceLinks(str, urls) {
|
||||||
|
let i = -1;
|
||||||
|
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
||||||
|
i++;
|
||||||
|
return `<a href="${urls[i]}">${v}</a>`;
|
||||||
|
});
|
||||||
|
return [`<div class="description">${s}</div>`];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="legal">
|
||||||
|
<div class="title">${state.translate('legalHeader')}</div>
|
||||||
|
${html(
|
||||||
|
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||||
|
'https://testpilot.firefox.com/terms',
|
||||||
|
'https://testpilot.firefox.com/privacy',
|
||||||
|
'https://testpilot.firefox.com/experiments/send'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
${html(
|
||||||
|
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||||
|
'https://www.mozilla.org/privacy/websites/',
|
||||||
|
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
21
app/templates/notFound.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download">
|
||||||
|
<div class="title">${state.translate('expiredPageHeader')}</div>
|
||||||
|
<div class="share-window">
|
||||||
|
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
||||||
|
</div>
|
||||||
|
<div class="expired-description">${state.translate(
|
||||||
|
'uploadPageExplainer'
|
||||||
|
)}</div>
|
||||||
|
<a class="send-new" href="/" data-state="notfound">${state.translate(
|
||||||
|
'sendYourFilesLink'
|
||||||
|
)}</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
74
app/templates/preview.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const notFound = require('./notFound');
|
||||||
|
const downloadPassword = require('./downloadPassword');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
const fxPromo = require('./fxPromo');
|
||||||
|
|
||||||
|
function getFileFromDOM() {
|
||||||
|
const el = document.getElementById('dl-file');
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nonce: el.getAttribute('data-nonce'),
|
||||||
|
pwd: !!+el.getAttribute('data-requires-password')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
state.fileInfo = state.fileInfo || getFileFromDOM();
|
||||||
|
if (!state.fileInfo) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
state.fileInfo.id = state.params.id;
|
||||||
|
state.fileInfo.key = state.params.key;
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
const size = fileInfo.size
|
||||||
|
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
||||||
|
: '';
|
||||||
|
let action = html`
|
||||||
|
<div>
|
||||||
|
<img src="${assets.get('illustration_download.svg')}"
|
||||||
|
id="download-img"
|
||||||
|
alt="${state.translate('downloadAltText')}"/>
|
||||||
|
<div>
|
||||||
|
<button id="download-btn"
|
||||||
|
class="btn"
|
||||||
|
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
if (fileInfo.pwd && !fileInfo.password) {
|
||||||
|
action = downloadPassword(state, emit);
|
||||||
|
} else if (!state.transfer) {
|
||||||
|
emit('preview');
|
||||||
|
}
|
||||||
|
const title = fileInfo.name
|
||||||
|
? state.translate('downloadFileName', { filename: fileInfo.name })
|
||||||
|
: state.translate('downloadFileTitle');
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download">
|
||||||
|
<div id="download-page-one">
|
||||||
|
<div class="title">
|
||||||
|
<span id="dl-file"
|
||||||
|
data-nonce="${fileInfo.nonce}"
|
||||||
|
data-requires-password="${fileInfo.pwd}">${title}</span>
|
||||||
|
<span id="dl-filesize">${' ' + size}</span>
|
||||||
|
</div>
|
||||||
|
<div class="description">${state.translate('downloadMessage')}</div>
|
||||||
|
${action}
|
||||||
|
</div>
|
||||||
|
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||||
|
</div>
|
||||||
|
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function download(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('download', fileInfo);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
29
app/templates/progress.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
const radius = 73;
|
||||||
|
const oRadius = radius + 10;
|
||||||
|
const oDiameter = oRadius * 2;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
module.exports = function(progressRatio) {
|
||||||
|
const dashOffset = (1 - progressRatio) * circumference;
|
||||||
|
const percent = Math.floor(progressRatio * 100);
|
||||||
|
const div = html`
|
||||||
|
<div class="progress-bar">
|
||||||
|
<svg id="progress" width="${oDiameter}" height="${
|
||||||
|
oDiameter
|
||||||
|
}" viewPort="0 0 ${oDiameter} ${oDiameter}" version="1.1">
|
||||||
|
<circle r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent"/>
|
||||||
|
<circle id="bar" r="${radius}" cx="${oRadius}" cy="${
|
||||||
|
oRadius
|
||||||
|
}" fill="transparent" transform="rotate(-90 ${oRadius} ${
|
||||||
|
oRadius
|
||||||
|
})" stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"/>
|
||||||
|
<text class="percentage" text-anchor="middle" x="50%" y="98"><tspan class="percent-number">${
|
||||||
|
percent
|
||||||
|
}</tspan><tspan class="percent-sign">%</tspan></text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
90
app/templates/share.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const notFound = require('./notFound');
|
||||||
|
const uploadPassword = require('./uploadPassword');
|
||||||
|
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||||
|
|
||||||
|
function passwordComplete(state, password) {
|
||||||
|
const el = html([
|
||||||
|
`<div class="selectPassword">${state.translate('passwordResult', {
|
||||||
|
password: '<pre></pre>'
|
||||||
|
})}</div>`
|
||||||
|
]);
|
||||||
|
el.lastElementChild.textContent = password;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
if (!file) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
file.password = file.password || '';
|
||||||
|
|
||||||
|
const passwordSection = file.password
|
||||||
|
? passwordComplete(state, file.password)
|
||||||
|
: uploadPassword(state, emit);
|
||||||
|
const div = html`
|
||||||
|
<div id="share-link" class="fadeIn">
|
||||||
|
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
|
||||||
|
<div id="share-window">
|
||||||
|
<div id="copy-text">
|
||||||
|
${state.translate('copyUrlFormLabelWithName', {
|
||||||
|
filename: file.name
|
||||||
|
})}</div>
|
||||||
|
<div id="copy">
|
||||||
|
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||||
|
<button id="copy-btn"
|
||||||
|
class="btn"
|
||||||
|
title="${state.translate('copyUrlFormButton')}"
|
||||||
|
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||||
|
</div>
|
||||||
|
${passwordSection}
|
||||||
|
<button id="delete-file"
|
||||||
|
class="btn"
|
||||||
|
title="${state.translate('deleteFileButton')}"
|
||||||
|
onclick=${deleteFile}>${state.translate('deleteFileButton')}</button>
|
||||||
|
<a class="send-new"
|
||||||
|
data-state="completed"
|
||||||
|
href="/"
|
||||||
|
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('share-link');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLink() {
|
||||||
|
if (allowedCopy()) {
|
||||||
|
emit('copy', { url: file.url, location: 'success-screen' });
|
||||||
|
const input = document.getElementById('link');
|
||||||
|
input.disabled = true;
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
copyBtn.classList.add('success');
|
||||||
|
copyBtn.replaceChild(
|
||||||
|
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||||
|
copyBtn.firstChild
|
||||||
|
);
|
||||||
|
await delay(2000);
|
||||||
|
input.disabled = false;
|
||||||
|
if (!copyBtn.parentNode.classList.contains('wait-password')) {
|
||||||
|
copyBtn.disabled = false;
|
||||||
|
}
|
||||||
|
copyBtn.classList.remove('success');
|
||||||
|
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'success-screen' });
|
||||||
|
await fadeOut('share-link');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
46
app/templates/unsupported.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const msg =
|
||||||
|
state.params.reason === 'outdated'
|
||||||
|
? html`
|
||||||
|
<div id="unsupported-browser">
|
||||||
|
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||||
|
<div class="description">${state.translate(
|
||||||
|
'notSupportedOutdatedDetail'
|
||||||
|
)}</div>
|
||||||
|
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||||
|
<img src="${assets.get(
|
||||||
|
'firefox_logo-only.svg'
|
||||||
|
)}" class="firefox-logo" alt="Firefox"/>
|
||||||
|
<div class="unsupported-button-text">${state.translate(
|
||||||
|
'updateFirefox'
|
||||||
|
)}</div>
|
||||||
|
</a>
|
||||||
|
<div class="unsupported-description">${state.translate(
|
||||||
|
'uploadPageExplainer'
|
||||||
|
)}</div>
|
||||||
|
</div>`
|
||||||
|
: html`
|
||||||
|
<div id="unsupported-browser">
|
||||||
|
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||||
|
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||||
|
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
||||||
|
'notSupportedLink'
|
||||||
|
)}</a></div>
|
||||||
|
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
||||||
|
<img src="${assets.get(
|
||||||
|
'firefox_logo-only.svg'
|
||||||
|
)}" class="firefox-logo" alt="Firefox"/>
|
||||||
|
<div class="unsupported-button-text">Firefox<br>
|
||||||
|
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="unsupported-description">${state.translate(
|
||||||
|
'uploadPageExplainer'
|
||||||
|
)}</div>
|
||||||
|
</div>`;
|
||||||
|
const div = html`<div id="page-one">${msg}</div>`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
38
app/templates/upload.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('./progress');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
|
||||||
|
const div = html`
|
||||||
|
<div id="upload-progress" class="fadeIn">
|
||||||
|
<div class="title" id="upload-filename">${state.translate(
|
||||||
|
'uploadingPageProgress',
|
||||||
|
{
|
||||||
|
filename: transfer.file.name,
|
||||||
|
size: bytes(transfer.file.size)
|
||||||
|
}
|
||||||
|
)}</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(transfer.progressRatio)}
|
||||||
|
<div class="upload">
|
||||||
|
<div class="progress-text">${state.translate(
|
||||||
|
transfer.msg,
|
||||||
|
transfer.sizes
|
||||||
|
)}</div>
|
||||||
|
<button id="cancel-upload" title="${state.translate(
|
||||||
|
'uploadingPageCancel'
|
||||||
|
)}" onclick=${cancel}>${state.translate('uploadingPageCancel')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel-upload');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = state.translate('uploadCancelNotification');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
65
app/templates/uploadPassword.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
const div = html`
|
||||||
|
<div class="selectPassword">
|
||||||
|
<div id="addPasswordWrapper">
|
||||||
|
<input id="addPassword" type="checkbox" autocomplete="off" onchange=${
|
||||||
|
togglePasswordInput
|
||||||
|
}/>
|
||||||
|
<label for="addPassword">
|
||||||
|
${state.translate('requirePasswordCheckbox')}</label>
|
||||||
|
</div>
|
||||||
|
<form class="setPassword hidden" onsubmit=${setPassword} data-no-csrf>
|
||||||
|
<input id="unlock-input"
|
||||||
|
class="unlock-input input-no-btn"
|
||||||
|
maxlength="64"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||||
|
oninput=${inputChanged}/>
|
||||||
|
<input type="submit"
|
||||||
|
id="unlock-btn"
|
||||||
|
class="btn btn-hidden"
|
||||||
|
value="${state.translate('addPasswordButton')}"/>
|
||||||
|
</form>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
const input = document.getElementById('unlock-input');
|
||||||
|
const btn = document.getElementById('unlock-btn');
|
||||||
|
if (input.value.length > 0) {
|
||||||
|
btn.classList.remove('btn-hidden');
|
||||||
|
input.classList.remove('input-no-btn');
|
||||||
|
} else {
|
||||||
|
btn.classList.add('btn-hidden');
|
||||||
|
input.classList.add('input-no-btn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePasswordInput(e) {
|
||||||
|
const unlockInput = document.getElementById('unlock-input');
|
||||||
|
const boxChecked = e.target.checked;
|
||||||
|
document
|
||||||
|
.querySelector('.setPassword')
|
||||||
|
.classList.toggle('hidden', !boxChecked);
|
||||||
|
if (boxChecked) {
|
||||||
|
unlockInput.focus();
|
||||||
|
} else {
|
||||||
|
unlockInput.value = '';
|
||||||
|
}
|
||||||
|
inputChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const password = document.getElementById('unlock-input').value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
document.getElementById('copy').classList.remove('wait-password');
|
||||||
|
document.getElementById('copy-btn').disabled = false;
|
||||||
|
emit('password', { password, file });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
73
app/templates/welcome.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const fileList = require('./fileList');
|
||||||
|
const fxPromo = require('./fxPromo');
|
||||||
|
const { fadeOut } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one" class="fadeIn">
|
||||||
|
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||||
|
<div class="description">
|
||||||
|
<div>${state.translate('uploadPageExplainer')}</div>
|
||||||
|
<a href="https://testpilot.firefox.com/experiments/send"
|
||||||
|
class="link">${state.translate('uploadPageLearnMore')}</a>
|
||||||
|
</div>
|
||||||
|
<div class="upload-window"
|
||||||
|
ondragover=${dragover}
|
||||||
|
ondragleave=${dragleave}>
|
||||||
|
<div id="upload-img">
|
||||||
|
<img src="${assets.get('upload.svg')}"
|
||||||
|
title="${state.translate('uploadSvgAlt')}"/>
|
||||||
|
</div>
|
||||||
|
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
||||||
|
<span id="file-size-msg">
|
||||||
|
<em>${state.translate('uploadPageSizeMessage')}</em>
|
||||||
|
</span>
|
||||||
|
<input id="file-upload"
|
||||||
|
type="file"
|
||||||
|
name="fileUploaded"
|
||||||
|
onfocus=${onfocus}
|
||||||
|
onblur=${onblur}
|
||||||
|
onchange=${upload} />
|
||||||
|
<label for="file-upload"
|
||||||
|
id="browse"
|
||||||
|
class="btn browse"
|
||||||
|
title="${state.translate('uploadPageBrowseButton1')}">
|
||||||
|
${state.translate('uploadPageBrowseButton1')}</label>
|
||||||
|
</div>
|
||||||
|
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
||||||
|
${fileList(state, emit)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function dragover(event) {
|
||||||
|
const div = document.querySelector('.upload-window');
|
||||||
|
div.classList.add('ondrag');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragleave(event) {
|
||||||
|
const div = document.querySelector('.upload-window');
|
||||||
|
div.classList.remove('ondrag');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onfocus(event) {
|
||||||
|
event.target.classList.add('has-focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onblur(event) {
|
||||||
|
event.target.classList.remove('has-focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.target;
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fadeOut('page-one');
|
||||||
|
emit('upload', { file, type: 'click' });
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
166
app/utils.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
const b64 = require('base64-js');
|
||||||
|
|
||||||
|
function arrayToB64(array) {
|
||||||
|
return b64
|
||||||
|
.fromByteArray(array)
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64ToArray(str) {
|
||||||
|
str = (str + '==='.slice((str.length + 3) % 4))
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
return b64.toByteArray(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(str) {
|
||||||
|
return str;
|
||||||
|
/* TODO: enable once we have an opt-in ui element
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
return;
|
||||||
|
} else if (Notification.permission === 'granted') {
|
||||||
|
new Notification(str);
|
||||||
|
} else if (Notification.permission !== 'denied') {
|
||||||
|
Notification.requestPermission(function(permission) {
|
||||||
|
if (permission === 'granted') new Notification(str);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShim(polyfill) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const shim = document.createElement('script');
|
||||||
|
shim.src = polyfill;
|
||||||
|
shim.addEventListener('load', () => resolve(true));
|
||||||
|
shim.addEventListener('error', () => resolve(false));
|
||||||
|
document.head.appendChild(shim);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function canHasSend(polyfill) {
|
||||||
|
try {
|
||||||
|
const key = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
new ArrayBuffer(8)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return loadShim(polyfill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFile(id) {
|
||||||
|
return /^[0-9a-fA-F]{10}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(str) {
|
||||||
|
const aux = document.createElement('input');
|
||||||
|
aux.setAttribute('value', str);
|
||||||
|
aux.contentEditable = true;
|
||||||
|
aux.readOnly = true;
|
||||||
|
document.body.appendChild(aux);
|
||||||
|
if (navigator.userAgent.match(/iphone|ipad|ipod/i)) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(aux);
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
aux.setSelectionRange(0, str.length);
|
||||||
|
} else {
|
||||||
|
aux.select();
|
||||||
|
}
|
||||||
|
const result = document.execCommand('copy');
|
||||||
|
document.body.removeChild(aux);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCALIZE_NUMBERS = !!(
|
||||||
|
typeof Intl === 'object' &&
|
||||||
|
Intl &&
|
||||||
|
typeof Intl.NumberFormat === 'function' &&
|
||||||
|
typeof navigator === 'object'
|
||||||
|
);
|
||||||
|
|
||||||
|
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||||
|
function bytes(num) {
|
||||||
|
if (num < 1) {
|
||||||
|
return '0B';
|
||||||
|
}
|
||||||
|
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
||||||
|
const n = Number(num / Math.pow(1000, exponent));
|
||||||
|
let nStr = n.toFixed(1);
|
||||||
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
try {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
nStr = n.toLocaleString(locale, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${nStr}${UNITS[exponent]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function percent(ratio) {
|
||||||
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
try {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
return ratio.toLocaleString(locale, { style: 'percent' });
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${Math.floor(ratio * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowedCopy() {
|
||||||
|
const support = !!document.queryCommandSupported;
|
||||||
|
return support ? document.queryCommandSupported('copy') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(delay = 100) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fadeOut(id) {
|
||||||
|
const classes = document.getElementById(id).classList;
|
||||||
|
classes.remove('fadeIn');
|
||||||
|
classes.add('fadeOut');
|
||||||
|
return delay(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fadeOut,
|
||||||
|
delay,
|
||||||
|
allowedCopy,
|
||||||
|
bytes,
|
||||||
|
percent,
|
||||||
|
copyToClipboard,
|
||||||
|
arrayToB64,
|
||||||
|
b64ToArray,
|
||||||
|
notify,
|
||||||
|
canHasSend,
|
||||||
|
isFile,
|
||||||
|
ONE_DAY_IN_MS
|
||||||
|
};
|
||||||
1
assets/check-16-blue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#0A84FF " d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/></svg>
|
||||||
|
After Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
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('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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +21,16 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#progress circle {
|
||||||
|
stroke: #eee;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress #bar {
|
||||||
|
transition: stroke-dashoffset 300ms linear;
|
||||||
|
stroke: #3b9dff;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -79,7 +88,7 @@ body {
|
|||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
background-color: #0297f8;
|
background-color: #0297f8;
|
||||||
background-image: url('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;
|
||||||
@@ -120,33 +129,74 @@ 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-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
/** 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +211,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;
|
||||||
@@ -175,8 +225,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;
|
||||||
@@ -200,28 +249,39 @@ 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;
|
||||||
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;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
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 {
|
||||||
@@ -284,17 +344,32 @@ tbody {
|
|||||||
width: 12%;
|
width: 12%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overflow-col {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-col {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-delete,
|
.icon-delete,
|
||||||
.icon-copy,
|
.icon-copy,
|
||||||
.icon-check {
|
.icon-check {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-copy[disabled="disabled"] {
|
.icon-copy[disabled='disabled'] {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-copied {
|
||||||
|
color: #0a8dff;
|
||||||
|
}
|
||||||
|
|
||||||
/* Popup container */
|
/* Popup container */
|
||||||
.popup {
|
.popup {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -325,7 +400,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;
|
||||||
@@ -412,12 +487,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: 53px;
|
|
||||||
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;
|
||||||
@@ -430,7 +501,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 {
|
||||||
@@ -452,10 +524,18 @@ 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;
|
||||||
@@ -463,6 +543,8 @@ tbody {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#share-window-r > div {
|
#share-window-r > div {
|
||||||
@@ -473,7 +555,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 {
|
||||||
@@ -490,9 +577,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;
|
||||||
@@ -513,21 +600,22 @@ tbody {
|
|||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
height: 60px;
|
|
||||||
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 {
|
||||||
@@ -562,6 +650,25 @@ tbody {
|
|||||||
color: #0287e8;
|
color: #0287e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectPassword {
|
||||||
|
padding: 10px 0;
|
||||||
|
align-self: left;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setPassword {
|
||||||
|
align-self: left;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 80%;
|
||||||
|
padding: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* upload-error */
|
/* upload-error */
|
||||||
#upload-error {
|
#upload-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -600,11 +707,15 @@ 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;
|
||||||
@@ -644,7 +755,6 @@ tbody {
|
|||||||
background: #0297f8;
|
background: #0297f8;
|
||||||
border: 1px solid #0297f8;
|
border: 1px solid #0297f8;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-weight: 300;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +768,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;
|
||||||
@@ -691,6 +801,62 @@ 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 {
|
||||||
|
flex: 0 1 165px;
|
||||||
|
background: #0297f8;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
border: 1px solid #0297f8;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#unlock-btn:hover {
|
||||||
|
background-color: #0287e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-no-btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* footer */
|
/* footer */
|
||||||
.footer {
|
.footer {
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -713,7 +879,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;
|
||||||
@@ -755,6 +921,65 @@ tbody {
|
|||||||
margin-bottom: -5px;
|
margin-bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#addPasswordWrapper {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPassword {
|
||||||
|
position: absolute;
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPasswordWrapper label {
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPassword:checked + label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-device-width: 992px), (max-width: 992px) {
|
@media (max-device-width: 992px), (max-width: 992px) {
|
||||||
.popup .popuptext {
|
.popup .popuptext {
|
||||||
left: auto;
|
left: auto;
|
||||||
@@ -825,22 +1050,40 @@ 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 {
|
||||||
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 {
|
||||||
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;
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
assets/send-fb.jpg
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
assets/send-twitter.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
1
assets/send_logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd" transform="translate(-0.231,0.11948695)"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
10
browserconfig.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square70x70logo src=“favicon-76.png”/>
|
||||||
|
<square150x150logo src="favicon-228.png"/>
|
||||||
|
<TileColor>#0297F8</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
5
browserslist
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
last 2 chrome versions
|
||||||
|
last 2 firefox versions
|
||||||
|
firefox esr
|
||||||
|
ie >= 9
|
||||||
|
safari >= 9
|
||||||
38
build/fluent_loader.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const { MessageContext } = require('fluent');
|
||||||
|
|
||||||
|
function toJSON(map) {
|
||||||
|
return JSON.stringify(Array.from(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(source) {
|
||||||
|
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||||
|
const result = localeExp.exec(this.resourcePath);
|
||||||
|
const locale = result && result[1];
|
||||||
|
// pre-parse the ftl
|
||||||
|
const context = new MessageContext(locale);
|
||||||
|
context.addMessages(source);
|
||||||
|
if (!locale) {
|
||||||
|
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
module.exports = \`
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
var fluent = require('fluent');
|
||||||
|
}
|
||||||
|
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||||
|
ctx._messages = new Map(${toJSON(context._messages)});
|
||||||
|
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)}'`;
|
||||||
|
};
|
||||||
@@ -16,7 +16,6 @@ deployment:
|
|||||||
latest:
|
latest:
|
||||||
branch: master
|
branch: master
|
||||||
commands:
|
commands:
|
||||||
- npm run build
|
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
- docker build -t mozilla/send:latest .
|
- docker build -t mozilla/send:latest .
|
||||||
- docker push mozilla/send:latest
|
- docker push mozilla/send:latest
|
||||||
@@ -24,14 +23,13 @@ deployment:
|
|||||||
tag: /.*/
|
tag: /.*/
|
||||||
owner: mozilla
|
owner: mozilla
|
||||||
commands:
|
commands:
|
||||||
- npm run build
|
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
- docker build -t mozilla/send:$CIRCLE_TAG .
|
||||||
- docker push mozilla/send:$CIRCLE_TAG
|
- docker push mozilla/send:$CIRCLE_TAG
|
||||||
|
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- npm run build:version
|
- npm run build
|
||||||
- npm run lint
|
- npm run lint
|
||||||
- npm test
|
- npm test
|
||||||
- nsp check
|
- nsp check
|
||||||
|
|||||||
32
common/assets.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const genmap = require('../build/generate_asset_map');
|
||||||
|
const isServer = typeof genmap === 'function';
|
||||||
|
const prefix = isServer ? '/' : '';
|
||||||
|
let manifest = {};
|
||||||
|
try {
|
||||||
|
//eslint-disable-next-line node/no-missing-require
|
||||||
|
manifest = require('../dist/manifest.json');
|
||||||
|
} catch (e) {
|
||||||
|
// use middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
const assets = isServer ? manifest : genmap;
|
||||||
|
|
||||||
|
function getAsset(name) {
|
||||||
|
return prefix + assets[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
get: getAsset,
|
||||||
|
setMiddleware: function(middleware) {
|
||||||
|
if (middleware) {
|
||||||
|
instance.get = function getAssetWithMiddleware(name) {
|
||||||
|
const f = middleware.fileSystem.readFileSync(
|
||||||
|
middleware.getFilenameFromUrl('/manifest.json')
|
||||||
|
);
|
||||||
|
return prefix + JSON.parse(f)[name];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = instance;
|
||||||
51
common/locales.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const gen = require('../build/generate_l10n_map');
|
||||||
|
|
||||||
|
const isServer = typeof gen === 'function';
|
||||||
|
const prefix = isServer ? '/' : '';
|
||||||
|
let manifest = {};
|
||||||
|
try {
|
||||||
|
//eslint-disable-next-line node/no-missing-require
|
||||||
|
manifest = require('../dist/manifest.json');
|
||||||
|
} catch (e) {
|
||||||
|
// use middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
const locales = isServer ? manifest : gen;
|
||||||
|
|
||||||
|
function getLocale(name) {
|
||||||
|
return prefix + locales[`public/locales/${name}/send.ftl`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverTranslator(name) {
|
||||||
|
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserTranslator() {
|
||||||
|
return locales.translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translator = isServer ? serverTranslator : browserTranslator;
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
get: getLocale,
|
||||||
|
getTranslator: translator,
|
||||||
|
setMiddleware: function(middleware) {
|
||||||
|
if (middleware) {
|
||||||
|
const _eval = require('require-from-string');
|
||||||
|
instance.get = function getLocaleWithMiddleware(name) {
|
||||||
|
const f = middleware.fileSystem.readFileSync(
|
||||||
|
middleware.getFilenameFromUrl('/manifest.json')
|
||||||
|
);
|
||||||
|
return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
|
||||||
|
};
|
||||||
|
instance.getTranslator = function(name) {
|
||||||
|
const f = middleware.fileSystem.readFileSync(
|
||||||
|
middleware.getFilenameFromUrl(instance.get(name))
|
||||||
|
);
|
||||||
|
return _eval(f.toString());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = instance;
|
||||||
@@ -8,5 +8,6 @@ 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
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ Triggered whenever a user stops uploading a file. Includes:
|
|||||||
- `cd2`
|
- `cd2`
|
||||||
- `cd6`
|
- `cd6`
|
||||||
|
|
||||||
|
#### `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:
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
env:
|
|
||||||
browser: true
|
|
||||||
jquery: true
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const Raven = require('raven-js');
|
|
||||||
const { unsupported } = require('./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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Raven
|
|
||||||
};
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
const { Raven } = require('./common');
|
|
||||||
const FileReceiver = require('./fileReceiver');
|
|
||||||
const { bytes, notify, gcmCompliant } = require('./utils');
|
|
||||||
const Storage = require('./storage');
|
|
||||||
const storage = new Storage(localStorage);
|
|
||||||
const links = require('./links');
|
|
||||||
const metrics = require('./metrics');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const $ = require('jquery');
|
|
||||||
|
|
||||||
function onUnload(size) {
|
|
||||||
metrics.cancelledDownload({ size });
|
|
||||||
}
|
|
||||||
|
|
||||||
function download() {
|
|
||||||
const $downloadBtn = $('#download-btn');
|
|
||||||
const $title = $('.title');
|
|
||||||
const $file = $('#dl-file');
|
|
||||||
const size = Number($file.attr('data-size'));
|
|
||||||
const ttl = Number($file.attr('data-ttl'));
|
|
||||||
const unloadHandler = onUnload.bind(null, size);
|
|
||||||
const startTime = Date.now();
|
|
||||||
const fileReceiver = new FileReceiver();
|
|
||||||
|
|
||||||
$downloadBtn.attr('disabled', 'disabled');
|
|
||||||
$('#download-page-one').attr('hidden', true);
|
|
||||||
$('#download-progress').removeAttr('hidden');
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileReceiver
|
|
||||||
.download()
|
|
||||||
.catch(err => {
|
|
||||||
metrics.stoppedDownload({ size, err });
|
|
||||||
|
|
||||||
if (err.message === 'notfound') {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
document.l10n.formatValue('errorPageHeader').then(translated => {
|
|
||||||
$title.text(translated);
|
|
||||||
});
|
|
||||||
$downloadBtn.attr('hidden', true);
|
|
||||||
$('#expired-img').removeAttr('hidden');
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
.then(([decrypted, fname]) => {
|
|
||||||
const endTime = Date.now();
|
|
||||||
const time = endTime - startTime;
|
|
||||||
const downloadTime = endTime - downloadEnd;
|
|
||||||
const speed = size / (downloadTime / 1000);
|
|
||||||
storage.totalDownloads += 1;
|
|
||||||
metrics.completedDownload({ size, time, speed });
|
|
||||||
progress.setText(' ');
|
|
||||||
document.l10n
|
|
||||||
.formatValues('downloadNotification', 'downloadFinish')
|
|
||||||
.then(translated => {
|
|
||||||
notify(translated[0]);
|
|
||||||
$title.text(translated[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataView = new DataView(decrypted);
|
|
||||||
const blob = new Blob([dataView]);
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = downloadUrl;
|
|
||||||
if (window.navigator.msSaveBlob) {
|
|
||||||
// if we are in microsoft edge or IE
|
|
||||||
window.navigator.msSaveBlob(blob, fname);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
a.download = fname;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
Raven.captureException(err);
|
|
||||||
return Promise.reject(err);
|
|
||||||
})
|
|
||||||
.then(() => links.setOpenInNewTab(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
$(() => {
|
|
||||||
const $file = $('#dl-file');
|
|
||||||
const filename = $file.attr('data-filename');
|
|
||||||
const b = Number($file.attr('data-size'));
|
|
||||||
const size = bytes(b);
|
|
||||||
document.l10n
|
|
||||||
.formatValue('downloadFileSize', { size })
|
|
||||||
.then(str => $('#dl-filesize').text(str));
|
|
||||||
document.l10n
|
|
||||||
.formatValue('downloadingPageProgress', { filename, size })
|
|
||||||
.then(str => $('#dl-title').text(str));
|
|
||||||
|
|
||||||
gcmCompliant()
|
|
||||||
.then(() => {
|
|
||||||
$('#download-btn').on('click', download);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
metrics.unsupported({ err }).then(() => {
|
|
||||||
location.replace('/unsupported/gcm');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
const EventEmitter = require('events');
|
|
||||||
const { hexToArray } = require('./utils');
|
|
||||||
|
|
||||||
class FileReceiver extends EventEmitter {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
download() {
|
|
||||||
return window.crypto.subtle
|
|
||||||
.importKey(
|
|
||||||
'jwk',
|
|
||||||
{
|
|
||||||
kty: 'oct',
|
|
||||||
k: location.hash.slice(1),
|
|
||||||
alg: 'A128GCM',
|
|
||||||
ext: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AES-GCM'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['encrypt', 'decrypt']
|
|
||||||
)
|
|
||||||
.then(key => {
|
|
||||||
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 fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
|
||||||
resolve([
|
|
||||||
{
|
|
||||||
data: this.result,
|
|
||||||
filename: meta.filename,
|
|
||||||
iv: meta.id
|
|
||||||
},
|
|
||||||
key
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('get', '/assets' + location.pathname.slice(0, -1), true);
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(([fdata, key]) => {
|
|
||||||
this.emit('decrypting');
|
|
||||||
return Promise.all([
|
|
||||||
window.crypto.subtle
|
|
||||||
.decrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: hexToArray(fdata.iv),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
fdata.data
|
|
||||||
)
|
|
||||||
.then(decrypted => {
|
|
||||||
return Promise.resolve(decrypted);
|
|
||||||
}),
|
|
||||||
decodeURIComponent(fdata.filename)
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FileReceiver;
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
const EventEmitter = require('events');
|
|
||||||
const { arrayToHex } = require('./utils');
|
|
||||||
|
|
||||||
class FileSender extends EventEmitter {
|
|
||||||
constructor(file) {
|
|
||||||
super();
|
|
||||||
this.file = file;
|
|
||||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
this.uploadXHR = new XMLHttpRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete(fileId, token) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!fileId || !token) {
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('post', '/delete/' + fileId, true);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(JSON.stringify({ delete_token: token }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.uploadXHR.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
upload() {
|
|
||||||
const self = this;
|
|
||||||
self.emit('loading');
|
|
||||||
return Promise.all([
|
|
||||||
window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['encrypt', 'decrypt']
|
|
||||||
),
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsArrayBuffer(this.file);
|
|
||||||
reader.onload = function(event) {
|
|
||||||
const plaintext = new Uint8Array(this.result);
|
|
||||||
resolve(plaintext);
|
|
||||||
};
|
|
||||||
reader.onerror = function(err) {
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
])
|
|
||||||
.then(([secretKey, plaintext]) => {
|
|
||||||
self.emit('encrypting');
|
|
||||||
return Promise.all([
|
|
||||||
window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: this.iv,
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
plaintext
|
|
||||||
),
|
|
||||||
window.crypto.subtle.exportKey('jwk', secretKey)
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
.then(([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 = self.uploadXHR;
|
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', e => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
self.emit('progress', [e.loaded, e.total]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
const responseObj = JSON.parse(xhr.responseText);
|
|
||||||
return resolve({
|
|
||||||
url: responseObj.url,
|
|
||||||
fileId: responseObj.id,
|
|
||||||
secretKey: keydata.k,
|
|
||||||
deleteToken: responseObj.delete
|
|
||||||
});
|
|
||||||
}
|
|
||||||
reject(xhr.status);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('post', '/upload', true);
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'X-File-Metadata',
|
|
||||||
JSON.stringify({
|
|
||||||
id: fileId,
|
|
||||||
filename: encodeURIComponent(file.name)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
xhr.send(fd);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FileSender;
|
|
||||||
@@ -1,23 +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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setOpenInNewTab
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const { bytes } = require('./utils');
|
|
||||||
const $ = require('jquery');
|
|
||||||
require('jquery-circle-progress');
|
|
||||||
|
|
||||||
let $progress = null;
|
|
||||||
let $percent = null;
|
|
||||||
let $text = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
$percent = $('.percent-number');
|
|
||||||
$text = $('.progress-text');
|
|
||||||
$progress = $('.progress-bar');
|
|
||||||
$progress.circleProgress({
|
|
||||||
value: 0.0,
|
|
||||||
startAngle: -Math.PI / 2,
|
|
||||||
fill: '#3B9DFF',
|
|
||||||
size: 158,
|
|
||||||
animation: { duration: 300 }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setProgress(params) {
|
|
||||||
const percent = params.complete / params.total;
|
|
||||||
$progress.circleProgress('value', percent);
|
|
||||||
$percent.text(`${Math.floor(percent * 100)}`);
|
|
||||||
document.l10n
|
|
||||||
.formatValue('fileSizeProgress', {
|
|
||||||
partialSize: bytes(params.complete),
|
|
||||||
totalSize: bytes(params.total)
|
|
||||||
})
|
|
||||||
.then(setText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setText(str) {
|
|
||||||
$text.text(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setProgress,
|
|
||||||
setText
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
const { isFile } = require('./utils');
|
|
||||||
|
|
||||||
class Storage {
|
|
||||||
constructor(engine) {
|
|
||||||
this.engine = engine;
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalDownloads() {
|
|
||||||
return Number(this.engine.getItem('totalDownloads'));
|
|
||||||
}
|
|
||||||
set totalDownloads(n) {
|
|
||||||
this.engine.setItem('totalDownloads', n);
|
|
||||||
}
|
|
||||||
get totalUploads() {
|
|
||||||
return Number(this.engine.getItem('totalUploads'));
|
|
||||||
}
|
|
||||||
set totalUploads(n) {
|
|
||||||
this.engine.setItem('totalUploads', n);
|
|
||||||
}
|
|
||||||
get referrer() {
|
|
||||||
return this.engine.getItem('referrer');
|
|
||||||
}
|
|
||||||
set referrer(str) {
|
|
||||||
this.engine.setItem('referrer', str);
|
|
||||||
}
|
|
||||||
|
|
||||||
get files() {
|
|
||||||
const fs = [];
|
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
|
||||||
const k = this.engine.key(i);
|
|
||||||
if (isFile(k)) {
|
|
||||||
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() {
|
|
||||||
let length = 0;
|
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
|
||||||
const k = this.engine.key(i);
|
|
||||||
if (isFile(k)) {
|
|
||||||
length += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileById(id) {
|
|
||||||
return this.engine.getItem(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
has(property) {
|
|
||||||
return this.engine.hasOwnProperty(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(property) {
|
|
||||||
this.engine.removeItem(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
addFile(id, file) {
|
|
||||||
this.engine.setItem(id, JSON.stringify(file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Storage;
|
|
||||||
@@ -1,476 +0,0 @@
|
|||||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
|
||||||
const { Raven } = require('./common');
|
|
||||||
const FileSender = require('./fileSender');
|
|
||||||
const {
|
|
||||||
bytes,
|
|
||||||
copyToClipboard,
|
|
||||||
notify,
|
|
||||||
gcmCompliant,
|
|
||||||
ONE_DAY_IN_MS
|
|
||||||
} = require('./utils');
|
|
||||||
const Storage = require('./storage');
|
|
||||||
const storage = new Storage(localStorage);
|
|
||||||
const metrics = require('./metrics');
|
|
||||||
const progress = require('./progress');
|
|
||||||
|
|
||||||
const $ = require('jquery');
|
|
||||||
|
|
||||||
const allowedCopy = () => {
|
|
||||||
const support = !!document.queryCommandSupported;
|
|
||||||
return support ? document.queryCommandSupported('copy') : false;
|
|
||||||
};
|
|
||||||
|
|
||||||
$(() => {
|
|
||||||
gcmCompliant()
|
|
||||||
.then(function() {
|
|
||||||
const $pageOne = $('#page-one');
|
|
||||||
const $copyBtn = $('#copy-btn');
|
|
||||||
const $link = $('#link');
|
|
||||||
const $uploadWindow = $('.upload-window');
|
|
||||||
const $uploadError = $('#upload-error');
|
|
||||||
const $uploadProgress = $('#upload-progress');
|
|
||||||
const $fileList = $('#file-list');
|
|
||||||
|
|
||||||
$pageOne.removeAttr('hidden');
|
|
||||||
$('#file-upload').on('change', onUpload);
|
|
||||||
|
|
||||||
$(document.body).on('dragover', allowDrop).on('drop', onUpload);
|
|
||||||
|
|
||||||
// reset copy button
|
|
||||||
$copyBtn.attr({
|
|
||||||
disabled: !allowedCopy(),
|
|
||||||
'data-l10n-id': 'copyUrlFormButton'
|
|
||||||
});
|
|
||||||
|
|
||||||
$link.attr('disabled', false);
|
|
||||||
|
|
||||||
const toggleHeader = () => {
|
|
||||||
//hide table header if empty list
|
|
||||||
if (document.querySelector('tbody').childNodes.length === 1) {
|
|
||||||
$fileList.attr('hidden', true);
|
|
||||||
} else {
|
|
||||||
$fileList.removeAttr('hidden');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const files = storage.files;
|
|
||||||
if (files.length === 0) {
|
|
||||||
toggleHeader();
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
for (let index in files) {
|
|
||||||
const id = files[index].fileId;
|
|
||||||
//check if file still exists before adding to list
|
|
||||||
checkExistence(id, files[index], true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy link to clipboard
|
|
||||||
$copyBtn.on('click', () => {
|
|
||||||
if (allowedCopy() && copyToClipboard($link.attr('value'))) {
|
|
||||||
metrics.copiedLink({ location: 'success-screen' });
|
|
||||||
|
|
||||||
//disable button for 3s
|
|
||||||
$copyBtn.attr('disabled', true);
|
|
||||||
$link.attr('disabled', true);
|
|
||||||
$copyBtn.html(
|
|
||||||
'<img src="/resources/check-16.svg" class="icon-check"></img>'
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
$copyBtn.attr({
|
|
||||||
disabled: false,
|
|
||||||
'data-l10n-id': 'copyUrlFormButton'
|
|
||||||
});
|
|
||||||
$link.attr('disabled', false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$uploadWindow
|
|
||||||
.on('dragover', () => {
|
|
||||||
$uploadWindow.addClass('ondrag');
|
|
||||||
})
|
|
||||||
.on('dragleave', () => {
|
|
||||||
$uploadWindow.removeClass('ondrag');
|
|
||||||
});
|
|
||||||
|
|
||||||
// on file upload by browse or drag & drop
|
|
||||||
function onUpload(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const clickOrDrop = event.type === 'drop' ? 'drop' : 'click';
|
|
||||||
|
|
||||||
// don't allow upload if not on upload page
|
|
||||||
if ($pageOne.attr('hidden')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.totalUploads += 1;
|
|
||||||
|
|
||||||
let file = '';
|
|
||||||
if (clickOrDrop === 'drop') {
|
|
||||||
if (!event.originalEvent.dataTransfer.files[0]) {
|
|
||||||
$uploadWindow.removeClass('ondrag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.originalEvent.dataTransfer.files.length > 1 ||
|
|
||||||
event.originalEvent.dataTransfer.files[0].size === 0
|
|
||||||
) {
|
|
||||||
$uploadWindow.removeClass('ondrag');
|
|
||||||
document.l10n
|
|
||||||
.formatValue('uploadPageMultipleFilesAlert')
|
|
||||||
.then(str => {
|
|
||||||
alert(str);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file = event.originalEvent.dataTransfer.files[0];
|
|
||||||
} else {
|
|
||||||
file = event.target.files[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > MAXFILESIZE) {
|
|
||||||
return document.l10n
|
|
||||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
|
||||||
.then(alert);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pageOne.attr('hidden', true);
|
|
||||||
$uploadError.attr('hidden', true);
|
|
||||||
$uploadProgress.removeAttr('hidden');
|
|
||||||
document.l10n
|
|
||||||
.formatValue('uploadingPageProgress', {
|
|
||||||
size: bytes(file.size),
|
|
||||||
filename: file.name
|
|
||||||
})
|
|
||||||
.then(str => {
|
|
||||||
$('#upload-filename').text(str);
|
|
||||||
});
|
|
||||||
document.l10n.formatValue('importingFile').then(progress.setText);
|
|
||||||
//don't allow drag and drop when not on page-one
|
|
||||||
$(document.body).off('drop', onUpload);
|
|
||||||
|
|
||||||
const fileSender = new FileSender(file);
|
|
||||||
$('#cancel-upload').on('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;
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#delete-file').on('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.attr('hidden', true);
|
|
||||||
$uploadProgress.attr('hidden', true);
|
|
||||||
$uploadError.attr('hidden', true);
|
|
||||||
$('#share-link').removeAttr('hidden');
|
|
||||||
|
|
||||||
populateFileList(fileData);
|
|
||||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
|
||||||
notify(str);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
// err is 0 when coming from a cancel upload event
|
|
||||||
if (err === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// only show error page when the error is anything other than user cancelling the upload
|
|
||||||
Raven.captureException(err);
|
|
||||||
$pageOne.attr('hidden', true);
|
|
||||||
$uploadProgress.attr('hidden', true);
|
|
||||||
$uploadError.removeAttr('hidden');
|
|
||||||
window.clearTimeout(t);
|
|
||||||
|
|
||||||
metrics.stoppedUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop,
|
|
||||||
err
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
function allowDrop(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkExistence(id, file, populate) {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
if (populate) {
|
|
||||||
populateFileList(file);
|
|
||||||
}
|
|
||||||
} else if (xhr.status === 404) {
|
|
||||||
storage.remove(id);
|
|
||||||
if (storage.numFiles === 0) {
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.open('get', '/exists/' + id, true);
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
//update file table with current files in storage
|
|
||||||
const populateFileList = file => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
const name = document.createElement('td');
|
|
||||||
const link = document.createElement('td');
|
|
||||||
const $copyIcon = $('<img>', {
|
|
||||||
src: '/resources/copy-16.svg',
|
|
||||||
class: 'icon-copy',
|
|
||||||
'data-l10n-id': 'copyUrlHover',
|
|
||||||
disabled: !allowedCopy()
|
|
||||||
});
|
|
||||||
const expiry = document.createElement('td');
|
|
||||||
const del = document.createElement('td');
|
|
||||||
const $delIcon = $('<img>', {
|
|
||||||
src: '/resources/close-16.svg',
|
|
||||||
class: 'icon-delete',
|
|
||||||
'data-l10n-id': 'deleteButtonHover'
|
|
||||||
});
|
|
||||||
const popupDiv = document.createElement('div');
|
|
||||||
const $popupText = $('<div>', { class: 'popuptext' });
|
|
||||||
const cellText = document.createTextNode(file.name);
|
|
||||||
|
|
||||||
const url = file.url.trim() + `#${file.secretKey}`.trim();
|
|
||||||
|
|
||||||
$link.attr('value', url);
|
|
||||||
$('#copy-text')
|
|
||||||
.attr('data-l10n-args', JSON.stringify({ filename: file.name }))
|
|
||||||
.attr('data-l10n-id', 'copyUrlFormLabelWithName');
|
|
||||||
|
|
||||||
$popupText.attr('tabindex', '-1');
|
|
||||||
|
|
||||||
name.appendChild(cellText);
|
|
||||||
|
|
||||||
// create delete button
|
|
||||||
|
|
||||||
const delSpan = document.createElement('span');
|
|
||||||
$(delSpan)
|
|
||||||
.addClass('icon-cancel-1')
|
|
||||||
.attr('data-l10n-id', 'deleteButtonHover');
|
|
||||||
del.appendChild(delSpan);
|
|
||||||
|
|
||||||
const linkSpan = document.createElement('span');
|
|
||||||
$(linkSpan).addClass('icon-docs').attr('data-l10n-id', 'copyUrlHover');
|
|
||||||
|
|
||||||
link.appendChild(linkSpan);
|
|
||||||
link.style.color = '#0A8DFF';
|
|
||||||
|
|
||||||
//copy link to clipboard when icon clicked
|
|
||||||
$copyIcon.on('click', () => {
|
|
||||||
// record copied event from upload list
|
|
||||||
metrics.copiedLink({ location: 'upload-list' });
|
|
||||||
copyToClipboard(url);
|
|
||||||
document.l10n.formatValue('copiedUrl').then(translated => {
|
|
||||||
link.innerHTML = translated;
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
const linkImg = document.createElement('img');
|
|
||||||
$(linkImg)
|
|
||||||
.addClass('icon-copy')
|
|
||||||
.attr('data-l10n-id', 'copyUrlHover')
|
|
||||||
.attr('src', '/resources/copy-16.svg');
|
|
||||||
|
|
||||||
$(link).html(linkImg);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
file.creationDate = new Date(file.creationDate);
|
|
||||||
|
|
||||||
const future = new Date();
|
|
||||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
|
||||||
|
|
||||||
let countdown = 0;
|
|
||||||
countdown = future.getTime() - Date.now();
|
|
||||||
let minutes = Math.floor(countdown / 1000 / 60);
|
|
||||||
let hours = Math.floor(minutes / 60);
|
|
||||||
let seconds = Math.floor(countdown / 1000 % 60);
|
|
||||||
|
|
||||||
const poll = () => {
|
|
||||||
countdown = future.getTime() - Date.now();
|
|
||||||
minutes = Math.floor(countdown / 1000 / 60);
|
|
||||||
hours = Math.floor(minutes / 60);
|
|
||||||
seconds = Math.floor(countdown / 1000 % 60);
|
|
||||||
let t;
|
|
||||||
|
|
||||||
if (hours >= 1) {
|
|
||||||
expiry.innerHTML = hours + 'h ' + minutes % 60 + 'm';
|
|
||||||
t = setTimeout(() => {
|
|
||||||
poll();
|
|
||||||
}, 60000);
|
|
||||||
} else if (hours === 0) {
|
|
||||||
expiry.innerHTML = minutes + 'm ' + seconds + 's';
|
|
||||||
t = window.setTimeout(() => {
|
|
||||||
poll();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
//remove from list when expired
|
|
||||||
if (countdown <= 0) {
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
$(expiry).parents('tr').remove();
|
|
||||||
window.clearTimeout(t);
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
|
|
||||||
// create popup
|
|
||||||
popupDiv.classList.add('popup');
|
|
||||||
const $popupMessage = $('<div>', { class: 'popup-message' });
|
|
||||||
$popupMessage.attr('data-l10n-id', 'deletePopupText');
|
|
||||||
const $popupAction = $('<div>', { class: 'popup-action' });
|
|
||||||
const $popupNvmSpan = $('<span>', { class: 'popup-no' });
|
|
||||||
$popupNvmSpan.attr('data-l10n-id', 'deletePopupCancel');
|
|
||||||
const $popupDelSpan = $('<span>', { class: 'popup-yes' });
|
|
||||||
$popupDelSpan.attr('data-l10n-id', 'deletePopupYes');
|
|
||||||
|
|
||||||
$popupText.html([$popupMessage, $popupAction]);
|
|
||||||
$popupAction.html([$popupNvmSpan, $popupDelSpan]);
|
|
||||||
|
|
||||||
// add data cells to table row
|
|
||||||
row.appendChild(name);
|
|
||||||
$(link).append($copyIcon);
|
|
||||||
row.appendChild(link);
|
|
||||||
row.appendChild(expiry);
|
|
||||||
$(popupDiv).append($popupText);
|
|
||||||
$(del).append($delIcon);
|
|
||||||
del.appendChild(popupDiv);
|
|
||||||
row.appendChild(del);
|
|
||||||
$('tbody').append(row); //add row to table
|
|
||||||
|
|
||||||
// delete file
|
|
||||||
$popupText.find('.popup-yes').on('click', e => {
|
|
||||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
|
||||||
$(e.target).parents('tr').remove();
|
|
||||||
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
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
});
|
|
||||||
toggleHeader();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// show popup
|
|
||||||
$delIcon.on('click', () => {
|
|
||||||
$popupText.addClass('show').focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// hide popup
|
|
||||||
$popupText.find('.popup-no').on('click', e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
$popupText.removeClass('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$popupText.on('click', e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
//close when popup loses focus
|
|
||||||
$popupText.on('blur', () => {
|
|
||||||
$popupText.removeClass('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
toggleHeader();
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
metrics.unsupported({ err }).then(() => {
|
|
||||||
location.replace('/unsupported/gcm');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,137 +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]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
bytes,
|
|
||||||
copyToClipboard,
|
|
||||||
arrayToHex,
|
|
||||||
hexToArray,
|
|
||||||
notify,
|
|
||||||
gcmCompliant,
|
|
||||||
isFile,
|
|
||||||
ONE_DAY_IN_MS
|
|
||||||
};
|
|
||||||
9339
package-lock.json
generated
180
package.json
@@ -1,69 +1,131 @@
|
|||||||
{
|
{
|
||||||
"name": "firefox-send",
|
"name": "firefox-send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "1.1.0",
|
"version": "2.1.2",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"dependencies": {
|
"repository": "mozilla/send",
|
||||||
"aws-sdk": "^2.89.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.7.1"
|
"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": [
|
||||||
"babel-core": "^6.25.0",
|
"prettier --single-quote --write",
|
||||||
"babel-loader": "^7.1.1",
|
"eslint",
|
||||||
"babel-plugin-add-module-exports": "^0.2.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",
|
||||||
"browserify": "^14.4.0",
|
"stylelint",
|
||||||
"eslint": "^4.3.0",
|
"git add"
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
]
|
||||||
"eslint-plugin-node": "^5.1.1",
|
|
||||||
"eslint-plugin-security": "^1.4.0",
|
|
||||||
"git-rev-sync": "^1.9.1",
|
|
||||||
"jquery": "^3.2.1",
|
|
||||||
"jquery-circle-progress": "^1.2.2",
|
|
||||||
"l20n": "^5.0.0",
|
|
||||||
"mocha": "^3.4.2",
|
|
||||||
"npm-run-all": "^4.0.2",
|
|
||||||
"prettier": "^1.5.3",
|
|
||||||
"proxyquire": "^1.8.0",
|
|
||||||
"raven-js": "^3.17.0",
|
|
||||||
"selenium-webdriver": "^3.5.0",
|
|
||||||
"sinon": "^2.3.8",
|
|
||||||
"stylelint": "^8.0.0",
|
|
||||||
"stylelint-config-standard": "^17.0.0",
|
|
||||||
"supertest": "^3.0.0",
|
|
||||||
"testpilot-ga": "^0.3.0",
|
|
||||||
"webcrypto-liner": "^0.1.25",
|
|
||||||
"webpack": "^3.4.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.2.0"
|
"node": ">=8.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/mozilla/send/",
|
"devDependencies": {
|
||||||
"license": "MPL-2.0",
|
"autoprefixer": "^7.1.6",
|
||||||
"repository": "mozilla/send",
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-plugin-yo-yoify": "^1.0.1",
|
||||||
|
"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.2.0",
|
||||||
|
"cross-env": "^5.1.1",
|
||||||
|
"css-loader": "^0.28.7",
|
||||||
|
"css-mqpacker": "^6.0.1",
|
||||||
|
"cssnano": "^3.10.0",
|
||||||
|
"eslint": "^4.10.0",
|
||||||
|
"eslint-plugin-mocha": "^4.11.0",
|
||||||
|
"eslint-plugin-node": "^5.2.1",
|
||||||
|
"eslint-plugin-security": "^1.4.0",
|
||||||
|
"expose-loader": "^0.7.3",
|
||||||
|
"extract-loader": "^1.0.1",
|
||||||
|
"file-loader": "^1.1.5",
|
||||||
|
"git-rev-sync": "^1.9.1",
|
||||||
|
"github-changes": "^1.1.1",
|
||||||
|
"html-loader": "^0.5.1",
|
||||||
|
"husky": "^0.14.3",
|
||||||
|
"lint-staged": "^4.3.0",
|
||||||
|
"mocha": "^3.5.3",
|
||||||
|
"nanobus": "^4.3.0",
|
||||||
|
"npm-run-all": "^4.1.2",
|
||||||
|
"postcss-loader": "^2.0.8",
|
||||||
|
"prettier": "^1.8.2",
|
||||||
|
"proxyquire": "^1.8.0",
|
||||||
|
"raven-js": "^3.19.1",
|
||||||
|
"redis-mock": "^0.20.0",
|
||||||
|
"require-from-string": "^2.0.1",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"selenium-webdriver": "^3.6.0",
|
||||||
|
"sinon": "^4.1.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.0.2",
|
||||||
|
"webpack": "^3.8.1",
|
||||||
|
"webpack-dev-server": "2.9.1",
|
||||||
|
"webpack-manifest-plugin": "^1.3.2",
|
||||||
|
"webpack-unassert-loader": "^1.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.149.0",
|
||||||
|
"body-parser": "^1.18.2",
|
||||||
|
"choo": "^6.5.1",
|
||||||
|
"cldr-core": "^32.0.0",
|
||||||
|
"connect-busboy": "0.0.2",
|
||||||
|
"convict": "^4.0.1",
|
||||||
|
"express": "^4.16.2",
|
||||||
|
"fluent": "^0.4.1",
|
||||||
|
"fluent-langneg": "^0.1.0",
|
||||||
|
"helmet": "^3.9.0",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"mozlog": "^2.1.1",
|
||||||
|
"raven": "^2.2.1",
|
||||||
|
"redis": "^2.8.0"
|
||||||
|
},
|
||||||
"availableLanguages": [
|
"availableLanguages": [
|
||||||
|
"en-US",
|
||||||
|
"ast",
|
||||||
"az",
|
"az",
|
||||||
|
"bs",
|
||||||
"ca",
|
"ca",
|
||||||
|
"cak",
|
||||||
"cs",
|
"cs",
|
||||||
"cy",
|
"cy",
|
||||||
"de",
|
"de",
|
||||||
"dsb",
|
"dsb",
|
||||||
"el",
|
"el",
|
||||||
"en-US",
|
"es-AR",
|
||||||
"es-CL",
|
"es-CL",
|
||||||
"es-ES",
|
"es-ES",
|
||||||
"es-MX",
|
"es-MX",
|
||||||
|
"et",
|
||||||
|
"fa",
|
||||||
"fr",
|
"fr",
|
||||||
"fy-NL",
|
"fy-NL",
|
||||||
"hsb",
|
"hsb",
|
||||||
@@ -71,6 +133,7 @@
|
|||||||
"id",
|
"id",
|
||||||
"it",
|
"it",
|
||||||
"ja",
|
"ja",
|
||||||
|
"ka",
|
||||||
"kab",
|
"kab",
|
||||||
"ko",
|
"ko",
|
||||||
"ms",
|
"ms",
|
||||||
@@ -84,32 +147,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": {
|
|
||||||
"build": "npm-run-all build:*",
|
|
||||||
"build:js": "webpack -p",
|
|
||||||
"build:version": "node scripts/version",
|
|
||||||
"build:vendor": "cp node_modules/l20n/dist/web/l20n.min.js node_modules/babel-polyfill/dist/polyfill.min.js public",
|
|
||||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
|
||||||
"dev": "npm run build && npm start",
|
|
||||||
"format": "prettier '{frontend/src/,scripts/,server/,test/**/!(bundle)}*.js' 'public/*.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 'public/*.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": "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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
postcss.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
const cssnano = require('cssnano');
|
||||||
|
const mqpacker = require('css-mqpacker');
|
||||||
|
|
||||||
|
const config = require('./server/config');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
plugins: [autoprefixer, mqpacker, cssnano]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.env === 'development') {
|
||||||
|
options.map = { inline: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = options;
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
80
public/locales/ar/send.ftl
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Firefox Send is a brand name and should not be localized.
|
||||||
|
title = فَيَرفُكس سِنْد
|
||||||
|
siteSubtitle = تجربة وِبّيّة
|
||||||
|
siteFeedback = الانطباعات
|
||||||
|
uploadPageHeader = شارِك ملفاتك بخصوصية وتعمية
|
||||||
|
uploadPageExplainer = أرسل الملفات عبر رابط آمن خاص ومعمّى تنتهي صلاحيته تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
||||||
|
uploadPageLearnMore = اطّلع على المزيد
|
||||||
|
uploadPageDropMessage = أسقِط ملفّك هنا لبدء الرفع
|
||||||
|
uploadPageSizeMessage = لتتحصل على أفضل تجربة، من المستحسن أن يكون الملف أصغر من 1 غ.بايت
|
||||||
|
uploadPageBrowseButton = اختر ملفّا على حاسوبك
|
||||||
|
.title = اختر ملفّا على حاسوبك
|
||||||
|
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
||||||
|
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
||||||
|
importingFile = يستورد…
|
||||||
|
encryptingFile = يعمّي…
|
||||||
|
decryptingFile = يفك التعمية…
|
||||||
|
notifyUploadDone = انتهى الرفع.
|
||||||
|
uploadingPageMessage = ما إن يُرفع الملف سيُتاح ضبط خيارات انتهاء صلاحيته.
|
||||||
|
uploadingPageCancel = ألغِ الرفع
|
||||||
|
.title = ألغِ الرفع
|
||||||
|
uploadCancelNotification = أُلغي الرفع.
|
||||||
|
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
||||||
|
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
||||||
|
uploadSvgAlt
|
||||||
|
.alt = ارفع
|
||||||
|
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
||||||
|
copyUrlFormButton = انسخ إلى الحافظة
|
||||||
|
.title = انسخ إلى الحافظة
|
||||||
|
copiedUrl = نُسخ!
|
||||||
|
deleteFileButton = احذف الملف
|
||||||
|
.title = احذف الملف
|
||||||
|
sendAnotherFileLink = أرسل ملفّا آخر
|
||||||
|
.title = أرسل ملفّا آخر
|
||||||
|
// Alternative text used on the download link/button (indicates an action).
|
||||||
|
downloadAltText
|
||||||
|
.alt = نزّل
|
||||||
|
downloadFileName = نزّل { $filename }
|
||||||
|
unlockInputLabel = أدخل كلمة السر
|
||||||
|
unlockInputPlaceholder = كلمة السر
|
||||||
|
downloadFileTitle = نزِّل الملف المعمّى
|
||||||
|
// Firefox Send is a brand name and should not be localized.
|
||||||
|
downloadMessage = يُرسل إليك صديقك ملفا عبر «فَيَرفُكس سِنْد»، وهي خدمة تتيح لك مشاركة الملفات عبر رابط آمن وخاص ومعمّى، حيث تنتهي صلاحياتها تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
||||||
|
// Text and title used on the download link/button (indicates an action).
|
||||||
|
downloadButtonLabel = نزّل
|
||||||
|
.title = نزّل
|
||||||
|
downloadNotification = لقد اكتمل التنزيل.
|
||||||
|
downloadFinish = اكتمل التنزيل
|
||||||
|
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||||
|
fileSizeProgress = ({ $partialSize } من أصل { $totalSize })
|
||||||
|
// Firefox Send is a brand name and should not be localized.
|
||||||
|
sendYourFilesLink = جرِّب «فَيَرفُكس سِنْد»
|
||||||
|
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
||||||
|
errorAltText
|
||||||
|
.alt = خطأ أثناء الرفع
|
||||||
|
errorPageHeader = حدث خطب ما.
|
||||||
|
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
||||||
|
errorPageLink = أرسل ملفا آخر
|
||||||
|
fileTooBig = حجم الملف كبير للغاية لرفعه. يجب أن يكون أصغر من { $size }.
|
||||||
|
notSupportedHeader = متصفحك غير مدعوم.
|
||||||
|
// Firefox Send is a brand name and should not be localized.
|
||||||
|
notSupportedDetail = للأسف فإن متصفحك لا يدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تجربة متصفح آخر، ونحن ننصحك بِفَيَرفُكس!
|
||||||
|
notSupportedLink = لماذا متصفحي غير مدعوم؟
|
||||||
|
notSupportedOutdatedDetail = للأسف فإن إصدارة فَيَرفُكس هذه لا تدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تحديث متصفحك.
|
||||||
|
updateFirefox = حدّث فَيَرفُكس
|
||||||
|
copyFileList = انسخ الرابط
|
||||||
|
deleteFileList = احذف
|
||||||
|
legalHeader = الشروط والخصوصية
|
||||||
|
deletePopupText = أأحذف هذا الملف؟
|
||||||
|
deletePopupYes = نعم
|
||||||
|
deletePopupCancel = ألغِ
|
||||||
|
deleteButtonHover
|
||||||
|
.title = احذف
|
||||||
|
copyUrlHover
|
||||||
|
.title = انسخ الرابط
|
||||||
|
footerLinkTerms = الشروط
|
||||||
|
footerLinkCookies = الكعكات
|
||||||
|
requirePasswordCheckbox = اطلب كلمة سر لتنزيل هذا الملف
|
||||||
|
addPasswordButton = أضِف كلمة سر
|
||||||
|
// This label is followed by the password needed to download a file
|
||||||
|
passwordResult = كلمة السر: { $password }
|
||||||