Compare commits
154 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 | ||
|
|
af0c497aab |
@@ -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
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Firefox Send
|
||||
|
||||
[](https://circleci.com/gh/mozilla/send)
|
||||
[](https://circleci.com/gh/mozilla/send)
|
||||
[](https://testpilot.firefox.com/experiments/send)
|
||||
|
||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
||||
|
||||
## What it does
|
||||
|
||||
A P2P file sharing experiment which allows you to send encrypted files to other users.
|
||||
A file sharing experiment which allows you to send encrypted files to other users.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
12
circle.yml
@@ -4,12 +4,19 @@ machine:
|
||||
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/send:latest .
|
||||
- docker push mozilla/send:latest
|
||||
@@ -17,7 +24,7 @@ deployment:
|
||||
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/send:$CIRCLE_TAG .
|
||||
- docker push mozilla/send:$CIRCLE_TAG
|
||||
@@ -26,3 +33,4 @@ 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
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
| Name | Description
|
||||
|------------------|-------------|
|
||||
| `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.
|
||||
| `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:
|
||||
|
||||
```sh
|
||||
$ 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/send:latest
|
||||
$ 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,71 +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');
|
||||
});
|
||||
});
|
||||
//link back to homepage
|
||||
$('.send-new').attr('href', window.location.origin);
|
||||
|
||||
$('.send-new').click(function(target) {
|
||||
target.preventDefault();
|
||||
sendEvent('recipient', 'restarted', {
|
||||
cd2: 'completed'
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
$('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
|
||||
target.preventDefault();
|
||||
const metric = findMetric(target.currentTarget.href);
|
||||
// record exited event by recipient
|
||||
sendEvent('recipient', 'exited', {
|
||||
cd3: metric
|
||||
}).then(() => {
|
||||
location.href = target.currentTarget.href;
|
||||
});
|
||||
});
|
||||
|
||||
const filename = $('#dl-filename').text();
|
||||
const bytelength = Number($('#dl-bytelength').text());
|
||||
const timeToExpiry = Number($('#dl-ttl').text());
|
||||
|
||||
//initiate progress bar
|
||||
$('#dl-progress').circleProgress({
|
||||
value: 0.0,
|
||||
startAngle: -Math.PI / 2,
|
||||
fill: '#3B9DFF',
|
||||
size: 158,
|
||||
animation: { duration: 300 }
|
||||
});
|
||||
$('#download-btn').click(download);
|
||||
function download() {
|
||||
const fileReceiver = new FileReceiver();
|
||||
const name = document.createElement('p');
|
||||
const $btn = $('#download-btn');
|
||||
storage.totalDownloads += 1;
|
||||
|
||||
fileReceiver.on('progress', percentComplete => {
|
||||
$('#download-page-one').hide();
|
||||
$('.send-new').hide();
|
||||
$('#download-progress').show();
|
||||
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) {
|
||||
fileReceiver.removeAllListeners('progress');
|
||||
$('#download-text').html('Download complete!');
|
||||
$('.send-new').show();
|
||||
$btn.text('Download complete!');
|
||||
$btn.attr('disabled', 'true');
|
||||
notify('Your download has finished.');
|
||||
}
|
||||
$('#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) {
|
||||
console.log('Decrypting');
|
||||
fileReceiver.removeAllListeners('progress');
|
||||
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) {
|
||||
console.log('Checking file integrity');
|
||||
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
|
||||
$('.progress-text').text(verifyingFile);
|
||||
});
|
||||
} else {
|
||||
console.log('Integrity check done');
|
||||
$('.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);
|
||||
|
||||
@@ -13,10 +13,7 @@ class FileReceiver extends EventEmitter {
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
const percentComplete = Math.floor(
|
||||
event.loaded / event.total * 100
|
||||
);
|
||||
this.emit('progress', percentComplete);
|
||||
this.emit('progress', [event.loaded, event.total]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,23 +67,18 @@ class FileReceiver extends EventEmitter {
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(fdata.iv),
|
||||
additionalData: hexToArray(fdata.aad)
|
||||
additionalData: hexToArray(fdata.aad),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
fdata.data
|
||||
)
|
||||
.then(decrypted => {
|
||||
this.emit('decrypting', false);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(decrypted);
|
||||
});
|
||||
return Promise.resolve(decrypted);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(fdata.filename);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(hexToArray(fdata.aad));
|
||||
})
|
||||
fdata.filename,
|
||||
hexToArray(fdata.aad)
|
||||
]);
|
||||
})
|
||||
.then(([decrypted, fname, proposedHash]) => {
|
||||
@@ -99,20 +91,12 @@ class FileReceiver extends EventEmitter {
|
||||
new Uint8Array(calculatedHash).toString() ===
|
||||
proposedHash.toString();
|
||||
if (!integrity) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('This file has been tampered with.');
|
||||
reject();
|
||||
});
|
||||
this.emit('unsafe', true);
|
||||
return Promise.reject();
|
||||
} else {
|
||||
this.emit('safe', true);
|
||||
return Promise.all([decrypted, decodeURIComponent(fname)]);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(decrypted);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(fname);
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class FileSender extends EventEmitter {
|
||||
super();
|
||||
this.file = file;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
}
|
||||
|
||||
static delete(fileId, token) {
|
||||
@@ -35,6 +36,10 @@ class FileSender extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
|
||||
upload() {
|
||||
const self = this;
|
||||
self.emit('loading', true);
|
||||
@@ -103,25 +108,26 @@ class FileSender extends EventEmitter {
|
||||
const fd = new FormData();
|
||||
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);
|
||||
self.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: responseObj.id,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.delete
|
||||
});
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,7 +137,7 @@ class FileSender extends EventEmitter {
|
||||
JSON.stringify({
|
||||
aad: arrayToHex(hash),
|
||||
id: fileId,
|
||||
filename: file.name
|
||||
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,41 +1,98 @@
|
||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
||||
require('./common');
|
||||
const FileSender = require('./fileSender');
|
||||
const { notify, gcmCompliant } = 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').hide();
|
||||
$('#compliance-error').show();
|
||||
$('#page-one').attr('hidden', true);
|
||||
sendEvent('sender', 'unsupported', {
|
||||
cd6: err
|
||||
}).then(() => {
|
||||
location.replace('/unsupported');
|
||||
});
|
||||
});
|
||||
|
||||
$('#file-upload').change(onUpload);
|
||||
$('#page-one').on('dragover', allowDrop).on('drop', 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();
|
||||
$('#upload-error').hide();
|
||||
$('#compliance-error').hide();
|
||||
|
||||
if (localStorage.length === 0) {
|
||||
const files = storage.files;
|
||||
if (files.length === 0) {
|
||||
toggleHeader();
|
||||
} else {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const id = localStorage.key(i);
|
||||
//check if file exists before adding to list
|
||||
checkExistence(id, true);
|
||||
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);
|
||||
@@ -44,123 +101,248 @@ $(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();
|
||||
$('#upload-error').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
|
||||
function onUpload(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// don't allow upload if not on upload page
|
||||
if ($('#page-one').attr('hidden')){
|
||||
return;
|
||||
}
|
||||
|
||||
storage.totalUploads += 1;
|
||||
|
||||
let file = '';
|
||||
if (event.type === 'drop') {
|
||||
if (!event.originalEvent.dataTransfer.files[0]) {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.originalEvent.dataTransfer.files.length > 1 ||
|
||||
event.originalEvent.dataTransfer.files[0].size === 0
|
||||
) {
|
||||
$('.upload-window').removeClass('ondrag');
|
||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
||||
alert(str);
|
||||
});
|
||||
return;
|
||||
}
|
||||
file = event.originalEvent.dataTransfer.files[0];
|
||||
} else {
|
||||
file = event.target.files[0];
|
||||
}
|
||||
const expiration = 24 * 60 * 60 * 1000; //will eventually come from a field
|
||||
|
||||
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);
|
||||
fileSender.on('progress', percentComplete => {
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').show();
|
||||
$('#upload-error').hide();
|
||||
$('#upload-filename').innerHTML += file.name;
|
||||
// update progress bar
|
||||
document
|
||||
.querySelector('#progress-bar')
|
||||
.style.setProperty('--progress', percentComplete + '%');
|
||||
$('#progress-text').html(`${percentComplete}%`);
|
||||
$('#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('loading', isStillLoading => {
|
||||
// The file is loading into Firefox at this stage
|
||||
if (isStillLoading) {
|
||||
console.log('Processing');
|
||||
} else {
|
||||
console.log('Finished processing');
|
||||
}
|
||||
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) {
|
||||
console.log('Hashing');
|
||||
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) {
|
||||
console.log('Encrypting');
|
||||
document.l10n.formatValue('encryptingFile').then(encryptingFile => {
|
||||
$('.progress-text').text(encryptingFile);
|
||||
});
|
||||
} else {
|
||||
console.log('Finished encrypting');
|
||||
uploadStart = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
creationDate: new Date(),
|
||||
expiry: expiration
|
||||
};
|
||||
localStorage.setItem(info.fileId, JSON.stringify(fileData));
|
||||
let t;
|
||||
const startTime = Date.now();
|
||||
const unexpiredFiles = storage.numFiles + 1;
|
||||
|
||||
$('#page-one').hide();
|
||||
$('#file-list').hide();
|
||||
$('#upload-progress').hide();
|
||||
$('#share-link').show();
|
||||
$('#upload-error').hide();
|
||||
// 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
|
||||
});
|
||||
|
||||
populateFileList(JSON.stringify(fileData));
|
||||
notify('Your upload has finished.');
|
||||
})
|
||||
.catch(err => {
|
||||
Raven.captureException(err);
|
||||
console.log(err);
|
||||
$('#page-one').hide();
|
||||
$('#upload-error').show();
|
||||
});
|
||||
// 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, populate) {
|
||||
function checkExistence(id, file, populate) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
if (populate) {
|
||||
populateFileList(localStorage.getItem(id));
|
||||
populateFileList(file);
|
||||
}
|
||||
} else if (xhr.status === 404) {
|
||||
localStorage.removeItem(id);
|
||||
storage.remove(id);
|
||||
if (storage.numFiles === 0) {
|
||||
toggleHeader();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -168,32 +350,73 @@ $(document).ready(function() {
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
//update file table with current files in localStorage
|
||||
//update file table with current files in storage
|
||||
function populateFileList(file) {
|
||||
try {
|
||||
file = JSON.parse(file);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -201,7 +424,7 @@ $(document).ready(function() {
|
||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||
|
||||
let countdown = 0;
|
||||
countdown = future.getTime() - new Date().getTime();
|
||||
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);
|
||||
@@ -209,84 +432,129 @@ $(document).ready(function() {
|
||||
poll();
|
||||
|
||||
function poll() {
|
||||
countdown = future.getTime() - new Date().getTime();
|
||||
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';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 3600000);
|
||||
} else if (hours === 1) {
|
||||
expiry.innerHTML = hours + 'h';
|
||||
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';
|
||||
expiry.innerHTML = minutes + 'm ' + seconds + 's';
|
||||
t = window.setTimeout(() => {
|
||||
poll();
|
||||
}, 1000);
|
||||
}
|
||||
//remove from list when expired
|
||||
if (countdown <= 0) {
|
||||
localStorage.removeItem(file.fileId);
|
||||
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');
|
||||
|
||||
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).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('.del-file').click(e => {
|
||||
$popupText.find('.popup-yes').click(e => {
|
||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||
$(e.target).parents('tr').remove();
|
||||
localStorage.removeItem(file.fileId);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
// add data cells to table row
|
||||
row.appendChild(name);
|
||||
row.appendChild(link);
|
||||
row.appendChild(expiry);
|
||||
popupDiv.appendChild(btn);
|
||||
$(popupDiv).append($popupText);
|
||||
del.appendChild(popupDiv);
|
||||
row.appendChild(del);
|
||||
|
||||
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').hide();
|
||||
$('#file-list').attr('hidden', true);
|
||||
} else {
|
||||
$('#file-list').show();
|
||||
$('#file-list').removeAttr('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ function arrayToHex(iv) {
|
||||
hexStr += iv[i].toString(16);
|
||||
}
|
||||
}
|
||||
window.hexStr = hexStr;
|
||||
return hexStr;
|
||||
}
|
||||
|
||||
@@ -70,9 +69,55 @@ function gcmCompliant() {
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
notify,
|
||||
gcmCompliant
|
||||
gcmCompliant,
|
||||
findMetric,
|
||||
isFile,
|
||||
sendEvent,
|
||||
ONE_DAY_IN_MS
|
||||
};
|
||||
|
||||
1565
package-lock.json
generated
48
package.json
@@ -1,41 +1,43 @@
|
||||
{
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "0.1.2",
|
||||
"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",
|
||||
"supertest": "^3.0.0",
|
||||
"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"
|
||||
@@ -44,14 +46,20 @@
|
||||
"license": "MPL-2.0",
|
||||
"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/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": "node server/server",
|
||||
"test": "mocha test/unit && mocha test/server",
|
||||
"version": "node scripts/version"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"JavaScript",
|
||||
"jQuery",
|
||||
"Node",
|
||||
"P2P",
|
||||
"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
|
||||
642
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,100 +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 {
|
||||
table-layout: fixed;
|
||||
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 */
|
||||
@@ -147,11 +271,11 @@ tbody {
|
||||
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 {
|
||||
@@ -159,32 +283,79 @@ tbody {
|
||||
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;
|
||||
@@ -200,61 +371,170 @@ tbody {
|
||||
#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;
|
||||
@@ -263,13 +543,19 @@ tbody {
|
||||
}
|
||||
|
||||
#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;
|
||||
@@ -277,6 +563,114 @@ tbody {
|
||||
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 |
@@ -4,12 +4,12 @@ const conf = convict({
|
||||
s3_bucket: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'P2P_S3_BUCKET'
|
||||
env: 'S3_BUCKET'
|
||||
},
|
||||
redis_host: {
|
||||
format: String,
|
||||
default: 'localhost',
|
||||
env: 'P2P_REDIS_HOST'
|
||||
env: 'REDIS_HOST'
|
||||
},
|
||||
listen_port: {
|
||||
format: 'port',
|
||||
@@ -25,17 +25,27 @@ const conf = convict({
|
||||
sentry_id: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'P2P_SENTRY_CLIENT'
|
||||
env: 'SENTRY_CLIENT'
|
||||
},
|
||||
sentry_dsn: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'P2P_SENTRY_DSN'
|
||||
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'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
103
server/server.js
@@ -32,38 +32,62 @@ app.engine(
|
||||
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\''],
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
'\'self\'',
|
||||
"'self'",
|
||||
'https://sentry.prod.mozaws.net',
|
||||
'https://www.google-analytics.com',
|
||||
'https://ssl.google-analytics.com'
|
||||
],
|
||||
imgSrc: [
|
||||
'\'self\'',
|
||||
"'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\'']
|
||||
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(busboy());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
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
|
||||
dsn: conf.sentry_id,
|
||||
maxFileSize: conf.max_file_size,
|
||||
expireSeconds: conf.expire_seconds,
|
||||
layout: false
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,15 +117,17 @@ app.get('/download/:id', (req, res) => {
|
||||
storage
|
||||
.length(id)
|
||||
.then(contentLength => {
|
||||
res.render('download', {
|
||||
filename: filename,
|
||||
filesize: bytes(contentLength),
|
||||
trackerId: conf.analytics_id,
|
||||
dsn: conf.sentry_id
|
||||
storage.ttl(id).then(timeToExpiry => {
|
||||
res.render('download', {
|
||||
filename: decodeURIComponent(filename),
|
||||
filesize: bytes(contentLength),
|
||||
sizeInBytes: contentLength,
|
||||
timeToExpiry: timeToExpiry
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
res.render('download');
|
||||
res.status(404).render('notfound');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -189,10 +215,10 @@ app.post('/upload', (req, res, next) => {
|
||||
}
|
||||
|
||||
if (
|
||||
!validateIV(meta.id) ||
|
||||
!meta.hasOwnProperty('aad') ||
|
||||
!meta.hasOwnProperty('id') ||
|
||||
!meta.hasOwnProperty('filename')
|
||||
!meta.hasOwnProperty('filename') ||
|
||||
!validateIV(meta.id)
|
||||
) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
@@ -205,15 +231,36 @@ app.post('/upload', (req, res, next) => {
|
||||
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
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ if (conf.s3_bucket) {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
ttl: ttl,
|
||||
length: awsLength,
|
||||
get: awsGet,
|
||||
set: awsSet,
|
||||
@@ -39,6 +40,7 @@ if (conf.s3_bucket) {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
ttl: ttl,
|
||||
length: localLength,
|
||||
get: localGet,
|
||||
set: localSet,
|
||||
@@ -73,6 +75,18 @@ function metadata(id) {
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -129,20 +143,24 @@ function localGet(id) {
|
||||
|
||||
function localSet(newId, file, filename, meta) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fstream = fs.createWriteStream(
|
||||
path.join(__dirname, '../static', newId)
|
||||
);
|
||||
const filepath = path.join(__dirname, '../static', newId);
|
||||
const fstream = fs.createWriteStream(filepath);
|
||||
file.pipe(fstream);
|
||||
fstream.on('close', () => {
|
||||
file.on('limit', () => {
|
||||
file.unpipe(fstream);
|
||||
fstream.destroy(new Error('limit'));
|
||||
});
|
||||
fstream.on('finish', () => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, 86400000);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||
resolve(meta.delete);
|
||||
});
|
||||
|
||||
fstream.on('error', () => {
|
||||
fstream.on('error', err => {
|
||||
log.error('localSet:', 'Failed upload of ' + newId);
|
||||
reject();
|
||||
fs.unlinkSync(filepath);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -211,21 +229,26 @@ function awsSet(newId, file, filename, meta) {
|
||||
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 {
|
||||
redis_client.hmset(newId, meta);
|
||||
|
||||
redis_client.expire(newId, 86400000);
|
||||
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
||||
resolve(meta.delete);
|
||||
}
|
||||
});
|
||||
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) {
|
||||
@@ -234,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);
|
||||
});
|
||||
}
|
||||
@@ -250,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);
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@@ -110,20 +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', {})
|
||||
.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));
|
||||
});
|
||||
|
||||
@@ -117,12 +117,12 @@ 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', {})
|
||||
.set('test', { pipe: sinon.stub(), on: sinon.stub() }, 'Filename.moz', {})
|
||||
.then(() => {
|
||||
assert(1);
|
||||
})
|
||||
|
||||
@@ -1,69 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Download your file</title>
|
||||
{{#if dsn}}
|
||||
{{> sentry dsn=dsn}}
|
||||
{{/if}}
|
||||
<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 trackerId}}
|
||||
{{> 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">Download File</button>
|
||||
</div>
|
||||
<div id='expired-img'>
|
||||
<img src='/resources/link_expired.png' />
|
||||
</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,113 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Firefox Send</title>
|
||||
{{#if dsn}}
|
||||
{{> sentry dsn=dsn}}
|
||||
{{/if}}
|
||||
<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 trackerId}}
|
||||
{{> 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">
|
||||
<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" name="fileUploaded" />
|
||||
</form>
|
||||
</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>
|
||||
|
||||
<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="upload-error">
|
||||
<div class="title">
|
||||
Upload error<br>
|
||||
This file cannot be uploaded!
|
||||
</div>
|
||||
<div class="send-new">
|
||||
Send another file
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="compliance-error">
|
||||
<div class="title">
|
||||
Encryption error<br>
|
||||
Your browser does not support gcm encryption.
|
||||
</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>
|
||||