Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e6d8eaddd | ||
|
|
faaa8afe17 | ||
|
|
84e8abf2c5 | ||
|
|
5db8a20b9d | ||
|
|
117c6ea12d | ||
|
|
83880d97e6 | ||
|
|
c3be5228cb | ||
|
|
ade214c69c | ||
|
|
4cb040d70d | ||
|
|
73ccce627c | ||
|
|
4043d35c8b | ||
|
|
a576cc0198 | ||
|
|
fa1f0208a4 | ||
|
|
193d3b1aef | ||
|
|
3b28ce88bf | ||
|
|
c77983d902 | ||
|
|
6ef9f8fa43 | ||
|
|
9136694d29 | ||
|
|
ec7f058afc | ||
|
|
62989ee2c9 | ||
|
|
85068e97ae | ||
|
|
dde2c4d5a9 | ||
|
|
20cf8d0a15 | ||
|
|
b2bd623a49 | ||
|
|
fc3001978c | ||
|
|
d6823f492d | ||
|
|
c3751c2efc | ||
|
|
70396e2f36 | ||
|
|
b557665d04 | ||
|
|
6393d70a33 | ||
|
|
834df8526f | ||
|
|
f5bd332ff8 | ||
|
|
0d5fb1740d | ||
|
|
5ed4db9689 | ||
|
|
31b810eb7d | ||
|
|
7243a10340 | ||
|
|
b18bcd3b6e | ||
|
|
09a6192bf5 | ||
|
|
9585850d6d | ||
|
|
14f3d837f9 | ||
|
|
d660eda64c | ||
|
|
2bbb0f14f3 | ||
|
|
fbf906fe5b | ||
|
|
463393552b | ||
|
|
18a811aa31 | ||
|
|
cb0d69c5cd | ||
|
|
b9eb653f1f | ||
|
|
37bb6fd982 | ||
|
|
e5d1e8f028 | ||
|
|
99477774cf | ||
|
|
8d419072d7 | ||
|
|
a2fcce9e40 | ||
|
|
55d3d1a792 | ||
|
|
88dbda87dc | ||
|
|
76a6f02eb7 | ||
|
|
34f26fc017 | ||
|
|
4f36f2befa | ||
|
|
f816c0bc12 | ||
|
|
60bfd1b67c | ||
|
|
fc7c7e2c71 | ||
|
|
a9e0ab17e5 | ||
|
|
80cf343516 | ||
|
|
744dbb3a6f | ||
|
|
e4301963a2 | ||
|
|
48da85b12b | ||
|
|
e02bc54172 | ||
|
|
3bb6a6fa44 | ||
|
|
64e7182f30 | ||
|
|
8c1ba8b45a | ||
|
|
85a7be01cb | ||
|
|
262322b9e8 | ||
|
|
88904621b3 | ||
|
|
da8f7c7172 | ||
|
|
caf594877f | ||
|
|
ffc9386221 | ||
|
|
6670d9ad69 | ||
|
|
6d8db25d61 | ||
|
|
a38a27dc60 | ||
|
|
69055a504b | ||
|
|
902010704a | ||
|
|
1992626da8 | ||
|
|
3c8cdbc7df | ||
|
|
72b07b82c9 | ||
|
|
846f5c7254 | ||
|
|
b0e18b29ff | ||
|
|
bbc9cab661 | ||
|
|
6179e07dd8 | ||
|
|
1363f4d6a2 | ||
|
|
3e47556560 | ||
|
|
aa3a2f8c55 | ||
|
|
6e7283664c | ||
|
|
fc89da153d | ||
|
|
8c4cb90b3a | ||
|
|
3cdd207953 | ||
|
|
5b1e2f38f4 | ||
|
|
5822900508 | ||
|
|
e898f35c46 | ||
|
|
7ed30f497b | ||
|
|
e66bc966d2 | ||
|
|
acfcae5dec | ||
|
|
95a83922df | ||
|
|
f7329e3316 | ||
|
|
cde63595e8 | ||
|
|
ca6efdae55 | ||
|
|
b712a9d175 | ||
|
|
45eccc1cad | ||
|
|
4068291f7c | ||
|
|
e4a724422e | ||
|
|
4fa22a6262 | ||
|
|
1fae4cfd8d | ||
|
|
9a6dbee694 | ||
|
|
716555f76f | ||
|
|
cc35206ee4 | ||
|
|
359e77f451 | ||
|
|
b805c78a9a | ||
|
|
a4ee6ad5e5 | ||
|
|
22b1c3a286 | ||
|
|
cd6648be56 | ||
|
|
8d00372824 | ||
|
|
729f716e97 | ||
|
|
93d2e91afa | ||
|
|
26b228a976 | ||
|
|
9735aa62bd | ||
|
|
4122f4dc3e | ||
|
|
b88cf22d8a | ||
|
|
6970e9228a | ||
|
|
5ce0846580 | ||
|
|
61c49fb329 | ||
|
|
2127857790 | ||
|
|
ba348b6839 | ||
|
|
188521f985 | ||
|
|
e0847e08c3 | ||
|
|
d287f67ac0 | ||
|
|
8f327fa439 | ||
|
|
ab60262cc9 | ||
|
|
e5f2b386bb | ||
|
|
61f131af58 | ||
|
|
2cf2fcebc9 | ||
|
|
c2e8139c6e | ||
|
|
ef9b15c1d7 | ||
|
|
e9c49073a8 | ||
|
|
cec3d6b548 | ||
|
|
3f89c2bf0a | ||
|
|
b419a6025f | ||
|
|
b07671719c | ||
|
|
6379a360fe | ||
|
|
89bc51c821 | ||
|
|
5dd5743871 | ||
|
|
91cc82d570 | ||
|
|
fcdb905430 | ||
|
|
9032e42912 | ||
|
|
00462947e3 | ||
|
|
b411447ebb | ||
|
|
52173bf6e7 | ||
|
|
9234bce75d | ||
|
|
b32e63c305 | ||
|
|
ee8ff3d220 | ||
|
|
3138f111e1 | ||
|
|
af0c497aab | ||
|
|
dad6132342 | ||
|
|
3ffdbd863b | ||
|
|
20b9766742 | ||
|
|
1e23548539 | ||
|
|
bfdab156e6 | ||
|
|
395c38b644 | ||
|
|
57c7c475fc | ||
|
|
191a0f93ff | ||
|
|
cdf45de8e2 | ||
|
|
6181ea6463 | ||
|
|
8c907c9029 | ||
|
|
6231385c74 | ||
|
|
109fd671e0 | ||
|
|
2682f95a2b | ||
|
|
fce615842d | ||
|
|
64998de423 | ||
|
|
2031158336 | ||
|
|
6aa79472bf | ||
|
|
c4b7a2bd97 | ||
|
|
6f7930e34d | ||
|
|
dc4682eaf5 | ||
|
|
125e6ecbdb | ||
|
|
412a785819 | ||
|
|
d63e22ab7e | ||
|
|
97d513db5f | ||
|
|
be470c6b6e | ||
|
|
1ce24f7e08 | ||
|
|
7ccf89b43b | ||
|
|
63fe2c7099 | ||
|
|
05da4937a1 | ||
|
|
d1ee285429 | ||
|
|
adf97a83f9 | ||
|
|
cbd1daca1e | ||
|
|
30f2e25903 | ||
|
|
fb41acb438 | ||
|
|
f845dd7d59 | ||
|
|
caa276b33c | ||
|
|
ccd8c2995e | ||
|
|
88ba5352d4 | ||
|
|
2735fa577f | ||
|
|
1908ce084d | ||
|
|
9026702e7b | ||
|
|
421dd30c9d | ||
|
|
a11b4b677c | ||
|
|
10e64000f4 | ||
|
|
67f586b65c | ||
|
|
05fe534e14 | ||
|
|
4cb34844aa | ||
|
|
34c367c49f | ||
|
|
50995238bd | ||
|
|
e00ff0d781 | ||
|
|
5d21c7c705 | ||
|
|
250503b2d3 | ||
|
|
a7fcb1a44f | ||
|
|
5b4a955969 | ||
|
|
5cd44be83c | ||
|
|
529c6d0fe7 |
@@ -5,3 +5,4 @@ static
|
||||
test
|
||||
scripts
|
||||
docs
|
||||
firefox
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
public/bundle.js
|
||||
public/webcrypto-shim.js
|
||||
public
|
||||
test/frontend/bundle.js
|
||||
firefox
|
||||
|
||||
@@ -26,4 +26,4 @@ rules:
|
||||
no-var: error
|
||||
one-var: [error, never]
|
||||
prefer-const: error
|
||||
quotes: [error, single]
|
||||
quotes: [error, single, {avoidEscape: true}]
|
||||
|
||||
5
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
public/bundle.js
|
||||
public/upload.js
|
||||
public/download.js
|
||||
public/version.json
|
||||
public/l20n.min.js
|
||||
static/*
|
||||
!static/info.txt
|
||||
test/frontend/bundle.js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends: stylelint-config-standard
|
||||
|
||||
rules:
|
||||
color-hex-case: upper
|
||||
color-hex-case: lower
|
||||
declaration-colon-newline-after: null
|
||||
selector-list-comma-newline-after: null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
FROM node:8-alpine
|
||||
|
||||
RUN apk add --no-cache git
|
||||
RUN addgroup -S -g 10001 app && adduser -S -D -G app -u 10001 app
|
||||
COPY . /app
|
||||
RUN chown -R app /app
|
||||
|
||||
54
README.md
@@ -1,3 +1,51 @@
|
||||
* Install the redis server if not installed.
|
||||
* To run the project, make sure you have a redis server running locally: redis-server /usr/local/etc/redis.conf
|
||||
* Follow instructions inside the console on the browser.
|
||||
# Firefox Send
|
||||
|
||||
[](https://circleci.com/gh/mozilla/send)
|
||||
[](https://testpilot.firefox.com/experiments/send)
|
||||
|
||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
||||
|
||||
## What it does
|
||||
|
||||
A file sharing experiment which allows you to send encrypted files to other users.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Node.js 8+](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/)
|
||||
|
||||
**NOTE:** To run the project, make sure you have a Redis server running locally:
|
||||
|
||||
```sh
|
||||
$ redis-server /usr/local/etc/redis.conf
|
||||
```
|
||||
|
||||
## How to use it
|
||||
|
||||
| Command | Description |
|
||||
|------------------|-------------|
|
||||
| `npm run dev` | Builds and starts the web server locally for development.
|
||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||
| `npm start` | Starts the Express web server.
|
||||
| `npm test` | Runs the suite of mocha tests.
|
||||
|
||||
## Localization
|
||||
|
||||
_Coming soon_ (see [#57](https://github.com/mozilla/send/issues/57))
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
||||
|
||||
## Testing
|
||||
|
||||
| ENVIRONMENT | URL
|
||||
|-------------|-----
|
||||
| Production | <https://send.firefox.com/>
|
||||
| Stage | <https://send.stage.mozaws.net/>
|
||||
| Development | <https://send.dev.mozaws.net/>
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License Version 2.0](LICENSE)
|
||||
|
||||
24
circle.yml
@@ -3,24 +3,34 @@ machine:
|
||||
version: 8
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
environment:
|
||||
PATH: "/home/ubuntu/send/firefox:$PATH"
|
||||
|
||||
dependencies:
|
||||
pre:
|
||||
- npm i -g get-firefox geckodriver nsp
|
||||
- get-firefox --platform linux --extract --target /home/ubuntu/send
|
||||
|
||||
deployment:
|
||||
latest:
|
||||
branch: master
|
||||
commands:
|
||||
- npm run predocker
|
||||
- npm run build
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- docker build -t mozilla/fileshare:latest .
|
||||
- docker push mozilla/fileshare:latest
|
||||
- docker build -t mozilla/send:latest .
|
||||
- docker push mozilla/send:latest
|
||||
tags:
|
||||
tags: /.*/
|
||||
tag: /.*/
|
||||
owner: mozilla
|
||||
commands:
|
||||
- npm run predocker
|
||||
- npm run build
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- docker build -t mozilla/fileshare:$CIRCLE_TAG .
|
||||
- docker push mozilla/fileshare:$CIRCLE_TAG
|
||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
||||
- docker push mozilla/send:$CIRCLE_TAG
|
||||
|
||||
test:
|
||||
override:
|
||||
- npm run lint
|
||||
- npm test
|
||||
- nsp check
|
||||
|
||||
@@ -7,6 +7,6 @@ services:
|
||||
ports:
|
||||
- "1443:1443"
|
||||
environment:
|
||||
- P2P_REDIS_HOST=redis
|
||||
- REDIS_HOST=redis
|
||||
redis:
|
||||
image: redis:alpine
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
Environment Variables:
|
||||
## Environment variables:
|
||||
|
||||
PORT - port the server will listen on (defaults to 1443)
|
||||
P2P_S3_BUCKET - the S3 bucket name
|
||||
P2P_REDIS_HOST - host name of the redis server
|
||||
NODE_ENV - production
|
||||
| Name | Description
|
||||
|------------------|-------------|
|
||||
| `PORT` | Port the server will listen on (defaults to 1443).
|
||||
| `S3_BUCKET` | The S3 bucket name.
|
||||
| `REDIS_HOST` | Host name of the Redis server.
|
||||
| `GOOGLE_ANALYTICS_ID` | Google Analytics ID
|
||||
| `SENTRY_CLIENT` | Sentry Client ID
|
||||
| `SENTRY_DSN` | Sentry DSN
|
||||
| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
|
||||
| `NODE_ENV` | "production"
|
||||
|
||||
Example
|
||||
## Example:
|
||||
|
||||
docker run --net=host -e 'NODE_ENV=production' -e 'P2P_S3_BUCKET=testpilot-p2p-dev' -e 'P2P_REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' mozilla/portal:latest
|
||||
```sh
|
||||
$ docker run --net=host -e 'NODE_ENV=production' \
|
||||
-e 'S3_BUCKET=testpilot-p2p-dev' \
|
||||
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
|
||||
-e 'GOOGLE_ANALYTICS_ID=UA-35433268-78' \
|
||||
-e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \
|
||||
-e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \
|
||||
mozilla/send:latest
|
||||
```
|
||||
|
||||
32
docs/faq.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## How big of a file can I transfer with Firefox Send?
|
||||
|
||||
There is a 2GB file size limit built in to Send, however, in practice you may
|
||||
be unable to send files that large. Send encrypts and decrypts the files in
|
||||
the browser which is great for security but will tax your system resources. In
|
||||
particular you can expect to see your memory usage go up by at least the size
|
||||
of the file when the transfer is processing. You can see [the results of some
|
||||
testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793).
|
||||
For the most reliable operation on common computers, it’s probably best to stay
|
||||
under a few hundred megabytes.
|
||||
|
||||
## Why is my browser not supported?
|
||||
|
||||
We’re using the [Web Cryptography JavaScript API with the AES-GCM
|
||||
algorithm](https://www.w3.org/TR/WebCryptoAPI/#aes-gcm) for our encryption.
|
||||
Many browsers support this standard and should work fine, but some have not
|
||||
implemented it yet (mobile browsers lag behind on this, in
|
||||
particular).
|
||||
|
||||
## How long are files available for?
|
||||
|
||||
Files are available to be downloaded for 24 hours, after which they are removed
|
||||
from the server. They are also removed immediately after a download completes.
|
||||
|
||||
## Can a file be downloaded more than once?
|
||||
|
||||
Not currently, but we're considering multiple download support in a future
|
||||
release.
|
||||
|
||||
|
||||
*Disclaimer: Send is an experiment and under active development. The answers
|
||||
here may change as we get feedback from you and the project matures.*
|
||||
136
docs/metrics.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Send Metrics
|
||||
The metrics collection and analysis plan for Send, a forthcoming Test Pilot experiment.
|
||||
|
||||
## Analysis
|
||||
Data collected by Send will be used to answer the following high-level questions:
|
||||
|
||||
- Do users send files?
|
||||
- How often? How many?
|
||||
- What is the retention?
|
||||
- What is the distribution of senders?
|
||||
- How do recipients interact with promotional UI elements?
|
||||
- Are file recipients converted to file senders?
|
||||
- Are non-Firefox users converted to Firefox users?
|
||||
- Where does it go wrong?
|
||||
- How often are there errors in uploading or downloading files?
|
||||
- What types of errors to users commonly see?
|
||||
- At what point do errors affect retention?
|
||||
|
||||
## Collection
|
||||
Data will be collected with Google Analytics and follow [Test Pilot standards](https://github.com/mozilla/testpilot/blob/master/docs/experiments/ga.md) for reporting.
|
||||
|
||||
### Custom Metrics
|
||||
- `cm1` - the size of the file, in bytes.
|
||||
- `cm2` - the amount of time it took to complete the file transfer, in milliseconds. Only include if the file completed transferring (ref: `cd2`).
|
||||
- `cm3` - the rate of the file transfer, in bytes per second. This is computed by dividing `cm1` by `cm2`, not by monitoring transfer speeds. Only include if the file completed transferring (ref: `cd2`).
|
||||
- `cm4` - the amount of time until the file will expire, in milliseconds.
|
||||
- `cm5` - the number of files the user has ever uploaded.
|
||||
- `cm6` - the number of unexpired files the user has uploaded.
|
||||
- `cm7` - the number of files the user has ever downloaded.
|
||||
|
||||
### Custom Dimensions
|
||||
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
||||
- `cd2` - the reason that the file transfer stopped. One of `completed`, `errored`, `cancelled`.
|
||||
- `cd3` - the destination of a link click. One of `experiment-page`, `download-firefox`, `twitter`, `github`, `cookies`, `terms`, `privacy`, `about`, `legal`, `mozilla`.
|
||||
- `cd4` - the location from which the user copied the URL to an upload file. One of `success-screen`, `upload-list`.
|
||||
- `cd5` - the referring location. One of `completed-download`, `errored-download`, `cancelled-download`, `completed-upload`, `errored-upload`, `cancelled-upload`, `testpilot`, `external`.
|
||||
- `cd6` - identifying information about an error. Exclude if there is no error involved. **TODO:** enumerate a list of possibilities.
|
||||
|
||||
### Events
|
||||
|
||||
_NB:_ due to how files are being tracked, there are no events indicating file expiry. This carries some risk: most notably, we can only derive expiration rates by looking at download rates, which is prone to skew if there are problems in data collection.
|
||||
|
||||
#### `upload-started`
|
||||
Triggered whenever a user begins uploading a file. Includes:
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `upload-started`
|
||||
- `cm1`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
- `cd1`
|
||||
- `cd5`
|
||||
|
||||
#### `upload-stopped`
|
||||
Triggered whenever a user stops uploading a file. Includes:
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `upload-stopped`
|
||||
- `cm1`
|
||||
- `cm2`
|
||||
- `cm3`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
- `cd1`
|
||||
- `cd2`
|
||||
- `cd6`
|
||||
|
||||
#### `download-started`
|
||||
Triggered whenever a user begins downloading a file. Includes:
|
||||
|
||||
- `ec` - `recipient`
|
||||
- `ea` - `download-started`
|
||||
- `cm1`
|
||||
- `cm4`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
|
||||
#### `download-stopped`
|
||||
Triggered whenever a user stops downloading a file.
|
||||
|
||||
- `ec` - `recipient`
|
||||
- `ea` - `download-stopped`
|
||||
- `cm1`
|
||||
- `cm2` (if possible and applicable)
|
||||
- `cm3` (if possible and applicable)
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
- `cd2`
|
||||
- `cd6`
|
||||
|
||||
#### `exited`
|
||||
Fired whenever a user follows a link external to Send.
|
||||
|
||||
- `ec` - `recipient`, `sender`, or `other`, as applicable.
|
||||
- `ea` - `exited`
|
||||
- `cd3`
|
||||
|
||||
#### `upload-deleted`
|
||||
Fired whenever a user deletes a file they’ve uploaded.
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `upload-deleted`
|
||||
- `cm1`
|
||||
- `cm2`
|
||||
- `cm3`
|
||||
- `cm4`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
- `cd1`
|
||||
- `cd4`
|
||||
|
||||
#### `copied`
|
||||
Fired whenever a user copies the URL of an upload file.
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `copied`
|
||||
- `cd4`
|
||||
|
||||
#### `restarted`
|
||||
Fired whenever the user interrupts any part of funnel to return to the start of it (e.g. with a “send another file” or “send your own files” link).
|
||||
|
||||
- `ec` - `recipient`, `sender`, or `other`, as applicable.
|
||||
- `ea` - `restarted`
|
||||
- `cd2`
|
||||
|
||||
#### `unsupported`
|
||||
Fired whenever a user is presented a message saying that their browser is unsupported due to missing crypto APIs.
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `unsupported`
|
||||
- `cd6`
|
||||
10
frontend/src/common.js
Normal file
@@ -0,0 +1,10 @@
|
||||
window.Raven = require('raven-js');
|
||||
window.Raven.config(window.dsn).install();
|
||||
window.dsn = undefined;
|
||||
|
||||
const testPilotGA = require('testpilot-ga');
|
||||
window.analytics = new testPilotGA({
|
||||
an: 'Firefox Send',
|
||||
ds: 'web',
|
||||
tid: window.trackerId
|
||||
});
|
||||
@@ -1,52 +1,175 @@
|
||||
require('./common');
|
||||
const FileReceiver = require('./fileReceiver');
|
||||
const { notify } = require('./utils');
|
||||
const { notify, findMetric, gcmCompliant, sendEvent } = require('./utils');
|
||||
const bytes = require('bytes');
|
||||
const Storage = require('./storage');
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
const $ = require('jquery');
|
||||
require('jquery-circle-progress');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#download-progress').hide();
|
||||
$('#send-file').click(() => {
|
||||
window.location.replace(`${window.location.origin}`);
|
||||
gcmCompliant().catch(err => {
|
||||
$('#download').attr('hidden', true);
|
||||
sendEvent('recipient', 'unsupported', {
|
||||
cd6: err
|
||||
}).then(() => {
|
||||
location.replace('/unsupported');
|
||||
});
|
||||
});
|
||||
const download = () => {
|
||||
const fileReceiver = new FileReceiver();
|
||||
const name = document.createElement('p');
|
||||
const $btn = $('#download-btn');
|
||||
//link back to homepage
|
||||
$('.send-new').attr('href', window.location.origin);
|
||||
|
||||
fileReceiver.on('progress', percentComplete => {
|
||||
$('#download-page-one').hide();
|
||||
$('.send-new').hide();
|
||||
$('#download-progress').show();
|
||||
$('.send-new').click(function(target) {
|
||||
target.preventDefault();
|
||||
sendEvent('recipient', 'restarted', {
|
||||
cd2: 'completed'
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
|
||||
target.preventDefault();
|
||||
const metric = findMetric(target.currentTarget.href);
|
||||
// record exited event by recipient
|
||||
sendEvent('recipient', 'exited', {
|
||||
cd3: metric
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
const filename = $('#dl-filename').text();
|
||||
const bytelength = Number($('#dl-bytelength').text());
|
||||
const timeToExpiry = Number($('#dl-ttl').text());
|
||||
|
||||
//initiate progress bar
|
||||
$('#dl-progress').circleProgress({
|
||||
value: 0.0,
|
||||
startAngle: -Math.PI / 2,
|
||||
fill: '#3B9DFF',
|
||||
size: 158,
|
||||
animation: { duration: 300 }
|
||||
});
|
||||
$('#download-btn').click(download);
|
||||
function download() {
|
||||
storage.totalDownloads += 1;
|
||||
|
||||
const fileReceiver = new FileReceiver();
|
||||
const unexpiredFiles = storage.numFiles;
|
||||
|
||||
fileReceiver.on('progress', progress => {
|
||||
window.onunload = function() {
|
||||
storage.referrer = 'cancelled-download';
|
||||
// record download-stopped (cancelled by tab close or reload)
|
||||
sendEvent('recipient', 'download-stopped', {
|
||||
cm1: bytelength,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
};
|
||||
|
||||
$('#download-page-one').attr('hidden', true);
|
||||
$('#download-progress').removeAttr('hidden');
|
||||
const percent = progress[0] / progress[1];
|
||||
// update progress bar
|
||||
document
|
||||
.querySelector('#progress-bar')
|
||||
.style.setProperty('--progress', percentComplete + '%');
|
||||
$('#progress-text').html(`${percentComplete}%`);
|
||||
//on complete
|
||||
if (percentComplete === 100) {
|
||||
$('#dl-progress').circleProgress('value', percent);
|
||||
$('.percent-number').text(`${Math.floor(percent * 100)}`);
|
||||
$('.progress-text').text(
|
||||
`${filename} (${bytes(progress[0], {
|
||||
decimalPlaces: 1,
|
||||
fixedDecimals: true
|
||||
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
|
||||
);
|
||||
});
|
||||
|
||||
let downloadEnd;
|
||||
fileReceiver.on('decrypting', isStillDecrypting => {
|
||||
// The file is being decrypted
|
||||
if (isStillDecrypting) {
|
||||
fileReceiver.removeAllListeners('progress');
|
||||
$('#download-text').html('Download complete!');
|
||||
$('.send-new').show();
|
||||
$btn.text('Download complete!');
|
||||
$btn.attr('disabled', 'true');
|
||||
notify('Your download has finished.');
|
||||
window.onunload = null;
|
||||
document.l10n.formatValue('decryptingFile').then(decryptingFile => {
|
||||
$('.progress-text').text(decryptingFile);
|
||||
});
|
||||
} else {
|
||||
console.log('Done decrypting');
|
||||
downloadEnd = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
fileReceiver.on('hashing', isStillHashing => {
|
||||
// The file is being hashed to make sure a malicious user hasn't tampered with it
|
||||
if (isStillHashing) {
|
||||
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
|
||||
$('.progress-text').text(verifyingFile);
|
||||
});
|
||||
} else {
|
||||
$('.progress-text').text(' ');
|
||||
document.l10n
|
||||
.formatValues('downloadNotification', 'downloadFinish')
|
||||
.then(translated => {
|
||||
notify(translated[0]);
|
||||
$('.title').text(translated[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// record download-started by recipient
|
||||
sendEvent('recipient', 'download-started', {
|
||||
cm1: bytelength,
|
||||
cm4: timeToExpiry,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
|
||||
fileReceiver
|
||||
.download()
|
||||
.catch(() => {
|
||||
$('.title').text(
|
||||
'This link has expired or never existed in the first place.'
|
||||
);
|
||||
$('#download-btn').hide();
|
||||
$('#expired-img').show();
|
||||
.catch(err => {
|
||||
// record download-stopped (errored) by recipient
|
||||
sendEvent('recipient', 'download-stopped', {
|
||||
cm1: bytelength,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'errored',
|
||||
cd6: err
|
||||
});
|
||||
|
||||
document.l10n.formatValue('expiredPageHeader').then(translated => {
|
||||
$('.title').text(translated);
|
||||
});
|
||||
$('#download-btn').attr('hidden', true);
|
||||
$('#expired-img').removeAttr('hidden');
|
||||
console.log('The file has expired, or has already been deleted.');
|
||||
return;
|
||||
})
|
||||
.then(([decrypted, fname]) => {
|
||||
name.innerText = fname;
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const downloadTime = endTime - downloadEnd;
|
||||
const downloadSpeed = bytelength / (downloadTime / 1000);
|
||||
|
||||
storage.referrer = 'completed-download';
|
||||
// record download-stopped (completed) by recipient
|
||||
sendEvent('recipient', 'download-stopped', {
|
||||
cm1: bytelength,
|
||||
cm2: totalTime,
|
||||
cm3: downloadSpeed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'completed'
|
||||
});
|
||||
|
||||
const dataView = new DataView(decrypted);
|
||||
const blob = new Blob([dataView]);
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
@@ -66,7 +189,5 @@ $(document).ready(function() {
|
||||
Raven.captureException(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
window.download = download;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
const EventEmitter = require('events');
|
||||
const { strToIv } = require('./utils');
|
||||
|
||||
const Raven = window.Raven;
|
||||
const { hexToArray } = require('./utils');
|
||||
|
||||
class FileReceiver extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.salt = strToIv(location.pathname.slice(10, -1));
|
||||
}
|
||||
|
||||
download() {
|
||||
@@ -15,11 +12,8 @@ class FileReceiver extends EventEmitter {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable) {
|
||||
const percentComplete = Math.floor(
|
||||
event.loaded / event.total * 100
|
||||
);
|
||||
this.emit('progress', percentComplete);
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.emit('progress', [event.loaded, event.total]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,11 +28,12 @@ class FileReceiver extends EventEmitter {
|
||||
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,
|
||||
fname: xhr
|
||||
.getResponseHeader('Content-Disposition')
|
||||
.match(/=(.+)/)[1]
|
||||
aad: meta.aad,
|
||||
filename: meta.filename,
|
||||
iv: meta.id
|
||||
});
|
||||
};
|
||||
|
||||
@@ -54,35 +49,55 @@ class FileReceiver extends EventEmitter {
|
||||
{
|
||||
kty: 'oct',
|
||||
k: location.hash.slice(1),
|
||||
alg: 'A128CBC',
|
||||
alg: 'A128GCM',
|
||||
ext: true
|
||||
},
|
||||
{
|
||||
name: 'AES-CBC'
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
])
|
||||
.then(([fdata, key]) => {
|
||||
const salt = this.salt;
|
||||
this.emit('decrypting', true);
|
||||
return Promise.all([
|
||||
window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
iv: salt
|
||||
},
|
||||
key,
|
||||
fdata.data
|
||||
),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(fdata.fname);
|
||||
})
|
||||
window.crypto.subtle
|
||||
.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(fdata.iv),
|
||||
additionalData: hexToArray(fdata.aad),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
fdata.data
|
||||
)
|
||||
.then(decrypted => {
|
||||
this.emit('decrypting', false);
|
||||
return Promise.resolve(decrypted);
|
||||
}),
|
||||
fdata.filename,
|
||||
hexToArray(fdata.aad)
|
||||
]);
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
return Promise.reject(err);
|
||||
.then(([decrypted, fname, proposedHash]) => {
|
||||
this.emit('hashing', true);
|
||||
return window.crypto.subtle
|
||||
.digest('SHA-256', decrypted)
|
||||
.then(calculatedHash => {
|
||||
this.emit('hashing', false);
|
||||
const integrity =
|
||||
new Uint8Array(calculatedHash).toString() ===
|
||||
proposedHash.toString();
|
||||
if (!integrity) {
|
||||
this.emit('unsafe', true);
|
||||
return Promise.reject();
|
||||
} else {
|
||||
this.emit('safe', true);
|
||||
return Promise.all([decrypted, decodeURIComponent(fname)]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const EventEmitter = require('events');
|
||||
const { ivToStr } = require('./utils');
|
||||
const { arrayToHex } = require('./utils');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
@@ -7,7 +7,8 @@ class FileSender extends EventEmitter {
|
||||
constructor(file) {
|
||||
super();
|
||||
this.file = file;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
}
|
||||
|
||||
static delete(fileId, token) {
|
||||
@@ -35,70 +36,110 @@ class FileSender extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
|
||||
upload() {
|
||||
const self = this;
|
||||
self.emit('loading', true);
|
||||
return Promise.all([
|
||||
window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
),
|
||||
window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.catch(err =>
|
||||
console.log('There was an error generating a crypto key')
|
||||
),
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(this.file);
|
||||
reader.onload = function(event) {
|
||||
resolve(new Uint8Array(this.result));
|
||||
self.emit('loading', false);
|
||||
self.emit('hashing', true);
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
window.crypto.subtle.digest('SHA-256', plaintext).then(hash => {
|
||||
self.emit('hashing', false);
|
||||
self.emit('encrypting', true);
|
||||
resolve({ plaintext: plaintext, hash: new Uint8Array(hash) });
|
||||
});
|
||||
};
|
||||
reader.onerror = function(err) {
|
||||
reject(err);
|
||||
};
|
||||
})
|
||||
])
|
||||
.then(([secretKey, plaintext]) => {
|
||||
.then(([secretKey, file]) => {
|
||||
return Promise.all([
|
||||
window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-CBC',
|
||||
iv: this.iv
|
||||
},
|
||||
secretKey,
|
||||
plaintext
|
||||
),
|
||||
window.crypto.subtle.exportKey('jwk', secretKey)
|
||||
window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
additionalData: file.hash,
|
||||
tagLength: 128
|
||||
},
|
||||
secretKey,
|
||||
file.plaintext
|
||||
)
|
||||
.then(encrypted => {
|
||||
self.emit('encrypting', false);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(encrypted);
|
||||
});
|
||||
}),
|
||||
window.crypto.subtle.exportKey('jwk', secretKey),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(file.hash);
|
||||
})
|
||||
]);
|
||||
})
|
||||
.then(([encrypted, keydata]) => {
|
||||
.then(([encrypted, keydata, hash]) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.file;
|
||||
const fileId = ivToStr(this.iv);
|
||||
const fileId = arrayToHex(this.iv);
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const fd = new FormData();
|
||||
fd.append('fname', file.name);
|
||||
fd.append('data', blob, file.name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const xhr = self.uploadXHR;
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = Math.floor(e.loaded / e.total * 100);
|
||||
this.emit('progress', percentComplete);
|
||||
self.emit('progress', [e.loaded, e.total]);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
// uuid field and url field
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
resolve({
|
||||
url: responseObj.url,
|
||||
fileId: fileId,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.uuid
|
||||
});
|
||||
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/' + fileId, true);
|
||||
xhr.open('post', '/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
JSON.stringify({
|
||||
aad: arrayToHex(hash),
|
||||
id: fileId,
|
||||
filename: encodeURIComponent(file.name)
|
||||
})
|
||||
);
|
||||
xhr.send(fd);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
window.Raven = require('raven-js');
|
||||
window.Raven.config(window.dsn).install();
|
||||
window.dsn = undefined;
|
||||
require('./upload');
|
||||
require('./download');
|
||||
66
frontend/src/storage.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const { isFile } = require('./utils');
|
||||
|
||||
class Storage {
|
||||
constructor(engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
get totalDownloads() {
|
||||
return Number(this.engine.getItem('totalDownloads'));
|
||||
}
|
||||
set totalDownloads(n) {
|
||||
this.engine.setItem('totalDownloads', n);
|
||||
}
|
||||
get totalUploads() {
|
||||
return Number(this.engine.getItem('totalUploads'));
|
||||
}
|
||||
set totalUploads(n) {
|
||||
this.engine.setItem('totalUploads', n);
|
||||
}
|
||||
get referrer() {
|
||||
return this.engine.getItem('referrer');
|
||||
}
|
||||
set referrer(str) {
|
||||
this.engine.setItem('referrer', str);
|
||||
}
|
||||
|
||||
get files() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
fs.push(JSON.parse(this.engine.getItem(k))); // parse or whatever else
|
||||
}
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
||||
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,25 +1,98 @@
|
||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
||||
require('./common');
|
||||
const FileSender = require('./fileSender');
|
||||
const { notify } = require('./utils');
|
||||
const {
|
||||
notify,
|
||||
gcmCompliant,
|
||||
findMetric,
|
||||
sendEvent,
|
||||
ONE_DAY_IN_MS
|
||||
} = require('./utils');
|
||||
const bytes = require('bytes');
|
||||
const Storage = require('./storage');
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
const $ = require('jquery');
|
||||
require('jquery-circle-progress');
|
||||
|
||||
const Raven = window.Raven;
|
||||
|
||||
if (storage.has('referrer')) {
|
||||
window.referrer = storage.referrer;
|
||||
storage.remove('referrer');
|
||||
} else {
|
||||
window.referrer = 'external';
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
gcmCompliant().catch(err => {
|
||||
$('#page-one').attr('hidden', true);
|
||||
sendEvent('sender', 'unsupported', {
|
||||
cd6: err
|
||||
}).then(() => {
|
||||
location.replace('/unsupported');
|
||||
});
|
||||
});
|
||||
|
||||
$('#file-upload').change(onUpload);
|
||||
|
||||
$('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
|
||||
target.preventDefault();
|
||||
const metric = findMetric(target.currentTarget.href);
|
||||
// record exited event by recipient
|
||||
sendEvent('sender', 'exited', {
|
||||
cd3: metric
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('#send-new-completed').click(function(target) {
|
||||
target.preventDefault();
|
||||
// record restarted event
|
||||
sendEvent('sender', 'restarted', {
|
||||
cd2: 'completed'
|
||||
}).then(() => {
|
||||
storage.referrer = 'completed-upload';
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('#send-new-error').click(function(target) {
|
||||
target.preventDefault();
|
||||
// record restarted event
|
||||
sendEvent('sender', 'restarted', {
|
||||
cd2: 'errored'
|
||||
}).then(() => {
|
||||
storage.referrer = 'errored-upload';
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('body').on('dragover', allowDrop).on('drop', onUpload);
|
||||
// reset copy button
|
||||
const $copyBtn = $('#copy-btn');
|
||||
$copyBtn.attr('disabled', false);
|
||||
$copyBtn.html('Copy');
|
||||
$('#link').attr('disabled', false);
|
||||
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
|
||||
|
||||
$('#page-one').show();
|
||||
$('#file-list').show();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').hide();
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const id = localStorage.key(i);
|
||||
populateFileList(localStorage.getItem(id));
|
||||
const files = storage.files;
|
||||
if (files.length === 0) {
|
||||
toggleHeader();
|
||||
} else {
|
||||
for (const index in files) {
|
||||
const id = files[index].fileId;
|
||||
//check if file still exists before adding to list
|
||||
checkExistence(id, files[index], true);
|
||||
}
|
||||
}
|
||||
|
||||
// copy link to clipboard
|
||||
$copyBtn.click(() => {
|
||||
// record copied event from success screen
|
||||
sendEvent('sender', 'copied', {
|
||||
cd4: 'success-screen'
|
||||
});
|
||||
const aux = document.createElement('input');
|
||||
aux.setAttribute('value', $('#link').attr('value'));
|
||||
document.body.appendChild(aux);
|
||||
@@ -28,135 +101,460 @@ $(document).ready(function() {
|
||||
document.body.removeChild(aux);
|
||||
//disable button for 3s
|
||||
$copyBtn.attr('disabled', true);
|
||||
$copyBtn.html('Copied!');
|
||||
$('#link').attr('disabled', true);
|
||||
$copyBtn.html(
|
||||
'<img src="/resources/check-16.svg" class="icon-check"></img>'
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
$copyBtn.attr('disabled', false);
|
||||
$copyBtn.html('Copy');
|
||||
$('#link').attr('disabled', false);
|
||||
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// link back to home page
|
||||
$('.send-new').click(() => {
|
||||
$('#page-one').show();
|
||||
$('#file-list').show();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').hide();
|
||||
$copyBtn.attr('disabled', false);
|
||||
$copyBtn.html('Copy');
|
||||
$('.upload-window').on('dragover', () => {
|
||||
$('.upload-window').addClass('ondrag');
|
||||
});
|
||||
$('.upload-window').on('dragleave', () => {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
});
|
||||
//initiate progress bar
|
||||
$('#ul-progress').circleProgress({
|
||||
value: 0.0,
|
||||
startAngle: -Math.PI / 2,
|
||||
fill: '#3B9DFF',
|
||||
size: 158,
|
||||
animation: { duration: 300 }
|
||||
});
|
||||
|
||||
//link back to homepage
|
||||
$('.send-new').attr('href', window.location);
|
||||
|
||||
// on file upload by browse or drag & drop
|
||||
window.onUpload = event => {
|
||||
function onUpload(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// don't allow upload if not on upload page
|
||||
if ($('#page-one').attr('hidden')){
|
||||
return;
|
||||
}
|
||||
|
||||
storage.totalUploads += 1;
|
||||
|
||||
let file = '';
|
||||
if (event.type === 'drop') {
|
||||
file = event.dataTransfer.files[0];
|
||||
if (!event.originalEvent.dataTransfer.files[0]) {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.originalEvent.dataTransfer.files.length > 1 ||
|
||||
event.originalEvent.dataTransfer.files[0].size === 0
|
||||
) {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
||||
alert(str);
|
||||
});
|
||||
return;
|
||||
}
|
||||
file = event.originalEvent.dataTransfer.files[0];
|
||||
} else {
|
||||
file = event.target.files[0];
|
||||
}
|
||||
|
||||
const fileSender = new FileSender(file);
|
||||
fileSender.on('progress', percentComplete => {
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').show();
|
||||
$('#upload-filename').innerHTML += file.name;
|
||||
// update progress bar
|
||||
document
|
||||
.querySelector('#progress-bar')
|
||||
.style.setProperty('--progress', percentComplete + '%');
|
||||
$('#progress-text').html(`${percentComplete}%`);
|
||||
});
|
||||
fileSender.upload().then(info => {
|
||||
const url = info.url.trim() + `#${info.secretKey}`.trim();
|
||||
$('#link').attr('value', url);
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken
|
||||
};
|
||||
localStorage.setItem(info.fileId, JSON.stringify(fileData));
|
||||
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').show();
|
||||
|
||||
populateFileList(JSON.stringify(fileData));
|
||||
notify('Your upload has finished.');
|
||||
});
|
||||
};
|
||||
|
||||
window.allowDrop = function(ev) {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
//update file table with current files in localStorage
|
||||
function populateFileList(file) {
|
||||
try {
|
||||
file = JSON.parse(file);
|
||||
} catch (e) {
|
||||
return;
|
||||
if (file.size > MAXFILESIZE) {
|
||||
return document.l10n
|
||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||
.then(alert);
|
||||
}
|
||||
|
||||
$('#page-one').attr('hidden', true);
|
||||
$('#upload-error').attr('hidden', true);
|
||||
$('#upload-progress').removeAttr('hidden');
|
||||
document.l10n.formatValue('importingFile').then(importingFile => {
|
||||
$('.progress-text').text(importingFile);
|
||||
});
|
||||
//don't allow drag and drop when not on page-one
|
||||
$('body').off('drop', onUpload);
|
||||
|
||||
const fileSender = new FileSender(file);
|
||||
$('#cancel-upload').click(() => {
|
||||
fileSender.cancel();
|
||||
location.reload();
|
||||
document.l10n.formatValue('uploadCancelNotification').then(str => {
|
||||
notify(str);
|
||||
});
|
||||
storage.referrer = 'cancelled-upload';
|
||||
|
||||
// record upload-stopped (cancelled) by sender
|
||||
sendEvent('sender', 'upload-stopped', {
|
||||
cm1: file.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
});
|
||||
|
||||
fileSender.on('progress', progress => {
|
||||
const percent = progress[0] / progress[1];
|
||||
// update progress bar
|
||||
$('#ul-progress').circleProgress('value', percent);
|
||||
$('#ul-progress').circleProgress().on('circle-animation-end', function() {
|
||||
$('.percent-number').text(`${Math.floor(percent * 100)}`);
|
||||
});
|
||||
$('.progress-text').text(
|
||||
`${file.name} (${bytes(progress[0], {
|
||||
decimalPlaces: 1,
|
||||
fixedDecimals: true
|
||||
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
|
||||
);
|
||||
});
|
||||
|
||||
fileSender.on('hashing', isStillHashing => {
|
||||
// The file is being hashed
|
||||
if (isStillHashing) {
|
||||
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
|
||||
$('.progress-text').text(verifyingFile);
|
||||
});
|
||||
} else {
|
||||
console.log('Finished hashing');
|
||||
}
|
||||
});
|
||||
|
||||
let uploadStart;
|
||||
fileSender.on('encrypting', isStillEncrypting => {
|
||||
// The file is being encrypted
|
||||
if (isStillEncrypting) {
|
||||
document.l10n.formatValue('encryptingFile').then(encryptingFile => {
|
||||
$('.progress-text').text(encryptingFile);
|
||||
});
|
||||
} else {
|
||||
console.log('Finished encrypting');
|
||||
uploadStart = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
let t;
|
||||
const startTime = Date.now();
|
||||
const unexpiredFiles = storage.numFiles + 1;
|
||||
|
||||
// record upload-started event by sender
|
||||
sendEvent('sender', 'upload-started', {
|
||||
cm1: file.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd5: window.referrer
|
||||
});
|
||||
|
||||
// For large files we need to give the ui a tick to breathe and update
|
||||
// before we kick off the FileSender
|
||||
setTimeout(() => {
|
||||
fileSender
|
||||
.upload()
|
||||
.then(info => {
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const uploadTime = endTime - uploadStart;
|
||||
const uploadSpeed = file.size / (uploadTime / 1000);
|
||||
const expiration = EXPIRE_SECONDS * 1000;
|
||||
|
||||
// record upload-stopped (completed) by sender
|
||||
sendEvent('sender', 'upload-stopped', {
|
||||
cm1: file.size,
|
||||
cm2: totalTime,
|
||||
cm3: uploadSpeed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd2: 'completed'
|
||||
});
|
||||
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken,
|
||||
creationDate: new Date(),
|
||||
expiry: expiration,
|
||||
totalTime: totalTime,
|
||||
typeOfUpload: event.type === 'drop' ? 'drop' : 'click',
|
||||
uploadSpeed: uploadSpeed
|
||||
};
|
||||
|
||||
storage.addFile(info.fileId, fileData);
|
||||
$('#upload-filename').attr(
|
||||
'data-l10n-id',
|
||||
'uploadSuccessConfirmHeader'
|
||||
);
|
||||
t = window.setTimeout(() => {
|
||||
$('#page-one').attr('hidden', true);
|
||||
$('#upload-progress').attr('hidden', true);
|
||||
$('#upload-error').attr('hidden', true);
|
||||
$('#share-link').removeAttr('hidden');
|
||||
}, 1000);
|
||||
|
||||
populateFileList(fileData);
|
||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
||||
notify(str);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
$('#page-one').attr('hidden', true);
|
||||
$('#upload-progress').attr('hidden', true);
|
||||
$('#upload-error').removeAttr('hidden');
|
||||
window.clearTimeout(t);
|
||||
|
||||
// record upload-stopped (errored) by sender
|
||||
sendEvent('sender', 'upload-stopped', {
|
||||
cm1: file.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||
cd2: 'errored',
|
||||
cd6: err
|
||||
});
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function checkExistence(id, file, populate) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
if (populate) {
|
||||
populateFileList(file);
|
||||
}
|
||||
} else if (xhr.status === 404) {
|
||||
storage.remove(id);
|
||||
if (storage.numFiles === 0) {
|
||||
toggleHeader();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open('get', '/exists/' + id, true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
//update file table with current files in storage
|
||||
function populateFileList(file) {
|
||||
const row = document.createElement('tr');
|
||||
const name = document.createElement('td');
|
||||
const link = document.createElement('td');
|
||||
const $copyIcon = $('<img>', {
|
||||
src: '/resources/copy-16.svg',
|
||||
class: 'icon-copy',
|
||||
'data-l10n-id': 'copyUrlHover'
|
||||
});
|
||||
const expiry = document.createElement('td');
|
||||
const del = document.createElement('td');
|
||||
del.setAttribute('align', 'center');
|
||||
const btn = document.createElement('button');
|
||||
const $delIcon = $('<img>', {
|
||||
src: '/resources/close-16.svg',
|
||||
class: 'icon-delete',
|
||||
'data-l10n-id': 'deleteButtonHover'
|
||||
});
|
||||
const popupDiv = document.createElement('div');
|
||||
const $popupText = $('<span>', { class: 'popuptext' });
|
||||
const $popupText = $('<div>', { class: 'popuptext' });
|
||||
const cellText = document.createTextNode(file.name);
|
||||
|
||||
const url = file.url.trim() + `#${file.secretKey}`.trim();
|
||||
|
||||
$('#link').attr('value', url);
|
||||
$('#copy-text').attr('data-l10n-args', '{"filename": "' + file.name + '"}');
|
||||
$('#copy-text').attr('data-l10n-id', 'copyUrlFormLabelWithName');
|
||||
$popupText.attr('tabindex', '-1');
|
||||
|
||||
name.appendChild(cellText);
|
||||
|
||||
// create delete button
|
||||
btn.innerHTML = 'x';
|
||||
btn.classList.add('delete-btn');
|
||||
link.innerHTML = file.url.trim() + `#${file.secretKey}`.trim();
|
||||
|
||||
const delSpan = document.createElement('span');
|
||||
$(delSpan).addClass('icon-cancel-1');
|
||||
$(delSpan).attr('data-l10n-id', 'deleteButtonHover');
|
||||
del.appendChild(delSpan);
|
||||
|
||||
const linkSpan = document.createElement('span');
|
||||
$(linkSpan).addClass('icon-docs');
|
||||
$(linkSpan).attr('data-l10n-id', 'copyUrlHover');
|
||||
link.appendChild(linkSpan);
|
||||
|
||||
link.style.color = '#0A8DFF';
|
||||
|
||||
//copy link to clipboard when icon clicked
|
||||
$copyIcon.click(function() {
|
||||
// record copied event from upload list
|
||||
sendEvent('sender', 'copied', {
|
||||
cd4: 'upload-list'
|
||||
});
|
||||
const aux = document.createElement('input');
|
||||
aux.setAttribute('value', url);
|
||||
document.body.appendChild(aux);
|
||||
aux.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(aux);
|
||||
document.l10n.formatValue('copiedUrl').then(translated => {
|
||||
link.innerHTML = translated;
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
const linkImg = document.createElement('img');
|
||||
$(linkImg).addClass('icon-copy');
|
||||
$(linkImg).attr('data-l10n-id', 'copyUrlHover');
|
||||
$(linkImg).attr('src', '/resources/copy-16.svg');
|
||||
$(link).html(linkImg);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
file.creationDate = new Date(file.creationDate);
|
||||
|
||||
const future = new Date();
|
||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||
|
||||
let countdown = 0;
|
||||
countdown = future.getTime() - Date.now();
|
||||
let minutes = Math.floor(countdown / 1000 / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let seconds = Math.floor(countdown / 1000 % 60);
|
||||
|
||||
poll();
|
||||
|
||||
function poll() {
|
||||
countdown = future.getTime() - Date.now();
|
||||
minutes = Math.floor(countdown / 1000 / 60);
|
||||
hours = Math.floor(minutes / 60);
|
||||
seconds = Math.floor(countdown / 1000 % 60);
|
||||
let t;
|
||||
|
||||
if (hours >= 1) {
|
||||
expiry.innerHTML = hours + 'h ' + minutes % 60 + 'm';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 60000);
|
||||
} else if (hours === 0) {
|
||||
expiry.innerHTML = minutes + 'm ' + seconds + 's';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 1000);
|
||||
}
|
||||
//remove from list when expired
|
||||
if (countdown <= 0) {
|
||||
storage.remove(file.fileId);
|
||||
$(expiry).parents('tr').remove();
|
||||
window.clearTimeout(t);
|
||||
toggleHeader();
|
||||
}
|
||||
}
|
||||
|
||||
// create popup
|
||||
popupDiv.classList.add('popup');
|
||||
$popupText.html(
|
||||
'<span class="del-file">Delete</span><span class="nvm" > Nevermind</span>'
|
||||
);
|
||||
const $popupMessage = $('<div>', { class: 'popup-message' });
|
||||
$popupMessage.attr('data-l10n-id', 'deletePopupText');
|
||||
|
||||
// delete file
|
||||
$popupText.find('.del-file').click(e => {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
$(e.target).parents('tr').remove();
|
||||
localStorage.removeItem(file.fileId);
|
||||
});
|
||||
});
|
||||
const $popupDelSpan = $('<span>', { class: 'popup-yes' });
|
||||
$popupDelSpan.attr('data-l10n-id', 'deletePopupYes');
|
||||
|
||||
const $popupNvmSpan = $('<span>', { class: 'popup-no' });
|
||||
$popupNvmSpan.attr('data-l10n-id', 'deletePopupCancel');
|
||||
|
||||
$popupText.html([$popupMessage, $popupDelSpan, $popupNvmSpan]);
|
||||
|
||||
// add data cells to table row
|
||||
row.appendChild(name);
|
||||
$(link).append($copyIcon);
|
||||
row.appendChild(link);
|
||||
row.appendChild(expiry);
|
||||
popupDiv.appendChild(btn);
|
||||
$(popupDiv).append($popupText);
|
||||
$(del).append($delIcon);
|
||||
del.appendChild(popupDiv);
|
||||
row.appendChild(del);
|
||||
$('tbody').append(row); //add row to table
|
||||
|
||||
const unexpiredFiles = storage.numFiles;
|
||||
|
||||
// delete file
|
||||
$popupText.find('.popup-yes').click(e => {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
$(e.target).parents('tr').remove();
|
||||
const timeToExpiry =
|
||||
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||
// record upload-deleted from file list
|
||||
sendEvent('sender', 'upload-deleted', {
|
||||
cm1: file.size,
|
||||
cm2: file.totalTime,
|
||||
cm3: file.uploadSpeed,
|
||||
cm4: timeToExpiry,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: file.typeOfUpload,
|
||||
cd4: 'upload-list'
|
||||
}).then(() => {
|
||||
storage.remove(file.fileId);
|
||||
});
|
||||
toggleHeader();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('delete-file').onclick = () => {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
const timeToExpiry =
|
||||
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||
// record upload-deleted from success screen
|
||||
sendEvent('sender', 'upload-deleted', {
|
||||
cm1: file.size,
|
||||
cm2: file.totalTime,
|
||||
cm3: file.uploadSpeed,
|
||||
cm4: timeToExpiry,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: unexpiredFiles,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: file.typeOfUpload,
|
||||
cd4: 'success-screen'
|
||||
}).then(() => {
|
||||
storage.remove(file.fileId);
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
};
|
||||
// show popup
|
||||
del.addEventListener('click', toggleShow);
|
||||
$delIcon.click(function() {
|
||||
$popupText.addClass('show');
|
||||
$popupText.focus();
|
||||
});
|
||||
// hide popup
|
||||
$popupText.find('.nvm').click(function(e) {
|
||||
$popupText.find('.popup-no').click(function(e) {
|
||||
e.stopPropagation();
|
||||
toggleShow();
|
||||
$popupText.removeClass('show');
|
||||
});
|
||||
$popupText.click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
//close when popup loses focus
|
||||
$popupText.blur(() => {
|
||||
$popupText.removeClass('show');
|
||||
});
|
||||
|
||||
$('tbody').append(row); //add row to table
|
||||
|
||||
function toggleShow() {
|
||||
$popupText.toggleClass('show');
|
||||
toggleHeader();
|
||||
}
|
||||
function toggleHeader() {
|
||||
//hide table header if empty list
|
||||
if (document.querySelector('tbody').childNodes.length === 1) {
|
||||
$('#file-list').attr('hidden', true);
|
||||
} else {
|
||||
$('#file-list').removeAttr('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function ivToStr(iv) {
|
||||
function arrayToHex(iv) {
|
||||
let hexStr = '';
|
||||
for (const i in iv) {
|
||||
if (iv[i] < 16) {
|
||||
@@ -7,12 +7,11 @@ function ivToStr(iv) {
|
||||
hexStr += iv[i].toString(16);
|
||||
}
|
||||
}
|
||||
window.hexStr = hexStr;
|
||||
return hexStr;
|
||||
}
|
||||
|
||||
function strToIv(str) {
|
||||
const iv = new Uint8Array(16);
|
||||
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);
|
||||
}
|
||||
@@ -32,8 +31,93 @@ function notify(str) {
|
||||
}
|
||||
}
|
||||
|
||||
function gcmCompliant() {
|
||||
try {
|
||||
return window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(key => {
|
||||
return window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
)
|
||||
.then(() => {
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject();
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
function findMetric(href) {
|
||||
switch (href) {
|
||||
case 'https://www.mozilla.org/':
|
||||
return 'mozilla';
|
||||
case 'https://www.mozilla.org/about/legal':
|
||||
return 'legal';
|
||||
case 'https://testpilot.firefox.com/about':
|
||||
return 'about';
|
||||
case 'https://testpilot.firefox.com/privacy':
|
||||
return 'privacy';
|
||||
case 'https://testpilot.firefox.com/terms':
|
||||
return 'terms';
|
||||
case 'https://www.mozilla.org/en-US/privacy/websites/#cookies':
|
||||
return 'cookies';
|
||||
case 'https://github.com/mozilla/send':
|
||||
return 'github';
|
||||
case 'https://twitter.com/FxTestPilot':
|
||||
return 'twitter';
|
||||
case 'https://www.mozilla.org/firefox/new/?scene=2':
|
||||
return 'download-firefox';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
function isFile(id) {
|
||||
return ![
|
||||
'referrer',
|
||||
'totalDownloads',
|
||||
'totalUploads',
|
||||
'testpilot_ga__cid'
|
||||
].includes(id);
|
||||
}
|
||||
|
||||
function sendEvent() {
|
||||
return window.analytics.sendEvent
|
||||
.apply(window.analytics, arguments)
|
||||
.catch(() => 0);
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
|
||||
module.exports = {
|
||||
ivToStr,
|
||||
strToIv,
|
||||
notify
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
notify,
|
||||
gcmCompliant,
|
||||
findMetric,
|
||||
isFile,
|
||||
sendEvent,
|
||||
ONE_DAY_IN_MS
|
||||
};
|
||||
|
||||
1589
package-lock.json
generated
55
package.json
@@ -1,56 +1,65 @@
|
||||
{
|
||||
"name": "portal-alpha",
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.62.0",
|
||||
"aws-sdk": "^2.89.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"bytes": "^2.5.0",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^3.0.0",
|
||||
"cross-env": "^5.0.1",
|
||||
"express": "^4.15.3",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"helmet": "^3.6.1",
|
||||
"jquery": "^3.2.1",
|
||||
"helmet": "^3.8.0",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.1.0",
|
||||
"raven-js": "^3.16.0",
|
||||
"redis": "^2.7.1",
|
||||
"uglify-es": "3.0.19"
|
||||
"redis": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^14.4.0",
|
||||
"eslint": "^4.0.0",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.0.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.4.4",
|
||||
"prettier": "^1.5.3",
|
||||
"proxyquire": "^1.8.0",
|
||||
"sinon": "^2.3.5",
|
||||
"stylelint": "^7.11.0",
|
||||
"raven-js": "^3.17.0",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^2.3.8",
|
||||
"stylelint": "^7.13.0",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"watchify": "^3.9.0"
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"uglifyify": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/something-awesome/",
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"repository": "mozilla/something-awesome",
|
||||
"repository": "mozilla/send",
|
||||
"scripts": {
|
||||
"predocker": "browserify frontend/src/main.js | uglifyjs > public/bundle.js && npm run version",
|
||||
"dev": "npm run version && watchify frontend/src/main.js -o public/bundle.js -d | node server/portal_server",
|
||||
"format": "prettier '{frontend/src/,scripts/,server/,test/}*.js' 'public/*.css' --single-quote --write",
|
||||
"build": "npm-run-all build:*",
|
||||
"build:upload": "browserify frontend/src/upload.js -g uglifyify -o public/upload.js",
|
||||
"build:download": "browserify frontend/src/download.js -g uglifyify -o public/download.js",
|
||||
"build:version": "node scripts/version",
|
||||
"build:l10n": "cp node_modules/l20n/dist/web/l20n.min.js public",
|
||||
"dev": "npm run build && npm start",
|
||||
"format": "prettier '{frontend/src/,scripts/,server/,test/**/}*.js' 'public/*.css' --single-quote --write",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'public/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"start": "cross-env NODE_ENV=production node server/portal_server",
|
||||
"test": "mocha",
|
||||
"version": "node scripts/version"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
27
public/contribute.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"repository": {
|
||||
"url": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0"
|
||||
},
|
||||
"participate": {
|
||||
"home": "https://github.com/mozilla/send/blob/master/README.md",
|
||||
"docs": "https://github.com/mozilla/send/blob/master/README.md"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/mozilla/send/issues",
|
||||
"report": "https://github.com/mozilla/send/issues/new"
|
||||
},
|
||||
"urls": {
|
||||
"prod": "https://send.firefox.com/",
|
||||
"stage": "https://send.stage.mozaws.net/",
|
||||
"dev": "https://send.dev.mozaws.net/"
|
||||
},
|
||||
"keywords": [
|
||||
"JavaScript",
|
||||
"jQuery",
|
||||
"Node",
|
||||
"Redis"
|
||||
]
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
96
public/locales/send.en-US.ftl
Normal file
@@ -0,0 +1,96 @@
|
||||
title = Firefox Send
|
||||
siteSubtitle = web experiment
|
||||
siteFeedback = Feedback
|
||||
|
||||
uploadPageHeader = Private, Encrypted File Sharing
|
||||
uploadPageExplainer = Send files through a safe, private, and encrypted link that automatically expires to ensure your stuff does not remain online forever.
|
||||
|
||||
uploadPageLearnMore = Learn more
|
||||
uploadPageDropMessage = Drop your file here to start uploading
|
||||
uploadPageSizeMessage = For the most reliable operation, it’s best to keep your file under 1GB
|
||||
uploadPageBrowseButton = Select a file on your computer
|
||||
.title = {uploadPageBrowseButtonTitle}
|
||||
uploadPageMultipleFilesAlert = Uploading multiple files or a folder is currently not supported.
|
||||
|
||||
uploadPageBrowseButtonTitle = Upload file
|
||||
|
||||
uploadingPageHeader = Uploading Your File
|
||||
importingFile = Importing...
|
||||
verifyingFile = Verifying...
|
||||
encryptingFile = Encrypting...
|
||||
decryptingFile = Decrypting...
|
||||
|
||||
notifyUploadDone = Your upload has finished.
|
||||
|
||||
|
||||
uploadingPageMessage = Once your file uploads you will be able to set expiry options.
|
||||
uploadingPageCancel = Cancel upload
|
||||
.title = {uploadingPageCancel}
|
||||
uploadCancelNotification = Your upload was cancelled.
|
||||
|
||||
uploadingPageLargeFileMessage = This file is large and may take awhile to upload. Sit tight!
|
||||
uploadingFileNotification = Notify me when the upload is complete.
|
||||
uploadSuccessConfirmHeader = Ready to Send
|
||||
|
||||
uploadSvgAlt.alt = Upload
|
||||
|
||||
|
||||
//Note the spec suggests that this string is editable. That feature will not appear at Launch
|
||||
uploadSuccessTimingHeader = The link to your file will expire after 1 download or in 24 hours.
|
||||
copyUrlFormLabel = Copy and share the link to send your file:
|
||||
copyUrlFormLabelWithName = Copy and share the link to send your file: { $filename }
|
||||
// Note: Title text for button should be the same
|
||||
copyUrlFormButton = Copy to clipboard
|
||||
.title = {copyUrlFormButton}
|
||||
|
||||
copiedUrl = Copied!
|
||||
|
||||
deleteFileButton = Delete file
|
||||
.title = {deleteFileButton}
|
||||
sendAnotherFileLink = Send another file
|
||||
.title = {sendAnotherFileLink}
|
||||
|
||||
downloadAltText.alt = Download
|
||||
downloadFileName = Download { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
downloadMessage = Your friend is sending you a file with Firefox Send, a service that allows you to share files with a safe, private, and encrypted link that automatically expires to ensure your stuff does not remain online forever.
|
||||
downloadButtonLabel = Download
|
||||
.title = {downloadButtonLabel}
|
||||
downloadNotification = Your download has completed.
|
||||
downloadFinish = Download Complete
|
||||
|
||||
sendYourFilesLink = Try Firefox Send
|
||||
.title = {sendYourFilesLink}
|
||||
downloadingPageProgress = Downloading { $filename } ({ $size })
|
||||
downloadingPageMessage = Please leave this tab open while we fetch your file and decrypt it.
|
||||
|
||||
errorAltText.alt = Upload error
|
||||
errorPageHeader = Something went wrong!
|
||||
errorPageMessage = There has been an error uploading the file.
|
||||
errorPageLink = Send another file
|
||||
|
||||
fileTooBig = That file is too big to upload. It should be less than { $size }.
|
||||
|
||||
linkExpiredAlt.alt = Link expired
|
||||
expiredPageHeader = This link has expired or never existed in the first place!
|
||||
notSupportedHeader = Your browser is not supported.
|
||||
notSupportedDetail = Unfortunately this browser does not support the web technology that powers Firefox Send. You'll need to try another browser. We recommend Firefox!
|
||||
downloadFirefoxButtonSub = Free Download
|
||||
|
||||
|
||||
sentFilesTitle1 = File
|
||||
sentFilesTitle2 = Copy URL
|
||||
sentFilesTitle3 = Expires In
|
||||
sentFilesTitle4 = Delete
|
||||
|
||||
deletePopupText = Delete this file?
|
||||
deletePopupYes = Yes
|
||||
deletePopupCancel = Cancel
|
||||
deleteButtonHover.title = {sentFilesTitle4}
|
||||
copyUrlHover.title = {sentFilesTitle2}
|
||||
|
||||
footerLinkLegal = Legal
|
||||
footerLinkAbout = About Test Pilot
|
||||
footerLinkPrivacy = Privacy
|
||||
footerLinkTerms = Terms
|
||||
footerLinkCookies = Cookies
|
||||
648
public/main.css
@@ -1,44 +1,156 @@
|
||||
/*** index.html ***/
|
||||
html {
|
||||
background: url('resources/background.png');
|
||||
font-family: 'Fira Sans';
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
background-size: contain;
|
||||
background: url('resources/send_bg.svg');
|
||||
font-family: -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'SF Pro Text',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-weight: 200;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input, select, textarea, button {
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 31px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.send-logo {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
color: #3e3d40;
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.site-subtitle {
|
||||
color: #3e3d40;
|
||||
font-size: 12px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.site-subtitle a {
|
||||
font-weight: bold;
|
||||
color: #3e3d40;
|
||||
transition: color 50ms;
|
||||
}
|
||||
|
||||
.send-logo:hover a {
|
||||
color: #0297f8;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
background-color: #0297f8;
|
||||
background-image: url('resources/feedback.svg');
|
||||
background-position: 4px 6px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 14px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #0297f8;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
opacity: 0.9;
|
||||
padding: 6px 6px 5px 20px;
|
||||
}
|
||||
|
||||
.feedback:hover,
|
||||
.feedback:focus {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.feedback:active {
|
||||
background-color: #0277d8;
|
||||
}
|
||||
|
||||
.all {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
max-width: 630px;
|
||||
margin: 0 auto;
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/** page-one **/
|
||||
.main-window {
|
||||
border: 1px solid;
|
||||
width: 606px;
|
||||
min-height: 447px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/** page-one **/
|
||||
.title {
|
||||
font-size: 14px;
|
||||
width: 80%;
|
||||
margin: 40px auto;
|
||||
font-size: 33px;
|
||||
line-height: 40px;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
max-width: 520px;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
line-height: 23px;
|
||||
max-width: 630px;
|
||||
text-align: center;
|
||||
margin: 0 auto 60px;
|
||||
color: #0c0c0d;
|
||||
width: 92%;
|
||||
}
|
||||
|
||||
.upload-window {
|
||||
border: 1px dashed;
|
||||
border: 1px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
width: 470px;
|
||||
height: 250px;
|
||||
border-radius: 5px;
|
||||
height: 255px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
transition: transform 150ms;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.upload-window.ondrag {
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
height: 251px;
|
||||
transform: scale(1.04);
|
||||
border-radius: 4.2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -46,92 +158,112 @@ input, select, textarea, button {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0094fb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#upload-text {
|
||||
font-size: 22px;
|
||||
color: #737373;
|
||||
margin: 20px 0 10px;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
}
|
||||
|
||||
#browse {
|
||||
float: right;
|
||||
color: #2D7EFF;
|
||||
}
|
||||
|
||||
#browse-text {
|
||||
float: left;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
#upload-img {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.upload-window > div:nth-child(2) {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.upload {
|
||||
font-size: 12px;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
background: #0297f8;
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
width: 240px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#browse:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 45px;
|
||||
float: right;
|
||||
#file-size-msg {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #737373;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
/** file-list **/
|
||||
th {
|
||||
font-size: 10px;
|
||||
color: #737373;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
color: #858585;
|
||||
font-weight: lighter;
|
||||
text-align: left;
|
||||
background: rgba(0, 148, 251, 0.05);
|
||||
height: 40px;
|
||||
border-top: 1px solid rgba(0, 148, 251, 0.1);
|
||||
padding: 0 19px;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 12px;
|
||||
font-size: 15px;
|
||||
vertical-align: top;
|
||||
color: #4a4a4a;
|
||||
padding: 17px 19px 0;
|
||||
line-height: 23px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||
}
|
||||
|
||||
tbody {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#uploaded-files {
|
||||
width: 472px;
|
||||
margin: 10px auto;
|
||||
margin: 45.3px auto;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
.icon-delete,
|
||||
.icon-copy,
|
||||
.icon-check {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Popup container */
|
||||
.popup {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* The actual popup (appears on top) */
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
width: 160px;
|
||||
background-color: #555;
|
||||
color: #FFF;
|
||||
min-width: 115px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #0297f8;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 8px 0;
|
||||
border-radius: 5px;
|
||||
padding: 7px 8px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
margin-left: -80px;
|
||||
bottom: 8px;
|
||||
right: -28px;
|
||||
transition: opacity 0.5s;
|
||||
opacity: 0;
|
||||
outline: 0;
|
||||
box-shadow: 3px 3px 7px #888;
|
||||
}
|
||||
|
||||
/* Popup arrow */
|
||||
@@ -139,11 +271,11 @@ td {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
right: 30px;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #555 transparent transparent;
|
||||
border-color: #0297f8 transparent transparent;
|
||||
}
|
||||
|
||||
.popup .show {
|
||||
@@ -151,32 +283,79 @@ td {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/** upload-progress **/
|
||||
#progress-bar {
|
||||
width: 300px;
|
||||
height: 5px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#FD9800,
|
||||
#D73000 var(--progress),
|
||||
white var(--progress)
|
||||
);
|
||||
border: 0.5px solid;
|
||||
border-radius: 5px;
|
||||
.popup-message {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/** share-link **/
|
||||
.share-window {
|
||||
margin: 0 auto;
|
||||
width: 470px;
|
||||
height: 250px;
|
||||
.popup-yes {
|
||||
color: #fff;
|
||||
background-color: #0297f8;
|
||||
border-radius: 5px;
|
||||
padding: 2px 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup-yes:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.popup-no {
|
||||
color: #4a4a4a;
|
||||
border-radius: 6px;
|
||||
padding: 3px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/** upload-progress **/
|
||||
.progress-bar {
|
||||
margin-top: 3px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#share-window-r {
|
||||
width: 50%;
|
||||
.percentage {
|
||||
position: absolute;
|
||||
letter-spacing: -0.78px;
|
||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||
}
|
||||
|
||||
.percent-number {
|
||||
font-size: 43.2px;
|
||||
line-height: 58px;
|
||||
}
|
||||
|
||||
.percent-sign {
|
||||
font-size: 28.8px;
|
||||
color: rgb(104, 104, 104);
|
||||
}
|
||||
|
||||
.upload {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
letter-spacing: -0.4px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 74px;
|
||||
}
|
||||
|
||||
#cancel-upload {
|
||||
color: #d70022;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/** share-link **/
|
||||
#share-window {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -192,61 +371,170 @@ td {
|
||||
#copy {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 640px;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
align-self: flex-start;
|
||||
margin-top: 60px;
|
||||
margin-bottom: 10px;
|
||||
color: #0c0c0d;
|
||||
}
|
||||
|
||||
#link {
|
||||
width: 216px;
|
||||
height: 41px;
|
||||
border: 1px solid #979797;
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 6px 0 0 6px;
|
||||
font-size: 24px;
|
||||
color: #737373;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
letter-spacing: 0;
|
||||
line-height: 23px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
#link:disabled {
|
||||
border: 1px solid #05a700;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#copy-btn {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
background: #337FEB;
|
||||
border: 1px solid #979797;
|
||||
flex: 0 1 165px;
|
||||
background: #0297f8;
|
||||
border-radius: 0 6px 6px 0;
|
||||
border: 1px solid #0297f8;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
height: 60px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#copy-btn:disabled {
|
||||
background: #47B04B;
|
||||
background: #05a700;
|
||||
border: 1px solid #05a700;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.send-new {
|
||||
font-size: 14px;
|
||||
margin: auto;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
color: #2D7EFF;
|
||||
#delete-file {
|
||||
width: 176px;
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
color: #313131;
|
||||
}
|
||||
|
||||
.send-new {
|
||||
font-size: 15px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
color: #0094fb;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* upload-error */
|
||||
#upload-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#upload-error[hidden],
|
||||
#unsupported-browser[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#upload-error-img {
|
||||
margin-bottom: 90px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* unsupported-browser */
|
||||
#unsupported-browser {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unsupported-description {
|
||||
font-size: 13px;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
color: #7d7d7d;
|
||||
margin: 0 auto 23px;
|
||||
}
|
||||
|
||||
#firefox-logo {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
#dl-firefox {
|
||||
margin-bottom: 181px;
|
||||
width: 260px;
|
||||
height: 80px;
|
||||
background: #12bc00;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
||||
font-family: 'Fira Sans';
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#dl-firefox-text {
|
||||
text-align: left;
|
||||
margin-left: 20.4px;
|
||||
}
|
||||
|
||||
#dl-firefox-text > span {
|
||||
font-family: 'Fira Sans';
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.69px;
|
||||
}
|
||||
|
||||
/** download.html **/
|
||||
#download-btn {
|
||||
font-size: 18px;
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
width: 214px;
|
||||
height: 87px;
|
||||
margin: 50px auto;
|
||||
width: 180px;
|
||||
height: 44px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
background: #337FEB;
|
||||
border: 1px solid #3EA050;
|
||||
border-radius: 6px;
|
||||
background: #0297f8;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 5px;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#download-btn:disabled {
|
||||
background: #47B04B;
|
||||
background: #47b04b;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
#download-page-one {
|
||||
#download {
|
||||
margin: 0 auto;
|
||||
width: 470px;
|
||||
height: 250px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -255,13 +543,19 @@ td {
|
||||
}
|
||||
|
||||
#expired-img {
|
||||
display: none;
|
||||
margin: 51px 0 71px;
|
||||
}
|
||||
|
||||
.expired-description {
|
||||
font-size: 15px;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
color: #7d7d7d;
|
||||
margin: 0 auto 23px;
|
||||
}
|
||||
|
||||
#download-progress {
|
||||
margin: 0 auto;
|
||||
width: 470px;
|
||||
height: 250px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -269,6 +563,114 @@ td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#download-text {
|
||||
margin-bottom: 40px;
|
||||
#download-progress[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#download-img {
|
||||
width: 283px;
|
||||
height: 196px;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
.footer {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 50px 10px 10px;
|
||||
}
|
||||
|
||||
.mozilla-logo {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.legal-links > a {
|
||||
margin-right: 30px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.legal-links > a:visited {
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.social-links > a {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.github,
|
||||
.twitter {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
@media (max-device-width: 768px) {
|
||||
.description {
|
||||
margin: 0 auto 25px;
|
||||
}
|
||||
|
||||
#copy {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#link {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.mozilla-logo {
|
||||
margin-left: -7px;
|
||||
}
|
||||
|
||||
.legal-links > * {
|
||||
display: block;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-device-width: 520px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#copy {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#link {
|
||||
font-size: 22px;
|
||||
padding: 15px 10px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
#copy-btn {
|
||||
border-radius: 0 0 6px 6px;
|
||||
flex: 0 1 65px;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 14px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 13px;
|
||||
padding: 17px 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 MiB |
1
public/resources/check-16.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 16 16"><path fill="#fff" 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: 257 B |
1
public/resources/close-16.svg
Normal file
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 286 B |
1
public/resources/copy-16.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path class="icon-copy" fill="#0A8DFF" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>
|
||||
|
After Width: | Height: | Size: 416 B |
1
public/resources/feedback.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="13" viewBox="0 0 15 13" xmlns="http://www.w3.org/2000/svg"><title>Combined Shape</title><path d="M10.274 9.193a5.957 5.957 0 0 1-2.98.778C4.37 9.97 2 7.963 2 5.485 2 3.008 4.37 1 7.294 1c2.924 0 5.294 2.008 5.294 4.485 0 .843-.274 1.632-.751 2.305l.577 2.21-2.14-.807zm-5.983-2.96a.756.756 0 0 0 .763-.748.756.756 0 0 0-.763-.747.756.756 0 0 0-.764.747c0 .413.342.748.764.748zm3.054 0a.756.756 0 0 0 .764-.748.756.756 0 0 0-.764-.747.756.756 0 0 0-.764.747c0 .413.342.748.764.748zm3.054 0a.756.756 0 0 0 .764-.748.756.756 0 0 0-.764-.747.756.756 0 0 0-.763.747c0 .413.342.748.763.748z" fill="#FFF" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 649 B |
1
public/resources/firefox_logo-only.svg
Normal file
|
After Width: | Height: | Size: 239 KiB |
1
public/resources/github-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="438.549" height="438.549" viewBox="0 0 438.549 438.549"><path d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 0 1-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
public/resources/illustration_download.svg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
1
public/resources/illustration_error.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
public/resources/illustration_expired.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 20 KiB |
1
public/resources/mozilla-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 578.55 185.54"><path d="M503.5 117.21c0 4.92 2.37 8.82 9 8.82 7.8 0 16.11-5.6 16.61-18.31a80.86 80.86 0 0 0-11-1c-7.83-.01-14.61 2.19-14.61 10.49z"/><path d="M0 0v185.54h578.55V0zm163.78 139.93h-32V96.87c0-13.22-4.41-18.31-13.05-18.31-10.51 0-14.75 7.46-14.75 18.14v26.64h10.12v16.61h-32V96.87c0-13.22-4.4-18.31-13.05-18.31-10.51 0-14.75 7.46-14.75 18.14v26.64h14.54v16.61H22.22v-16.61h10.17V80.09h-11V63.48h32.87V75c4.58-8.13 12.55-13.05 23.22-13.05 11 0 21.19 5.26 24.92 16.45 4.24-10.17 12.88-16.45 24.92-16.45 13.73 0 26.28 8.31 26.28 26.45v34.94h10.17zm48.65 1.69c-23.56 0-39.84-14.41-39.84-38.82 0-22.38 13.56-40.86 41-40.86s40.86 18.48 40.86 39.84c.02 24.42-17.61 39.85-42.02 39.85zm121.72-1.69h-66.8l-2.2-11.53 42-48.32h-23.9l-3.39 11.87-15.77-1.69 2.71-26.79H334L335.69 75l-42.4 48.34H318l3.56-11.87 17.29 1.69zm41.36 0h-22.89v-27.46h22.89zm0-49h-22.89V63.48h22.89zm12 49L420.6 23.34h21.53l-33.06 116.59zm44.42 0L465 23.34h21.53l-33.04 116.59zm113.92 1.69c-10.17 0-15.76-5.94-16.78-15.26-4.41 7.8-12.21 15.26-24.58 15.26-11 0-23.56-5.94-23.56-21.87 0-18.82 18.14-23.22 35.6-23.22a100.23 100.23 0 0 1 12.55.68v-2.54c0-7.8-.17-17.12-12.55-17.12-4.58 0-8.14.34-11.7 2.2L502 90.6l-17.46-1.87 3.39-19.83c13.39-5.43 20.17-7 32.72-7 16.45 0 30.35 8.48 30.35 25.94v33.23c0 4.41 1.69 5.94 5.26 5.94a11.5 11.5 0 0 0 3.22-.51l.17 11.53a29.57 29.57 0 0 1-13.77 3.6z"/><path d="M213.27 78.73c-11.19 0-18.14 8.3-18.14 22.72 0 13.22 6.1 23.39 18 23.39 11.36 0 18.82-9.15 18.82-23.73-.03-15.43-8.33-22.38-18.68-22.38z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
public/resources/send_bg.svg
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
1
public/resources/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"><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.1 KiB |
|
Before Width: | Height: | Size: 22 KiB |
1
public/resources/twitter-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 612"><path d="M612 116.258a250.714 250.714 0 0 1-72.088 19.772c25.929-15.527 45.777-40.155 55.184-69.411-24.322 14.379-51.169 24.82-79.775 30.48-22.907-24.437-55.49-39.658-91.63-39.658-69.334 0-125.551 56.217-125.551 125.513 0 9.828 1.109 19.427 3.251 28.606-104.326-5.24-196.835-55.223-258.75-131.174-10.823 18.51-16.98 40.078-16.98 63.101 0 43.559 22.181 81.993 55.835 104.479a125.556 125.556 0 0 1-56.867-15.756v1.568c0 60.806 43.291 111.554 100.693 123.104-10.517 2.83-21.607 4.398-33.08 4.398-8.107 0-15.947-.803-23.634-2.333 15.985 49.907 62.336 86.199 117.253 87.194-42.947 33.654-97.099 53.655-155.916 53.655-10.134 0-20.116-.612-29.944-1.721 55.567 35.681 121.536 56.485 192.438 56.485 230.948 0 357.188-191.291 357.188-357.188l-.421-16.253c24.666-17.593 46.005-39.697 62.794-64.861z" fill="#010002"/></svg>
|
||||
|
After Width: | Height: | Size: 873 B |
@@ -1,93 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="148px" height="113px" viewBox="0 0 148 113" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 14</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter x="-81.6%" y="-21.2%" width="210.5%" height="180.8%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="-10" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.126754982 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter x="-83.8%" y="-21.6%" width="213.5%" height="182.4%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feOffset dx="-10" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.126754982 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter x="-78.0%" y="-21.6%" width="204.9%" height="182.4%" filterUnits="objectBoundingBox" id="filter-3">
|
||||
<feOffset dx="-10" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.126754982 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter x="-58.8%" y="-21.7%" width="178.4%" height="189.1%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="-10" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="Sidebar" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-14" transform="translate(19.000000, 1.000000)">
|
||||
<g id="Group-11" filter="url(#filter-1)" transform="translate(28.573415, 37.189410) rotate(-25.000000) translate(-28.573415, -37.189410) translate(9.573415, 11.189410)">
|
||||
<g id="noun_775738_cc" stroke="#00C8D8" stroke-width="2">
|
||||
<g id="Group-10" transform="translate(0.143028, 0.571288)">
|
||||
<path d="M0,3.11187997 C0,1.44994393 1.33795046,0.102678571 2.99700349,0.102678571 L24.2431693,0.102678571 L36.6460185,10.8658088 L36.6460185,46.9983075 C36.6460185,48.658562 35.2983063,50.0044643 33.647121,50.0044643 L2.9988975,50.0044643 C1.34265214,50.0044643 0,48.6697121 0,46.9952629 L0,3.11187997 Z" id="Rectangle-7" fill="#BFF1F5"></path>
|
||||
<path d="M24.430679,0.631293163 C24.430679,0.0822437139 24.7706307,-0.0632105526 25.1876619,0.304366627 L36.3833191,10.1723775 C36.8013892,10.5408704 36.6879598,10.8395929 36.1429992,10.8395929 L27.4265882,10.8395929 C25.7719932,10.8395929 24.430679,9.48831667 24.430679,7.83754409 L24.430679,0.631293163 Z" id="Rectangle-8" fill="#2FD2DF"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="noun_1029125_cc" transform="translate(3.901518, 8.349380)" fill-rule="nonzero" fill="#2FD2DF">
|
||||
<g id="Group">
|
||||
<path d="M24.1801115,8.08668825 C23.3856986,7.07396809 21.8961743,7.07396809 21.1514122,8.08668825 L12.1646161,19.682334 C11.9660128,19.9355141 11.6184572,19.9355141 11.419854,19.73297 L7.89464663,15.8339974 C7.10023371,14.9225493 5.7100111,14.9225493 4.91559818,15.8339974 L0.943533576,20.2393301 C0.546327116,20.6950542 0.347723886,21.2520503 0.347723886,21.8596824 L0.347723886,36.5947607 C0.347723886,37.1517567 0.794581154,37.5568448 1.29108923,37.5568448 L27.8542712,37.5568448 C28.4004301,37.5568448 28.7976366,37.1011207 28.7976366,36.5947607 L28.7976366,14.7706413 C28.7976366,14.2136452 28.5990334,13.7072851 28.3011285,13.3021971 L24.1801115,8.08668825 Z" id="Shape"></path>
|
||||
<ellipse id="Oval" cx="5.51140787" cy="3.98517161" rx="3.17765168" ry="3.2407045"></ellipse>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-10" filter="url(#filter-2)" transform="translate(44.984738, 0.000000)">
|
||||
<path d="M0,3.11187997 C0,1.44994393 1.33795046,0.102678571 2.99700349,0.102678571 L24.2431693,0.102678571 L36.6460185,10.8658088 L36.6460185,46.9983075 C36.6460185,48.658562 35.2983063,50.0044643 33.647121,50.0044643 L2.9988975,50.0044643 C1.34265214,50.0044643 0,48.6697121 0,46.9952629 L0,3.11187997 Z" id="Rectangle-7" stroke="#FF5B6D" stroke-width="2" fill="#FFDEE2"></path>
|
||||
<path d="M5.44129888,23.9323409 L30.6937957,23.9323409 C31.424931,23.9323409 32.0211705,23.4883258 32.0211705,22.9438547 C32.0211705,22.3993836 31.424931,21.9553685 30.6937957,21.9553685 L5.44129888,21.9553685 C4.71016356,21.9553685 4.11392405,22.3993836 4.11392405,22.9438547 C4.11392405,23.4883258 4.71016356,23.9323409 5.44129888,23.9323409 Z" id="Shape" fill="#FF9CA7" fill-rule="nonzero"></path>
|
||||
<path d="M5.44129888,17.9049859 L30.6937957,17.9049859 C31.424931,17.9049859 32.0211705,17.4609707 32.0211705,16.9164996 C32.0211705,16.3720286 31.424931,15.9280134 30.6937957,15.9280134 L5.44129888,15.9280134 C4.71016356,15.9280134 4.11392405,16.3720286 4.11392405,16.9164996 C4.11392405,17.4609707 4.71016356,17.9049859 5.44129888,17.9049859 Z" id="Shape" fill="#FF9CA7" fill-rule="nonzero"></path>
|
||||
<path d="M5.44129888,29.959696 L30.6937957,29.959696 C31.424931,29.959696 32.0211705,29.5156809 32.0211705,28.9712098 C32.0211705,28.4267387 31.424931,27.9827235 30.6937957,27.9827235 L5.44129888,27.9827235 C4.71016356,27.9827235 4.11392405,28.4267387 4.11392405,28.9712098 C4.11392405,29.5156809 4.71016356,29.959696 5.44129888,29.959696 Z" id="Shape" fill="#FF9CA7" fill-rule="nonzero"></path>
|
||||
<path d="M5.44129888,35.9870511 L30.6937957,35.9870511 C31.424931,35.9870511 32.0211705,35.5430359 32.0211705,34.9985649 C32.0211705,34.4540938 31.424931,34.0100786 30.6937957,34.0100786 L5.44129888,34.0100786 C4.71016356,34.0100786 4.11392405,34.4540938 4.11392405,34.9985649 C4.11392405,35.5430359 4.71016356,35.9870511 5.44129888,35.9870511 Z" id="Shape" fill="#FF9CA7" fill-rule="nonzero"></path>
|
||||
<path d="M5.44129888,42.0144062 L30.6937957,42.0144062 C31.424931,42.0144062 32.0211705,41.570391 32.0211705,41.0259199 C32.0211705,40.4814489 31.424931,40.0374337 30.6937957,40.0374337 L5.44129888,40.0374337 C4.71016356,40.0374337 4.11392405,40.4814489 4.11392405,41.0259199 C4.11392405,41.5724001 4.71016356,42.0144062 5.44129888,42.0144062 Z" id="Shape" fill="#FF9CA7" fill-rule="nonzero"></path>
|
||||
<path d="M24.430679,0.631293163 C24.430679,0.0822437139 24.7706307,-0.0632105526 25.1876619,0.304366627 L36.3833191,10.1723775 C36.8013892,10.5408704 36.6879598,10.8395929 36.1429992,10.8395929 L27.4265882,10.8395929 C25.7719932,10.8395929 24.430679,9.48831667 24.430679,7.83754409 L24.430679,0.631293163 Z" id="Rectangle-8" stroke="#FF5B6D" stroke-width="2" fill="#FF9CA7"></path>
|
||||
</g>
|
||||
<g id="Group-10" filter="url(#filter-3)" transform="translate(99.755713, 40.795908) rotate(25.000000) translate(-99.755713, -40.795908) translate(79.255713, 15.295908)">
|
||||
<path d="M-2.72848411e-12,3.11187997 C-2.72848411e-12,1.44994393 1.34238993,0.102678571 2.99799713,0.102678571 L37.4077263,0.102678571 L36.6460185,10.8658088 L36.6460185,46.9983075 C36.6460185,48.658562 35.2983063,50.0044643 33.647121,50.0044643 L2.9988975,50.0044643 C1.34265214,50.0044643 -2.72848411e-12,48.6697121 -2.72848411e-12,46.9952629 L-2.72848411e-12,3.11187997 Z" id="Rectangle-7" stroke="#FFBC38" stroke-width="2" fill="#FFEECD"></path>
|
||||
<g id="Group" transform="translate(11.566360, 15.461370)" stroke-linecap="round" stroke="#FFCD6A" stroke-linejoin="round">
|
||||
<g transform="translate(3.291139, 15.607143)" id="Shape" stroke-width="1.5">
|
||||
<path d="M0.507658228,0 L6.07462025,0"></path>
|
||||
<path d="M0.507658228,1.64285714 L6.07462025,1.64285714"></path>
|
||||
<path d="M1.80189873,3.28571429 L4.78037975,3.28571429"></path>
|
||||
</g>
|
||||
<g id="Shape" stroke-width="2">
|
||||
<path d="M12.8823418,6.68478571 C12.8823418,3.197 10.0618354,0.369642857 6.58227848,0.369642857 C3.10272152,0.369642857 0.28221519,3.197 0.28221519,6.68478571 C0.28221519,8.54367857 1.08772152,10.2095357 2.36221519,11.3652857 L3.79879747,13.7646786 L9.36658228,13.7646786 L10.8023418,11.3661071 C12.0768354,10.2095357 12.8823418,8.54367857 12.8823418,6.68478571 Z"></path>
|
||||
<polyline points="5.85164557 13.7991786 4.73759494 9.82264286 8.42696203 9.82264286 7.31291139 13.7991786"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M59.4642883,25.2355179 L31.4194394,25.2355179 C30.0529755,25.2355179 29.4086334,24.2834821 28.8404333,23.4439821 C28.2288404,22.5387679 27.6508156,21.6844821 26.2032973,21.6844821 L15.5761548,21.6844821 C14.0655941,21.6844821 12.8366772,23.3429464 12.8366772,25.3809107 L12.8366772,27.8451964 C13.4900254,27.0204821 14.4684103,26.4824464 15.5761548,26.4824464 L59.4642883,26.4824464 C60.5720328,26.4824464 61.5504176,27.0204821 62.2037658,27.8451964 L62.2037658,27.2882679 C62.2029471,25.251125 60.9740302,25.2355179 59.4642883,25.2355179 Z" id="Shape" stroke="#FFBC38" stroke-width="2" fill="#FFCD6A" transform="translate(37.520222, 24.764839) rotate(90.000000) translate(-37.520222, -24.764839) "></path>
|
||||
</g>
|
||||
<g id="Group-12" filter="url(#filter-4)" transform="translate(37.025316, 46.000000)">
|
||||
<path d="M24.196055,2.68359965 C24.8899407,1.8177448 26.0064693,1.82521173 26.6836283,2.69225814 L40.872699,20.8601973 C41.5526621,21.7308341 41.2103747,22.4366247 40.1012607,22.4366247 L31.1853858,22.4366247 L31.1853858,38.5236322 L19.6604889,38.5236322 L19.6604889,22.4366247 L10.3594066,22.4366247 C9.25859767,22.4366247 8.92499102,21.7393653 9.62260704,20.8688558 L24.196055,2.68359965 Z" id="Polygon" fill="#1F7FFF"></path>
|
||||
<g id="noun_47280_cc" fill-rule="nonzero" fill="#165CE4">
|
||||
<path d="M31.2533248,39.1323944 L19.5010415,39.1323944 C19.1136036,39.1323944 18.8553116,38.8732394 18.8553116,38.484507 L18.8553116,23.0647887 L8.45906105,23.0647887 C8.20076911,23.0647887 8.00705015,22.9352113 7.87790418,22.6760563 C7.74875821,22.4816901 7.8133312,22.1577465 7.94247717,21.9633803 L24.8605993,0.777464789 C25.1188912,0.453521127 25.6354751,0.453521127 25.893767,0.777464789 L42.7473161,21.8985915 C42.8764621,22.028169 42.9410351,22.2225352 42.9410351,22.4169014 C42.9410351,22.8056338 42.6827432,23.0647887 42.2953052,23.0647887 L42.2953052,23.0647887 L31.8990546,23.0647887 L31.8990546,38.484507 C31.8990546,38.8732394 31.5761897,39.1323944 31.2533248,39.1323944 Z M20.1467714,37.8366197 L30.543022,37.8366197 L30.543022,22.4169014 C30.543022,22.028169 30.8013139,21.7690141 31.1887518,21.7690141 L40.9392726,21.7690141 L25.3771831,2.26760563 L9.81509373,21.7690141 L19.5656145,21.7690141 C19.9530524,21.7690141 20.2113443,22.028169 20.2113443,22.4169014 L20.2113443,37.8366197 L20.1467714,37.8366197 Z" id="Shape"></path>
|
||||
<path d="M49.9794905,45.8056338 L1.22688672,45.8056338 C0.839448806,45.8056338 0.581156866,45.5464789 0.581156866,45.1577465 L0.581156866,36.8647887 C0.581156866,36.4760563 0.839448806,36.2169014 1.22688672,36.2169014 C1.61432463,36.2169014 1.87261657,36.4760563 1.87261657,36.8647887 L1.87261657,44.5098592 L49.3337606,44.5098592 L49.3337606,36.8647887 C49.3337606,36.4760563 49.5920526,36.2169014 49.9794905,36.2169014 C50.3669284,36.2169014 50.6252203,36.4760563 50.6252203,36.8647887 L50.6252203,45.1577465 C50.6252203,45.4816901 50.3023554,45.8056338 49.9794905,45.8056338 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="57" height="57" viewBox="0 0 57 57" xmlns="http://www.w3.org/2000/svg"><title>upload</title><g transform="translate(1 1)" stroke-width="2" stroke="#7FC9FD" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path d="M18 24l10-9 10 9M28 39.545V15"/><circle cx="27.5" cy="27.5" r="27.5"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 336 B |
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /download/
|
||||
@@ -3,13 +3,13 @@ const convict = require('convict');
|
||||
const conf = convict({
|
||||
s3_bucket: {
|
||||
format: String,
|
||||
default: 'localhost',
|
||||
env: 'P2P_S3_BUCKET'
|
||||
default: '',
|
||||
env: 'S3_BUCKET'
|
||||
},
|
||||
redis_host: {
|
||||
format: String,
|
||||
default: 'localhost',
|
||||
env: 'P2P_REDIS_HOST'
|
||||
env: 'REDIS_HOST'
|
||||
},
|
||||
listen_port: {
|
||||
format: 'port',
|
||||
@@ -19,24 +19,33 @@ const conf = convict({
|
||||
},
|
||||
analytics_id: {
|
||||
format: String,
|
||||
default: 'UA-101393094-1',
|
||||
default: '',
|
||||
env: 'GOOGLE_ANALYTICS_ID'
|
||||
},
|
||||
sentry_id: {
|
||||
format: String,
|
||||
default:
|
||||
'https://cdf9a4f43a584f759586af8ceb2194f2@sentry.prod.mozaws.net/238',
|
||||
env: 'P2P_SENTRY_CLIENT'
|
||||
default: '',
|
||||
env: 'SENTRY_CLIENT'
|
||||
},
|
||||
sentry_dsn: {
|
||||
format: String,
|
||||
default: 'localhost',
|
||||
env: 'P2P_SENTRY_DSN'
|
||||
default: '',
|
||||
env: 'SENTRY_DSN'
|
||||
},
|
||||
env: {
|
||||
format: ['production', 'development', 'test'],
|
||||
default: 'development',
|
||||
env: 'NODE_ENV'
|
||||
},
|
||||
max_file_size: {
|
||||
format: Number,
|
||||
default: 1024 * 1024 * 1024 * 2,
|
||||
env: 'MAX_FILE_SIZE'
|
||||
},
|
||||
expire_seconds: {
|
||||
format: Number,
|
||||
default: 86400,
|
||||
env: 'EXPIRE_SECONDS'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -45,8 +54,3 @@ conf.validate({ allowed: 'strict' });
|
||||
|
||||
const props = conf.getProperties();
|
||||
module.exports = props;
|
||||
|
||||
module.exports.notLocalHost =
|
||||
props.env === 'production' &&
|
||||
props.s3_bucket !== 'localhost' &&
|
||||
props.sentry_dsn !== 'localhost';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const conf = require('./config.js');
|
||||
|
||||
const notLocalHost = conf.notLocalHost;
|
||||
const isProduction = conf.env === 'production';
|
||||
|
||||
const mozlog = require('mozlog')({
|
||||
app: 'FirefoxFileshare',
|
||||
level: notLocalHost ? 'INFO' : 'verbose',
|
||||
fmt: notLocalHost ? 'heka' : 'pretty',
|
||||
debug: !notLocalHost
|
||||
app: 'FirefoxSend',
|
||||
level: isProduction ? 'INFO' : 'verbose',
|
||||
fmt: isProduction ? 'heka' : 'pretty',
|
||||
debug: !isProduction
|
||||
});
|
||||
|
||||
module.exports = mozlog;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
const express = require('express');
|
||||
const exphbs = require('express-handlebars');
|
||||
const busboy = require('connect-busboy');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const bytes = require('bytes');
|
||||
const conf = require('./config.js');
|
||||
const storage = require('./storage.js');
|
||||
const Raven = require('raven');
|
||||
|
||||
const notLocalHost = conf.notLocalHost;
|
||||
|
||||
if (notLocalHost) {
|
||||
Raven.config(conf.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('portal.server');
|
||||
|
||||
const STATIC_PATH = path.join(__dirname, '../public');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.engine(
|
||||
'handlebars',
|
||||
exphbs({
|
||||
defaultLayout: 'main',
|
||||
partialsDir: 'views/partials/'
|
||||
})
|
||||
);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
app.use(helmet());
|
||||
app.use(busboy());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
shouldRenderAnalytics: notLocalHost,
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/exists/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
storage
|
||||
.exists(id)
|
||||
.then(() => {
|
||||
res.sendStatus(200);
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.get('/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
storage.filename(id).then(filename => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
res.render('download', {
|
||||
filename: filename,
|
||||
filesize: bytes(contentLength),
|
||||
shouldRenderAnalytics: notLocalHost,
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
res.render('download');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/assets/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.filename(id)
|
||||
.then(reply => {
|
||||
storage.length(id).then(contentLength => {
|
||||
res.writeHead(200, {
|
||||
'Content-Disposition': 'attachment; filename=' + reply,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': contentLength
|
||||
});
|
||||
const file_stream = storage.get(id);
|
||||
|
||||
file_stream.on(notLocalHost ? 'finish' : 'close', () => {
|
||||
storage
|
||||
.forceDelete(id)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.info('DeleteError:', id);
|
||||
});
|
||||
});
|
||||
|
||||
file_stream.pipe(res);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/delete/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const delete_token = req.body.delete_token;
|
||||
|
||||
if (!delete_token) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
|
||||
storage
|
||||
.delete(id, delete_token)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.post('/upload/:id', (req, res, next) => {
|
||||
if (!validateID(req.params.id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
req.pipe(req.busboy);
|
||||
req.busboy.on('file', (fieldname, file, filename) => {
|
||||
log.info('Uploading:', req.params.id);
|
||||
|
||||
const protocol = notLocalHost ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${req.params.id}/`;
|
||||
|
||||
storage.set(req.params.id, file, filename, url).then(linkAndID => {
|
||||
res.json(linkAndID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/__heartbeat__', (req, res) => {
|
||||
storage.ping().then(() => res.sendStatus(200), () => res.sendStatus(500));
|
||||
});
|
||||
|
||||
app.get('/__version__', (req, res) => {
|
||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
||||
});
|
||||
|
||||
app.listen(conf.listen_port, () => {
|
||||
log.info('startServer:', `Portal app listening on port ${conf.listen_port}!`);
|
||||
});
|
||||
|
||||
const validateID = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{32}$/) !== null;
|
||||
};
|
||||
294
server/server.js
Normal file
@@ -0,0 +1,294 @@
|
||||
const express = require('express');
|
||||
const exphbs = require('express-handlebars');
|
||||
const busboy = require('connect-busboy');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const bytes = require('bytes');
|
||||
const conf = require('./config.js');
|
||||
const storage = require('./storage.js');
|
||||
const Raven = require('raven');
|
||||
const crypto = require('crypto');
|
||||
|
||||
if (conf.sentry_dsn) {
|
||||
Raven.config(conf.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('send.server');
|
||||
|
||||
const STATIC_PATH = path.join(__dirname, '../public');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.engine(
|
||||
'handlebars',
|
||||
exphbs({
|
||||
defaultLayout: 'main',
|
||||
partialsDir: 'views/partials/'
|
||||
})
|
||||
);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
helmet.hsts({
|
||||
maxAge: 31536000,
|
||||
force: conf.env === 'production'
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
'https://sentry.prod.mozaws.net',
|
||||
'https://www.google-analytics.com',
|
||||
'https://ssl.google-analytics.com'
|
||||
],
|
||||
imgSrc: [
|
||||
"'self'",
|
||||
'https://www.google-analytics.com',
|
||||
'https://ssl.google-analytics.com'
|
||||
],
|
||||
scriptSrc: ["'self'", 'https://ssl.google-analytics.com'],
|
||||
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||
formAction: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"]
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
busboy({
|
||||
limits: {
|
||||
fileSize: conf.max_file_size
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index');
|
||||
});
|
||||
|
||||
app.get('/unsupported', (req, res) => {
|
||||
res.render('unsupported');
|
||||
});
|
||||
|
||||
app.get('/jsconfig.js', (req, res) => {
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.render('jsconfig', {
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id,
|
||||
maxFileSize: conf.max_file_size,
|
||||
expireSeconds: conf.expire_seconds,
|
||||
layout: false
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/exists/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.exists(id)
|
||||
.then(() => {
|
||||
res.sendStatus(200);
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.get('/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage.filename(id).then(filename => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
storage.ttl(id).then(timeToExpiry => {
|
||||
res.render('download', {
|
||||
filename: decodeURIComponent(filename),
|
||||
filesize: bytes(contentLength),
|
||||
sizeInBytes: contentLength,
|
||||
timeToExpiry: timeToExpiry
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(404).render('notfound');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/assets/download/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.metadata(id)
|
||||
.then(meta => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
res.writeHead(200, {
|
||||
'Content-Disposition': 'attachment; filename=' + meta.filename,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': contentLength,
|
||||
'X-File-Metadata': JSON.stringify(meta)
|
||||
});
|
||||
const file_stream = storage.get(id);
|
||||
|
||||
file_stream.on('end', () => {
|
||||
storage
|
||||
.forceDelete(id)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.info('DeleteError:', id);
|
||||
});
|
||||
});
|
||||
|
||||
file_stream.pipe(res);
|
||||
})
|
||||
.catch(err => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/delete/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const delete_token = req.body.delete_token;
|
||||
|
||||
if (!delete_token) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
storage
|
||||
.delete(id, delete_token)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', id);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
})
|
||||
.catch(err => res.sendStatus(404));
|
||||
});
|
||||
|
||||
app.post('/upload', (req, res, next) => {
|
||||
const newId = crypto.randomBytes(5).toString('hex');
|
||||
let meta;
|
||||
|
||||
try {
|
||||
meta = JSON.parse(req.header('X-File-Metadata'));
|
||||
} catch (err) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!meta.hasOwnProperty('aad') ||
|
||||
!meta.hasOwnProperty('id') ||
|
||||
!meta.hasOwnProperty('filename') ||
|
||||
!validateIV(meta.id)
|
||||
) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
meta.delete = crypto.randomBytes(10).toString('hex');
|
||||
log.info('meta', meta);
|
||||
req.pipe(req.busboy);
|
||||
|
||||
req.busboy.on('file', (fieldname, file, filename) => {
|
||||
log.info('Uploading:', newId);
|
||||
|
||||
storage.set(newId, file, filename, meta).then(
|
||||
() => {
|
||||
const protocol = conf.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
res.json({
|
||||
url,
|
||||
delete: meta.delete,
|
||||
id: newId
|
||||
});
|
||||
},
|
||||
err => {
|
||||
if (err.message === 'limit') {
|
||||
return res.sendStatus(413);
|
||||
}
|
||||
res.sendStatus(500);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
req.on('close', err => {
|
||||
storage
|
||||
.forceDelete(newId)
|
||||
.then(err => {
|
||||
if (!err) {
|
||||
log.info('Deleted:', newId);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.info('DeleteError:', newId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/__heartbeat__', (req, res) => {
|
||||
storage.ping().then(() => res.sendStatus(200), () => res.sendStatus(500));
|
||||
});
|
||||
|
||||
app.get('/__version__', (req, res) => {
|
||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
||||
});
|
||||
|
||||
const server = app.listen(conf.listen_port, () => {
|
||||
log.info('startServer:', `Send app listening on port ${conf.listen_port}!`);
|
||||
});
|
||||
|
||||
const validateID = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
};
|
||||
|
||||
const validateIV = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
server: server,
|
||||
storage: storage
|
||||
};
|
||||
@@ -4,13 +4,10 @@ const s3 = new AWS.S3();
|
||||
const conf = require('./config.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const notLocalHost = conf.notLocalHost;
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('portal.storage');
|
||||
const log = mozlog('send.storage');
|
||||
|
||||
const redis = require('redis');
|
||||
const redis_client = redis.createClient({
|
||||
@@ -22,30 +19,74 @@ redis_client.on('error', err => {
|
||||
log.info('Redis:', err);
|
||||
});
|
||||
|
||||
if (notLocalHost) {
|
||||
if (conf.s3_bucket) {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
ttl: ttl,
|
||||
length: awsLength,
|
||||
get: awsGet,
|
||||
set: awsSet,
|
||||
aad: aad,
|
||||
setField: setField,
|
||||
delete: awsDelete,
|
||||
forceDelete: awsForceDelete,
|
||||
ping: awsPing
|
||||
ping: awsPing,
|
||||
flushall: flushall,
|
||||
quit: quit,
|
||||
metadata
|
||||
};
|
||||
} else {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
ttl: ttl,
|
||||
length: localLength,
|
||||
get: localGet,
|
||||
set: localSet,
|
||||
aad: aad,
|
||||
setField: setField,
|
||||
delete: localDelete,
|
||||
forceDelete: localForceDelete,
|
||||
ping: localPing
|
||||
ping: localPing,
|
||||
flushall: flushall,
|
||||
quit: quit,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
function flushall() {
|
||||
redis_client.flushdb();
|
||||
}
|
||||
|
||||
function quit() {
|
||||
redis_client.quit();
|
||||
}
|
||||
|
||||
function metadata(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.hgetall(id, (err, reply) => {
|
||||
if (!err) {
|
||||
resolve(reply);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ttl(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.ttl(id, (err, reply) => {
|
||||
if (!err) {
|
||||
resolve(reply * 1000);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filename(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.hget(id, 'filename', (err, reply) => {
|
||||
@@ -70,6 +111,22 @@ function exists(id) {
|
||||
});
|
||||
}
|
||||
|
||||
function setField(id, key, value) {
|
||||
redis_client.hset(id, key, value);
|
||||
}
|
||||
|
||||
function aad(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.hget(id, 'aad', (err, reply) => {
|
||||
if (!err) {
|
||||
resolve(reply);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function localLength(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
@@ -84,25 +141,26 @@ function localGet(id) {
|
||||
return fs.createReadStream(path.join(__dirname, '../static', id));
|
||||
}
|
||||
|
||||
function localSet(id, file, filename, url) {
|
||||
function localSet(newId, file, filename, meta) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fstream = fs.createWriteStream(path.join(__dirname, '../static', id));
|
||||
const filepath = path.join(__dirname, '../static', newId);
|
||||
const fstream = fs.createWriteStream(filepath);
|
||||
file.pipe(fstream);
|
||||
fstream.on('close', () => {
|
||||
const uuid = crypto.randomBytes(10).toString('hex');
|
||||
|
||||
redis_client.hmset([id, 'filename', filename, 'delete', uuid]);
|
||||
redis_client.expire(id, 86400000);
|
||||
log.info('localSet:', 'Upload Finished of ' + id);
|
||||
resolve({
|
||||
uuid: uuid,
|
||||
url: url
|
||||
});
|
||||
file.on('limit', () => {
|
||||
file.unpipe(fstream);
|
||||
fstream.destroy(new Error('limit'));
|
||||
});
|
||||
fstream.on('finish', () => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||
resolve(meta.delete);
|
||||
});
|
||||
|
||||
fstream.on('error', () => {
|
||||
log.error('localSet:', 'Failed upload of ' + id);
|
||||
reject();
|
||||
fstream.on('error', err => {
|
||||
log.error('localSet:', 'Failed upload of ' + newId);
|
||||
fs.unlinkSync(filepath);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -165,32 +223,32 @@ function awsGet(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function awsSet(id, file, filename, url) {
|
||||
function awsSet(newId, file, filename, meta) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Key: id,
|
||||
Key: newId,
|
||||
Body: file
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
s3.upload(params, function(err, _data) {
|
||||
if (err) {
|
||||
log.info('awsUploadError:', err.stack); // an error occurred
|
||||
reject();
|
||||
} else {
|
||||
const uuid = crypto.randomBytes(10).toString('hex');
|
||||
|
||||
redis_client.hmset([id, 'filename', filename, 'delete', uuid]);
|
||||
|
||||
redis_client.expire(id, 86400000);
|
||||
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
||||
resolve({
|
||||
uuid: uuid,
|
||||
url: url
|
||||
});
|
||||
}
|
||||
});
|
||||
let hitLimit = false;
|
||||
const upload = s3.upload(params);
|
||||
file.on('limit', () => {
|
||||
hitLimit = true;
|
||||
upload.abort();
|
||||
});
|
||||
return upload.promise().then(
|
||||
() => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
||||
},
|
||||
err => {
|
||||
if (hitLimit) {
|
||||
throw new Error('limit');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function awsDelete(id, delete_token) {
|
||||
@@ -199,13 +257,13 @@ function awsDelete(id, delete_token) {
|
||||
if (!reply || delete_token !== reply) {
|
||||
reject();
|
||||
} else {
|
||||
redis_client.del(id);
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
s3.deleteObject(params, function(err, _data) {
|
||||
redis_client.del(id);
|
||||
err ? reject(err) : resolve(err);
|
||||
});
|
||||
}
|
||||
@@ -215,13 +273,13 @@ function awsDelete(id, delete_token) {
|
||||
|
||||
function awsForceDelete(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.del(id);
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
s3.deleteObject(params, function(err, _data) {
|
||||
redis_client.del(id);
|
||||
err ? reject(err) : resolve(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,3 +19,5 @@ rules:
|
||||
mocha/no-pending-tests: error
|
||||
mocha/no-return-and-callback: warn
|
||||
mocha/no-skipped-tests: error
|
||||
|
||||
no-console: off # ¯\_(ツ)_/¯
|
||||
|
||||
2
test/frontend/.eslintrc.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
env:
|
||||
browser: true
|
||||
22
test/frontend/driver.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const webdriver = require('selenium-webdriver');
|
||||
const path = require('path');
|
||||
const until = webdriver.until;
|
||||
|
||||
const driver = new webdriver.Builder()
|
||||
.forBrowser('firefox')
|
||||
.build();
|
||||
|
||||
driver.get(path.join('file:///', __dirname, '/frontend.test.html'));
|
||||
driver.wait(until.titleIs('Mocha Tests'), 10000);
|
||||
driver.wait(until.titleMatches(/^[0-1]$/), 10000);
|
||||
|
||||
driver.getTitle().then(title => {
|
||||
driver.quit().then(() => {
|
||||
if (title === '0') {
|
||||
console.log('Frontend tests have passed.');
|
||||
} else {
|
||||
throw new Error('Frontend tests are failing. ' +
|
||||
'Please open the frontend.test.html file in a browser.');
|
||||
}
|
||||
})
|
||||
})
|
||||
22
test/frontend/frontend.bundle.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class FakeFile extends Blob {
|
||||
constructor(name, data, opt) {
|
||||
super(data, opt);
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
window.Raven = {
|
||||
captureException: function(err) {
|
||||
console.error(err, err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
window.FakeFile = FakeFile;
|
||||
window.FileSender = require('../../frontend/src/fileSender');
|
||||
window.FileReceiver = require('../../frontend/src/fileReceiver');
|
||||
window.sinon = require('sinon');
|
||||
window.server = window.sinon.fakeServer.create();
|
||||
window.assert = require('assert');
|
||||
const utils = require('../../frontend/src/utils');
|
||||
window.hexToArray = utils.hexToArray;
|
||||
window.arrayToHex = utils.arrayToHex;
|
||||
24
test/frontend/frontend.test.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Mocha Tests</title>
|
||||
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css">
|
||||
<script src="bundle.js"></script>
|
||||
<meta charset="utf-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<script src="../../node_modules/mocha/mocha.js"></script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
|
||||
<script src="frontend.test.js"></script>
|
||||
|
||||
<script>
|
||||
mocha.checkLeaks();
|
||||
mocha.globals(['jQuery']);
|
||||
mocha.run(function(err) {
|
||||
document.title = err;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
360
test/frontend/frontend.test.js
Normal file
@@ -0,0 +1,360 @@
|
||||
const FileSender = window.FileSender;
|
||||
const FileReceiver = window.FileReceiver;
|
||||
const FakeFile = window.FakeFile;
|
||||
const assert = window.assert;
|
||||
const server = window.server;
|
||||
const hexToArray = window.hexToArray;
|
||||
const arrayToHex = window.arrayToHex;
|
||||
const sinon = window.sinon;
|
||||
|
||||
let file;
|
||||
let encryptedIV;
|
||||
let fileHash;
|
||||
let secretKey;
|
||||
let originalBlob;
|
||||
|
||||
describe('File Sender', function() {
|
||||
before(function() {
|
||||
server.respondImmediately = true;
|
||||
server.respondWith(
|
||||
'POST',
|
||||
'/upload',
|
||||
function(request) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(request.requestBody.get('data'));
|
||||
|
||||
reader.onload = function(event) {
|
||||
file = this.result;
|
||||
}
|
||||
|
||||
const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']);
|
||||
request.respond(
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
JSON.stringify({url: 'some url',
|
||||
id: responseObj.id,
|
||||
delete: responseObj.delete})
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('Should get a loading event emission', function() {
|
||||
const file = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||
const fs = new FileSender(file);
|
||||
let testLoading = true;
|
||||
|
||||
fs.on('loading', isStillLoading => {
|
||||
assert(!(!testLoading && isStillLoading));
|
||||
testLoading = isStillLoading;
|
||||
})
|
||||
|
||||
return fs.upload()
|
||||
.then(info => {
|
||||
assert(info);
|
||||
assert(!testLoading);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err, err.stack);
|
||||
assert.fail();
|
||||
});
|
||||
})
|
||||
|
||||
it('Should get a hashing event emission', function() {
|
||||
const file = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||
const fs = new FileSender(file);
|
||||
let testHashing = true;
|
||||
|
||||
fs.on('hashing', isStillHashing => {
|
||||
assert(!(!testHashing && isStillHashing));
|
||||
testHashing = isStillHashing;
|
||||
})
|
||||
|
||||
return fs.upload()
|
||||
.then(info => {
|
||||
assert(info);
|
||||
assert(!testHashing);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err, err.stack);
|
||||
assert.fail();
|
||||
});
|
||||
})
|
||||
|
||||
it('Should get a encrypting event emission', function() {
|
||||
const file = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||
const fs = new FileSender(file);
|
||||
let testEncrypting = true;
|
||||
|
||||
fs.on('encrypting', isStillEncrypting => {
|
||||
assert(!(!testEncrypting && isStillEncrypting));
|
||||
testEncrypting = isStillEncrypting;
|
||||
})
|
||||
|
||||
return fs.upload()
|
||||
.then(info => {
|
||||
assert(info);
|
||||
assert(!testEncrypting);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err, err.stack);
|
||||
assert.fail();
|
||||
});
|
||||
})
|
||||
|
||||
it('Should encrypt a file properly', function(done) {
|
||||
const newFile = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||
const fs = new FileSender(newFile);
|
||||
fs.upload().then(info => {
|
||||
const key = info.secretKey;
|
||||
secretKey = info.secretKey;
|
||||
const IV = info.fileId;
|
||||
encryptedIV = info.fileId;
|
||||
|
||||
const readRaw = new FileReader;
|
||||
readRaw.onload = function(event) {
|
||||
const rawArray = new Uint8Array(this.result);
|
||||
originalBlob = rawArray;
|
||||
|
||||
window.crypto.subtle.digest('SHA-256', rawArray).then(hash => {
|
||||
fileHash = hash;
|
||||
window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
{
|
||||
kty: 'oct',
|
||||
k: key,
|
||||
alg: 'A128GCM',
|
||||
ext: true,
|
||||
},
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(cryptoKey => {
|
||||
window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(IV),
|
||||
additionalData: hash,
|
||||
tagLength: 128
|
||||
},
|
||||
cryptoKey,
|
||||
rawArray
|
||||
)
|
||||
.then(encrypted => {
|
||||
assert(new Uint8Array(encrypted).toString() ===
|
||||
new Uint8Array(file).toString());
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
readRaw.readAsArrayBuffer(newFile);
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe('File Receiver', function() {
|
||||
|
||||
class FakeXHR {
|
||||
constructor() {
|
||||
this.response = file;
|
||||
this.status = 200;
|
||||
}
|
||||
|
||||
static setup() {
|
||||
FakeXHR.prototype.open = sinon.spy();
|
||||
FakeXHR.prototype.send = function () {
|
||||
this.onload();
|
||||
}
|
||||
|
||||
FakeXHR.prototype.originalXHR = window.XMLHttpRequest;
|
||||
|
||||
FakeXHR.prototype.getResponseHeader = function () {
|
||||
return JSON.stringify({
|
||||
aad: arrayToHex(new Uint8Array(fileHash)),
|
||||
filename: 'hello_world.txt',
|
||||
id: encryptedIV
|
||||
})
|
||||
}
|
||||
window.XMLHttpRequest = FakeXHR;
|
||||
}
|
||||
|
||||
static restore() {
|
||||
// originalXHR is a sinon FakeXMLHttpRequest, since
|
||||
// fakeServer.create() is called in frontend.bundle.js
|
||||
window.XMLHttpRequest.prototype.originalXHR.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const cb = function(done) {
|
||||
if (file === undefined ||
|
||||
encryptedIV === undefined ||
|
||||
fileHash === undefined ||
|
||||
secretKey === undefined) {
|
||||
assert.fail('Please run file sending tests before trying to receive the files.');
|
||||
done();
|
||||
}
|
||||
|
||||
FakeXHR.setup();
|
||||
done();
|
||||
}
|
||||
|
||||
before(cb)
|
||||
|
||||
after(function() {
|
||||
FakeXHR.restore();
|
||||
})
|
||||
|
||||
it('Should decrypt properly', function() {
|
||||
const fr = new FileReceiver();
|
||||
location.hash = secretKey;
|
||||
return fr.download().then(([decrypted, name]) => {
|
||||
assert(name);
|
||||
assert(new Uint8Array(decrypted).toString() ===
|
||||
new Uint8Array(originalBlob).toString())
|
||||
}).catch(err => {
|
||||
console.log(err, err.stack);
|
||||
assert.fail();
|
||||
})
|
||||
})
|
||||
|
||||
it('Should emit decrypting events', function() {
|
||||
const fr = new FileReceiver();
|
||||
location.hash = secretKey;
|
||||
|
||||
let testDecrypting = true;
|
||||
|
||||
fr.on('decrypting', isStillDecrypting => {
|
||||
assert(!(!testDecrypting && isStillDecrypting));
|
||||
testDecrypting = isStillDecrypting;
|
||||
});
|
||||
|
||||
fr.on('safe', isSafe => {
|
||||
assert(isSafe);
|
||||
})
|
||||
|
||||
return fr.download().then(([decrypted, name]) => {
|
||||
assert(decrypted);
|
||||
assert(name);
|
||||
assert(!testDecrypting);
|
||||
}).catch(err => {
|
||||
console.log(err, err.stack);
|
||||
assert.fail();
|
||||
})
|
||||
})
|
||||
|
||||
it('Should emit hashing events', function() {
|
||||
const fr = new FileReceiver();
|
||||
location.hash = secretKey;
|
||||
|
||||
let testHashing = true;
|
||||
|
||||
fr.on('hashing', isStillHashing => {
|
||||
assert(!(!testHashing && isStillHashing));
|
||||
testHashing = isStillHashing;
|
||||
});
|
||||
|
||||
fr.on('safe', isSafe => {
|
||||
assert(isSafe);
|
||||
})
|
||||
|
||||
return fr.download().then(([decrypted, name]) => {
|
||||
assert(decrypted);
|
||||
assert(name);
|
||||
assert(!testHashing);
|
||||
}).catch(err => {
|
||||
assert.fail();
|
||||
})
|
||||
})
|
||||
|
||||
it('Should catch fraudulent checksums', function(done) {
|
||||
// Use the secret key and file hash of the previous file to encrypt,
|
||||
// which has a different hash than this one (different strings).
|
||||
const newFile = new FakeFile('hello_world.txt',
|
||||
['This is some data, with a changed hash.'])
|
||||
const readRaw = new FileReader();
|
||||
|
||||
readRaw.onload = function(event) {
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
{
|
||||
kty: 'oct',
|
||||
k: secretKey,
|
||||
alg: 'A128GCM',
|
||||
ext: true
|
||||
},
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(key => {
|
||||
// The file hash used here is the hash of the fake
|
||||
// file from the previous test; it's a phony checksum.
|
||||
return window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(encryptedIV),
|
||||
additionalData: fileHash,
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
plaintext
|
||||
)
|
||||
})
|
||||
.then(encrypted => {
|
||||
file = encrypted;
|
||||
const fr = new FileReceiver();
|
||||
location.hash = secretKey;
|
||||
|
||||
fr.on('unsafe', isUnsafe => {
|
||||
assert(isUnsafe)
|
||||
})
|
||||
|
||||
fr.on('safe', () => {
|
||||
// This event should not be emitted.
|
||||
assert.fail();
|
||||
})
|
||||
|
||||
fr.download().then(() => {
|
||||
assert.fail();
|
||||
done();
|
||||
}).catch(err => {
|
||||
assert(1);
|
||||
done();
|
||||
})
|
||||
})
|
||||
}
|
||||
readRaw.readAsArrayBuffer(newFile);
|
||||
})
|
||||
|
||||
it('Should not decrypt with an incorrect checksum', function() {
|
||||
FakeXHR.prototype.getResponseHeader = function () {
|
||||
return JSON.stringify({
|
||||
aad: 'some_bad_hashz',
|
||||
filename: 'hello_world.txt',
|
||||
id: encryptedIV
|
||||
})
|
||||
}
|
||||
|
||||
const fr = new FileReceiver();
|
||||
location.hash = secretKey;
|
||||
|
||||
return fr.download().then(([decrypted, name]) => {
|
||||
assert(decrypted);
|
||||
assert(name);
|
||||
assert.fail();
|
||||
}).catch(err => {
|
||||
assert(1);
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
170
test/server/server.test.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
const request = require('supertest');
|
||||
const fs = require('fs');
|
||||
|
||||
|
||||
const logStub = {};
|
||||
logStub.info = sinon.stub();
|
||||
logStub.error = sinon.stub();
|
||||
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
'./log.js': function() {
|
||||
return logStub;
|
||||
}
|
||||
});
|
||||
|
||||
storage.flushall();
|
||||
|
||||
describe('Server integration tests', function() {
|
||||
let server;
|
||||
let storage;
|
||||
let uuid;
|
||||
let fileId;
|
||||
|
||||
before(function() {
|
||||
const app = proxyquire('../../server/server', {
|
||||
'./log.js': function() {
|
||||
return logStub;
|
||||
}
|
||||
});
|
||||
|
||||
server = app.server;
|
||||
storage = app.storage;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
storage.flushall();
|
||||
storage.quit();
|
||||
server.close();
|
||||
})
|
||||
|
||||
function upload() {
|
||||
return request(server).post('/upload')
|
||||
.field('fname', 'test_upload.txt')
|
||||
.set('X-File-Metadata', JSON.stringify({
|
||||
aad: '11111',
|
||||
id: '111111111111111111111111',
|
||||
filename: 'test_upload.txt'
|
||||
}))
|
||||
.attach('file', './test/test_upload.txt')
|
||||
}
|
||||
|
||||
it('Responds with a 200 when the service is up', function() {
|
||||
return request(server).get('/').expect(200);
|
||||
});
|
||||
|
||||
it('Rejects with a 404 when a file id is not valid', function() {
|
||||
return request(server).post('/upload/123')
|
||||
.field('fname', 'test_upload.txt')
|
||||
.set('X-File-Metadata', JSON.stringify({
|
||||
'silly': 'text'
|
||||
}))
|
||||
.attach('file', './test/test_upload.txt')
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it('Accepts a file and stores it when properly uploaded', function(done) {
|
||||
upload().then(res => {
|
||||
assert(res.body.hasOwnProperty('delete'));
|
||||
uuid = res.body.delete;
|
||||
assert(res.body.hasOwnProperty('url'));
|
||||
assert(res.body.hasOwnProperty('id'));
|
||||
fileId = res.body.id;
|
||||
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
|
||||
if (err) {
|
||||
done(new Error('The file does not exist'));
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Responds with a 200 if a file exists', function() {
|
||||
return request(server).get('/exists/' + fileId)
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it('Exists in the redis server', function() {
|
||||
return storage.exists(fileId)
|
||||
.then(() => assert(1))
|
||||
.catch(err => assert.fail())
|
||||
})
|
||||
|
||||
it('Fails delete if the delete token does not match', function() {
|
||||
return request(server).post('/delete/' + fileId)
|
||||
.send({ delete_token: 11 })
|
||||
.expect(404);
|
||||
})
|
||||
|
||||
it('Fails delete if the id is invalid', function() {
|
||||
return request(server).post('/delete/1')
|
||||
.expect(404);
|
||||
})
|
||||
|
||||
it('Successfully deletes if the id is valid and the delete token matches', function(done) {
|
||||
request(server).post('/delete/' + fileId)
|
||||
.send({ delete_token: uuid })
|
||||
.expect(200)
|
||||
.then(() => {
|
||||
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
|
||||
if (err) {
|
||||
done();
|
||||
} else {
|
||||
done(new Error('The file does not exist'));
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Responds with a 404 if a file does not exist', function() {
|
||||
return request(server).get('/exists/notfound')
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it('Uploads properly after a delete', function(done) {
|
||||
upload().then(res => {
|
||||
assert(res.body.hasOwnProperty('delete'));
|
||||
uuid = res.body.delete;
|
||||
assert(res.body.hasOwnProperty('url'));
|
||||
assert(res.body.hasOwnProperty('id'));
|
||||
fileId = res.body.id;
|
||||
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
|
||||
if (err) {
|
||||
done(new Error('The file does not exist'));
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Responds with a 200 for the download page', function() {
|
||||
return request(server).get('/download/' + fileId)
|
||||
.expect(200);
|
||||
})
|
||||
|
||||
it('Downloads a file properly', function() {
|
||||
return request(server).get('/assets/download/' + fileId)
|
||||
.then(res => {
|
||||
assert(res.header.hasOwnProperty('content-disposition'));
|
||||
assert(res.header.hasOwnProperty('content-type'))
|
||||
assert(res.header.hasOwnProperty('content-length'))
|
||||
assert(res.header.hasOwnProperty('x-file-metadata'))
|
||||
assert.equal(res.header['content-disposition'], 'attachment; filename=test_upload.txt')
|
||||
assert.equal(res.header['content-type'], 'application/octet-stream')
|
||||
})
|
||||
})
|
||||
|
||||
it('The file is deleted after one download', function() {
|
||||
assert(!fs.existsSync('./static/' + fileId));
|
||||
})
|
||||
|
||||
it('No longer exists in the redis server', function() {
|
||||
return storage.exists(fileId)
|
||||
.then(() => assert.fail())
|
||||
.catch(err => assert(1))
|
||||
})
|
||||
});
|
||||
1
test/test_upload.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is a test.
|
||||
@@ -3,9 +3,6 @@ const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const conf = require('../server/config.js');
|
||||
conf.notLocalHost = true;
|
||||
|
||||
const redisStub = {};
|
||||
const exists = sinon.stub();
|
||||
const hget = sinon.stub();
|
||||
@@ -46,13 +43,16 @@ const awsStub = {
|
||||
}
|
||||
};
|
||||
|
||||
const storage = proxyquire('../server/storage', {
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
redis: redisStub,
|
||||
fs: fsStub,
|
||||
'./log.js': function() {
|
||||
return logStub;
|
||||
},
|
||||
'aws-sdk': awsStub
|
||||
'aws-sdk': awsStub,
|
||||
'./config.js': {
|
||||
s3_bucket: 'test'
|
||||
}
|
||||
});
|
||||
|
||||
describe('Testing Length using aws', function() {
|
||||
@@ -110,23 +110,20 @@ describe('Testing Set using aws', function() {
|
||||
it('Should pass when the file is successfully uploaded', function() {
|
||||
const buf = Buffer.alloc(10);
|
||||
sinon.stub(crypto, 'randomBytes').returns(buf);
|
||||
s3Stub.upload.callsArgWith(1, null, {});
|
||||
s3Stub.upload.returns({promise: () => Promise.resolve()});
|
||||
return storage
|
||||
.set('123', {}, 'Filename.moz', 'url.com')
|
||||
.then(reply => {
|
||||
assert.equal(reply.uuid, buf.toString('hex'));
|
||||
assert.equal(reply.url, 'url.com');
|
||||
assert.notEqual(reply.uuid, null);
|
||||
.set('123', {on: sinon.stub()}, 'Filename.moz', {})
|
||||
.then(() => {
|
||||
assert(expire.calledOnce);
|
||||
assert(expire.calledWith('123', 86400000));
|
||||
assert(expire.calledWith('123', 86400));
|
||||
})
|
||||
.catch(err => assert.fail());
|
||||
});
|
||||
|
||||
it('Should fail if there was an error during uploading', function() {
|
||||
s3Stub.upload.callsArgWith(1, new Error(), null);
|
||||
s3Stub.upload.returns({promise: () => Promise.reject()});
|
||||
return storage
|
||||
.set('123', {}, 'Filename.moz', 'url.com')
|
||||
.set('123', {on: sinon.stub()}, 'Filename.moz', 'url.com')
|
||||
.then(_reply => assert.fail())
|
||||
.catch(err => assert(1));
|
||||
});
|
||||
@@ -2,8 +2,7 @@ const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
|
||||
const conf = require('../server/config.js');
|
||||
conf.notLocalHost = false;
|
||||
// const conf = require('../server/config.js');
|
||||
|
||||
const redisStub = {};
|
||||
const exists = sinon.stub();
|
||||
@@ -33,7 +32,7 @@ const logStub = {};
|
||||
logStub.info = sinon.stub();
|
||||
logStub.error = sinon.stub();
|
||||
|
||||
const storage = proxyquire('../server/storage', {
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
redis: redisStub,
|
||||
fs: fsStub,
|
||||
'./log.js': function() {
|
||||
@@ -118,15 +117,14 @@ describe('Testing Get from local filesystem', function() {
|
||||
describe('Testing Set to local filesystem', function() {
|
||||
it('Successfully writes the file to the local filesystem', function() {
|
||||
const stub = sinon.stub();
|
||||
stub.withArgs('close', sinon.match.any).callsArgWithAsync(1);
|
||||
stub.withArgs('finish', sinon.match.any).callsArgWithAsync(1);
|
||||
stub.withArgs('error', sinon.match.any).returns(1);
|
||||
fsStub.createWriteStream.returns({ on: stub });
|
||||
|
||||
return storage
|
||||
.set('test', { pipe: sinon.stub() }, 'Filename.moz', 'moz.la')
|
||||
.then(reply => {
|
||||
assert(reply.uuid);
|
||||
assert.equal(reply.url, 'moz.la');
|
||||
.set('test', { pipe: sinon.stub(), on: sinon.stub() }, 'Filename.moz', {})
|
||||
.then(() => {
|
||||
assert(1);
|
||||
})
|
||||
.catch(err => assert.fail());
|
||||
});
|
||||
@@ -1,63 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Download your file</title>
|
||||
{{> sentry dsn=dsn}}
|
||||
<script src="/bundle.js"></script>
|
||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||
{{#if shouldRenderAnalytics}}
|
||||
{{> analytics trackerId=trackerId}}
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="main-window">
|
||||
<div id="download">
|
||||
{{#if filename}}
|
||||
<div class="title">
|
||||
Your friend is sending you a file: <br />
|
||||
{{{filename}}} ({{{filesize}}})
|
||||
</div>
|
||||
<div id="download-page-one">
|
||||
<div>
|
||||
<button id="download-btn" onclick="download()">Download File</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="download-progress">
|
||||
<div id="download-text">
|
||||
Downloading File...
|
||||
</div>
|
||||
<div class="upload">
|
||||
<!-- progress bar here -->
|
||||
<div id="progress-bar"></div>
|
||||
<div id="progress-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="send-new" id="send-file">
|
||||
Send your own files
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
|
||||
<div class="title">
|
||||
This link has expired or never existed in the first place.
|
||||
</div>
|
||||
|
||||
<div class="share-window">
|
||||
<img src="/resources/link_expired.png" alt="Link expired" />
|
||||
</div>
|
||||
<div class="send-new" id="send-file">
|
||||
Send your own files
|
||||
</div>
|
||||
{{/if}}
|
||||
<div id="download">
|
||||
<script src="/download.js"></script>
|
||||
<div id="download-page-one">
|
||||
<div class="title">
|
||||
<span id="dl-filename"
|
||||
data-l10n-id="downloadFileName"
|
||||
data-l10n-args='{"filename": "{{filename}}"}'></span>
|
||||
<span data-l10n-id="downloadFileSize"
|
||||
data-l10n-args='{"size": "{{filesize}}"}'></span>
|
||||
<span id="dl-bytelength" hidden="true">{{sizeInBytes}}</span>
|
||||
<span id="dl-ttl" hidden="true">{{timeToExpiry}}</span>
|
||||
</div>
|
||||
<div class="description" data-l10n-id="downloadMessage"></div>
|
||||
<img src="/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
|
||||
<div>
|
||||
<button id="download-btn" data-l10n-id="downloadButtonLabel"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <ul id="downloaded_files">
|
||||
</ul> -->
|
||||
<div id="download-progress" hidden="true">
|
||||
<div class="title"
|
||||
data-l10n-id="downloadingPageProgress"
|
||||
data-l10n-args='{"filename": "{{filename}}", "size": "{{filesize}}"}'>
|
||||
</div>
|
||||
<div class="description" data-l10n-id="downloadingPageMessage"></div>
|
||||
<!-- progress bar here -->
|
||||
<div class="progress-bar" id="dl-progress">
|
||||
<div class="percentage">
|
||||
<span class="percent-number"></span>
|
||||
<span class="percent-sign">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload">
|
||||
<div class="progress-text">{{filename}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<a class="send-new" data-l10n-id="sendYourFilesLink"></a>
|
||||
</div>
|
||||
|
||||
@@ -1,94 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Firefox Fileshare</title>
|
||||
{{> sentry dsn=dsn}}
|
||||
<script src="/bundle.js"></script>
|
||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||
{{#if shouldRenderAnalytics}}
|
||||
{{> analytics trackerId=trackerId}}
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="main-window">
|
||||
<div id="page-one">
|
||||
<div class="title">
|
||||
Share your files quickly, privately and securely.
|
||||
</div>
|
||||
<div class="upload-window" ondrop="onUpload(event)" ondragover="allowDrop(event)">
|
||||
<div id="upload-img"><img src="/resources/upload.svg" alt="Upload"/></div>
|
||||
<div>
|
||||
DRAG & DROP
|
||||
</div>
|
||||
<div class="upload">
|
||||
<div id="browse-text">
|
||||
your file/folder here or
|
||||
</div>
|
||||
<div id="browse">
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" class="file-upload">browse</label>
|
||||
<input id="file-upload" type="file" onchange="onUpload(event)" name="fileUploaded" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="file-list">
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- htmllint attr-bans="false" -->
|
||||
<th width="30%">File</th>
|
||||
<th width="45%">Copy URL</th>
|
||||
<th width="18%">Expires in</th>
|
||||
<th width="7%">Delete</th>
|
||||
<!-- htmllint tag-bans="$previous" -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="upload-progress">
|
||||
<div class="title" id="upload-filename">
|
||||
Uploading
|
||||
</div>
|
||||
<div class="upload-window">
|
||||
<div id="upload-img"><img src="/resources/upload.svg" alt="Upload" /></div>
|
||||
<div class="upload">
|
||||
<!-- progress bar here -->
|
||||
<div id="progress-bar"></div>
|
||||
<div id="progress-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="share-link">
|
||||
<div class="title">
|
||||
Copy the link below to share your file!
|
||||
</div>
|
||||
<div class="share-window">
|
||||
<img src="/resources/share.png" alt="Share" />
|
||||
<div id="share-window-r">
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="" readonly/>
|
||||
<button id="copy-btn">Copy</button>
|
||||
</div>
|
||||
<div>
|
||||
This link expires after one download
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="send-new">
|
||||
Send another file
|
||||
</div>
|
||||
</div>
|
||||
<div id="page-one">
|
||||
<script src="/upload.js"></script>
|
||||
<div class="title" data-l10n-id="uploadPageHeader"></div>
|
||||
<div class="description">
|
||||
<div data-l10n-id="uploadPageExplainer"></div>
|
||||
<a href="https://testpilot.firefox.com/experiments/send" class="link" data-l10n-id="uploadPageLearnMore"></a>
|
||||
</div>
|
||||
<div class="upload-window" >
|
||||
<div id="upload-img"><img data-l10n-id="uploadSvgAlt" src="/resources/upload.svg"/></div>
|
||||
<div id="upload-text" data-l10n-id="uploadPageDropMessage"></div>
|
||||
<span id="file-size-msg"><em data-l10n-id="uploadPageSizeMessage"></em></span>
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" id="browse"
|
||||
data-l10n-id="uploadPageBrowseButton"></label>
|
||||
<input id="file-upload" type="file" name="fileUploaded" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<div id="file-list">
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- htmllint attr-bans="false" -->
|
||||
<th width="35%" data-l10n-id="sentFilesTitle1"></th>
|
||||
<th width="25%" data-l10n-id="sentFilesTitle2"></th>
|
||||
<th width="21%" data-l10n-id="sentFilesTitle3"></th>
|
||||
<th width="12%" data-l10n-id="sentFilesTitle4"></th>
|
||||
<!-- htmllint tag-bans="$previous" -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-progress" hidden="true">
|
||||
<div class="title" id="upload-filename" data-l10n-id="uploadingPageHeader"></div>
|
||||
<div class="description"></div>
|
||||
<!-- progress bar here -->
|
||||
<div class="progress-bar" id="ul-progress">
|
||||
<div class="percentage">
|
||||
<span class="percent-number">0</span>
|
||||
<span class="percent-sign">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload">
|
||||
<div class="progress-text"></div>
|
||||
<div id="cancel-upload"
|
||||
data-l10n-id="uploadingPageCancel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="share-link" hidden="true">
|
||||
<div class="title" data-l10n-id="uploadSuccessTimingHeader"></div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text" data-l10n-id="copyUrlFormLabel"></div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="" readonly/>
|
||||
<button id="copy-btn" data-l10n-id="copyUrlFormButton"></button>
|
||||
</div>
|
||||
<button id="delete-file" data-l10n-id="deleteFileButton"></button>
|
||||
<a class="send-new" id="send-new-completed" data-l10n-id="sendAnotherFileLink"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-error" hidden="true">
|
||||
<div class="title" data-l10n-id="errorPageHeader"></div>
|
||||
<div class="expired-description" data-l10n-id="errorPageMessage"></div>
|
||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="/resources/illustration_error.svg"/>
|
||||
<a class="send-new" id="send-new-error" data-l10n-id="sendAnotherFileLink"></a>
|
||||
</div>
|
||||
|
||||
8
views/jsconfig.handlebars
Normal file
@@ -0,0 +1,8 @@
|
||||
{{#if dsn}}
|
||||
window.dsn = '{{{dsn}}}';
|
||||
{{/if}}
|
||||
{{#if trackerId}}
|
||||
window.trackerId = '{{{trackerId}}}';
|
||||
{{/if}}
|
||||
const MAXFILESIZE = {{{maxFileSize}}};
|
||||
const EXPIRE_SECONDS = {{{expireSeconds}}};
|
||||
@@ -1 +1,45 @@
|
||||
{{{body}}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Firefox Send</title>
|
||||
<script src="/jsconfig.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/send.{locale}.ftl">
|
||||
<script defer src="/l20n.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="send-logo">
|
||||
<img src="/resources/send_logo.svg" alt="Send"/>
|
||||
<h1 class="site-title">Send</h1>
|
||||
<div class="site-subtitle">
|
||||
<a href="https://testpilot.firefox.com" target="_blank">Firefox Test Pilot</a>
|
||||
<div data-l10n-id="siteSubtitle">web experiment</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank" data-l10n-id="siteFeedback">Feedback</a>
|
||||
</header>
|
||||
<div class="all">
|
||||
{{{body}}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="legal-links">
|
||||
<a href="https://www.mozilla.org"><img class="mozilla-logo" src="/resources/mozilla-logo.svg"/></a>
|
||||
<a href="https://www.mozilla.org/about/legal" data-l10n-id="footerLinkLegal"></a>
|
||||
<a href="https://testpilot.firefox.com/about" data-l10n-id="footerLinkAbout"></a>
|
||||
<a href="https://testpilot.firefox.com/privacy" data-l10n-id="footerLinkPrivacy"></a>
|
||||
<a href="https://testpilot.firefox.com/terms" data-l10n-id="footerLinkTerms"></a>
|
||||
<a href="https://www.mozilla.org/en-US/privacy/websites/#cookies" data-l10n-id="footerLinkCookies"></a>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://github.com/mozilla/send" target="_blank"><img class="github" src="/resources/github-icon.svg"/></a>
|
||||
<a href="https://twitter.com/FxTestPilot" target="_blank"><img class="twitter" src="/resources/twitter-icon.svg"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
8
views/notfound.handlebars
Normal file
@@ -0,0 +1,8 @@
|
||||
<div id="download">
|
||||
<div class="title" data-l10n-id="expiredPageHeader"></div>
|
||||
<div class="share-window">
|
||||
<img src="/resources/illustration_expired.svg" id="expired-img" data-l10n-id="linkExpiredAlt"/>
|
||||
</div>
|
||||
<div class="expired-description" data-l10n-id="uploadPageExplainer"></div>
|
||||
<a class="send-new" href="/" id="expired-send-new" data-l10n-id="sendYourFilesLink"></a>
|
||||
</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{{trackerId}}}', 'auto');
|
||||
|
||||
if (window.URL && document.referrer) {
|
||||
ga("set", "referrer", (new URL(document.referrer)).origin);
|
||||
} else {
|
||||
ga("set", "referrer", "");
|
||||
}
|
||||
|
||||
ga('set', 'anonymizeIp', true);
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
<script>
|
||||
window.dsn = '{{{dsn}}}';
|
||||
</script>
|
||||
11
views/unsupported.handlebars
Normal file
@@ -0,0 +1,11 @@
|
||||
<div id="unsupported-browser">
|
||||
<div class="title" data-l10n-id="notSupportedHeader"></div>
|
||||
<div class="description" data-l10n-id="notSupportedDetail"></div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2" target="_blank">
|
||||
<img src="/resources/firefox_logo-only.svg" id="firefox-logo" alt="Firefox"/>
|
||||
<div id="dl-firefox-text">Firefox<br>
|
||||
<span data-l10n-id="downloadFirefoxButtonSub"></span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description" data-l10n-id="uploadPageExplainer"></div>
|
||||
</div>
|
||||