Merge branch 'master' of https://github.com/mozilla/send into responsive-and-feedback
This commit is contained in:
commit
dde2c4d5a9
|
@ -5,3 +5,4 @@ static
|
||||||
test
|
test
|
||||||
scripts
|
scripts
|
||||||
docs
|
docs
|
||||||
|
firefox
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
public/bundle.js
|
public
|
||||||
public/webcrypto-shim.js
|
|
||||||
test/frontend/bundle.js
|
test/frontend/bundle.js
|
||||||
firefox
|
firefox
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
public/bundle.js
|
public/upload.js
|
||||||
|
public/download.js
|
||||||
public/version.json
|
public/version.json
|
||||||
|
public/l20n.min.js
|
||||||
static/*
|
static/*
|
||||||
!static/info.txt
|
!static/info.txt
|
||||||
test/frontend/bundle.js
|
test/frontend/bundle.js
|
||||||
|
|
|
@ -16,7 +16,7 @@ deployment:
|
||||||
latest:
|
latest:
|
||||||
branch: master
|
branch: master
|
||||||
commands:
|
commands:
|
||||||
- npm run predocker
|
- npm run build
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
- docker build -t mozilla/send:latest .
|
- docker build -t mozilla/send:latest .
|
||||||
- docker push mozilla/send:latest
|
- docker push mozilla/send:latest
|
||||||
|
@ -24,7 +24,7 @@ deployment:
|
||||||
tag: /.*/
|
tag: /.*/
|
||||||
owner: mozilla
|
owner: mozilla
|
||||||
commands:
|
commands:
|
||||||
- npm run predocker
|
- npm run build
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
- docker build -t mozilla/send:$CIRCLE_TAG .
|
||||||
- docker push mozilla/send:$CIRCLE_TAG
|
- docker push mozilla/send:$CIRCLE_TAG
|
||||||
|
|
|
@ -32,10 +32,9 @@ Data will be collected with Google Analytics and follow [Test Pilot standards](h
|
||||||
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
||||||
- `cd2` - the reason that the file transfer stopped. One of `completed`, `errored`, `cancelled`.
|
- `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`.
|
- `cd3` - the destination of a link click. One of `experiment-page`, `download-firefox`, `twitter`, `github`, `cookies`, `terms`, `privacy`, `about`, `legal`, `mozilla`.
|
||||||
- `cd4` - from where the URL for a file was copied. One of `finished-screen`, `file-list`.
|
- `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`.
|
- `cd5` - the referring location. One of `completed-download`, `errored-download`, `cancelled-download`, `completed-upload`, `errored-upload`, `cancelled-upload`, `testpilot`, `external`.
|
||||||
- `cd6` - the location from which the user copied the URL to an upload file. One of `success-screen`, `upload-list`.
|
- `cd6` - identifying information about an error. Exclude if there is no error involved. **TODO:** enumerate a list of possibilities.
|
||||||
- `cd7` - identifying information about an error. Exclude if there is no error involved. **TODO:** enumerate a list of possibilities.
|
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
|
@ -66,7 +65,7 @@ Triggered whenever a user stops uploading a file. Includes:
|
||||||
- `cm7`
|
- `cm7`
|
||||||
- `cd1`
|
- `cd1`
|
||||||
- `cd2`
|
- `cd2`
|
||||||
- `cd7`
|
- `cd6`
|
||||||
|
|
||||||
#### `download-started`
|
#### `download-started`
|
||||||
Triggered whenever a user begins downloading a file. Includes:
|
Triggered whenever a user begins downloading a file. Includes:
|
||||||
|
@ -91,7 +90,7 @@ Triggered whenever a user stops downloading a file.
|
||||||
- `cm6`
|
- `cm6`
|
||||||
- `cm7`
|
- `cm7`
|
||||||
- `cd2`
|
- `cd2`
|
||||||
- `cd7`
|
- `cd6`
|
||||||
|
|
||||||
#### `exited`
|
#### `exited`
|
||||||
Fired whenever a user follows a link external to Send.
|
Fired whenever a user follows a link external to Send.
|
||||||
|
@ -113,13 +112,14 @@ Fired whenever a user deletes a file they’ve uploaded.
|
||||||
- `cm6`
|
- `cm6`
|
||||||
- `cm7`
|
- `cm7`
|
||||||
- `cd1`
|
- `cd1`
|
||||||
|
- `cd4`
|
||||||
|
|
||||||
#### `copied`
|
#### `copied`
|
||||||
Fired whenever a user copies the URL of an upload file.
|
Fired whenever a user copies the URL of an upload file.
|
||||||
|
|
||||||
- `ec` - `sender`
|
- `ec` - `sender`
|
||||||
- `ea` - `copied`
|
- `ea` - `copied`
|
||||||
- `cd6`
|
- `cd4`
|
||||||
|
|
||||||
#### `restarted`
|
#### `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).
|
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).
|
||||||
|
@ -133,4 +133,4 @@ Fired whenever a user is presented a message saying that their browser is unsupp
|
||||||
|
|
||||||
- `ec` - `sender`
|
- `ec` - `sender`
|
||||||
- `ea` - `unsupported`
|
- `ea` - `unsupported`
|
||||||
- `cd7`
|
- `cd6`
|
||||||
|
|
|
@ -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,14 +1,54 @@
|
||||||
|
require('./common');
|
||||||
const FileReceiver = require('./fileReceiver');
|
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');
|
const $ = require('jquery');
|
||||||
require('jquery-circle-progress');
|
require('jquery-circle-progress');
|
||||||
|
|
||||||
const Raven = window.Raven;
|
const Raven = window.Raven;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
gcmCompliant().catch(err => {
|
||||||
|
$('#download').attr('hidden', true);
|
||||||
|
sendEvent('recipient', 'unsupported', {
|
||||||
|
cd6: err
|
||||||
|
}).then(() => {
|
||||||
|
location.replace('/unsupported');
|
||||||
|
});
|
||||||
|
});
|
||||||
//link back to homepage
|
//link back to homepage
|
||||||
$('.send-new').attr('href', window.location.origin);
|
$('.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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#expired-send-new').click(function() {
|
||||||
|
storage.referrer = 'errored-download';
|
||||||
|
});
|
||||||
|
|
||||||
const filename = $('#dl-filename').html();
|
const filename = $('#dl-filename').html();
|
||||||
|
const bytelength = Number($('#dl-bytelength').text());
|
||||||
|
const timeToExpiry = Number($('#dl-ttl').text());
|
||||||
|
|
||||||
//initiate progress bar
|
//initiate progress bar
|
||||||
$('#dl-progress').circleProgress({
|
$('#dl-progress').circleProgress({
|
||||||
|
@ -20,46 +60,57 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
$('#download-btn').click(download);
|
$('#download-btn').click(download);
|
||||||
function download() {
|
function download() {
|
||||||
|
storage.totalDownloads += 1;
|
||||||
|
|
||||||
const fileReceiver = new FileReceiver();
|
const fileReceiver = new FileReceiver();
|
||||||
|
const unexpiredFiles = storage.numFiles;
|
||||||
|
|
||||||
fileReceiver.on('progress', progress => {
|
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-page-one').attr('hidden', true);
|
||||||
$('#download-progress').removeAttr('hidden');
|
$('#download-progress').removeAttr('hidden');
|
||||||
const percent = progress[0] / progress[1];
|
const percent = progress[0] / progress[1];
|
||||||
// update progress bar
|
// update progress bar
|
||||||
$('#dl-progress').circleProgress('value', percent);
|
$('#dl-progress').circleProgress('value', percent);
|
||||||
$('.percent-number').html(`${Math.floor(percent * 100)}`);
|
$('.percent-number').html(`${Math.floor(percent * 100)}`);
|
||||||
if (progress[1] < 1000000) {
|
$('.progress-text').text(
|
||||||
$('.progress-text').html(
|
`${filename} (${bytes(progress[0], {
|
||||||
`${filename} (${(progress[0] / 1000).toFixed(1)}KB of
|
decimalPlaces: 1,
|
||||||
${(progress[1] / 1000).toFixed(1)}KB)`
|
fixedDecimals: true
|
||||||
);
|
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
|
||||||
} else if (progress[1] < 1000000000) {
|
);
|
||||||
$('.progress-text').html(
|
|
||||||
`${filename} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000).toFixed(1)}MB)`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$('.progress-text').html(
|
|
||||||
`${filename} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000000).toFixed(1)}GB)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
//on complete
|
//on complete
|
||||||
if (percent === 1) {
|
if (percent === 1) {
|
||||||
fileReceiver.removeAllListeners('progress');
|
fileReceiver.removeAllListeners('progress');
|
||||||
document.l10n.formatValues('downloadNotification', 'downloadFinish')
|
document.l10n
|
||||||
.then(translated => {
|
.formatValues('downloadNotification', 'downloadFinish')
|
||||||
notify(translated[0]);
|
.then(translated => {
|
||||||
$('.title').html(translated[1]);
|
notify(translated[0]);
|
||||||
});
|
$('.title').html(translated[1]);
|
||||||
|
});
|
||||||
|
window.onunload = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let downloadEnd;
|
||||||
fileReceiver.on('decrypting', isStillDecrypting => {
|
fileReceiver.on('decrypting', isStillDecrypting => {
|
||||||
// The file is being decrypted
|
// The file is being decrypted
|
||||||
if (isStillDecrypting) {
|
if (isStillDecrypting) {
|
||||||
console.log('Decrypting');
|
console.log('Decrypting');
|
||||||
} else {
|
} else {
|
||||||
console.log('Done decrypting');
|
console.log('Done decrypting');
|
||||||
|
downloadEnd = Date.now();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -72,19 +123,56 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
fileReceiver
|
||||||
.download()
|
.download()
|
||||||
.catch(() => {
|
.catch(err => {
|
||||||
document.l10n.formatValue('expiredPageHeader')
|
// record download-stopped (errored) by recipient
|
||||||
.then(translated => {
|
sendEvent('recipient', 'download-stopped', {
|
||||||
$('.title').text(translated);
|
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);
|
$('#download-btn').attr('hidden', true);
|
||||||
$('#expired-img').removeAttr('hidden');
|
$('#expired-img').removeAttr('hidden');
|
||||||
console.log('The file has expired, or has already been deleted.');
|
console.log('The file has expired, or has already been deleted.');
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
.then(([decrypted, fname]) => {
|
.then(([decrypted, fname]) => {
|
||||||
|
const endTime = Date.now();
|
||||||
|
const totalTime = endTime - startTime;
|
||||||
|
const downloadTime = endTime - downloadEnd;
|
||||||
|
const downloadSpeed = bytelength / (downloadTime / 1000);
|
||||||
|
|
||||||
|
storage.referrer = 'completed-download';
|
||||||
|
// record download-stopped (completed) by recipient
|
||||||
|
sendEvent('recipient', 'download-stopped', {
|
||||||
|
cm1: bytelength,
|
||||||
|
cm2: totalTime,
|
||||||
|
cm3: downloadSpeed,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: unexpiredFiles,
|
||||||
|
cm7: storage.totalDownloads,
|
||||||
|
cd2: 'completed'
|
||||||
|
});
|
||||||
|
|
||||||
const dataView = new DataView(decrypted);
|
const dataView = new DataView(decrypted);
|
||||||
const blob = new Blob([dataView]);
|
const blob = new Blob([dataView]);
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
|
|
@ -118,14 +118,16 @@ class FileSender extends EventEmitter {
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = () => {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
// uuid field and url field
|
if (xhr.status === 200) {
|
||||||
const responseObj = JSON.parse(xhr.responseText);
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
resolve({
|
return resolve({
|
||||||
url: responseObj.url,
|
url: responseObj.url,
|
||||||
fileId: responseObj.id,
|
fileId: responseObj.id,
|
||||||
secretKey: keydata.k,
|
secretKey: keydata.k,
|
||||||
deleteToken: responseObj.delete
|
deleteToken: responseObj.delete
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
reject(xhr.status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
window.Raven = require('raven-js');
|
|
||||||
window.Raven.config(window.dsn).install();
|
|
||||||
window.dsn = undefined;
|
|
||||||
require('./upload');
|
|
||||||
require('./download');
|
|
|
@ -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,17 +1,74 @@
|
||||||
|
/* global MAXFILESIZE */
|
||||||
|
require('./common');
|
||||||
const FileSender = require('./fileSender');
|
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');
|
const $ = require('jquery');
|
||||||
require('jquery-circle-progress');
|
require('jquery-circle-progress');
|
||||||
|
|
||||||
const Raven = window.Raven;
|
const Raven = window.Raven;
|
||||||
|
|
||||||
|
if (storage.has('referrer')) {
|
||||||
|
window.referrer = storage.referrer;
|
||||||
|
storage.remove('referrer');
|
||||||
|
} else {
|
||||||
|
window.referrer = 'external';
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
gcmCompliant().catch(err => {
|
gcmCompliant().catch(err => {
|
||||||
$('#page-one').attr('hidden', true);
|
$('#page-one').attr('hidden', true);
|
||||||
$('#unsupported-browser').removeAttr('hidden');
|
sendEvent('sender', 'unsupported', {
|
||||||
|
cd6: err
|
||||||
|
}).then(() => {
|
||||||
|
location.replace('/unsupported');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#file-upload').change(onUpload);
|
$('#file-upload').change(onUpload);
|
||||||
|
|
||||||
|
$('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
|
||||||
|
target.preventDefault();
|
||||||
|
const metric = findMetric(target.currentTarget.href);
|
||||||
|
// record exited event by recipient
|
||||||
|
sendEvent('sender', 'exited', {
|
||||||
|
cd3: metric
|
||||||
|
}).then(() => {
|
||||||
|
location.href = target.currentTarget.href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#send-new-completed').click(function(target) {
|
||||||
|
target.preventDefault();
|
||||||
|
// record restarted event
|
||||||
|
sendEvent('sender', 'restarted', {
|
||||||
|
cd2: 'completed'
|
||||||
|
}).then(() => {
|
||||||
|
storage.referrer = 'completed-upload';
|
||||||
|
location.href = target.currentTarget.href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#send-new-error').click(function(target) {
|
||||||
|
target.preventDefault();
|
||||||
|
// record restarted event
|
||||||
|
sendEvent('sender', 'restarted', {
|
||||||
|
cd2: 'errored'
|
||||||
|
}).then(() => {
|
||||||
|
storage.referrer = 'errored-upload';
|
||||||
|
location.href = target.currentTarget.href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('body').on('dragover', allowDrop).on('drop', onUpload);
|
$('body').on('dragover', allowDrop).on('drop', onUpload);
|
||||||
// reset copy button
|
// reset copy button
|
||||||
const $copyBtn = $('#copy-btn');
|
const $copyBtn = $('#copy-btn');
|
||||||
|
@ -19,18 +76,24 @@ $(document).ready(function() {
|
||||||
$('#link').attr('disabled', false);
|
$('#link').attr('disabled', false);
|
||||||
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
|
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
|
||||||
|
|
||||||
if (localStorage.length === 0) {
|
const files = storage.files;
|
||||||
|
console.log(files);
|
||||||
|
if (files.length === 0) {
|
||||||
toggleHeader();
|
toggleHeader();
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (const index in files) {
|
||||||
const id = localStorage.key(i);
|
const id = files[index].fileId;
|
||||||
//check if file exists before adding to list
|
//check if file still exists before adding to list
|
||||||
checkExistence(id, true);
|
checkExistence(id, files[index], true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy link to clipboard
|
// copy link to clipboard
|
||||||
$copyBtn.click(() => {
|
$copyBtn.click(() => {
|
||||||
|
// record copied event from success screen
|
||||||
|
sendEvent('sender', 'copied', {
|
||||||
|
cd4: 'success-screen'
|
||||||
|
});
|
||||||
const aux = document.createElement('input');
|
const aux = document.createElement('input');
|
||||||
aux.setAttribute('value', $('#link').attr('value'));
|
aux.setAttribute('value', $('#link').attr('value'));
|
||||||
document.body.appendChild(aux);
|
document.body.appendChild(aux);
|
||||||
|
@ -71,6 +134,9 @@ $(document).ready(function() {
|
||||||
// on file upload by browse or drag & drop
|
// on file upload by browse or drag & drop
|
||||||
function onUpload(event) {
|
function onUpload(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
storage.totalUploads += 1;
|
||||||
|
|
||||||
let file = '';
|
let file = '';
|
||||||
if (event.type === 'drop') {
|
if (event.type === 'drop') {
|
||||||
if (
|
if (
|
||||||
|
@ -88,6 +154,12 @@ $(document).ready(function() {
|
||||||
file = event.target.files[0];
|
file = event.target.files[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
return document.l10n
|
||||||
|
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||||
|
.then(alert);
|
||||||
|
}
|
||||||
|
|
||||||
$('#page-one').attr('hidden', true);
|
$('#page-one').attr('hidden', true);
|
||||||
$('#upload-error').attr('hidden', true);
|
$('#upload-error').attr('hidden', true);
|
||||||
$('#upload-progress').removeAttr('hidden');
|
$('#upload-progress').removeAttr('hidden');
|
||||||
|
@ -102,6 +174,17 @@ $(document).ready(function() {
|
||||||
document.l10n.formatValue('uploadCancelNotification').then(str => {
|
document.l10n.formatValue('uploadCancelNotification').then(str => {
|
||||||
notify(str);
|
notify(str);
|
||||||
});
|
});
|
||||||
|
storage.referrer = 'cancelled-upload';
|
||||||
|
|
||||||
|
// record upload-stopped (cancelled) by sender
|
||||||
|
sendEvent('sender', 'upload-stopped', {
|
||||||
|
cm1: file.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: unexpiredFiles,
|
||||||
|
cm7: storage.totalDownloads,
|
||||||
|
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||||
|
cd2: 'cancelled'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
fileSender.on('progress', progress => {
|
fileSender.on('progress', progress => {
|
||||||
|
@ -111,25 +194,12 @@ $(document).ready(function() {
|
||||||
$('#ul-progress').circleProgress().on('circle-animation-end', function() {
|
$('#ul-progress').circleProgress().on('circle-animation-end', function() {
|
||||||
$('.percent-number').html(`${Math.floor(percent * 100)}`);
|
$('.percent-number').html(`${Math.floor(percent * 100)}`);
|
||||||
});
|
});
|
||||||
if (progress[1] < 1000000) {
|
$('.progress-text').text(
|
||||||
$('.progress-text').text(
|
`${file.name} (${bytes(progress[0], {
|
||||||
`${file.name} (${(progress[0] / 1000).toFixed(
|
decimalPlaces: 1,
|
||||||
1
|
fixedDecimals: true
|
||||||
)}KB of ${(progress[1] / 1000).toFixed(1)}KB)`
|
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
|
||||||
);
|
);
|
||||||
} else if (progress[1] < 1000000000) {
|
|
||||||
$('.progress-text').text(
|
|
||||||
`${file.name} (${(progress[0] / 1000000).toFixed(
|
|
||||||
1
|
|
||||||
)}MB of ${(progress[1] / 1000000).toFixed(1)}MB)`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$('.progress-text').text(
|
|
||||||
`${file.name} (${(progress[0] / 1000000).toFixed(
|
|
||||||
1
|
|
||||||
)}MB of ${(progress[1] / 1000000000).toFixed(1)}GB)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fileSender.on('loading', isStillLoading => {
|
fileSender.on('loading', isStillLoading => {
|
||||||
|
@ -150,28 +220,66 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let uploadStart;
|
||||||
fileSender.on('encrypting', isStillEncrypting => {
|
fileSender.on('encrypting', isStillEncrypting => {
|
||||||
// The file is being encrypted
|
// The file is being encrypted
|
||||||
if (isStillEncrypting) {
|
if (isStillEncrypting) {
|
||||||
console.log('Encrypting');
|
console.log('Encrypting');
|
||||||
} else {
|
} else {
|
||||||
console.log('Finished encrypting');
|
console.log('Finished encrypting');
|
||||||
|
uploadStart = Date.now();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let t = '';
|
|
||||||
|
let t;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const unexpiredFiles = storage.numFiles + 1;
|
||||||
|
|
||||||
|
// record upload-started event by sender
|
||||||
|
sendEvent('sender', 'upload-started', {
|
||||||
|
cm1: file.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: unexpiredFiles,
|
||||||
|
cm7: storage.totalDownloads,
|
||||||
|
cd1: event.type === 'drop' ? 'drop' : 'click',
|
||||||
|
cd5: window.referrer
|
||||||
|
});
|
||||||
|
|
||||||
fileSender
|
fileSender
|
||||||
.upload()
|
.upload()
|
||||||
.then(info => {
|
.then(info => {
|
||||||
|
const endTime = Date.now();
|
||||||
|
const totalTime = endTime - startTime;
|
||||||
|
const uploadTime = endTime - uploadStart;
|
||||||
|
const uploadSpeed = file.size / (uploadTime / 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 = {
|
const fileData = {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
fileId: info.fileId,
|
fileId: info.fileId,
|
||||||
url: info.url,
|
url: info.url,
|
||||||
secretKey: info.secretKey,
|
secretKey: info.secretKey,
|
||||||
deleteToken: info.deleteToken,
|
deleteToken: info.deleteToken,
|
||||||
creationDate: new Date(),
|
creationDate: new Date(),
|
||||||
expiry: expiration
|
expiry: expiration,
|
||||||
|
totalTime: totalTime,
|
||||||
|
typeOfUpload: event.type === 'drop' ? 'drop' : 'click',
|
||||||
|
uploadSpeed: uploadSpeed
|
||||||
};
|
};
|
||||||
localStorage.setItem(info.fileId, JSON.stringify(fileData));
|
|
||||||
|
storage.addFile(info.fileId, fileData);
|
||||||
$('#upload-filename').attr(
|
$('#upload-filename').attr(
|
||||||
'data-l10n-id',
|
'data-l10n-id',
|
||||||
'uploadSuccessConfirmHeader'
|
'uploadSuccessConfirmHeader'
|
||||||
|
@ -183,7 +291,7 @@ $(document).ready(function() {
|
||||||
$('#share-link').removeAttr('hidden');
|
$('#share-link').removeAttr('hidden');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
populateFileList(JSON.stringify(fileData));
|
populateFileList(fileData);
|
||||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
document.l10n.formatValue('notifyUploadDone').then(str => {
|
||||||
notify(str);
|
notify(str);
|
||||||
});
|
});
|
||||||
|
@ -195,6 +303,17 @@ $(document).ready(function() {
|
||||||
$('#upload-progress').attr('hidden', true);
|
$('#upload-progress').attr('hidden', true);
|
||||||
$('#upload-error').removeAttr('hidden');
|
$('#upload-error').removeAttr('hidden');
|
||||||
window.clearTimeout(t);
|
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
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,16 +321,19 @@ $(document).ready(function() {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExistence(id, populate) {
|
function checkExistence(id, file, populate) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = () => {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
if (populate) {
|
if (populate) {
|
||||||
populateFileList(localStorage.getItem(id));
|
populateFileList(file);
|
||||||
}
|
}
|
||||||
} else if (xhr.status === 404) {
|
} else if (xhr.status === 404) {
|
||||||
localStorage.removeItem(id);
|
storage.remove(id);
|
||||||
|
if (storage.numFiles === 0) {
|
||||||
|
toggleHeader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -219,14 +341,8 @@ $(document).ready(function() {
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
//update file table with current files in localStorage
|
//update file table with current files in storage
|
||||||
function populateFileList(file) {
|
function populateFileList(file) {
|
||||||
try {
|
|
||||||
file = JSON.parse(file);
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
const name = document.createElement('td');
|
const name = document.createElement('td');
|
||||||
const link = document.createElement('td');
|
const link = document.createElement('td');
|
||||||
|
@ -271,6 +387,10 @@ $(document).ready(function() {
|
||||||
|
|
||||||
//copy link to clipboard when icon clicked
|
//copy link to clipboard when icon clicked
|
||||||
$copyIcon.click(function() {
|
$copyIcon.click(function() {
|
||||||
|
// record copied event from upload list
|
||||||
|
sendEvent('sender', 'copied', {
|
||||||
|
cd4: 'upload-list'
|
||||||
|
});
|
||||||
const aux = document.createElement('input');
|
const aux = document.createElement('input');
|
||||||
aux.setAttribute('value', url);
|
aux.setAttribute('value', url);
|
||||||
document.body.appendChild(aux);
|
document.body.appendChild(aux);
|
||||||
|
@ -295,7 +415,7 @@ $(document).ready(function() {
|
||||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||||
|
|
||||||
let countdown = 0;
|
let countdown = 0;
|
||||||
countdown = future.getTime() - new Date().getTime();
|
countdown = future.getTime() - Date.now();
|
||||||
let minutes = Math.floor(countdown / 1000 / 60);
|
let minutes = Math.floor(countdown / 1000 / 60);
|
||||||
let hours = Math.floor(minutes / 60);
|
let hours = Math.floor(minutes / 60);
|
||||||
let seconds = Math.floor(countdown / 1000 % 60);
|
let seconds = Math.floor(countdown / 1000 % 60);
|
||||||
|
@ -303,7 +423,7 @@ $(document).ready(function() {
|
||||||
poll();
|
poll();
|
||||||
|
|
||||||
function poll() {
|
function poll() {
|
||||||
countdown = future.getTime() - new Date().getTime();
|
countdown = future.getTime() - Date.now();
|
||||||
minutes = Math.floor(countdown / 1000 / 60);
|
minutes = Math.floor(countdown / 1000 / 60);
|
||||||
hours = Math.floor(minutes / 60);
|
hours = Math.floor(minutes / 60);
|
||||||
seconds = Math.floor(countdown / 1000 % 60);
|
seconds = Math.floor(countdown / 1000 % 60);
|
||||||
|
@ -322,7 +442,7 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
//remove from list when expired
|
//remove from list when expired
|
||||||
if (countdown <= 0) {
|
if (countdown <= 0) {
|
||||||
localStorage.removeItem(file.fileId);
|
storage.remove(file.fileId);
|
||||||
$(expiry).parents('tr').remove();
|
$(expiry).parents('tr').remove();
|
||||||
window.clearTimeout(t);
|
window.clearTimeout(t);
|
||||||
toggleHeader();
|
toggleHeader();
|
||||||
|
@ -352,18 +472,51 @@ $(document).ready(function() {
|
||||||
row.appendChild(del);
|
row.appendChild(del);
|
||||||
$('tbody').append(row); //add row to table
|
$('tbody').append(row); //add row to table
|
||||||
|
|
||||||
|
const unexpiredFiles = storage.numFiles;
|
||||||
|
|
||||||
// delete file
|
// delete file
|
||||||
$popupText.find('.del-file').click(e => {
|
$popupText.find('.del-file').click(e => {
|
||||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||||
$(e.target).parents('tr').remove();
|
$(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();
|
toggleHeader();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('delete-file').onclick = () => {
|
document.getElementById('delete-file').onclick = () => {
|
||||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||||
localStorage.removeItem(file.fileId);
|
const timeToExpiry =
|
||||||
location.reload();
|
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
|
// show popup
|
||||||
|
|
|
@ -69,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 = {
|
module.exports = {
|
||||||
arrayToHex,
|
arrayToHex,
|
||||||
hexToArray,
|
hexToArray,
|
||||||
notify,
|
notify,
|
||||||
gcmCompliant
|
gcmCompliant,
|
||||||
|
findMetric,
|
||||||
|
isFile,
|
||||||
|
sendEvent,
|
||||||
|
ONE_DAY_IN_MS
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
|
@ -1,44 +1,43 @@
|
||||||
{
|
{
|
||||||
"name": "firefox-send",
|
"name": "firefox-send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "0.1.2",
|
"version": "0.2.0",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "^2.62.0",
|
"aws-sdk": "^2.88.0",
|
||||||
"body-parser": "^1.17.2",
|
"body-parser": "^1.17.2",
|
||||||
"bytes": "^2.5.0",
|
"bytes": "^2.5.0",
|
||||||
"connect-busboy": "0.0.2",
|
"connect-busboy": "0.0.2",
|
||||||
"convict": "^3.0.0",
|
"convict": "^3.0.0",
|
||||||
"cross-env": "^5.0.1",
|
|
||||||
"express": "^4.15.3",
|
"express": "^4.15.3",
|
||||||
"express-handlebars": "^3.0.0",
|
"express-handlebars": "^3.0.0",
|
||||||
"helmet": "^3.6.1",
|
"helmet": "^3.8.0",
|
||||||
"jquery": "^3.2.1",
|
|
||||||
"jquery-circle-progress": "^1.2.2",
|
|
||||||
"l20n": "^5.0.0",
|
|
||||||
"mozlog": "^2.1.1",
|
"mozlog": "^2.1.1",
|
||||||
"raven": "^2.1.0",
|
"raven": "^2.1.0",
|
||||||
"raven-js": "^3.16.0",
|
"redis": "^2.7.1"
|
||||||
"redis": "^2.7.1",
|
|
||||||
"selenium-webdriver": "^3.4.0",
|
|
||||||
"supertest": "^3.0.0",
|
|
||||||
"uglify-es": "3.0.19"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^14.4.0",
|
"browserify": "^14.4.0",
|
||||||
"eslint": "^4.0.0",
|
"eslint": "^4.3.0",
|
||||||
"eslint-plugin-mocha": "^4.11.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",
|
"eslint-plugin-security": "^1.4.0",
|
||||||
"git-rev-sync": "^1.9.1",
|
"git-rev-sync": "^1.9.1",
|
||||||
|
"jquery": "^3.2.1",
|
||||||
|
"jquery-circle-progress": "^1.2.2",
|
||||||
|
"l20n": "^5.0.0",
|
||||||
"mocha": "^3.4.2",
|
"mocha": "^3.4.2",
|
||||||
"npm-run-all": "^4.0.2",
|
"npm-run-all": "^4.0.2",
|
||||||
"prettier": "^1.4.4",
|
"prettier": "^1.5.3",
|
||||||
"proxyquire": "^1.8.0",
|
"proxyquire": "^1.8.0",
|
||||||
"sinon": "^2.3.5",
|
"raven-js": "^3.17.0",
|
||||||
"stylelint": "^7.11.0",
|
"selenium-webdriver": "^3.4.0",
|
||||||
|
"sinon": "^2.3.8",
|
||||||
|
"stylelint": "^7.13.0",
|
||||||
"stylelint-config-standard": "^16.0.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": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
|
@ -47,15 +46,20 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"repository": "mozilla/send",
|
"repository": "mozilla/send",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predocker": "browserify frontend/src/main.js | uglifyjs > public/bundle.js && npm run version",
|
"build": "npm-run-all build:*",
|
||||||
"dev": "npm run version && watchify frontend/src/main.js -o public/bundle.js -d | node server/server",
|
"build:upload": "browserify frontend/src/upload.js -g uglifyify -o public/upload.js",
|
||||||
"format": "prettier '{frontend/src/,scripts/,server/,test/}*.js' 'public/*.css' --single-quote --write",
|
"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": "npm-run-all lint:*",
|
||||||
"lint:css": "stylelint 'public/*.css'",
|
"lint:css": "stylelint 'public/*.css'",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"start": "node server/server",
|
"start": "node server/server",
|
||||||
"test": "mocha test/unit && mocha test/server && npm run test-browser && node test/frontend/driver.js",
|
"test": "npm-run-all test:*",
|
||||||
"test-browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d",
|
"test:unit": "mocha test/unit",
|
||||||
"version": "node scripts/version"
|
"test:server": "mocha test/server",
|
||||||
|
"test:browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d && node test/frontend/driver.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ errorPageHeader = Something went wrong!
|
||||||
errorPageMessage = There has been an error uploading the file.
|
errorPageMessage = There has been an error uploading the file.
|
||||||
errorPageLink = Send another file
|
errorPageLink = Send another file
|
||||||
|
|
||||||
|
fileTooBig = That file is too big to upload. It should be less than { $size }.
|
||||||
|
|
||||||
linkExpiredAlt.alt = Link expired
|
linkExpiredAlt.alt = Link expired
|
||||||
expiredPageHeader = This link has expired or never existed in the first place!
|
expiredPageHeader = This link has expired or never existed in the first place!
|
||||||
|
|
|
@ -38,7 +38,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-title {
|
.site-title {
|
||||||
font-size: 34px;
|
color: #3e3d40;
|
||||||
|
font-size: 32px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -46,14 +47,14 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-subtitle {
|
.site-subtitle {
|
||||||
|
color: #3e3d40;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
color: #0c0c0d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-subtitle a {
|
.site-subtitle a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0c0c0d;
|
color: #3e3d40;
|
||||||
transition: color 50ms;
|
transition: color 50ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -585,7 +586,7 @@ tbody {
|
||||||
margin-bottom: -5px;
|
margin-bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-device-width: 768px) {
|
||||||
.description {
|
.description {
|
||||||
margin: 0 auto 25px;
|
margin: 0 auto 25px;
|
||||||
}
|
}
|
||||||
|
@ -608,7 +609,16 @@ tbody {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-device-width: 520px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#copy {
|
#copy {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -36,6 +36,11 @@ const conf = convict({
|
||||||
format: ['production', 'development', 'test'],
|
format: ['production', 'development', 'test'],
|
||||||
default: 'development',
|
default: 'development',
|
||||||
env: 'NODE_ENV'
|
env: 'NODE_ENV'
|
||||||
|
},
|
||||||
|
max_file_size: {
|
||||||
|
format: Number,
|
||||||
|
default: 1024 * 1024 * 1024 * 2,
|
||||||
|
env: 'P2P_MAX_FILE_SIZE'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,6 @@ const mozlog = require('./log.js');
|
||||||
const log = mozlog('send.server');
|
const log = mozlog('send.server');
|
||||||
|
|
||||||
const STATIC_PATH = path.join(__dirname, '../public');
|
const STATIC_PATH = path.join(__dirname, '../public');
|
||||||
const L20N = path.join(__dirname, '../node_modules/l20n');
|
|
||||||
const LOCALES = path.join(__dirname, '../public/locales');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
@ -64,21 +62,30 @@ app.use(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
app.use(busboy());
|
app.use(
|
||||||
|
busboy({
|
||||||
|
limits: {
|
||||||
|
fileSize: conf.max_file_size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(express.static(STATIC_PATH));
|
app.use(express.static(STATIC_PATH));
|
||||||
app.use('/l20n', express.static(L20N));
|
|
||||||
app.use('/locales', express.static(LOCALES));
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.render('index');
|
res.render('index');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/unsupported', (req, res) => {
|
||||||
|
res.render('unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/jsconfig.js', (req, res) => {
|
app.get('/jsconfig.js', (req, res) => {
|
||||||
res.set('Content-Type', 'application/javascript');
|
res.set('Content-Type', 'application/javascript');
|
||||||
res.render('jsconfig', {
|
res.render('jsconfig', {
|
||||||
trackerId: conf.analytics_id,
|
trackerId: conf.analytics_id,
|
||||||
dsn: conf.sentry_id,
|
dsn: conf.sentry_id,
|
||||||
|
maxFileSize: conf.max_file_size,
|
||||||
layout: false
|
layout: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -109,15 +116,17 @@ app.get('/download/:id', (req, res) => {
|
||||||
storage
|
storage
|
||||||
.length(id)
|
.length(id)
|
||||||
.then(contentLength => {
|
.then(contentLength => {
|
||||||
res.render('download', {
|
storage.ttl(id).then(timeToExpiry => {
|
||||||
filename: decodeURIComponent(filename),
|
res.render('download', {
|
||||||
filesize: bytes(contentLength),
|
filename: decodeURIComponent(filename),
|
||||||
trackerId: conf.analytics_id,
|
filesize: bytes(contentLength),
|
||||||
dsn: conf.sentry_id
|
sizeInBytes: contentLength,
|
||||||
|
timeToExpiry: timeToExpiry
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
res.render('download');
|
res.status(404).render('notfound');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -221,15 +230,23 @@ app.post('/upload', (req, res, next) => {
|
||||||
req.busboy.on('file', (fieldname, file, filename) => {
|
req.busboy.on('file', (fieldname, file, filename) => {
|
||||||
log.info('Uploading:', newId);
|
log.info('Uploading:', newId);
|
||||||
|
|
||||||
storage.set(newId, file, filename, meta).then(() => {
|
storage.set(newId, file, filename, meta).then(
|
||||||
const protocol = conf.env === 'production' ? 'https' : req.protocol;
|
() => {
|
||||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
const protocol = conf.env === 'production' ? 'https' : req.protocol;
|
||||||
res.json({
|
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||||
url,
|
res.json({
|
||||||
delete: meta.delete,
|
url,
|
||||||
id: newId
|
delete: meta.delete,
|
||||||
});
|
id: newId
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (err.message === 'limit') {
|
||||||
|
return res.sendStatus(413);
|
||||||
|
}
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('close', err => {
|
req.on('close', err => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ if (conf.s3_bucket) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
exists: exists,
|
exists: exists,
|
||||||
|
ttl: ttl,
|
||||||
length: awsLength,
|
length: awsLength,
|
||||||
get: awsGet,
|
get: awsGet,
|
||||||
set: awsSet,
|
set: awsSet,
|
||||||
|
@ -39,6 +40,7 @@ if (conf.s3_bucket) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
exists: exists,
|
exists: exists,
|
||||||
|
ttl: ttl,
|
||||||
length: localLength,
|
length: localLength,
|
||||||
get: localGet,
|
get: localGet,
|
||||||
set: localSet,
|
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) {
|
function filename(id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
redis_client.hget(id, 'filename', (err, reply) => {
|
redis_client.hget(id, 'filename', (err, reply) => {
|
||||||
|
@ -129,20 +143,24 @@ function localGet(id) {
|
||||||
|
|
||||||
function localSet(newId, file, filename, meta) {
|
function localSet(newId, file, filename, meta) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const fstream = fs.createWriteStream(
|
const filepath = path.join(__dirname, '../static', newId);
|
||||||
path.join(__dirname, '../static', newId)
|
const fstream = fs.createWriteStream(filepath);
|
||||||
);
|
|
||||||
file.pipe(fstream);
|
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.hmset(newId, meta);
|
||||||
redis_client.expire(newId, 86400000);
|
redis_client.expire(newId, 86400000);
|
||||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||||
resolve(meta.delete);
|
resolve(meta.delete);
|
||||||
});
|
});
|
||||||
|
|
||||||
fstream.on('error', () => {
|
fstream.on('error', err => {
|
||||||
log.error('localSet:', 'Failed upload of ' + newId);
|
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,
|
Key: newId,
|
||||||
Body: file
|
Body: file
|
||||||
};
|
};
|
||||||
|
let hitLimit = false;
|
||||||
return new Promise((resolve, reject) => {
|
const upload = s3.upload(params);
|
||||||
s3.upload(params, function(err, _data) {
|
file.on('limit', () => {
|
||||||
if (err) {
|
hitLimit = true;
|
||||||
log.info('awsUploadError:', err.stack); // an error occurred
|
upload.abort();
|
||||||
reject();
|
|
||||||
} else {
|
|
||||||
redis_client.hmset(newId, meta);
|
|
||||||
|
|
||||||
redis_client.expire(newId, 86400000);
|
|
||||||
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
|
||||||
resolve(meta.delete);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
return upload.promise().then(
|
||||||
|
() => {
|
||||||
|
redis_client.hmset(newId, meta);
|
||||||
|
redis_client.expire(newId, 86400000);
|
||||||
|
log.info('awsUploadFinish', 'Upload Finished of ' + filename);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (hitLimit) {
|
||||||
|
throw new Error('limit');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function awsDelete(id, delete_token) {
|
function awsDelete(id, delete_token) {
|
||||||
|
|
|
@ -19,3 +19,5 @@ rules:
|
||||||
mocha/no-pending-tests: error
|
mocha/no-pending-tests: error
|
||||||
mocha/no-return-and-callback: warn
|
mocha/no-return-and-callback: warn
|
||||||
mocha/no-skipped-tests: error
|
mocha/no-skipped-tests: error
|
||||||
|
|
||||||
|
no-console: off # ¯\_(ツ)_/¯
|
||||||
|
|
|
@ -110,9 +110,9 @@ describe('Testing Set using aws', function() {
|
||||||
it('Should pass when the file is successfully uploaded', function() {
|
it('Should pass when the file is successfully uploaded', function() {
|
||||||
const buf = Buffer.alloc(10);
|
const buf = Buffer.alloc(10);
|
||||||
sinon.stub(crypto, 'randomBytes').returns(buf);
|
sinon.stub(crypto, 'randomBytes').returns(buf);
|
||||||
s3Stub.upload.callsArgWith(1, null, {});
|
s3Stub.upload.returns({promise: () => Promise.resolve()});
|
||||||
return storage
|
return storage
|
||||||
.set('123', {}, 'Filename.moz', {})
|
.set('123', {on: sinon.stub()}, 'Filename.moz', {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
assert(expire.calledOnce);
|
assert(expire.calledOnce);
|
||||||
assert(expire.calledWith('123', 86400000));
|
assert(expire.calledWith('123', 86400000));
|
||||||
|
@ -121,9 +121,9 @@ describe('Testing Set using aws', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should fail if there was an error during uploading', function() {
|
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
|
return storage
|
||||||
.set('123', {}, 'Filename.moz', 'url.com')
|
.set('123', {on: sinon.stub()}, 'Filename.moz', 'url.com')
|
||||||
.then(_reply => assert.fail())
|
.then(_reply => assert.fail())
|
||||||
.catch(err => assert(1));
|
.catch(err => assert(1));
|
||||||
});
|
});
|
||||||
|
|
|
@ -117,12 +117,12 @@ describe('Testing Get from local filesystem', function() {
|
||||||
describe('Testing Set to local filesystem', function() {
|
describe('Testing Set to local filesystem', function() {
|
||||||
it('Successfully writes the file to the local filesystem', function() {
|
it('Successfully writes the file to the local filesystem', function() {
|
||||||
const stub = sinon.stub();
|
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);
|
stub.withArgs('error', sinon.match.any).returns(1);
|
||||||
fsStub.createWriteStream.returns({ on: stub });
|
fsStub.createWriteStream.returns({ on: stub });
|
||||||
|
|
||||||
return storage
|
return storage
|
||||||
.set('test', { pipe: sinon.stub() }, 'Filename.moz', {})
|
.set('test', { pipe: sinon.stub(), on: sinon.stub() }, 'Filename.moz', {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
assert(1);
|
assert(1);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div id="download">
|
<div id="download">
|
||||||
{{#if filename}}
|
<script src="/download.js"></script>
|
||||||
<div id="download-page-one">
|
<div id="download-page-one">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span id="dl-filename"
|
<span id="dl-filename"
|
||||||
|
@ -7,6 +7,8 @@
|
||||||
data-l10n-args='{"filename": "{{filename}}"}'></span>
|
data-l10n-args='{"filename": "{{filename}}"}'></span>
|
||||||
<span data-l10n-id="downloadFileSize"
|
<span data-l10n-id="downloadFileSize"
|
||||||
data-l10n-args='{"size": "{{filesize}}"}'></span>
|
data-l10n-args='{"size": "{{filesize}}"}'></span>
|
||||||
|
<span id="dl-bytelength" hidden="true">{{sizeInBytes}}</span>
|
||||||
|
<span id="dl-ttl" hidden="true">{{timeToExpiry}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="description" data-l10n-id="downloadMessage"></div>
|
<div class="description" data-l10n-id="downloadMessage"></div>
|
||||||
<img src="/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
|
<img src="/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
|
||||||
|
@ -34,13 +36,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="send-new" data-l10n-id="sendYourFilesLink"></a>
|
<a class="send-new" data-l10n-id="sendYourFilesLink"></a>
|
||||||
{{else}}
|
|
||||||
<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" data-l10n-id="sendYourFilesLink"></a>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<div id="page-one">
|
<div id="page-one">
|
||||||
|
<script src="/upload.js"></script>
|
||||||
<div class="title" data-l10n-id="uploadPageHeader"></div>
|
<div class="title" data-l10n-id="uploadPageHeader"></div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<div data-l10n-id="uploadPageExplainer"></div>
|
<div data-l10n-id="uploadPageExplainer"></div>
|
||||||
|
@ -60,7 +61,7 @@
|
||||||
<button id="copy-btn" data-l10n-id="copyUrlFormButton"></button>
|
<button id="copy-btn" data-l10n-id="copyUrlFormButton"></button>
|
||||||
</div>
|
</div>
|
||||||
<button id="delete-file" data-l10n-id="deleteFileButton"></button>
|
<button id="delete-file" data-l10n-id="deleteFileButton"></button>
|
||||||
<a class="send-new" data-l10n-id="sendAnotherFileLink"></a>
|
<a class="send-new" id="send-new-completed" data-l10n-id="sendAnotherFileLink"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -68,17 +69,5 @@
|
||||||
<div class="title" data-l10n-id="errorPageHeader"></div>
|
<div class="title" data-l10n-id="errorPageHeader"></div>
|
||||||
<div class="expired-description" data-l10n-id="errorPageMessage"></div>
|
<div class="expired-description" data-l10n-id="errorPageMessage"></div>
|
||||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="/resources/illustration_error.svg"/>
|
<img id="upload-error-img" data-l10n-id="errorAltText" src="/resources/illustration_error.svg"/>
|
||||||
<a class="send-new" data-l10n-id="sendAnotherFileLink"></a>
|
<a class="send-new" id="send-new-error" data-l10n-id="sendAnotherFileLink"></a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="unsupported-browser" hidden="true">
|
|
||||||
<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>
|
</div>
|
||||||
|
|
|
@ -4,3 +4,4 @@ window.dsn = '{{{dsn}}}';
|
||||||
{{#if trackerId}}
|
{{#if trackerId}}
|
||||||
window.trackerId = '{{{trackerId}}}';
|
window.trackerId = '{{{trackerId}}}';
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
const MAXFILESIZE = {{{maxFileSize}}};
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
<head>
|
<head>
|
||||||
<title>Firefox Send</title>
|
<title>Firefox Send</title>
|
||||||
<script src="/jsconfig.js"></script>
|
<script src="/jsconfig.js"></script>
|
||||||
<script src="/bundle.js"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="/main.css" />
|
<link rel="stylesheet" type="text/css" href="/main.css" />
|
||||||
<link rel="stylesheet" href="https://code.cdn.mozilla.net/fonts/fira.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="defaultLanguage" content="en-US">
|
||||||
<meta name="availableLanguages" content="en-US">
|
<meta name="availableLanguages" content="en-US">
|
||||||
<link rel="localization" href="/locales/send.{locale}.ftl">
|
<link rel="localization" href="/locales/send.{locale}.ftl">
|
||||||
<script defer src="/l20n/dist/web/l20n.js"></script>
|
<script defer src="/l20n.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="legal-links">
|
<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"><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://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/about" data-l10n-id="footerLinkAbout"></a>
|
||||||
<a href="https://testpilot.firefox.com/privacy" data-l10n-id="footerLinkPrivacy"></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://testpilot.firefox.com/terms" data-l10n-id="footerLinkTerms"></a>
|
||||||
|
|
|
@ -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" id="expired-send-new" data-l10n-id="sendYourFilesLink"></a>
|
||||||
|
</div>
|
|
@ -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>
|
Loading…
Reference in New Issue