Merge pull request #537 from mozilla/choo
a few changes to make A/B testing easier
@ -1,12 +1,8 @@
|
||||
node_modules
|
||||
.git
|
||||
.DS_Store
|
||||
static
|
||||
test
|
||||
scripts
|
||||
docs
|
||||
firefox
|
||||
assets
|
||||
docs
|
||||
public
|
||||
views
|
||||
webpack
|
||||
frontend
|
||||
test
|
||||
|
@ -1,3 +1,3 @@
|
||||
public
|
||||
test/frontend/bundle.js
|
||||
dist
|
||||
assets
|
||||
firefox
|
||||
|
6
.gitignore
vendored
@ -1,6 +1,2 @@
|
||||
.DS_Store
|
||||
dist
|
||||
node_modules
|
||||
static/*
|
||||
!static/info.txt
|
||||
test/frontend/bundle.js
|
||||
dist
|
||||
|
@ -12,4 +12,4 @@ RUN npm install --production && npm cache clean --force
|
||||
ENV PORT=1443
|
||||
EXPOSE $PORT
|
||||
|
||||
CMD ["npm", "start"]
|
||||
CMD ["npm", "run", "prod"]
|
||||
|
@ -30,11 +30,12 @@ $ redis-server /usr/local/etc/redis.conf
|
||||
|
||||
| Command | Description |
|
||||
|------------------|-------------|
|
||||
| `npm run dev` | Builds and starts the web server locally for development.
|
||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||
| `npm start` | Starts the Express web server.
|
||||
| `npm test` | Runs the suite of mocha tests.
|
||||
| `npm start` | Runs the server in development configuration.
|
||||
| `npm run build` | Builds the production assets.
|
||||
| `npm run prod` | Runs the server in production configuration.
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
env:
|
||||
browser: true
|
||||
node: false
|
||||
node: true
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
24
app/dragManager.js
Normal file
@ -0,0 +1,24 @@
|
||||
export default function(state, emitter) {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.body.addEventListener('dragover', event => {
|
||||
if (state.route === '/') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('drop', event => {
|
||||
if (state.route === '/' && !state.transfer) {
|
||||
event.preventDefault();
|
||||
document.querySelector('.upload-window').classList.remove('ondrag');
|
||||
const target = event.dataTransfer;
|
||||
if (target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (target.files.length > 1 || target.files[0].size === 0) {
|
||||
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||
}
|
||||
const file = target.files[0];
|
||||
emitter.emit('upload', { file, type: 'drop' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
202
app/fileManager.js
Normal file
@ -0,0 +1,202 @@
|
||||
/* global EXPIRE_SECONDS */
|
||||
import FileSender from './fileSender';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { copyToClipboard, delay, fadeOut } from './utils';
|
||||
import * as metrics from './metrics';
|
||||
|
||||
function saveFile(file) {
|
||||
const dataView = new DataView(file.plaintext);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
if (window.navigator.msSaveBlob) {
|
||||
return window.navigator.msSaveBlob(blob, file.name);
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = file.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
function openLinksInNewTab(links, should = true) {
|
||||
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||
if (should) {
|
||||
links.forEach(l => {
|
||||
l.setAttribute('target', '_blank');
|
||||
l.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
} else {
|
||||
links.forEach(l => {
|
||||
l.removeAttribute('target');
|
||||
l.removeAttribute('rel');
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
function exists(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
||||
resolve(xhr.status === 200);
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => resolve(false);
|
||||
xhr.ontimeout = () => resolve(false);
|
||||
xhr.open('get', '/api/exists/' + id);
|
||||
xhr.timeout = 2000;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
export default function(state, emitter) {
|
||||
let lastRender = 0;
|
||||
|
||||
function render() {
|
||||
emitter.emit('render');
|
||||
}
|
||||
|
||||
async function checkFiles() {
|
||||
const files = state.storage.files;
|
||||
let rerender = false;
|
||||
for (const file of files) {
|
||||
const ok = await exists(file.id);
|
||||
if (!ok) {
|
||||
state.storage.remove(file.id);
|
||||
rerender = true;
|
||||
}
|
||||
}
|
||||
if (rerender) {
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
emitter.on('DOMContentLoaded', checkFiles);
|
||||
|
||||
emitter.on('navigate', checkFiles);
|
||||
|
||||
emitter.on('render', () => {
|
||||
lastRender = Date.now();
|
||||
});
|
||||
|
||||
emitter.on('delete', async ({ file, location }) => {
|
||||
try {
|
||||
metrics.deletedUpload({
|
||||
size: file.size,
|
||||
time: file.time,
|
||||
speed: file.speed,
|
||||
type: file.type,
|
||||
ttl: file.expiresAt - Date.now(),
|
||||
location
|
||||
});
|
||||
state.storage.remove(file.id);
|
||||
await FileSender.delete(file.id, file.deleteToken);
|
||||
} catch (e) {
|
||||
state.raven.captureException(e);
|
||||
}
|
||||
state.fileInfo = null;
|
||||
});
|
||||
|
||||
emitter.on('cancel', () => {
|
||||
state.transfer.cancel();
|
||||
});
|
||||
|
||||
emitter.on('upload', async ({ file, type }) => {
|
||||
const size = file.size;
|
||||
const sender = new FileSender(file);
|
||||
sender.on('progress', render);
|
||||
sender.on('encrypting', render);
|
||||
state.transfer = sender;
|
||||
render();
|
||||
const links = openLinksInNewTab();
|
||||
await delay(200);
|
||||
try {
|
||||
const start = Date.now();
|
||||
metrics.startedUpload({ size, type });
|
||||
const info = await sender.upload();
|
||||
const time = Date.now() - start;
|
||||
const speed = size / (time / 1000);
|
||||
metrics.completedUpload({ size, time, speed, type });
|
||||
await delay(1000);
|
||||
await fadeOut('upload-progress');
|
||||
info.name = file.name;
|
||||
info.size = size;
|
||||
info.type = type;
|
||||
info.time = time;
|
||||
info.speed = speed;
|
||||
info.createdAt = Date.now();
|
||||
info.url = `${info.url}#${info.secretKey}`;
|
||||
info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
|
||||
state.fileInfo = info;
|
||||
state.storage.addFile(state.fileInfo);
|
||||
openLinksInNewTab(links, false);
|
||||
state.transfer = null;
|
||||
state.storage.totalUploads += 1;
|
||||
emitter.emit('pushState', `/share/${info.id}`);
|
||||
} catch (err) {
|
||||
state.transfer = null;
|
||||
if (err.message === '0') {
|
||||
//cancelled. do nothing
|
||||
metrics.cancelledUpload({ size, type });
|
||||
return render();
|
||||
}
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedUpload({ size, type, err });
|
||||
emitter.emit('replaceState', '/error');
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('download', async file => {
|
||||
const size = file.size;
|
||||
const url = `/api/download/${file.id}`;
|
||||
const receiver = new FileReceiver(url, file.key);
|
||||
receiver.on('progress', render);
|
||||
receiver.on('decrypting', render);
|
||||
state.transfer = receiver;
|
||||
const links = openLinksInNewTab();
|
||||
render();
|
||||
try {
|
||||
const start = Date.now();
|
||||
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||
const f = await receiver.download();
|
||||
const time = Date.now() - start;
|
||||
const speed = size / (time / 1000);
|
||||
await delay(1000);
|
||||
await fadeOut('download-progress');
|
||||
saveFile(f);
|
||||
state.storage.totalDownloads += 1;
|
||||
metrics.completedDownload({ size, time, speed });
|
||||
emitter.emit('pushState', '/completed');
|
||||
} catch (err) {
|
||||
// TODO cancelled download
|
||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
||||
if (location === '/error') {
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedDownload({ size, err });
|
||||
}
|
||||
emitter.emit('replaceState', location);
|
||||
} finally {
|
||||
state.transfer = null;
|
||||
openLinksInNewTab(links, false);
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('copy', ({ url, location }) => {
|
||||
copyToClipboard(url);
|
||||
metrics.copiedLink({ location });
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
// poll for rerendering the file list countdown timers
|
||||
if (
|
||||
state.route === '/' &&
|
||||
state.storage.files.length > 0 &&
|
||||
Date.now() - lastRender > 30000
|
||||
) {
|
||||
render();
|
||||
}
|
||||
}, 60000);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import EventEmitter from 'events';
|
||||
import { hexToArray } from './utils';
|
||||
import Nanobus from 'nanobus';
|
||||
import { hexToArray, bytes } from './utils';
|
||||
|
||||
export default class FileReceiver extends EventEmitter {
|
||||
export default class FileReceiver extends Nanobus {
|
||||
constructor(url, k) {
|
||||
super();
|
||||
super('FileReceiver');
|
||||
this.key = window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
{
|
||||
@ -19,6 +19,23 @@ export default class FileReceiver extends EventEmitter {
|
||||
['decrypt']
|
||||
);
|
||||
this.url = url;
|
||||
this.msg = 'fileSizeProgress';
|
||||
this.progress = [0, 1];
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
|
||||
get sizes() {
|
||||
return {
|
||||
partialSize: bytes(this.progress[0]),
|
||||
totalSize: bytes(this.progress[1])
|
||||
};
|
||||
}
|
||||
|
||||
cancel() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
@ -27,7 +44,8 @@ export default class FileReceiver extends EventEmitter {
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.emit('progress', [event.loaded, event.total]);
|
||||
this.progress = [event.loaded, event.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
};
|
||||
|
||||
@ -61,6 +79,7 @@ export default class FileReceiver extends EventEmitter {
|
||||
async download() {
|
||||
const key = await this.key;
|
||||
const file = await this.downloadFile();
|
||||
this.msg = 'decryptingFile';
|
||||
this.emit('decrypting');
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
@ -71,6 +90,7 @@ export default class FileReceiver extends EventEmitter {
|
||||
key,
|
||||
file.data
|
||||
);
|
||||
this.msg = 'downloadFinish';
|
||||
return {
|
||||
plaintext,
|
||||
name: decodeURIComponent(file.name),
|
@ -1,10 +1,13 @@
|
||||
import EventEmitter from 'events';
|
||||
import { arrayToHex } from './utils';
|
||||
import Nanobus from 'nanobus';
|
||||
import { arrayToHex, bytes } from './utils';
|
||||
|
||||
export default class FileSender extends EventEmitter {
|
||||
export default class FileSender extends Nanobus {
|
||||
constructor(file) {
|
||||
super();
|
||||
super('FileSender');
|
||||
this.file = file;
|
||||
this.msg = 'importingFile';
|
||||
this.progress = [0, 1];
|
||||
this.cancelled = false;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
this.key = window.crypto.subtle.generateKey(
|
||||
@ -17,13 +20,13 @@ export default class FileSender extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
static delete(fileId, token) {
|
||||
static delete(id, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!fileId || !token) {
|
||||
if (!id || !token) {
|
||||
return reject();
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('post', '/delete/' + fileId, true);
|
||||
xhr.open('POST', `/api/delete/${id}`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
@ -36,9 +39,23 @@ export default class FileSender extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
|
||||
get sizes() {
|
||||
return {
|
||||
partialSize: bytes(this.progress[0]),
|
||||
totalSize: bytes(this.progress[1])
|
||||
};
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
if (this.msg === 'fileSizeProgress') {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
}
|
||||
|
||||
readFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -57,7 +74,7 @@ export default class FileSender extends EventEmitter {
|
||||
uploadFile(encrypted, keydata) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.file;
|
||||
const fileId = arrayToHex(this.iv);
|
||||
const id = arrayToHex(this.iv);
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const fd = new FormData();
|
||||
@ -67,41 +84,49 @@ export default class FileSender extends EventEmitter {
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
this.emit('progress', [e.loaded, e.total]);
|
||||
this.progress = [e.loaded, e.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
this.progress = [1, 1];
|
||||
this.msg = 'notifyUploadDone';
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
return resolve({
|
||||
url: responseObj.url,
|
||||
fileId: responseObj.id,
|
||||
id: responseObj.id,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.delete
|
||||
});
|
||||
}
|
||||
reject(xhr.status);
|
||||
this.msg = 'errorPageHeader';
|
||||
reject(new Error(xhr.status));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('post', '/upload', true);
|
||||
xhr.open('post', '/api/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
JSON.stringify({
|
||||
id: fileId,
|
||||
id: id,
|
||||
filename: encodeURIComponent(file.name)
|
||||
})
|
||||
);
|
||||
xhr.send(fd);
|
||||
this.msg = 'fileSizeProgress';
|
||||
});
|
||||
}
|
||||
|
||||
async upload() {
|
||||
this.emit('loading');
|
||||
const key = await this.key;
|
||||
const plaintext = await this.readFile();
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
this.msg = 'encryptingFile';
|
||||
this.emit('encrypting');
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
@ -112,6 +137,9 @@ export default class FileSender extends EventEmitter {
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
||||
return this.uploadFile(encrypted, keydata);
|
||||
}
|
38
app/main.js
Normal file
@ -0,0 +1,38 @@
|
||||
import app from './routes';
|
||||
import log from 'choo-log';
|
||||
import locale from '../common/locales';
|
||||
import fileManager from './fileManager';
|
||||
import dragManager from './dragManager';
|
||||
import { canHasSend } from './utils';
|
||||
import assets from '../common/assets';
|
||||
import storage from './storage';
|
||||
import metrics from './metrics';
|
||||
import Raven from 'raven-js';
|
||||
|
||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||
}
|
||||
|
||||
app.use(log());
|
||||
|
||||
app.use((state, emitter) => {
|
||||
// init state
|
||||
state.transfer = null;
|
||||
state.fileInfo = null;
|
||||
state.translate = locale.getTranslator();
|
||||
state.storage = storage;
|
||||
state.raven = Raven;
|
||||
emitter.on('DOMContentLoaded', async () => {
|
||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||
if (!ok) {
|
||||
const reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
||||
emitter.emit('replaceState', `/unsupported/${reason}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.use(metrics);
|
||||
app.use(fileManager);
|
||||
app.use(dragManager);
|
||||
|
||||
app.mount('#page-one');
|
@ -1,12 +1,11 @@
|
||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||
import Storage from './storage';
|
||||
const storage = new Storage();
|
||||
import storage from './storage';
|
||||
|
||||
let hasLocalStorage = false;
|
||||
try {
|
||||
hasLocalStorage = !!localStorage;
|
||||
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||
} catch (e) {
|
||||
// don't care
|
||||
// when disabled, any mention of localStorage throws an error
|
||||
}
|
||||
|
||||
const analytics = new testPilotGA({
|
||||
@ -15,14 +14,19 @@ const analytics = new testPilotGA({
|
||||
tid: window.GOOGLE_ANALYTICS_ID
|
||||
});
|
||||
|
||||
const category = location.pathname.includes('/download')
|
||||
? 'recipient'
|
||||
: 'sender';
|
||||
let appState = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
export default function initialize(state, emitter) {
|
||||
appState = state;
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
addExitHandlers();
|
||||
addRestartHandlers();
|
||||
});
|
||||
//TODO restart handlers... somewhere
|
||||
});
|
||||
}
|
||||
|
||||
function category() {
|
||||
return appState.route === '/' ? 'sender' : 'recipient';
|
||||
}
|
||||
|
||||
function sendEvent() {
|
||||
return (
|
||||
@ -62,11 +66,11 @@ function urlToMetric(url) {
|
||||
}
|
||||
|
||||
function setReferrer(state) {
|
||||
if (category === 'sender') {
|
||||
if (category() === 'sender') {
|
||||
if (state) {
|
||||
storage.referrer = `${state}-upload`;
|
||||
}
|
||||
} else if (category === 'recipient') {
|
||||
} else if (category() === 'recipient') {
|
||||
if (state) {
|
||||
storage.referrer = `${state}-download`;
|
||||
}
|
||||
@ -87,10 +91,10 @@ function takeReferrer() {
|
||||
}
|
||||
|
||||
function startedUpload(params) {
|
||||
return sendEvent(category, 'upload-started', {
|
||||
return sendEvent('sender', 'upload-started', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles + 1,
|
||||
cm6: storage.files.length + 1,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd5: takeReferrer()
|
||||
@ -99,10 +103,10 @@ function startedUpload(params) {
|
||||
|
||||
function cancelledUpload(params) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent(category, 'upload-stopped', {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'cancelled'
|
||||
@ -110,12 +114,12 @@ function cancelledUpload(params) {
|
||||
}
|
||||
|
||||
function completedUpload(params) {
|
||||
return sendEvent(category, 'upload-stopped', {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'completed'
|
||||
@ -123,20 +127,20 @@ function completedUpload(params) {
|
||||
}
|
||||
|
||||
function startedDownload(params) {
|
||||
return sendEvent(category, 'download-started', {
|
||||
return sendEvent('recipient', 'download-started', {
|
||||
cm1: params.size,
|
||||
cm4: params.ttl,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
}
|
||||
|
||||
function stoppedDownload(params) {
|
||||
return sendEvent(category, 'download-stopped', {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'errored',
|
||||
cd6: params.err
|
||||
@ -145,20 +149,20 @@ function stoppedDownload(params) {
|
||||
|
||||
function cancelledDownload(params) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent(category, 'download-stopped', {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
}
|
||||
|
||||
function stoppedUpload(params) {
|
||||
return sendEvent(category, 'upload-stopped', {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'errored',
|
||||
@ -167,25 +171,25 @@ function stoppedUpload(params) {
|
||||
}
|
||||
|
||||
function completedDownload(params) {
|
||||
return sendEvent(category, 'download-stopped', {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'completed'
|
||||
});
|
||||
}
|
||||
|
||||
function deletedUpload(params) {
|
||||
return sendEvent(category, 'upload-deleted', {
|
||||
return sendEvent(category(), 'upload-deleted', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm4: params.ttl,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd4: params.location
|
||||
@ -193,19 +197,19 @@ function deletedUpload(params) {
|
||||
}
|
||||
|
||||
function unsupported(params) {
|
||||
return sendEvent(category, 'unsupported', {
|
||||
return sendEvent(category(), 'unsupported', {
|
||||
cd6: params.err
|
||||
});
|
||||
}
|
||||
|
||||
function copiedLink(params) {
|
||||
return sendEvent(category, 'copied', {
|
||||
return sendEvent('sender', 'copied', {
|
||||
cd4: params.location
|
||||
});
|
||||
}
|
||||
|
||||
function exitEvent(target) {
|
||||
return sendEvent(category, 'exited', {
|
||||
return sendEvent(category(), 'exited', {
|
||||
cd3: urlToMetric(target.currentTarget.href)
|
||||
});
|
||||
}
|
||||
@ -219,21 +223,13 @@ function addExitHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
function restartEvent(state) {
|
||||
function restart(state) {
|
||||
setReferrer(state);
|
||||
return sendEvent(category, 'restarted', {
|
||||
return sendEvent(category(), 'restarted', {
|
||||
cd2: state
|
||||
});
|
||||
}
|
||||
|
||||
function addRestartHandlers() {
|
||||
const elements = Array.from(document.querySelectorAll('.send-new'));
|
||||
elements.forEach(el => {
|
||||
const state = el.getAttribute('data-state');
|
||||
el.addEventListener('click', restartEvent.bind(null, state));
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
copiedLink,
|
||||
startedUpload,
|
||||
@ -245,5 +241,6 @@ export {
|
||||
cancelledDownload,
|
||||
stoppedDownload,
|
||||
completedDownload,
|
||||
restart,
|
||||
unsupported
|
||||
};
|
9
app/routes/download.js
Normal file
@ -0,0 +1,9 @@
|
||||
const preview = require('../templates/preview');
|
||||
const download = require('../templates/download');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.transfer) {
|
||||
return download(state, emit);
|
||||
}
|
||||
return preview(state, emit);
|
||||
};
|
9
app/routes/home.js
Normal file
@ -0,0 +1,9 @@
|
||||
const welcome = require('../templates/welcome');
|
||||
const upload = require('../templates/upload');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.transfer) {
|
||||
return upload(state, emit);
|
||||
}
|
||||
return welcome(state, emit);
|
||||
};
|
17
app/routes/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
const choo = require('choo');
|
||||
const download = require('./download');
|
||||
|
||||
const app = choo();
|
||||
|
||||
app.route('/', require('./home'));
|
||||
app.route('/share/:id', require('../templates/share'));
|
||||
app.route('/download/:id', download);
|
||||
app.route('/download/:id/:key', download);
|
||||
app.route('/completed', require('../templates/completed'));
|
||||
app.route('/unsupported/:reason', require('../templates/unsupported'));
|
||||
app.route('/legal', require('../templates/legal'));
|
||||
app.route('/error', require('../templates/error'));
|
||||
app.route('/blank', require('../templates/blank'));
|
||||
app.route('*', require('../templates/notFound'));
|
||||
|
||||
module.exports = app;
|
@ -26,13 +26,30 @@ class Mem {
|
||||
}
|
||||
}
|
||||
|
||||
export default class Storage {
|
||||
class Storage {
|
||||
constructor() {
|
||||
try {
|
||||
this.engine = localStorage || new Mem();
|
||||
} catch (e) {
|
||||
this.engine = new Mem();
|
||||
}
|
||||
this._files = this.loadFiles();
|
||||
}
|
||||
|
||||
loadFiles() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
try {
|
||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
||||
} catch (err) {
|
||||
// obviously you're not a golfer
|
||||
this.engine.removeItem(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
||||
}
|
||||
|
||||
get totalDownloads() {
|
||||
@ -55,34 +72,7 @@ export default class Storage {
|
||||
}
|
||||
|
||||
get files() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
try {
|
||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
||||
} catch (err) {
|
||||
// obviously you're not a golfer
|
||||
this.engine.removeItem(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.sort((file1, file2) => {
|
||||
const creationDate1 = new Date(file1.creationDate);
|
||||
const creationDate2 = new Date(file2.creationDate);
|
||||
return creationDate1 - creationDate2;
|
||||
});
|
||||
}
|
||||
|
||||
get numFiles() {
|
||||
let length = 0;
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
length += 1;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
return this._files;
|
||||
}
|
||||
|
||||
getFileById(id) {
|
||||
@ -94,10 +84,16 @@ export default class Storage {
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
if (isFile(property)) {
|
||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||
}
|
||||
this.engine.removeItem(property);
|
||||
}
|
||||
|
||||
addFile(id, file) {
|
||||
this.engine.setItem(id, JSON.stringify(file));
|
||||
addFile(file) {
|
||||
this._files.push(file);
|
||||
this.engine.setItem(file.id, JSON.stringify(file));
|
||||
}
|
||||
}
|
||||
|
||||
export default new Storage();
|
9
app/templates/blank.js
Normal file
@ -0,0 +1,9 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`<div id="page-one"></div>`;
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
31
app/templates/completed.js
Normal file
@ -0,0 +1,31 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="download" class="fadeIn">
|
||||
<div id="download-progress">
|
||||
<div id="dl-title" class="title">${state.translate(
|
||||
'downloadFinish'
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(1)}
|
||||
<div class="upload">
|
||||
<div class="progress-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
||||
'sendYourFilesLink'
|
||||
)}</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('download');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
28
app/templates/download.js
Normal file
@ -0,0 +1,28 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state) {
|
||||
const transfer = state.transfer;
|
||||
const div = html`
|
||||
<div id="download-progress" class="fadeIn">
|
||||
<div id="dl-title" class="title">${state.translate(
|
||||
'downloadingPageProgress',
|
||||
{
|
||||
filename: state.fileInfo.name,
|
||||
size: bytes(state.fileInfo.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
};
|
12
app/templates/error.js
Normal file
@ -0,0 +1,12 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`
|
||||
<div id="upload-error">
|
||||
<div class="title">${state.translate('errorPageHeader')}</div>
|
||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="${assets.get(
|
||||
'illustration_error.svg'
|
||||
)}"/>
|
||||
</div>`;
|
||||
};
|
84
app/templates/file.js
Normal file
@ -0,0 +1,84 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
function timeLeft(milliseconds) {
|
||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const seconds = Math.floor(milliseconds / 1000 % 60);
|
||||
if (hours >= 1) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (hours === 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = function(file, state, emit) {
|
||||
const ttl = file.expiresAt - Date.now();
|
||||
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
||||
const row = html`
|
||||
<tr id="${file.id}">
|
||||
<td>${file.name}</td>
|
||||
<td>
|
||||
<img onclick=${copyClick} src="${assets.get(
|
||||
'copy-16.svg'
|
||||
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
|
||||
<span class="text-copied" hidden="true">${state.translate(
|
||||
'copiedUrl'
|
||||
)}</span>
|
||||
</td>
|
||||
<td>${remaining}</td>
|
||||
<td>
|
||||
<img onclick=${showPopup} src="${assets.get(
|
||||
'close-16.svg'
|
||||
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
||||
<div class="popup">
|
||||
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
||||
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
||||
<div class="popup-action">
|
||||
<span class="popup-no" onclick=${cancel}>${state.translate(
|
||||
'deletePopupCancel'
|
||||
)}</span>
|
||||
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
|
||||
'deletePopupYes'
|
||||
)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
function copyClick(e) {
|
||||
emit('copy', { url: file.url, location: 'upload-list' });
|
||||
const icon = e.target;
|
||||
const text = e.target.nextSibling;
|
||||
icon.hidden = true;
|
||||
text.hidden = false;
|
||||
setTimeout(() => {
|
||||
icon.hidden = false;
|
||||
text.hidden = true;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function showPopup() {
|
||||
const tr = document.getElementById(file.id);
|
||||
const popup = tr.querySelector('.popuptext');
|
||||
popup.classList.add('show');
|
||||
popup.focus();
|
||||
}
|
||||
|
||||
function cancel(e) {
|
||||
e.stopPropagation();
|
||||
const tr = document.getElementById(file.id);
|
||||
const popup = tr.querySelector('.popuptext');
|
||||
popup.classList.remove('show');
|
||||
}
|
||||
|
||||
function deleteFile() {
|
||||
emit('delete', { file, location: 'upload-list' });
|
||||
emit('render');
|
||||
}
|
||||
|
||||
return row;
|
||||
};
|
28
app/templates/fileList.js
Normal file
@ -0,0 +1,28 @@
|
||||
const html = require('choo/html');
|
||||
const file = require('./file');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
let table = '';
|
||||
if (state.storage.files.length) {
|
||||
table = html`
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
||||
<th id="copy-file-list">${state.translate('copyFileList')}</th>
|
||||
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
|
||||
<th id="delete-file-list">${state.translate('deleteFileList')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.storage.files.map(f => file(f, state, emit))}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div id="file-list">
|
||||
${table}
|
||||
</div>
|
||||
`;
|
||||
};
|
38
app/templates/legal.js
Normal file
@ -0,0 +1,38 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
function replaceLinks(str, urls) {
|
||||
let i = -1;
|
||||
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
||||
i++;
|
||||
return `<a href="${urls[i]}">${v}</a>`;
|
||||
});
|
||||
return [`<div class="description">${s}</div>`];
|
||||
}
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="legal">
|
||||
<div class="title">${state.translate('legalHeader')}</div>
|
||||
${html(
|
||||
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||
'https://testpilot.firefox.com/terms',
|
||||
'https://testpilot.firefox.com/privacy',
|
||||
'https://testpilot.firefox.com/experiments/send'
|
||||
])
|
||||
)}
|
||||
${html(
|
||||
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||
'https://www.mozilla.org/privacy/websites/',
|
||||
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||
])
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
27
app/templates/notFound.js
Normal file
@ -0,0 +1,27 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div class="title">${state.translate('expiredPageHeader')}</div>
|
||||
<div class="share-window">
|
||||
<img src="${assets.get(
|
||||
'illustration_expired.svg'
|
||||
)}" id="expired-img" data-l10n-id="linkExpiredAlt"/>
|
||||
</div>
|
||||
<div class="expired-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
<a class="send-new" href="/" data-state="notfound">${state.translate(
|
||||
'sendYourFilesLink'
|
||||
)}</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
65
app/templates/preview.js
Normal file
@ -0,0 +1,65 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
function getFileFromDOM() {
|
||||
const el = document.getElementById('dl-file');
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
const data = el.dataset;
|
||||
return {
|
||||
name: data.name,
|
||||
size: parseInt(data.size, 10),
|
||||
ttl: parseInt(data.ttl, 10)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
state.fileInfo = state.fileInfo || getFileFromDOM();
|
||||
if (!state.fileInfo) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
state.fileInfo.id = state.params.id;
|
||||
state.fileInfo.key = state.params.key;
|
||||
const fileInfo = state.fileInfo;
|
||||
const size = bytes(fileInfo.size);
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div id="download-page-one">
|
||||
<div class="title">
|
||||
<span id="dl-file"
|
||||
data-name="${fileInfo.name}"
|
||||
data-size="${fileInfo.size}"
|
||||
data-ttl="${fileInfo.ttl}">${state.translate('downloadFileName', {
|
||||
filename: fileInfo.name
|
||||
})}</span>
|
||||
<span id="dl-filesize">${' ' +
|
||||
state.translate('downloadFileSize', { size })}</span>
|
||||
</div>
|
||||
<div class="description">${state.translate('downloadMessage')}</div>
|
||||
<img src="${assets.get(
|
||||
'illustration_download.svg'
|
||||
)}" id="download-img" alt="${state.translate('downloadAltText')}"/>
|
||||
<div>
|
||||
<button id="download-btn" class="btn" onclick=${download}>${state.translate(
|
||||
'downloadButtonLabel'
|
||||
)}</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
emit('download', fileInfo);
|
||||
}
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
21
app/templates/progress.js
Normal file
@ -0,0 +1,21 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
const radius = 73;
|
||||
const oRadius = radius + 10;
|
||||
const oDiameter = oRadius * 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
module.exports = function(progressRatio) {
|
||||
const dashOffset = (1 - progressRatio) * circumference;
|
||||
const percent = Math.floor(progressRatio * 100);
|
||||
const div = html`
|
||||
<div class="progress-bar">
|
||||
<svg id="progress" width="${oDiameter}" height="${oDiameter}" viewPort="0 0 ${oDiameter} ${oDiameter}" version="1.1">
|
||||
<circle r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent"/>
|
||||
<circle id="bar" r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent" transform="rotate(-90 ${oRadius} ${oRadius})" stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"/>
|
||||
<text class="percentage" text-anchor="middle" x="50%" y="98"><tspan class="percent-number">${percent}</tspan><tspan class="percent-sign">%</tspan></text>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
61
app/templates/share.js
Normal file
@ -0,0 +1,61 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
if (!file) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
const div = html`
|
||||
<div id="share-link" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text">${state.translate('copyUrlFormLabelWithName', {
|
||||
filename: file.name
|
||||
})}</div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||
<button id="copy-btn" class="btn" onclick=${copyLink}>${state.translate(
|
||||
'copyUrlFormButton'
|
||||
)}</button>
|
||||
</div>
|
||||
<button id="delete-file" class="btn" onclick=${deleteFile}>${state.translate(
|
||||
'deleteFileButton'
|
||||
)}</button>
|
||||
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
||||
'sendAnotherFileLink'
|
||||
)}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
if (allowedCopy()) {
|
||||
emit('copy', { url: file.url, location: 'success-screen' });
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
copyBtn.disabled = true;
|
||||
copyBtn.replaceChild(
|
||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||
copyBtn.firstChild
|
||||
);
|
||||
await delay(2000);
|
||||
copyBtn.disabled = false;
|
||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
emit('delete', { file, location: 'success-screen' });
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
return div;
|
||||
};
|
50
app/templates/unsupported.js
Normal file
@ -0,0 +1,50 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const msg =
|
||||
state.params.reason === 'outdated'
|
||||
? html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate(
|
||||
'notSupportedOutdatedDetail'
|
||||
)}</div>
|
||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">${state.translate(
|
||||
'updateFirefox'
|
||||
)}</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`
|
||||
: html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
||||
'notSupportedLink'
|
||||
)}</a></div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">Firefox<br>
|
||||
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`;
|
||||
const div = html`<div id="page-one">${msg}</div>`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
38
app/templates/upload.js
Normal file
@ -0,0 +1,38 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const transfer = state.transfer;
|
||||
|
||||
const div = html`
|
||||
<div id="upload-progress" class="fadeIn">
|
||||
<div class="title" id="upload-filename">${state.translate(
|
||||
'uploadingPageProgress',
|
||||
{
|
||||
filename: transfer.file.name,
|
||||
size: bytes(transfer.file.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
<button id="cancel-upload" onclick=${cancel}>${state.translate(
|
||||
'uploadingPageCancel'
|
||||
)}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function cancel() {
|
||||
const btn = document.getElementById('cancel-upload');
|
||||
btn.disabled = true;
|
||||
btn.textContent = state.translate('uploadCancelNotification');
|
||||
emit('cancel');
|
||||
}
|
||||
return div;
|
||||
};
|
55
app/templates/welcome.js
Normal file
@ -0,0 +1,55 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const fileList = require('./fileList');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="page-one" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||
<div class="description">
|
||||
<div>${state.translate('uploadPageExplainer')}</div>
|
||||
<a href="https://testpilot.firefox.com/experiments/send" class="link">${state.translate(
|
||||
'uploadPageLearnMore'
|
||||
)}</a>
|
||||
</div>
|
||||
<div class="upload-window" ondragover=${dragover} ondragleave=${dragleave}>
|
||||
<div id="upload-img"><img src="${assets.get(
|
||||
'upload.svg'
|
||||
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
||||
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
||||
<span id="file-size-msg"><em>${state.translate(
|
||||
'uploadPageSizeMessage'
|
||||
)}</em></span>
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" id="browse" class="btn">${state.translate(
|
||||
'uploadPageBrowseButton1'
|
||||
)}</label>
|
||||
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
||||
</form>
|
||||
</div>
|
||||
${fileList(state, emit)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
function dragover(event) {
|
||||
event.target.classList.add('ondrag');
|
||||
}
|
||||
|
||||
function dragleave(event) {
|
||||
event.target.classList.remove('ondrag');
|
||||
}
|
||||
|
||||
async function upload(event) {
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const file = target.files[0];
|
||||
await fadeOut('page-one');
|
||||
emit('upload', { file, type: 'click' });
|
||||
}
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
@ -35,47 +35,39 @@ function notify(str) {
|
||||
*/
|
||||
}
|
||||
|
||||
function gcmCompliant() {
|
||||
function loadShim(polyfill) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shim = document.createElement('script');
|
||||
shim.src = polyfill;
|
||||
shim.addEventListener('load', () => resolve(true));
|
||||
shim.addEventListener('error', () => resolve(false));
|
||||
document.head.appendChild(shim);
|
||||
});
|
||||
}
|
||||
|
||||
async function canHasSend(polyfill) {
|
||||
try {
|
||||
return window.crypto.subtle
|
||||
.generateKey(
|
||||
const key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(key => {
|
||||
return window.crypto.subtle
|
||||
.encrypt(
|
||||
);
|
||||
|
||||
await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
)
|
||||
.then(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
return loadShim();
|
||||
});
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return loadShim();
|
||||
}
|
||||
function loadShim() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shim = document.createElement('script');
|
||||
shim.src = '/cryptofill.js';
|
||||
shim.addEventListener('load', resolve);
|
||||
shim.addEventListener('error', reject);
|
||||
document.head.appendChild(shim);
|
||||
});
|
||||
return loadShim(polyfill);
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +99,8 @@ function copyToClipboard(str) {
|
||||
const LOCALIZE_NUMBERS = !!(
|
||||
typeof Intl === 'object' &&
|
||||
Intl &&
|
||||
typeof Intl.NumberFormat === 'function'
|
||||
typeof Intl.NumberFormat === 'function' &&
|
||||
typeof navigator === 'object'
|
||||
);
|
||||
|
||||
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||
@ -134,9 +127,22 @@ function allowedCopy() {
|
||||
return support ? document.queryCommandSupported('copy') : false;
|
||||
}
|
||||
|
||||
function delay(delay = 100) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
function fadeOut(id) {
|
||||
const classes = document.getElementById(id).classList;
|
||||
classes.remove('fadeIn');
|
||||
classes.add('fadeOut');
|
||||
return delay(300);
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
|
||||
export {
|
||||
module.exports = {
|
||||
fadeOut,
|
||||
delay,
|
||||
allowedCopy,
|
||||
bytes,
|
||||
percent,
|
||||
@ -144,7 +150,7 @@ export {
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
notify,
|
||||
gcmCompliant,
|
||||
canHasSend,
|
||||
isFile,
|
||||
ONE_DAY_IN_MS
|
||||
};
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 287 B |
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@ -1,6 +1,6 @@
|
||||
/*** index.html ***/
|
||||
html {
|
||||
background: url('../../public/resources/send_bg.svg');
|
||||
background: url('./send_bg.svg');
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||
font-weight: 200;
|
||||
@ -89,7 +89,7 @@ body {
|
||||
|
||||
.feedback {
|
||||
background-color: #0297f8;
|
||||
background-image: url('../../public/resources/feedback.svg');
|
||||
background-image: url('./feedback.svg');
|
||||
background-position: 2px 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px;
|
||||
@ -154,6 +154,36 @@ a {
|
||||
|
||||
/** page-one **/
|
||||
|
||||
.fadeOut {
|
||||
opacity: 0;
|
||||
animation: fadeout 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
opacity: 1;
|
||||
animation: fadein 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 33px;
|
||||
line-height: 40px;
|
||||
@ -431,12 +461,8 @@ tbody {
|
||||
}
|
||||
|
||||
.percentage {
|
||||
position: absolute;
|
||||
letter-spacing: -0.78px;
|
||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||
top: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-45%);
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@ -449,7 +475,8 @@ tbody {
|
||||
|
||||
.percent-sign {
|
||||
font-size: 28.8px;
|
||||
color: rgb(104, 104, 104);
|
||||
stroke: none;
|
||||
fill: #686868;
|
||||
}
|
||||
|
||||
.upload {
|
||||
@ -471,10 +498,18 @@ tbody {
|
||||
|
||||
#cancel-upload {
|
||||
color: #d70022;
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#cancel-upload:disabled {
|
||||
text-decoration: none;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/** share-link **/
|
||||
#share-window {
|
||||
margin: 0 auto;
|
||||
@ -624,7 +659,7 @@ tbody {
|
||||
#update-firefox {
|
||||
margin-bottom: 181px;
|
||||
height: 80px;
|
||||
background: #12bc00;
|
||||
background: #98e02b;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
38
build/fluent_loader.js
Normal file
@ -0,0 +1,38 @@
|
||||
const { MessageContext } = require('fluent');
|
||||
|
||||
function toJSON(map) {
|
||||
return JSON.stringify(Array.from(map));
|
||||
}
|
||||
|
||||
module.exports = function(source) {
|
||||
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||
const result = localeExp.exec(this.resourcePath);
|
||||
const locale = result && result[1];
|
||||
// pre-parse the ftl
|
||||
const context = new MessageContext(locale);
|
||||
context.addMessages(source);
|
||||
if (!locale) {
|
||||
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||
}
|
||||
return `
|
||||
module.exports = \`
|
||||
if (typeof window === 'undefined') {
|
||||
var fluent = require('fluent');
|
||||
}
|
||||
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||
ctx._messages = new Map(${toJSON(context._messages)});
|
||||
function translate(id, data) {
|
||||
var msg = ctx.getMessage(id);
|
||||
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||
msg = msg.attrs.title || msg.attrs.alt
|
||||
}
|
||||
return ctx.format(msg, data);
|
||||
}
|
||||
if (typeof window === 'undefined') {
|
||||
module.exports = translate;
|
||||
}
|
||||
else {
|
||||
window.translate = translate;
|
||||
}
|
||||
\``;
|
||||
};
|
19
build/generate_asset_map.js
Normal file
@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(f) {
|
||||
return `"${f}": require('../assets/${f}')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
|
||||
const code = `module.exports = {
|
||||
"package.json": require('../package.json'),
|
||||
${files.map(kv).join(',\n')}
|
||||
};`;
|
||||
return {
|
||||
code,
|
||||
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
22
build/generate_l10n_map.js
Normal file
@ -0,0 +1,22 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(d) {
|
||||
return `"${d}": require('../public/locales/${d}/send.ftl')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||
const code = `
|
||||
module.exports = {
|
||||
translate: function (id, data) { return window.translate(id, data) },
|
||||
${dirs.map(kv).join(',\n')}
|
||||
};`;
|
||||
return {
|
||||
code,
|
||||
dependencies: dirs.map(d =>
|
||||
require.resolve(`../public/locales/${d}/send.ftl`)
|
||||
),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
11
build/package_json_loader.js
Normal file
@ -0,0 +1,11 @@
|
||||
const commit = require('git-rev-sync').short();
|
||||
|
||||
module.exports = function(source) {
|
||||
const pkg = JSON.parse(source);
|
||||
const version = {
|
||||
commit,
|
||||
source: pkg.homepage,
|
||||
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
||||
};
|
||||
return `module.exports = '${JSON.stringify(version)}'`;
|
||||
};
|
32
common/assets.js
Normal file
@ -0,0 +1,32 @@
|
||||
const genmap = require('../build/generate_asset_map');
|
||||
const isServer = typeof genmap === 'function';
|
||||
const prefix = isServer ? '/' : '';
|
||||
let manifest = {};
|
||||
try {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
manifest = require('../dist/manifest.json');
|
||||
} catch (e) {
|
||||
// use middleware
|
||||
}
|
||||
|
||||
const assets = isServer ? manifest : genmap;
|
||||
|
||||
function getAsset(name) {
|
||||
return prefix + assets[name];
|
||||
}
|
||||
|
||||
const instance = {
|
||||
get: getAsset,
|
||||
setMiddleware: function(middleware) {
|
||||
if (middleware) {
|
||||
instance.get = function getAssetWithMiddleware(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl('/manifest.json')
|
||||
);
|
||||
return prefix + JSON.parse(f)[name];
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = instance;
|
51
common/locales.js
Normal file
@ -0,0 +1,51 @@
|
||||
const gen = require('../build/generate_l10n_map');
|
||||
|
||||
const isServer = typeof gen === 'function';
|
||||
const prefix = isServer ? '/' : '';
|
||||
let manifest = {};
|
||||
try {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
manifest = require('../dist/manifest.json');
|
||||
} catch (e) {
|
||||
// use middleware
|
||||
}
|
||||
|
||||
const locales = isServer ? manifest : gen;
|
||||
|
||||
function getLocale(name) {
|
||||
return prefix + locales[`public/locales/${name}/send.ftl`];
|
||||
}
|
||||
|
||||
function serverTranslator(name) {
|
||||
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
||||
}
|
||||
|
||||
function browserTranslator() {
|
||||
return locales.translate;
|
||||
}
|
||||
|
||||
const translator = isServer ? serverTranslator : browserTranslator;
|
||||
|
||||
const instance = {
|
||||
get: getLocale,
|
||||
getTranslator: translator,
|
||||
setMiddleware: function(middleware) {
|
||||
if (middleware) {
|
||||
const _eval = require('require-from-string');
|
||||
instance.get = function getLocaleWithMiddleware(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl('/manifest.json')
|
||||
);
|
||||
return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
|
||||
};
|
||||
instance.getTranslator = function(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl(instance.get(name))
|
||||
);
|
||||
return _eval(f.toString());
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = instance;
|
@ -1,20 +0,0 @@
|
||||
import Raven from 'raven-js';
|
||||
import { unsupported } from './metrics';
|
||||
|
||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (
|
||||
ua.indexOf('firefox') > -1 &&
|
||||
parseInt(ua.match(/firefox\/*([^\n\r]*)\./)[1], 10) <= 49
|
||||
) {
|
||||
unsupported({
|
||||
err: new Error('Firefox is outdated.')
|
||||
}).then(() => {
|
||||
location.replace('/unsupported/outdated');
|
||||
});
|
||||
}
|
||||
|
||||
export { Raven };
|
@ -1,119 +0,0 @@
|
||||
import { Raven } from './common';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { bytes, notify, gcmCompliant } from './utils';
|
||||
import Storage from './storage';
|
||||
import * as links from './links';
|
||||
import * as metrics from './metrics';
|
||||
import * as progress from './progress';
|
||||
|
||||
const storage = new Storage();
|
||||
function onUnload(size) {
|
||||
metrics.cancelledDownload({ size });
|
||||
}
|
||||
|
||||
async function download() {
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
const downloadPanel = document.getElementById('download-page-one');
|
||||
const progressPanel = document.getElementById('download-progress');
|
||||
const file = document.getElementById('dl-file');
|
||||
const size = Number(file.getAttribute('data-size'));
|
||||
const ttl = Number(file.getAttribute('data-ttl'));
|
||||
const unloadHandler = onUnload.bind(null, size);
|
||||
const startTime = Date.now();
|
||||
const fileReceiver = new FileReceiver(
|
||||
'/assets' + location.pathname.slice(0, -1),
|
||||
location.hash.slice(1)
|
||||
);
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
downloadPanel.hidden = true;
|
||||
progressPanel.hidden = false;
|
||||
metrics.startedDownload({ size, ttl });
|
||||
links.setOpenInNewTab(true);
|
||||
window.addEventListener('unload', unloadHandler);
|
||||
|
||||
fileReceiver.on('progress', data => {
|
||||
progress.setProgress({ complete: data[0], total: data[1] });
|
||||
});
|
||||
|
||||
let downloadEnd;
|
||||
fileReceiver.on('decrypting', () => {
|
||||
downloadEnd = Date.now();
|
||||
window.removeEventListener('unload', unloadHandler);
|
||||
fileReceiver.removeAllListeners('progress');
|
||||
document.l10n.formatValue('decryptingFile').then(progress.setText);
|
||||
});
|
||||
|
||||
try {
|
||||
const file = await fileReceiver.download();
|
||||
const endTime = Date.now();
|
||||
const time = endTime - startTime;
|
||||
const downloadTime = endTime - downloadEnd;
|
||||
const speed = size / (downloadTime / 1000);
|
||||
|
||||
links.setOpenInNewTab(false);
|
||||
storage.totalDownloads += 1;
|
||||
metrics.completedDownload({ size, time, speed });
|
||||
progress.setText(' ');
|
||||
document.l10n
|
||||
.formatValues('downloadNotification', 'downloadFinish')
|
||||
.then(translated => {
|
||||
notify(translated[0]);
|
||||
document.getElementById('dl-title').textContent = translated[1];
|
||||
document.querySelector('#download-progress .description').textContent =
|
||||
' ';
|
||||
});
|
||||
const dataView = new DataView(file.plaintext);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
if (window.navigator.msSaveBlob) {
|
||||
window.navigator.msSaveBlob(blob, file.name);
|
||||
return;
|
||||
}
|
||||
a.download = file.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
} catch (err) {
|
||||
metrics.stoppedDownload({ size, err });
|
||||
|
||||
if (err.message === 'notfound') {
|
||||
location.reload();
|
||||
} else {
|
||||
progressPanel.hidden = true;
|
||||
downloadPanel.hidden = true;
|
||||
document.getElementById('upload-error').hidden = false;
|
||||
}
|
||||
Raven.captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const file = document.getElementById('dl-file');
|
||||
const filename = file.getAttribute('data-filename');
|
||||
const b = Number(file.getAttribute('data-size'));
|
||||
const size = bytes(b);
|
||||
document.l10n.formatValue('downloadFileSize', { size }).then(str => {
|
||||
document.getElementById('dl-filesize').textContent = str;
|
||||
});
|
||||
document.l10n
|
||||
.formatValue('downloadingPageProgress', { filename, size })
|
||||
.then(str => {
|
||||
document.getElementById('dl-title').textContent = str;
|
||||
});
|
||||
|
||||
gcmCompliant()
|
||||
.then(() => {
|
||||
document
|
||||
.getElementById('download-btn')
|
||||
.addEventListener('click', download);
|
||||
})
|
||||
.catch(err => {
|
||||
metrics.unsupported({ err }).then(() => {
|
||||
location.replace('/unsupported/gcm');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,163 +0,0 @@
|
||||
import FileSender from './fileSender';
|
||||
import Storage from './storage';
|
||||
import * as metrics from './metrics';
|
||||
import { allowedCopy, copyToClipboard, ONE_DAY_IN_MS } from './utils';
|
||||
import bel from 'bel';
|
||||
import copyImg from '../../public/resources/copy-16.svg';
|
||||
import closeImg from '../../public/resources/close-16.svg';
|
||||
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const storage = new Storage();
|
||||
let fileList = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
fileList = document.getElementById('file-list');
|
||||
toggleHeader();
|
||||
Promise.all(
|
||||
storage.files.map(file => {
|
||||
const id = file.fileId;
|
||||
return checkExistence(id).then(exists => {
|
||||
if (exists) {
|
||||
addFile(storage.getFileById(id));
|
||||
} else {
|
||||
storage.remove(id);
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
.catch(err => console.error(err))
|
||||
.then(toggleHeader);
|
||||
});
|
||||
|
||||
function toggleHeader() {
|
||||
fileList.hidden = storage.files.length === 0;
|
||||
}
|
||||
|
||||
function timeLeft(milliseconds) {
|
||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const seconds = Math.floor(milliseconds / 1000 % 60);
|
||||
if (hours >= 1) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (hours === 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
return 'Expired';
|
||||
}
|
||||
|
||||
function addFile(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
file.creationDate = new Date(file.creationDate);
|
||||
const url = `${file.url}#${file.secretKey}`;
|
||||
const future = new Date();
|
||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||
const countdown = future.getTime() - Date.now();
|
||||
|
||||
const row = bel`
|
||||
<tr>
|
||||
<td>${file.name}</td>
|
||||
<td>
|
||||
<span class="icon-docs" data-l10n-id="copyUrlHover"></span>
|
||||
<img onclick=${copyClick} src="${copyImg}" class="icon-copy" data-l10n-id="copyUrlHover">
|
||||
<span data-l10n-id="copiedUrl" class="text-copied" hidden="true"></span>
|
||||
</td>
|
||||
<td>${timeLeft(countdown)}</td>
|
||||
<td>
|
||||
<span class="icon-cancel-1" data-l10n-id="deleteButtonHover" title="Delete"></span>
|
||||
<img onclick=${showPopup} src="${closeImg}" class="icon-delete" data-l10n-id="deleteButtonHover" title="Delete">
|
||||
<div class="popup">
|
||||
<div class="popuptext" onclick=${stopProp} onblur=${cancel} tabindex="-1">
|
||||
<div class="popup-message" data-l10n-id="deletePopupText"></div>
|
||||
<div class="popup-action">
|
||||
<span class="popup-no" onclick=${cancel} data-l10n-id="deletePopupCancel"></span>
|
||||
<span class="popup-yes" onclick=${deleteFile} data-l10n-id="deletePopupYes"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
const popup = row.querySelector('.popuptext');
|
||||
const timeCol = row.querySelectorAll('td')[2];
|
||||
if (!allowedCopy()) {
|
||||
row.querySelector('.icon-copy').disabled = true;
|
||||
}
|
||||
|
||||
fileList.querySelector('tbody').appendChild(row);
|
||||
toggleHeader();
|
||||
poll();
|
||||
|
||||
function copyClick(e) {
|
||||
metrics.copiedLink({ location: 'upload-list' });
|
||||
copyToClipboard(url);
|
||||
const icon = e.target;
|
||||
const text = e.target.nextSibling;
|
||||
icon.hidden = true;
|
||||
text.hidden = false;
|
||||
setTimeout(() => {
|
||||
icon.hidden = false;
|
||||
text.hidden = true;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function poll() {
|
||||
const countdown = future.getTime() - Date.now();
|
||||
if (countdown <= 0) {
|
||||
storage.remove(file.fileId);
|
||||
row.parentNode.removeChild(row);
|
||||
toggleHeader();
|
||||
}
|
||||
timeCol.textContent = timeLeft(countdown);
|
||||
setTimeout(poll, countdown >= HOUR ? 60000 : 1000);
|
||||
}
|
||||
|
||||
function deleteFile() {
|
||||
FileSender.delete(file.fileId, file.deleteToken);
|
||||
const ttl = ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||
metrics.deletedUpload({
|
||||
size: file.size,
|
||||
time: file.totalTime,
|
||||
speed: file.uploadSpeed,
|
||||
type: file.typeOfUpload,
|
||||
location: 'upload-list',
|
||||
ttl
|
||||
});
|
||||
row.parentNode.removeChild(row);
|
||||
storage.remove(file.fileId);
|
||||
toggleHeader();
|
||||
}
|
||||
|
||||
function showPopup() {
|
||||
popup.classList.add('show');
|
||||
popup.focus();
|
||||
}
|
||||
|
||||
function cancel(e) {
|
||||
e.stopPropagation();
|
||||
popup.classList.remove('show');
|
||||
}
|
||||
|
||||
function stopProp(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkExistence(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
||||
resolve(xhr.status === 200);
|
||||
}
|
||||
};
|
||||
xhr.onerror = reject;
|
||||
xhr.ontimeout = reject;
|
||||
xhr.open('get', '/exists/' + id);
|
||||
xhr.timeout = 2000;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
export { addFile };
|
@ -1,21 +0,0 @@
|
||||
let links = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
links = Array.from(document.querySelectorAll('a:not([target])'));
|
||||
});
|
||||
|
||||
function setOpenInNewTab(bool) {
|
||||
if (bool === false) {
|
||||
links.forEach(l => {
|
||||
l.removeAttribute('target');
|
||||
l.removeAttribute('rel');
|
||||
});
|
||||
} else {
|
||||
links.forEach(l => {
|
||||
l.setAttribute('target', '_blank');
|
||||
l.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { setOpenInNewTab };
|
@ -1,47 +0,0 @@
|
||||
import { bytes, percent } from './utils';
|
||||
|
||||
let percentText = null;
|
||||
let text = null;
|
||||
let title = null;
|
||||
let bar = null;
|
||||
let updateTitle = false;
|
||||
|
||||
const radius = 73;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
percentText = document.querySelector('.percent-number');
|
||||
text = document.querySelector('.progress-text');
|
||||
bar = document.getElementById('bar');
|
||||
title = document.querySelector('title');
|
||||
});
|
||||
|
||||
document.addEventListener('blur', function() {
|
||||
updateTitle = true;
|
||||
});
|
||||
|
||||
document.addEventListener('focus', function() {
|
||||
updateTitle = false;
|
||||
return title && (title.textContent = 'Firefox Send');
|
||||
});
|
||||
|
||||
function setProgress(params) {
|
||||
const ratio = params.complete / params.total;
|
||||
bar.setAttribute('stroke-dashoffset', (1 - ratio) * circumference);
|
||||
percentText.textContent = Math.floor(ratio * 100);
|
||||
if (updateTitle) {
|
||||
title.textContent = percent(ratio);
|
||||
}
|
||||
document.l10n
|
||||
.formatValue('fileSizeProgress', {
|
||||
partialSize: bytes(params.complete),
|
||||
totalSize: bytes(params.total)
|
||||
})
|
||||
.then(setText);
|
||||
}
|
||||
|
||||
function setText(str) {
|
||||
text.textContent = str;
|
||||
}
|
||||
|
||||
export { setProgress, setText };
|
@ -1,257 +0,0 @@
|
||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
||||
import { Raven } from './common';
|
||||
import FileSender from './fileSender';
|
||||
import {
|
||||
allowedCopy,
|
||||
bytes,
|
||||
copyToClipboard,
|
||||
notify,
|
||||
gcmCompliant,
|
||||
ONE_DAY_IN_MS
|
||||
} from './utils';
|
||||
import Storage from './storage';
|
||||
import * as metrics from './metrics';
|
||||
import * as progress from './progress';
|
||||
import * as fileList from './fileList';
|
||||
import checkImg from '../../public/resources/check-16.svg';
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
async function upload(event) {
|
||||
event.preventDefault();
|
||||
const pageOne = document.getElementById('page-one');
|
||||
const link = document.getElementById('link');
|
||||
const uploadWindow = document.querySelector('.upload-window');
|
||||
const uploadError = document.getElementById('upload-error');
|
||||
const uploadProgress = document.getElementById('upload-progress');
|
||||
const clickOrDrop = event.type === 'drop' ? 'drop' : 'click';
|
||||
|
||||
// don't allow upload if not on upload page
|
||||
if (pageOne.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
storage.totalUploads += 1;
|
||||
|
||||
let file = '';
|
||||
if (clickOrDrop === 'drop') {
|
||||
if (!event.dataTransfer.files[0]) {
|
||||
uploadWindow.classList.remove('ondrag');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.dataTransfer.files.length > 1 ||
|
||||
event.dataTransfer.files[0].size === 0
|
||||
) {
|
||||
uploadWindow.classList.remove('ondrag');
|
||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
||||
alert(str);
|
||||
});
|
||||
return;
|
||||
}
|
||||
file = event.dataTransfer.files[0];
|
||||
} else {
|
||||
file = event.target.files[0];
|
||||
}
|
||||
|
||||
if (file.size > MAXFILESIZE) {
|
||||
return document.l10n
|
||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||
.then(alert);
|
||||
}
|
||||
|
||||
pageOne.hidden = true;
|
||||
uploadError.hidden = true;
|
||||
uploadProgress.hidden = false;
|
||||
document.l10n
|
||||
.formatValue('uploadingPageProgress', {
|
||||
size: bytes(file.size),
|
||||
filename: file.name
|
||||
})
|
||||
.then(str => {
|
||||
document.getElementById('upload-filename').textContent = str;
|
||||
});
|
||||
document.l10n.formatValue('importingFile').then(progress.setText);
|
||||
//don't allow drag and drop when not on page-one
|
||||
document.body.removeEventListener('drop', upload);
|
||||
|
||||
const fileSender = new FileSender(file);
|
||||
document.getElementById('cancel-upload').addEventListener('click', () => {
|
||||
fileSender.cancel();
|
||||
metrics.cancelledUpload({
|
||||
size: file.size,
|
||||
type: clickOrDrop
|
||||
});
|
||||
location.reload();
|
||||
});
|
||||
|
||||
let uploadStart;
|
||||
fileSender.on('progress', data => {
|
||||
uploadStart = uploadStart || Date.now();
|
||||
progress.setProgress({
|
||||
complete: data[0],
|
||||
total: data[1]
|
||||
});
|
||||
});
|
||||
|
||||
fileSender.on('encrypting', () => {
|
||||
document.l10n.formatValue('encryptingFile').then(progress.setText);
|
||||
});
|
||||
|
||||
let t;
|
||||
const startTime = Date.now();
|
||||
metrics.startedUpload({
|
||||
size: file.size,
|
||||
type: clickOrDrop
|
||||
});
|
||||
// For large files we need to give the ui a tick to breathe and update
|
||||
// before we kick off the FileSender
|
||||
setTimeout(() => {
|
||||
fileSender
|
||||
.upload()
|
||||
.then(info => {
|
||||
const endTime = Date.now();
|
||||
const time = endTime - startTime;
|
||||
const uploadTime = endTime - uploadStart;
|
||||
const speed = file.size / (uploadTime / 1000);
|
||||
const expiration = EXPIRE_SECONDS * 1000;
|
||||
|
||||
link.setAttribute('value', `${info.url}#${info.secretKey}`);
|
||||
|
||||
const copyText = document.getElementById('copy-text');
|
||||
copyText.setAttribute(
|
||||
'data-l10n-args',
|
||||
JSON.stringify({ filename: file.name })
|
||||
);
|
||||
copyText.setAttribute('data-l10n-id', 'copyUrlFormLabelWithName');
|
||||
|
||||
metrics.completedUpload({
|
||||
size: file.size,
|
||||
time,
|
||||
speed,
|
||||
type: clickOrDrop
|
||||
});
|
||||
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken,
|
||||
creationDate: new Date(),
|
||||
expiry: expiration,
|
||||
totalTime: time,
|
||||
typeOfUpload: clickOrDrop,
|
||||
uploadSpeed: speed
|
||||
};
|
||||
|
||||
document.getElementById('delete-file').addEventListener('click', () => {
|
||||
FileSender.delete(fileData.fileId, fileData.deleteToken).then(() => {
|
||||
const ttl =
|
||||
ONE_DAY_IN_MS - (Date.now() - fileData.creationDate.getTime());
|
||||
metrics
|
||||
.deletedUpload({
|
||||
size: fileData.size,
|
||||
time: fileData.totalTime,
|
||||
speed: fileData.uploadSpeed,
|
||||
type: fileData.typeOfUpload,
|
||||
location: 'success-screen',
|
||||
ttl
|
||||
})
|
||||
.then(() => {
|
||||
storage.remove(fileData.fileId);
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
storage.addFile(info.fileId, fileData);
|
||||
|
||||
pageOne.hidden = true;
|
||||
uploadProgress.hidden = true;
|
||||
uploadError.hidden = true;
|
||||
document.getElementById('share-link').hidden = false;
|
||||
|
||||
fileList.addFile(fileData);
|
||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
||||
notify(str);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
// err is 0 when coming from a cancel upload event
|
||||
if (err === 0) {
|
||||
return;
|
||||
}
|
||||
// only show error page when the error is anything other than user cancelling the upload
|
||||
Raven.captureException(err);
|
||||
pageOne.hidden = true;
|
||||
uploadProgress.hidden = true;
|
||||
uploadError.hidden = false;
|
||||
window.clearTimeout(t);
|
||||
|
||||
metrics.stoppedUpload({
|
||||
size: file.size,
|
||||
type: clickOrDrop,
|
||||
err
|
||||
});
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
gcmCompliant()
|
||||
.then(function() {
|
||||
const pageOne = document.getElementById('page-one');
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
const link = document.getElementById('link');
|
||||
const uploadWindow = document.querySelector('.upload-window');
|
||||
|
||||
pageOne.hidden = false;
|
||||
document.getElementById('file-upload').addEventListener('change', upload);
|
||||
|
||||
document.body.addEventListener('dragover', allowDrop);
|
||||
document.body.addEventListener('drop', upload);
|
||||
|
||||
// reset copy button
|
||||
copyBtn.disabled = !allowedCopy();
|
||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
||||
|
||||
link.disabled = false;
|
||||
|
||||
// copy link to clipboard
|
||||
copyBtn.addEventListener('click', () => {
|
||||
if (allowedCopy() && copyToClipboard(link.getAttribute('value'))) {
|
||||
metrics.copiedLink({ location: 'success-screen' });
|
||||
|
||||
//disable button for 3s
|
||||
copyBtn.disabled = true;
|
||||
link.disabled = true;
|
||||
copyBtn.innerHTML = `<img src="${checkImg}" class="icon-check"></img>`;
|
||||
setTimeout(() => {
|
||||
copyBtn.disabled = !allowedCopy();
|
||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
||||
link.disabled = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
uploadWindow.addEventListener('dragover', () =>
|
||||
uploadWindow.classList.add('ondrag')
|
||||
);
|
||||
uploadWindow.addEventListener('dragleave', () =>
|
||||
uploadWindow.classList.remove('ondrag')
|
||||
);
|
||||
|
||||
// on file upload by browse or drag & drop
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
metrics.unsupported({ err }).then(() => {
|
||||
location.replace('/unsupported/gcm');
|
||||
});
|
||||
});
|
||||
});
|
4941
package-lock.json
generated
196
package.json
@ -3,73 +3,109 @@
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "1.1.2",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.98.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^3.0.0",
|
||||
"express": "^4.15.3",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"helmet": "^3.8.0",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.1.0",
|
||||
"redis": "^2.8.0"
|
||||
"repository": "mozilla/send",
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && webpack -p",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'assets/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"lint-locales": "node scripts/lint-locales",
|
||||
"lint-locales:dev": "npm run lint-locales",
|
||||
"lint-locales:prod": "npm run lint-locales -- --production",
|
||||
"format": "prettier '!(dist|node_modules|assets)/**/*.js' 'assets/*.css' --single-quote --write",
|
||||
"get-prod-locales": "node scripts/get-prod-locales",
|
||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
||||
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
||||
"release": "npm-run-all contributors changelog",
|
||||
"test": "mocha test/unit",
|
||||
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
||||
"prod": "node server/prod.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asmcrypto.js": "0.0.11",
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"bel": "^5.0.3",
|
||||
"browserify": "^14.4.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"cross-env": "^5.0.5",
|
||||
"css-loader": "^0.28.4",
|
||||
"css-mqpacker": "^6.0.1",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"extract-loader": "^1.0.0",
|
||||
"file-loader": "^0.11.2",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"github-changes": "^1.1.0",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"husky": "^0.14.3",
|
||||
"l20n": "^5.0.0",
|
||||
"lint-staged": "^4.0.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^3.4.2",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"postcss-cli": "^4.1.0",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"prettier": "^1.5.3",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.17.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^2.3.8",
|
||||
"stylelint": "^8.0.0",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"webcrypto-liner": "^0.1.25",
|
||||
"webpack": "^3.5.4",
|
||||
"webpack-dev-middleware": "^1.12.0"
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"prettier --single-quote --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
],
|
||||
"*.css": [
|
||||
"prettier --single-quote --write",
|
||||
"stylelint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.2.0"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"repository": "mozilla/send",
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-yo-yoify": "^0.7.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"choo-log": "^7.2.1",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"cross-env": "^5.0.5",
|
||||
"css-loader": "^0.28.5",
|
||||
"css-mqpacker": "^6.0.1",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.5.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"expose-loader": "^0.7.3",
|
||||
"extract-loader": "^1.0.1",
|
||||
"file-loader": "^0.11.2",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"github-changes": "^1.1.0",
|
||||
"html-loader": "^0.5.1",
|
||||
"husky": "^0.14.3",
|
||||
"lint-staged": "^4.0.4",
|
||||
"mocha": "^3.5.0",
|
||||
"nanobus": "^4.2.0",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"prettier": "^1.5.3",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.17.0",
|
||||
"require-from-string": "^1.2.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^3.2.1",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"val-loader": "^1.0.2",
|
||||
"webpack": "^3.5.5",
|
||||
"webpack-dev-server": "^2.7.1",
|
||||
"webpack-manifest-plugin": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.103.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"choo": "^6.0.0",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^4.0.0",
|
||||
"express": "^4.15.4",
|
||||
"express-request-language": "^1.1.12",
|
||||
"fluent": "^0.4.1",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"helmet": "^3.8.1",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.1.2",
|
||||
"redis": "^2.8.0"
|
||||
},
|
||||
"availableLanguages": [
|
||||
"en-US",
|
||||
"ast",
|
||||
"az",
|
||||
"ca",
|
||||
@ -78,7 +114,6 @@
|
||||
"de",
|
||||
"dsb",
|
||||
"el",
|
||||
"en-US",
|
||||
"es-AR",
|
||||
"es-CL",
|
||||
"es-ES",
|
||||
@ -108,42 +143,5 @@
|
||||
"vi",
|
||||
"zh-CN",
|
||||
"zh-TW"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm-run-all build:*",
|
||||
"build:js": "webpack -p",
|
||||
"build:version": "node scripts/version",
|
||||
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
||||
"dev": "npm run clean && npm run build && npm start",
|
||||
"format": "prettier '{,frontend/src/,scripts/,server/,test/**/!(bundle)}*.{js,css}' --single-quote --write",
|
||||
"get-prod-locales": "node scripts/get-prod-locales",
|
||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'frontend/src/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"lint-locales": "node scripts/lint-locales",
|
||||
"lint-locales:dev": "npm run lint-locales",
|
||||
"lint-locales:prod": "npm run lint-locales -- --production",
|
||||
"release": "npm-run-all contributors changelog",
|
||||
"start": "node server/server",
|
||||
"test": "cross-env NODE_ENV=test npm-run-all test:*",
|
||||
"test:unit": "mocha test/unit",
|
||||
"test:server": "mocha test/server",
|
||||
"test--browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d && node test/frontend/driver.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"prettier --single-quote --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
],
|
||||
"*.css": [
|
||||
"prettier --single-quote --write",
|
||||
"stylelint",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ const autoprefixer = require('autoprefixer');
|
||||
const cssnano = require('cssnano');
|
||||
const mqpacker = require('css-mqpacker');
|
||||
|
||||
const conf = require('./server/config');
|
||||
const config = require('./server/config');
|
||||
|
||||
const options = {
|
||||
plugins: [autoprefixer, mqpacker, cssnano]
|
||||
};
|
||||
|
||||
if (conf.env === 'development') {
|
||||
if (config.env === 'development') {
|
||||
options.map = { inline: true };
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@ -1,21 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pkg = require('../package.json');
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
let commit;
|
||||
|
||||
try {
|
||||
commit = require('git-rev-sync').short();
|
||||
} catch (err) {
|
||||
// Whatever...
|
||||
}
|
||||
|
||||
const filename = path.join(__dirname, '..', 'dist', 'public', 'version.json');
|
||||
const filedata = {
|
||||
commit,
|
||||
source: pkg.homepage,
|
||||
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
||||
};
|
||||
mkdirp.sync(path.dirname(filename));
|
||||
fs.writeFileSync(filename, JSON.stringify(filedata, null, 2) + '\n');
|
13
server/dev.js
Normal file
@ -0,0 +1,13 @@
|
||||
const assets = require('../common/assets');
|
||||
const locales = require('../common/locales');
|
||||
const routes = require('./routes');
|
||||
const pages = require('./routes/pages');
|
||||
|
||||
module.exports = function(app, devServer) {
|
||||
assets.setMiddleware(devServer.middleware);
|
||||
locales.setMiddleware(devServer.middleware);
|
||||
routes(app);
|
||||
// webpack-dev-server routes haven't been added yet
|
||||
// so wait for next tick to add 404 handler
|
||||
process.nextTick(() => app.use(pages.notfound));
|
||||
};
|
16
server/languages.js
Normal file
@ -0,0 +1,16 @@
|
||||
const { availableLanguages } = require('../package.json');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function allLangs() {
|
||||
const langs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||
langs.unshift('en-US'); // default first, TODO change for fluent-langneg
|
||||
return langs;
|
||||
}
|
||||
|
||||
if (config.l10n_dev) {
|
||||
module.exports = allLangs();
|
||||
} else {
|
||||
module.exports = availableLanguages;
|
||||
}
|
98
server/layout.js
Normal file
@ -0,0 +1,98 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../common/assets');
|
||||
const locales = require('../common/locales');
|
||||
|
||||
module.exports = function(state, body = '') {
|
||||
const firaTag = state.fira
|
||||
? html`<link rel="stylesheet" type="text/css" href="https://code.cdn.mozilla.net/fonts/fira.css" />`
|
||||
: '';
|
||||
return html`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta property="og:title" content="${state.title}"/>
|
||||
<meta name="twitter:title" content="${state.title}"/>
|
||||
<meta name="description" content="${state.description}"/>
|
||||
<meta property="og:description" content="${state.description}"/>
|
||||
<meta name="twitter:description" content="${state.description}"/>
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
<meta property="og:image" content="${state.baseUrl}${assets.get(
|
||||
'send-fb.jpg'
|
||||
)}"/>
|
||||
<meta name="twitter:image" content="${state.baseUrl}${assets.get(
|
||||
'send-twitter.jpg'
|
||||
)}"/>
|
||||
<meta property="og:url" content="${state.baseUrl}"/>
|
||||
|
||||
<title>${state.title}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="${assets.get('main.css')}" />
|
||||
<link rel="icon" type="image/png" href="${assets.get(
|
||||
'favicon-32x32.png'
|
||||
)}" sizes="32x32" />
|
||||
${firaTag}
|
||||
<script defer src="/jsconfig.js"></script>
|
||||
<script defer src="${assets.get('runtime.js')}"></script>
|
||||
<script defer src="${assets.get('vendor.js')}"></script>
|
||||
<script defer src="${locales.get(state.locale)}"></script>
|
||||
<script defer src="${assets.get('app.js')}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="send-logo">
|
||||
<a href="/">
|
||||
<img src="${assets.get(
|
||||
'send_logo.svg'
|
||||
)}" alt="Send"/><h1 class="site-title">Send</h1>
|
||||
</a>
|
||||
<div class="site-subtitle">
|
||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||
<div>${state.translate('siteSubtitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
|
||||
'siteFeedback'
|
||||
)}</a>
|
||||
</header>
|
||||
<div class="all">
|
||||
<noscript>
|
||||
<h2>Firefox Send requires JavaScript</h2>
|
||||
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
||||
<p>Please enable JavaScript and try again.</p>
|
||||
</noscript>
|
||||
${body}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="legal-links">
|
||||
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
|
||||
'mozilla-logo.svg'
|
||||
)}" alt="mozilla"/></a>
|
||||
<a href="https://www.mozilla.org/about/legal">${state.translate(
|
||||
'footerLinkLegal'
|
||||
)}</a>
|
||||
<a href="https://testpilot.firefox.com/about">${state.translate(
|
||||
'footerLinkAbout'
|
||||
)}</a>
|
||||
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
|
||||
<a href="/legal">${state.translate('footerLinkTerms')}</a>
|
||||
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
|
||||
'footerLinkCookies'
|
||||
)}</a>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
|
||||
'github-icon.svg'
|
||||
)}" alt="github"/></a>
|
||||
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
|
||||
'twitter-icon.svg'
|
||||
)}" alt="twitter"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
const conf = require('./config.js');
|
||||
const conf = require('./config');
|
||||
|
||||
const isProduction = conf.env === 'production';
|
||||
|
||||
|
26
server/prod.js
Normal file
@ -0,0 +1,26 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const Raven = require('raven');
|
||||
const config = require('./config');
|
||||
const routes = require('./routes');
|
||||
const pages = require('./routes/pages');
|
||||
|
||||
if (config.sentry_dsn) {
|
||||
Raven.config(config.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.static(path.resolve(__dirname, '../dist/'), {
|
||||
setHeaders: function(res) {
|
||||
res.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
routes(app);
|
||||
|
||||
app.use(pages.notfound);
|
||||
|
||||
app.listen(1443);
|
30
server/routes/delete.js
Normal file
@ -0,0 +1,30 @@
|
||||
const storage = require('../storage');
|
||||
|
||||
function validateID(route_id) {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
}
|
||||
|
||||
module.exports = async function(req, res) {
|
||||
const id = req.params.id;
|
||||
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const delete_token = req.body.delete_token;
|
||||
|
||||
if (!delete_token) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const err = await storage.delete(id, delete_token);
|
||||
if (!err) {
|
||||
res.sendStatus(200);
|
||||
}
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
};
|
38
server/routes/download.js
Normal file
@ -0,0 +1,38 @@
|
||||
const storage = require('../storage');
|
||||
const mozlog = require('../log');
|
||||
const log = mozlog('send.download');
|
||||
|
||||
function validateID(route_id) {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
}
|
||||
|
||||
module.exports = async function(req, res) {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
try {
|
||||
const meta = await storage.metadata(id);
|
||||
const contentLength = await storage.length(id);
|
||||
res.writeHead(200, {
|
||||
'Content-Disposition': `attachment; filename=${meta.filename}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': contentLength,
|
||||
'X-File-Metadata': JSON.stringify(meta)
|
||||
});
|
||||
const file_stream = storage.get(id);
|
||||
|
||||
file_stream.on('end', async () => {
|
||||
try {
|
||||
await storage.forceDelete(id);
|
||||
} catch (e) {
|
||||
log.info('DeleteError:', id);
|
||||
}
|
||||
});
|
||||
|
||||
file_stream.pipe(res);
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
};
|
19
server/routes/exists.js
Normal file
@ -0,0 +1,19 @@
|
||||
const storage = require('../storage');
|
||||
|
||||
function validateID(route_id) {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
}
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
try {
|
||||
await storage.exists(id);
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
};
|
80
server/routes/index.js
Normal file
@ -0,0 +1,80 @@
|
||||
const busboy = require('connect-busboy');
|
||||
const helmet = require('helmet');
|
||||
const bodyParser = require('body-parser');
|
||||
const requestLanguage = require('express-request-language');
|
||||
const languages = require('../languages');
|
||||
const storage = require('../storage');
|
||||
const config = require('../config');
|
||||
const pages = require('./pages');
|
||||
// const lang = require('fluent-langneg')
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(
|
||||
requestLanguage({
|
||||
languages
|
||||
})
|
||||
);
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
helmet.hsts({
|
||||
maxAge: 31536000,
|
||||
force: config.env === 'production'
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
'https://sentry.prod.mozaws.net',
|
||||
'https://www.google-analytics.com'
|
||||
],
|
||||
imgSrc: ["'self'", 'https://www.google-analytics.com'],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||
formAction: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
reportUri: '/__cspreport__'
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
busboy({
|
||||
limits: {
|
||||
fileSize: config.max_file_size
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(bodyParser.json());
|
||||
app.get('/', pages.index);
|
||||
app.get('/legal', pages.legal);
|
||||
app.get('/jsconfig.js', require('./jsconfig'));
|
||||
app.get('/share/:id', pages.blank);
|
||||
app.get('/download/:id', pages.download);
|
||||
app.get('/completed', pages.blank);
|
||||
app.get('/unsupported/:reason', pages.unsupported);
|
||||
app.post('/api/upload', require('./upload'));
|
||||
app.get('/api/download/:id', require('./download'));
|
||||
app.get('/api/exists/:id', require('./exists'));
|
||||
app.post('/api/delete/:id', require('./delete'));
|
||||
|
||||
app.get('/__version__', function(req, res) {
|
||||
res.sendFile(require.resolve('../../dist/version.json'));
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', function(req, res) {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('__heartbeat__', async (req, res) => {
|
||||
try {
|
||||
await storage.ping();
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
};
|
46
server/routes/jsconfig.js
Normal file
@ -0,0 +1,46 @@
|
||||
const config = require('../config');
|
||||
|
||||
let sentry = '';
|
||||
if (config.sentry_id) {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
const version = require('../../dist/version.json');
|
||||
sentry = `
|
||||
var RAVEN_CONFIG = {
|
||||
release: '${version.version}',
|
||||
tags: {
|
||||
commit: '${version.commit}'
|
||||
},
|
||||
dataCallback: function (data) {
|
||||
var hash = window.location.hash;
|
||||
if (hash) {
|
||||
return JSON.parse(JSON.stringify(data).replace(new RegExp(hash.slice(1), 'g'), ''));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
var SENTRY_ID = '${config.sentry_id}';
|
||||
`;
|
||||
}
|
||||
|
||||
let ga = '';
|
||||
if (config.analytics_id) {
|
||||
ga = `var GOOGLE_ANALYTICS_ID = ${config.analytics_id};`;
|
||||
}
|
||||
|
||||
/* eslint-disable no-useless-escape */
|
||||
const jsconfig = `
|
||||
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
|
||||
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
|
||||
if (isIE && !isUnsupportedPage) {
|
||||
window.location.replace('/unsupported/ie');
|
||||
}
|
||||
var MAXFILESIZE = ${config.max_file_size};
|
||||
var EXPIRE_SECONDS = ${config.expire_seconds};
|
||||
${ga}
|
||||
${sentry}
|
||||
`;
|
||||
|
||||
module.exports = function(req, res) {
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.send(jsconfig);
|
||||
};
|
68
server/routes/pages.js
Normal file
@ -0,0 +1,68 @@
|
||||
const routes = require('../../app/routes');
|
||||
const storage = require('../storage');
|
||||
const state = require('../state');
|
||||
|
||||
function validateID(route_id) {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
}
|
||||
|
||||
function stripEvents(str) {
|
||||
// For CSP we need to remove all the event handler placeholders.
|
||||
// It's ok, app.js will add them when it attaches to the DOM.
|
||||
return str.replace(/\son\w+=""/g, '');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
index: function(req, res) {
|
||||
res.send(stripEvents(routes.toString('/', state(req))));
|
||||
},
|
||||
|
||||
blank: function(req, res) {
|
||||
res.send(stripEvents(routes.toString('/blank', state(req))));
|
||||
},
|
||||
|
||||
download: async function(req, res, next) {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const efilename = await storage.filename(id);
|
||||
const name = decodeURIComponent(efilename);
|
||||
const size = await storage.length(id);
|
||||
const ttl = await storage.ttl(id);
|
||||
res.send(
|
||||
stripEvents(
|
||||
routes.toString(
|
||||
`/download/${req.params.id}`,
|
||||
Object.assign(state(req), {
|
||||
fileInfo: { name, size, ttl }
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
unsupported: function(req, res) {
|
||||
res.send(
|
||||
stripEvents(
|
||||
routes.toString(
|
||||
`/unsupported/${req.params.reason}`,
|
||||
Object.assign(state(req), { fira: true })
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
legal: function(req, res) {
|
||||
res.send(stripEvents(routes.toString('/legal', state(req))));
|
||||
},
|
||||
|
||||
notfound: function(req, res) {
|
||||
res.status(404).send(stripEvents(routes.toString('/404', state(req))));
|
||||
}
|
||||
};
|
65
server/routes/upload.js
Normal file
@ -0,0 +1,65 @@
|
||||
const crypto = require('crypto');
|
||||
const storage = require('../storage');
|
||||
const config = require('../config');
|
||||
const mozlog = require('../log');
|
||||
|
||||
const log = mozlog('send.upload');
|
||||
|
||||
const validateIV = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
||||
};
|
||||
|
||||
module.exports = function(req, res) {
|
||||
const newId = crypto.randomBytes(5).toString('hex');
|
||||
let meta;
|
||||
|
||||
try {
|
||||
meta = JSON.parse(req.header('X-File-Metadata'));
|
||||
} catch (e) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!meta.hasOwnProperty('id') ||
|
||||
!meta.hasOwnProperty('filename') ||
|
||||
!validateIV(meta.id)
|
||||
) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
meta.delete = crypto.randomBytes(10).toString('hex');
|
||||
req.pipe(req.busboy);
|
||||
|
||||
req.busboy.on(
|
||||
'file',
|
||||
async (fieldname, file, filename, encoding, mimeType) => {
|
||||
try {
|
||||
meta.mimeType = mimeType || 'application/octet-stream';
|
||||
await storage.set(newId, file, filename, meta);
|
||||
const protocol = config.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
res.json({
|
||||
url,
|
||||
delete: meta.delete,
|
||||
id: newId
|
||||
});
|
||||
} catch (e) {
|
||||
log.error('upload', e);
|
||||
if (e.message === 'limit') {
|
||||
return res.sendStatus(413);
|
||||
}
|
||||
res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
req.on('close', async err => {
|
||||
try {
|
||||
await storage.forceDelete(newId);
|
||||
} catch (e) {
|
||||
log.info('DeleteError:', newId);
|
||||
}
|
||||
});
|
||||
};
|
328
server/server.js
@ -1,328 +0,0 @@
|
||||
const express = require('express');
|
||||
const exphbs = require('express-handlebars');
|
||||
const busboy = require('connect-busboy');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const conf = require('./config.js');
|
||||
const storage = require('./storage.js');
|
||||
const Raven = require('raven');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const version = require('../dist/public/version.json');
|
||||
|
||||
if (conf.sentry_dsn) {
|
||||
Raven.config(conf.sentry_dsn).install();
|
||||
}
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
|
||||
const log = mozlog('send.server');
|
||||
|
||||
const STATIC_PATH = path.join(__dirname, '../dist/public');
|
||||
|
||||
const app = express();
|
||||
|
||||
function allLangs() {
|
||||
return fs
|
||||
.readdirSync(path.join(STATIC_PATH, 'locales'))
|
||||
.map(function(f) {
|
||||
return f.split('.')[0];
|
||||
})
|
||||
.join(',');
|
||||
}
|
||||
|
||||
function prodLangs() {
|
||||
return require('../package.json').availableLanguages.join(',');
|
||||
}
|
||||
|
||||
const availableLanguages = conf.l10n_dev ? allLangs() : prodLangs();
|
||||
|
||||
// dev middleware is broken at the moment because of how webpack builds the
|
||||
// handlebars templates. Leaving the commented code here as a mark of shame.
|
||||
|
||||
// if (conf.env === 'development') {
|
||||
// const webpack = require('webpack');
|
||||
// const webpackDevMiddleware = require('webpack-dev-middleware');
|
||||
// const config = require('../webpack.config.js');
|
||||
// config.devtool = 'inline-source-map';
|
||||
// const compiler = webpack(config);
|
||||
// const wdm = webpackDevMiddleware(compiler, {
|
||||
// publicPath: config.output.publicPath
|
||||
// });
|
||||
// app.use(wdm);
|
||||
// }
|
||||
app.set('views', 'dist/views/');
|
||||
app.engine(
|
||||
'handlebars',
|
||||
exphbs({
|
||||
defaultLayout: 'main',
|
||||
layoutsDir: 'dist/views/layouts',
|
||||
helpers: {
|
||||
availableLanguages,
|
||||
baseUrl: conf.base_url,
|
||||
title: 'Firefox Send',
|
||||
description:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.'
|
||||
}
|
||||
})
|
||||
);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
helmet.hsts({
|
||||
maxAge: 31536000,
|
||||
force: conf.env === 'production'
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
'https://sentry.prod.mozaws.net',
|
||||
'https://www.google-analytics.com'
|
||||
],
|
||||
imgSrc: ["'self'", 'https://www.google-analytics.com'],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||
formAction: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
reportUri: '/__cspreport__'
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
busboy({
|
||||
limits: {
|
||||
fileSize: conf.max_file_size
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(bodyParser.json());
|
||||
app.use(
|
||||
'/resources',
|
||||
express.static(path.join(STATIC_PATH, 'resources'), {
|
||||
setHeaders: function(res) {
|
||||
res.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(express.static(STATIC_PATH));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index');
|
||||
});
|
||||
|
||||
app.get('/unsupported/:reason', (req, res) => {
|
||||
const outdated = req.params.reason === 'outdated';
|
||||
res.render('unsupported', {
|
||||
outdated,
|
||||
fira: true
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/legal', (req, res) => {
|
||||
res.render('legal');
|
||||
});
|
||||
|
||||
app.get('/jsconfig.js', (req, res) => {
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.render('jsconfig', {
|
||||
googleAnalyticsId: conf.analytics_id,
|
||||
sentryId: conf.sentry_id,
|
||||
version: version.version,
|
||||
commit: version.commit,
|
||||
maxFileSize: conf.max_file_size,
|
||||
expireSeconds: conf.expire_seconds,
|
||||
layout: false
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/exists/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await storage.exists(id);
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/download/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.status(404).render('notfound');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const efilename = await storage.filename(id);
|
||||
const filename = decodeURIComponent(efilename);
|
||||
const filenameJson = JSON.stringify({ filename });
|
||||
const sizeInBytes = await storage.length(id);
|
||||
const ttl = await storage.ttl(id);
|
||||
res.render('download', {
|
||||
filename,
|
||||
filenameJson,
|
||||
sizeInBytes,
|
||||
ttl
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(404).render('notfound');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/assets/download/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const meta = await storage.metadata(id);
|
||||
const contentLength = await storage.length(id);
|
||||
res.writeHead(200, {
|
||||
'Content-Disposition': `attachment; filename=${meta.filename}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': contentLength,
|
||||
'X-File-Metadata': JSON.stringify(meta)
|
||||
});
|
||||
const file_stream = storage.get(id);
|
||||
|
||||
file_stream.on('end', async () => {
|
||||
try {
|
||||
await storage.forceDelete(id);
|
||||
} catch (e) {
|
||||
log.info('DeleteError:', id);
|
||||
}
|
||||
});
|
||||
|
||||
file_stream.pipe(res);
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/delete/:id', async (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
if (!validateID(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const delete_token = req.body.delete_token;
|
||||
|
||||
if (!delete_token) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const err = await storage.delete(id, delete_token);
|
||||
if (!err) {
|
||||
res.sendStatus(200);
|
||||
}
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/upload', (req, res, next) => {
|
||||
const newId = crypto.randomBytes(5).toString('hex');
|
||||
let meta;
|
||||
|
||||
try {
|
||||
meta = JSON.parse(req.header('X-File-Metadata'));
|
||||
} catch (e) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!meta.hasOwnProperty('id') ||
|
||||
!meta.hasOwnProperty('filename') ||
|
||||
!validateIV(meta.id)
|
||||
) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
meta.delete = crypto.randomBytes(10).toString('hex');
|
||||
req.pipe(req.busboy);
|
||||
|
||||
req.busboy.on(
|
||||
'file',
|
||||
async (fieldname, file, filename, encoding, mimeType) => {
|
||||
try {
|
||||
meta.mimeType = mimeType || 'application/octet-stream';
|
||||
await storage.set(newId, file, filename, meta);
|
||||
const protocol = conf.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
res.json({
|
||||
url,
|
||||
delete: meta.delete,
|
||||
id: newId
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message === 'limit') {
|
||||
return res.sendStatus(413);
|
||||
}
|
||||
res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
req.on('close', async err => {
|
||||
try {
|
||||
await storage.forceDelete(newId);
|
||||
} catch (e) {
|
||||
log.info('DeleteError:', newId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/__lbheartbeat__', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/__heartbeat__', async (req, res) => {
|
||||
try {
|
||||
await storage.ping();
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/__version__', (req, res) => {
|
||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
||||
});
|
||||
|
||||
const server = app.listen(conf.listen_port, () => {
|
||||
log.info('startServer:', `Send app listening on port ${conf.listen_port}!`);
|
||||
});
|
||||
|
||||
const validateID = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||
};
|
||||
|
||||
const validateIV = route_id => {
|
||||
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
server: server,
|
||||
storage: storage
|
||||
};
|
20
server/state.js
Normal file
@ -0,0 +1,20 @@
|
||||
const config = require('./config');
|
||||
const layout = require('./layout');
|
||||
const locales = require('../common/locales');
|
||||
|
||||
module.exports = function(req) {
|
||||
const locale = req.language || 'en-US';
|
||||
return {
|
||||
locale,
|
||||
translate: locales.getTranslator(locale),
|
||||
title: 'Firefox Send',
|
||||
description:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||
baseUrl: config.base_url,
|
||||
ui: {},
|
||||
storage: {
|
||||
files: []
|
||||
},
|
||||
layout
|
||||
};
|
||||
};
|
@ -1,17 +1,18 @@
|
||||
const AWS = require('aws-sdk');
|
||||
const s3 = new AWS.S3();
|
||||
|
||||
const conf = require('./config.js');
|
||||
const config = require('./config');
|
||||
const { tmpdir } = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const mozlog = require('./log.js');
|
||||
const mozlog = require('./log');
|
||||
|
||||
const log = mozlog('send.storage');
|
||||
|
||||
const redis = require('redis');
|
||||
const redis_client = redis.createClient({
|
||||
host: conf.redis_host,
|
||||
host: config.redis_host,
|
||||
connect_timeout: 10000
|
||||
});
|
||||
|
||||
@ -19,7 +20,9 @@ redis_client.on('error', err => {
|
||||
log.error('Redis:', err);
|
||||
});
|
||||
|
||||
if (conf.s3_bucket) {
|
||||
let tempDir = null;
|
||||
|
||||
if (config.s3_bucket) {
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
@ -36,6 +39,8 @@ if (conf.s3_bucket) {
|
||||
metadata
|
||||
};
|
||||
} else {
|
||||
tempDir = fs.mkdtempSync(`${tmpdir()}${path.sep}send-`);
|
||||
log.info('tempDir', tempDir);
|
||||
module.exports = {
|
||||
filename: filename,
|
||||
exists: exists,
|
||||
@ -113,7 +118,7 @@ function setField(id, key, value) {
|
||||
function localLength(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(fs.statSync(path.join(__dirname, '../static', id)).size);
|
||||
resolve(fs.statSync(path.join(tempDir, id)).size);
|
||||
} catch (err) {
|
||||
reject();
|
||||
}
|
||||
@ -121,12 +126,12 @@ function localLength(id) {
|
||||
}
|
||||
|
||||
function localGet(id) {
|
||||
return fs.createReadStream(path.join(__dirname, '../static', id));
|
||||
return fs.createReadStream(path.join(tempDir, id));
|
||||
}
|
||||
|
||||
function localSet(newId, file, filename, meta) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filepath = path.join(__dirname, '../static', newId);
|
||||
const filepath = path.join(tempDir, newId);
|
||||
const fstream = fs.createWriteStream(filepath);
|
||||
file.pipe(fstream);
|
||||
file.on('limit', () => {
|
||||
@ -135,7 +140,7 @@ function localSet(newId, file, filename, meta) {
|
||||
});
|
||||
fstream.on('finish', () => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
redis_client.expire(newId, config.expire_seconds);
|
||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||
resolve(meta.delete);
|
||||
});
|
||||
@ -156,7 +161,7 @@ function localDelete(id, delete_token) {
|
||||
} else {
|
||||
redis_client.del(id);
|
||||
log.info('Deleted:', id);
|
||||
resolve(fs.unlinkSync(path.join(__dirname, '../static', id)));
|
||||
resolve(fs.unlinkSync(path.join(tempDir, id)));
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -165,7 +170,7 @@ function localDelete(id, delete_token) {
|
||||
function localForceDelete(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
redis_client.del(id);
|
||||
resolve(fs.unlinkSync(path.join(__dirname, '../static', id)));
|
||||
resolve(fs.unlinkSync(path.join(tempDir, id)));
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,7 +184,7 @@ function localPing() {
|
||||
|
||||
function awsLength(id) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -195,7 +200,7 @@ function awsLength(id) {
|
||||
|
||||
function awsGet(id) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
@ -208,7 +213,7 @@ function awsGet(id) {
|
||||
|
||||
function awsSet(newId, file, filename, meta) {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: newId,
|
||||
Body: file
|
||||
};
|
||||
@ -221,7 +226,7 @@ function awsSet(newId, file, filename, meta) {
|
||||
return upload.promise().then(
|
||||
() => {
|
||||
redis_client.hmset(newId, meta);
|
||||
redis_client.expire(newId, conf.expire_seconds);
|
||||
redis_client.expire(newId, config.expire_seconds);
|
||||
},
|
||||
err => {
|
||||
if (hitLimit) {
|
||||
@ -240,7 +245,7 @@ function awsDelete(id, delete_token) {
|
||||
reject();
|
||||
} else {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
@ -256,7 +261,7 @@ function awsDelete(id, delete_token) {
|
||||
function awsForceDelete(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {
|
||||
Bucket: conf.s3_bucket,
|
||||
Bucket: config.s3_bucket,
|
||||
Key: id
|
||||
};
|
||||
|
||||
@ -269,6 +274,6 @@ function awsForceDelete(id) {
|
||||
|
||||
function awsPing() {
|
||||
return localPing().then(() =>
|
||||
s3.headBucket({ Bucket: conf.s3_bucket }).promise()
|
||||
s3.headBucket({ Bucket: config.s3_bucket }).promise()
|
||||
);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
This is where downloaded files are stored.
|
@ -12,11 +12,11 @@ window.Raven = {
|
||||
};
|
||||
|
||||
window.FakeFile = FakeFile;
|
||||
window.FileSender = require('../../frontend/src/fileSender');
|
||||
window.FileReceiver = require('../../frontend/src/fileReceiver');
|
||||
window.FileSender = require('../../app/fileSender');
|
||||
window.FileReceiver = require('../../app/fileReceiver');
|
||||
window.sinon = require('sinon');
|
||||
window.server = window.sinon.fakeServer.create();
|
||||
window.assert = require('assert');
|
||||
const utils = require('../../frontend/src/utils');
|
||||
const utils = require('../../app/utils');
|
||||
window.hexToArray = utils.hexToArray;
|
||||
window.arrayToHex = utils.arrayToHex;
|
||||
|
@ -46,11 +46,11 @@ const awsStub = {
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
redis: redisStub,
|
||||
fs: fsStub,
|
||||
'./log.js': function() {
|
||||
'./log': function() {
|
||||
return logStub;
|
||||
},
|
||||
'aws-sdk': awsStub,
|
||||
'./config.js': {
|
||||
'./config': {
|
||||
s3_bucket: 'test'
|
||||
}
|
||||
});
|
||||
|
@ -2,8 +2,6 @@ const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
|
||||
// const conf = require('../server/config.js');
|
||||
|
||||
const redisStub = {};
|
||||
const exists = sinon.stub();
|
||||
const hget = sinon.stub();
|
||||
@ -35,7 +33,7 @@ logStub.error = sinon.stub();
|
||||
const storage = proxyquire('../../server/storage', {
|
||||
redis: redisStub,
|
||||
fs: fsStub,
|
||||
'./log.js': function() {
|
||||
'./log': function() {
|
||||
return logStub;
|
||||
}
|
||||
});
|
||||
|
@ -1,28 +0,0 @@
|
||||
{{!-- This file should be es5 only --}}
|
||||
var isIE = /trident\/7\.|msie/i.test(navigator.userAgent);
|
||||
var isUnsupportedPage = /\/unsupported/.test(location.pathname);
|
||||
if (isIE && !isUnsupportedPage) {
|
||||
window.location.replace('/unsupported/ie');
|
||||
}
|
||||
|
||||
{{#if sentryId}}
|
||||
var RAVEN_CONFIG = {
|
||||
release: '{{{version}}}',
|
||||
tags: {
|
||||
commit: '{{{commit}}}'
|
||||
},
|
||||
dataCallback: function (data) {
|
||||
var hash = window.location.hash;
|
||||
if (hash) {
|
||||
return JSON.parse(JSON.stringify(data).replace(new RegExp(hash.slice(1), 'g'), ''));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
var SENTRY_ID = '{{{sentryId}}}';
|
||||
{{/if}}
|
||||
{{#if googleAnalyticsId}}
|
||||
var GOOGLE_ANALYTICS_ID = '{{{googleAnalyticsId}}}';
|
||||
{{/if}}
|
||||
var MAXFILESIZE = {{{maxFileSize}}};
|
||||
var EXPIRE_SECONDS = {{{expireSeconds}}};
|
@ -1,17 +1,16 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlPlugin = require('html-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
vendor: ['babel-polyfill', 'raven-js'],
|
||||
upload: ['./frontend/src/upload.js'],
|
||||
download: ['./frontend/src/download.js']
|
||||
vendor: ['babel-polyfill', 'raven-js', 'fluent', 'choo'],
|
||||
app: ['./app/main.js']
|
||||
},
|
||||
output: {
|
||||
filename: 'resources/[name].[chunkhash].js',
|
||||
path: path.resolve(__dirname, 'dist/public'),
|
||||
filename: '[name].[chunkhash:8].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
@ -20,19 +19,48 @@ module.exports = {
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [
|
||||
path.resolve(__dirname, 'frontend'),
|
||||
path.resolve(__dirname, 'app'),
|
||||
path.resolve(__dirname, 'common'),
|
||||
path.resolve(__dirname, 'node_modules/testpilot-ga/src')
|
||||
],
|
||||
options: {
|
||||
babelrc: false,
|
||||
presets: [['es2015', { modules: false }], 'stage-2']
|
||||
presets: [['env', { modules: false }], 'stage-2'],
|
||||
plugins: ['yo-yoify']
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: [path.dirname(require.resolve('fluent'))],
|
||||
use: [
|
||||
{
|
||||
loader: 'expose-loader',
|
||||
options: 'fluent'
|
||||
},
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [['env', { modules: false }]]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: require.resolve('./assets/cryptofill'),
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[hash:8].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(svg|png|jpg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'resources/[name].[hash].[ext]'
|
||||
name: '[name].[hash:8].[ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -41,7 +69,7 @@ module.exports = {
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'resources/[name].[hash].[ext]'
|
||||
name: '[name].[hash:8].[ext]'
|
||||
}
|
||||
},
|
||||
'extract-loader',
|
||||
@ -50,75 +78,64 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.hbs$/,
|
||||
test: require.resolve('./package.json'),
|
||||
use: [
|
||||
{
|
||||
loader: 'html-loader',
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
interpolate: 'require',
|
||||
minimize: false
|
||||
}
|
||||
name: 'version.json'
|
||||
}
|
||||
},
|
||||
'extract-loader',
|
||||
'./build/package_json_loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.ftl$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[path][name].[hash:8].js'
|
||||
}
|
||||
},
|
||||
'extract-loader',
|
||||
'./build/fluent_loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: require.resolve('./build/generate_asset_map.js'),
|
||||
use: ['babel-loader', 'val-loader']
|
||||
},
|
||||
{
|
||||
test: require.resolve('./build/generate_l10n_map.js'),
|
||||
use: ['babel-loader', 'val-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin([
|
||||
{
|
||||
context: 'public',
|
||||
from: 'locales/**/*.ftl'
|
||||
},
|
||||
{
|
||||
context: 'public',
|
||||
from: '*.*'
|
||||
},
|
||||
{
|
||||
from: 'views/**',
|
||||
to: '../'
|
||||
},
|
||||
{
|
||||
context: 'node_modules/l20n/dist/web',
|
||||
from: 'l20n.min.js'
|
||||
}
|
||||
]),
|
||||
new HtmlPlugin({
|
||||
filename: '../views/index.handlebars',
|
||||
template: 'webpack/upload.hbs',
|
||||
chunks: ['upload']
|
||||
}),
|
||||
new HtmlPlugin({
|
||||
filename: '../views/download.handlebars',
|
||||
template: 'webpack/download.hbs',
|
||||
chunks: ['download']
|
||||
}),
|
||||
new HtmlPlugin({
|
||||
filename: '../views/legal.handlebars',
|
||||
template: 'webpack/legal.hbs',
|
||||
inject: false
|
||||
}),
|
||||
new HtmlPlugin({
|
||||
filename: '../views/notfound.handlebars',
|
||||
template: 'webpack/notfound.hbs',
|
||||
inject: false
|
||||
}),
|
||||
new HtmlPlugin({
|
||||
filename: '../views/layouts/main.handlebars',
|
||||
template: 'webpack/layout.hbs',
|
||||
inject: 'head',
|
||||
excludeChunks: ['upload', 'download']
|
||||
}),
|
||||
new HtmlPlugin({
|
||||
filename: '../views/unsupported.handlebars',
|
||||
template: 'webpack/unsupported.hbs',
|
||||
inject: false
|
||||
}),
|
||||
new webpack.IgnorePlugin(/dist/),
|
||||
new webpack.IgnorePlugin(/require-from-string/),
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor'
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'runtime'
|
||||
})
|
||||
]
|
||||
}),
|
||||
new ManifestPlugin()
|
||||
],
|
||||
devServer: {
|
||||
compress: true,
|
||||
setup:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? require('./server/dev')
|
||||
: undefined
|
||||
}
|
||||
};
|
||||
|
@ -1,43 +0,0 @@
|
||||
<div id="download">
|
||||
<div id="download-page-one">
|
||||
<div class="title">
|
||||
<span id="dl-file"
|
||||
data-filename="{{filename}}"
|
||||
data-size="{{sizeInBytes}}"
|
||||
data-ttl="{{ttl}}"
|
||||
data-l10n-id="downloadFileName"
|
||||
data-l10n-args='{{filenameJson}}'></span>
|
||||
<span id="dl-filesize"></span>
|
||||
</div>
|
||||
<div class="description" data-l10n-id="downloadMessage"></div>
|
||||
<img src="../public/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
|
||||
<div>
|
||||
<button id="download-btn" class="btn" data-l10n-id="downloadButtonLabel"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="download-progress" hidden="true">
|
||||
<div id="dl-title" class="title"></div>
|
||||
<div class="description" data-l10n-id="downloadingPageMessage"></div>
|
||||
<div class="progress-bar">
|
||||
<svg id="progress" width="166" height="166" viewPort="0 0 166 166" version="1.1">
|
||||
<circle r="73" cx="83" cy="83" fill="transparent"/>
|
||||
<circle id="bar" r="73" cx="83" cy="83" fill="transparent" transform="rotate(-90 83 83)" stroke-dasharray="458.67" stroke-dashoffset="458.67"/>
|
||||
</svg>
|
||||
<div class="percentage">
|
||||
<span class="percent-number"></span>
|
||||
<span class="percent-sign">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload">
|
||||
<div class="progress-text">{{filename}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-error" hidden="true">
|
||||
<div class="title" data-l10n-id="errorPageHeader"></div>
|
||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="../public/resources/illustration_error.svg"/>
|
||||
</div>
|
||||
|
||||
<a class="send-new" data-state="completed" data-l10n-id="sendYourFilesLink" href="/"></a>
|
||||
</div>
|
@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="{{availableLanguages}}">
|
||||
|
||||
<meta property="og:title" content="{{title}}"/>
|
||||
<meta name="twitter:title" content="{{title}}"/>
|
||||
<meta name="description" content="{{description}}"/>
|
||||
<meta property="og:description" content="{{description}}"/>
|
||||
<meta name="twitter:description" content="{{description}}"/>
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
<meta property="og:image" content="{{baseUrl}}${require('../public/resources/send-fb.jpg')}"/>
|
||||
<meta name="twitter:image" content="{{baseUrl}}${require('../public/resources/send-twitter.jpg')}"/>
|
||||
<meta property="og:url" content="{{baseUrl}}"/>
|
||||
|
||||
<title>{{title}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="${require('../frontend/src/main.css')}" />
|
||||
{{#if fira}}
|
||||
<link rel="stylesheet" type="text/css" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
||||
{{/if}}
|
||||
|
||||
<link rel="icon" type="image/png" href="${require('../public/resources/favicon-32x32.png')}" sizes="32x32" />
|
||||
<link rel="localization" href="/locales/{locale}/send.ftl">
|
||||
|
||||
<script src="/jsconfig.js"></script>
|
||||
<script defer src="/l20n.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="send-logo">
|
||||
<a href="/">
|
||||
<img src="../public/resources/send_logo.svg" alt="Send"/><h1 class="site-title">Send</h1>
|
||||
</a>
|
||||
<div class="site-subtitle">
|
||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||
<div data-l10n-id="siteSubtitle">web experiment</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank" data-l10n-id="siteFeedback">Feedback</a>
|
||||
</header>
|
||||
<div class="all">
|
||||
<noscript>
|
||||
<h2>Firefox Send requires JavaScript</h2>
|
||||
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
||||
<p>Please enable JavaScript and try again.</p>
|
||||
</noscript>
|
||||
{{{body}}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="legal-links">
|
||||
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="../public/resources/mozilla-logo.svg" alt="mozilla"/></a>
|
||||
<a href="https://www.mozilla.org/about/legal" data-l10n-id="footerLinkLegal">Legal</a>
|
||||
<a href="https://testpilot.firefox.com/about" data-l10n-id="footerLinkAbout">About Test Pilot</a>
|
||||
<a href="/legal" data-l10n-id="footerLinkPrivacy">Privacy</a>
|
||||
<a href="/legal" data-l10n-id="footerLinkTerms">Terms</a>
|
||||
<a href="https://www.mozilla.org/privacy/websites/#cookies" data-l10n-id="footerLinkCookies">Cookies</a>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="../public/resources/github-icon.svg" alt="github"/></a>
|
||||
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="../public/resources/twitter-icon.svg" alt="twitter"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
<div id="legal">
|
||||
<div class="title" data-l10n-id="legalHeader"></div>
|
||||
<div class="description" data-l10n-id="legalNoticeTestPilot">
|
||||
<a href="https://testpilot.firefox.com/terms"></a>
|
||||
<a href="https://testpilot.firefox.com/privacy"></a>
|
||||
<a href="https://testpilot.firefox.com/experiments/send"></a>
|
||||
</div>
|
||||
<div class="description" data-l10n-id="legalNoticeMozilla">
|
||||
<a href="https://www.mozilla.org/privacy/websites/"></a>
|
||||
<a href="https://www.mozilla.org/about/legal/terms/mozilla/"></a>
|
||||
</div>
|
||||
</div>
|
@ -1,8 +0,0 @@
|
||||
<div id="download">
|
||||
<div class="title" data-l10n-id="expiredPageHeader"></div>
|
||||
<div class="share-window">
|
||||
<img src="../public/resources/illustration_expired.svg" id="expired-img" data-l10n-id="linkExpiredAlt"/>
|
||||
</div>
|
||||
<div class="expired-description" data-l10n-id="uploadPageExplainer"></div>
|
||||
<a class="send-new" href="/" data-state="notfound" data-l10n-id="sendYourFilesLink"></a>
|
||||
</div>
|
@ -1,20 +0,0 @@
|
||||
<div id="unsupported-browser">
|
||||
<div class="title" data-l10n-id="notSupportedHeader">Your browser is not supported.</div>
|
||||
{{#if outdated}}
|
||||
<div class="description" data-l10n-id="notSupportedOutdatedDetail">Unfortunately this version of Firefox does not support the web technology that powers Firefox Send. You’ll need to update your browser.</div>
|
||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||
<img src="../public/resources/firefox_logo-only.svg" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text" data-l10n-id="updateFirefox">Update Firefox</div>
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="description" data-l10n-id="notSupportedDetail">Unfortunately this browser does not support the web technology that powers Firefox Send. You’ll need to try another browser. We recommend Firefox!</div>
|
||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported" data-l10n-id="notSupportedLink">Why is my browser not supported?</a></div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
||||
<img src="../public/resources/firefox_logo-only.svg" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">Firefox<br>
|
||||
<span data-l10n-id="downloadFirefoxButtonSub">Free Download</span>
|
||||
</div>
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="unsupported-description" data-l10n-id="uploadPageExplainer">Send files through a safe, private, and encrypted link that automatically expires to ensure your stuff does not remain online forever.</div>
|
||||
</div>
|
@ -1,75 +0,0 @@
|
||||
<div id="page-one" hidden>
|
||||
<div class="title" data-l10n-id="uploadPageHeader"></div>
|
||||
<div class="description">
|
||||
<div data-l10n-id="uploadPageExplainer"></div>
|
||||
<a href="https://testpilot.firefox.com/experiments/send" class="link" data-l10n-id="uploadPageLearnMore"></a>
|
||||
</div>
|
||||
<div class="upload-window" >
|
||||
<div id="upload-img"><img data-l10n-id="uploadSvgAlt" src="../public/resources/upload.svg"/></div>
|
||||
<div id="upload-text" data-l10n-id="uploadPageDropMessage"></div>
|
||||
<span id="file-size-msg"><em data-l10n-id="uploadPageSizeMessage"></em></span>
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" id="browse"
|
||||
data-l10n-id="uploadPageBrowseButton1" class="btn"></label>
|
||||
<input id="file-upload" type="file" name="fileUploaded" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="file-list">
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- htmllint attr-bans="false" -->
|
||||
<th id="uploaded-file" data-l10n-id="uploadedFile"></th>
|
||||
<th id="copy-file-list" data-l10n-id="copyFileList"></th>
|
||||
<th id="expiry-file-list" data-l10n-id="expiryFileList"></th>
|
||||
<th id="delete-file-list" data-l10n-id="deleteFileList"></th>
|
||||
<!-- htmllint tag-bans="$previous" -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-progress" hidden="true">
|
||||
<div class="title" id="upload-filename"></div>
|
||||
<div class="description"></div>
|
||||
<div class="progress-bar">
|
||||
<svg id="progress" width="166" height="166" viewPort="0 0 166 166" version="1.1">
|
||||
<circle r="73" cx="83" cy="83" fill="transparent"/>
|
||||
<circle id="bar" r="73" cx="83" cy="83" fill="transparent" transform="rotate(-90 83 83)" stroke-dasharray="458.67" stroke-dashoffset="458.67"/>
|
||||
</svg>
|
||||
<div class="percentage">
|
||||
<span class="percent-number">0</span>
|
||||
<span class="percent-sign">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload">
|
||||
<div class="progress-text"></div>
|
||||
<div id="cancel-upload"
|
||||
data-l10n-id="uploadingPageCancel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="share-link" hidden="true">
|
||||
<div class="title" data-l10n-id="uploadSuccessTimingHeader"></div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text"></div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="" readonly/>
|
||||
<button id="copy-btn" class="btn" data-l10n-id="copyUrlFormButton"></button>
|
||||
</div>
|
||||
<button id="delete-file" class="btn" data-l10n-id="deleteFileButton"></button>
|
||||
<a class="send-new" data-state="completed" data-l10n-id="sendAnotherFileLink" href="/"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-error" hidden="true">
|
||||
<div class="title" data-l10n-id="errorPageHeader"></div>
|
||||
<div class="expired-description" data-l10n-id="errorPageMessage"></div>
|
||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="../public/resources/illustration_error.svg"/>
|
||||
<a class="send-new" href="/" data-state="errored" data-l10n-id="sendAnotherFileLink"></a>
|
||||
</div>
|