Merge branch 'master' of https://github.com/mozilla/send into responsive-and-feedback

This commit is contained in:
John Gruen 2017-07-24 17:59:51 +02:00
commit dde2c4d5a9
28 changed files with 1304 additions and 3659 deletions

View File

@ -5,3 +5,4 @@ static
test test
scripts scripts
docs docs
firefox

View File

@ -1,4 +1,3 @@
public/bundle.js public
public/webcrypto-shim.js
test/frontend/bundle.js test/frontend/bundle.js
firefox firefox

4
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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 theyve 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`

10
frontend/src/common.js Normal file
View 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
});

View File

@ -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
.formatValues('downloadNotification', 'downloadFinish')
.then(translated => { .then(translated => {
notify(translated[0]); notify(translated[0]);
$('.title').html(translated[1]); $('.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,11 +123,31 @@ $(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', {
cm1: bytelength,
cm5: storage.totalUploads,
cm6: unexpiredFiles,
cm7: storage.totalDownloads,
cd2: 'errored',
cd6: err
});
document.l10n.formatValue('expiredPageHeader').then(translated => {
$('.title').text(translated); $('.title').text(translated);
}); });
$('#download-btn').attr('hidden', true); $('#download-btn').attr('hidden', true);
@ -85,6 +156,23 @@ $(document).ready(function() {
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);

View File

@ -118,15 +118,17 @@ 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);
}
}; };
xhr.open('post', '/upload', true); xhr.open('post', '/upload', true);

View File

@ -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
View 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;

View File

@ -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} (${(progress[0] / 1000).toFixed( `${file.name} (${bytes(progress[0], {
1 decimalPlaces: 1,
)}KB of ${(progress[1] / 1000).toFixed(1)}KB)` fixedDecimals: true
})} 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,19 +472,52 @@ $(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 =
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(); location.reload();
}); });
});
}; };
// show popup // show popup
$delIcon.click(function() { $delIcon.click(function() {

View File

@ -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
}; };

4127
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }
} }

View File

@ -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!

View File

@ -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;

View File

@ -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'
} }
}); });

View File

@ -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 => {
storage.ttl(id).then(timeToExpiry => {
res.render('download', { res.render('download', {
filename: decodeURIComponent(filename), filename: decodeURIComponent(filename),
filesize: bytes(contentLength), filesize: bytes(contentLength),
trackerId: conf.analytics_id, sizeInBytes: contentLength,
dsn: conf.sentry_id timeToExpiry: timeToExpiry
});
}); });
}) })
.catch(() => { .catch(() => {
res.render('download'); res.status(404).render('notfound');
}); });
}); });
}); });
@ -221,7 +230,8 @@ 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 protocol = conf.env === 'production' ? 'https' : req.protocol;
const url = `${protocol}://${req.get('host')}/download/${newId}/`; const url = `${protocol}://${req.get('host')}/download/${newId}/`;
res.json({ res.json({
@ -229,7 +239,14 @@ app.post('/upload', (req, res, next) => {
delete: meta.delete, delete: meta.delete,
id: newId id: newId
}); });
}); },
err => {
if (err.message === 'limit') {
return res.sendStatus(413);
}
res.sendStatus(500);
}
);
}); });
req.on('close', err => { req.on('close', err => {

View File

@ -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 { return upload.promise().then(
() => {
redis_client.hmset(newId, meta); redis_client.hmset(newId, meta);
redis_client.expire(newId, 86400000); redis_client.expire(newId, 86400000);
log.info('awsUploadFinish', 'Upload Finished of ' + filename); log.info('awsUploadFinish', 'Upload Finished of ' + filename);
resolve(meta.delete); },
err => {
if (hitLimit) {
throw new Error('limit');
} else {
throw err;
} }
}); }
}); );
} }
function awsDelete(id, delete_token) { function awsDelete(id, delete_token) {

View File

@ -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 # ¯\_(ツ)_/¯

View File

@ -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));
}); });

View File

@ -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);
}) })

View File

@ -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>

View File

@ -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>

View File

@ -4,3 +4,4 @@ window.dsn = '{{{dsn}}}';
{{#if trackerId}} {{#if trackerId}}
window.trackerId = '{{{trackerId}}}'; window.trackerId = '{{{trackerId}}}';
{{/if}} {{/if}}
const MAXFILESIZE = {{{maxFileSize}}};

View File

@ -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">
@ -30,7 +30,7 @@
<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>

View 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" id="expired-send-new" data-l10n-id="sendYourFilesLink"></a>
</div>

View 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>