diff --git a/.dockerignore b/.dockerignore index 7522f1ba..ac82f884 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ static test scripts docs +firefox diff --git a/.eslintignore b/.eslintignore index a5892099..a435bfcc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,3 @@ -public/bundle.js -public/webcrypto-shim.js +public test/frontend/bundle.js -firefox \ No newline at end of file +firefox diff --git a/.gitignore b/.gitignore index e1f30354..dc0910df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ .DS_Store node_modules -public/bundle.js +public/upload.js +public/download.js public/version.json +public/l20n.min.js static/* !static/info.txt test/frontend/bundle.js diff --git a/.stylelintrc b/.stylelintrc index 3c593e83..c0c673c7 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,6 +1,6 @@ extends: stylelint-config-standard rules: - color-hex-case: upper + color-hex-case: lower declaration-colon-newline-after: null selector-list-comma-newline-after: null diff --git a/circle.yml b/circle.yml index ed714bbd..e25fa992 100644 --- a/circle.yml +++ b/circle.yml @@ -16,7 +16,7 @@ deployment: latest: branch: master commands: - - npm run predocker + - npm run build - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS - docker build -t mozilla/send:latest . - docker push mozilla/send:latest @@ -24,7 +24,7 @@ deployment: tag: /.*/ owner: mozilla commands: - - npm run predocker + - npm run build - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS - docker build -t mozilla/send:$CIRCLE_TAG . - docker push mozilla/send:$CIRCLE_TAG diff --git a/docker-compose.yml b/docker-compose.yml index 8274bde1..f72bf161 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,6 @@ services: ports: - "1443:1443" environment: - - P2P_REDIS_HOST=redis + - REDIS_HOST=redis redis: image: redis:alpine diff --git a/docs/docker.md b/docs/docker.md index f45b770f..f94b23b3 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -3,12 +3,22 @@ | Name | Description |------------------|-------------| | `PORT` | Port the server will listen on (defaults to 1443). -| `P2P_S3_BUCKET` | The S3 bucket name. -| `P2P_REDIS_HOST` | Host name of the Redis server. +| `S3_BUCKET` | The S3 bucket name. +| `REDIS_HOST` | Host name of the Redis server. +| `GOOGLE_ANALYTICS_ID` | Google Analytics ID +| `SENTRY_CLIENT` | Sentry Client ID +| `SENTRY_DSN` | Sentry DSN +| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648) | `NODE_ENV` | "production" ## Example: ```sh -$ docker run --net=host -e 'NODE_ENV=production' -e 'P2P_S3_BUCKET=testpilot-p2p-dev' -e 'P2P_REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' mozilla/send:latest +$ docker run --net=host -e 'NODE_ENV=production' \ + -e 'S3_BUCKET=testpilot-p2p-dev' \ + -e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \ + -e 'GOOGLE_ANALYTICS_ID=UA-35433268-78' \ + -e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \ + -e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \ + mozilla/send:latest ``` diff --git a/docs/metrics.md b/docs/metrics.md index 035a57fa..54d27deb 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -112,6 +112,7 @@ Fired whenever a user deletes a file they’ve uploaded. - `cm6` - `cm7` - `cd1` +- `cd4` #### `copied` Fired whenever a user copies the URL of an upload file. diff --git a/frontend/src/common.js b/frontend/src/common.js new file mode 100644 index 00000000..5fa712a3 --- /dev/null +++ b/frontend/src/common.js @@ -0,0 +1,10 @@ +window.Raven = require('raven-js'); +window.Raven.config(window.dsn).install(); +window.dsn = undefined; + +const testPilotGA = require('testpilot-ga'); +window.analytics = new testPilotGA({ + an: 'Firefox Send', + ds: 'web', + tid: window.trackerId +}); diff --git a/frontend/src/download.js b/frontend/src/download.js index a6d96449..b0e05fe9 100644 --- a/frontend/src/download.js +++ b/frontend/src/download.js @@ -1,90 +1,175 @@ +require('./common'); const FileReceiver = require('./fileReceiver'); -const { notify } = require('./utils'); +const { notify, findMetric, gcmCompliant, sendEvent } = require('./utils'); +const bytes = require('bytes'); +const Storage = require('./storage'); +const storage = new Storage(localStorage); + const $ = require('jquery'); require('jquery-circle-progress'); const Raven = window.Raven; + $(document).ready(function() { + gcmCompliant().catch(err => { + $('#download').attr('hidden', true); + sendEvent('recipient', 'unsupported', { + cd6: err + }).then(() => { + location.replace('/unsupported'); + }); + }); //link back to homepage $('.send-new').attr('href', window.location.origin); - const filename = $('#dl-filename').html(); + $('.send-new').click(function(target) { + target.preventDefault(); + sendEvent('recipient', 'restarted', { + cd2: 'completed' + }).then(() => { + location.href = target.currentTarget.href; + }); + }); + + $('.legal-links a, .social-links a, #dl-firefox').click(function(target) { + target.preventDefault(); + const metric = findMetric(target.currentTarget.href); + // record exited event by recipient + sendEvent('recipient', 'exited', { + cd3: metric + }).then(() => { + location.href = target.currentTarget.href; + }); + }); + + const filename = $('#dl-filename').text(); + const bytelength = Number($('#dl-bytelength').text()); + const timeToExpiry = Number($('#dl-ttl').text()); //initiate progress bar $('#dl-progress').circleProgress({ value: 0.0, startAngle: -Math.PI / 2, - fill: '#00C8D7', + fill: '#3B9DFF', size: 158, animation: { duration: 300 } }); $('#download-btn').click(download); function download() { + storage.totalDownloads += 1; + const fileReceiver = new FileReceiver(); + const unexpiredFiles = storage.numFiles; fileReceiver.on('progress', progress => { + window.onunload = function() { + storage.referrer = 'cancelled-download'; + // record download-stopped (cancelled by tab close or reload) + sendEvent('recipient', 'download-stopped', { + cm1: bytelength, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd2: 'cancelled' + }); + }; + $('#download-page-one').attr('hidden', true); $('#download-progress').removeAttr('hidden'); const percent = progress[0] / progress[1]; // update progress bar $('#dl-progress').circleProgress('value', percent); - $('.percent-number').html(`${Math.floor(percent * 100)}`); - if (progress[1] < 1000000) { - $('.progress-text').html( - `${filename} (${(progress[0] / 1000).toFixed(1)}KB of - ${(progress[1] / 1000).toFixed(1)}KB)` - ); - } else if (progress[1] < 1000000000) { - $('.progress-text').html( - `${filename} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000).toFixed(1)}MB)` - ); - } else { - $('.progress-text').html( - `${filename} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000000).toFixed(1)}GB)` - ); - } - //on complete - if (percent === 1) { - fileReceiver.removeAllListeners('progress'); - document.l10n.formatValues('downloadNotification', 'downloadFinish') - .then(translated => { - notify(translated[0]); - $('.title').html(translated[1]); - }); - } + $('.percent-number').text(`${Math.floor(percent * 100)}`); + $('.progress-text').text( + `${filename} (${bytes(progress[0], { + decimalPlaces: 1, + fixedDecimals: true + })} of ${bytes(progress[1], { decimalPlaces: 1 })})` + ); }); + let downloadEnd; fileReceiver.on('decrypting', isStillDecrypting => { // The file is being decrypted if (isStillDecrypting) { - console.log('Decrypting'); + fileReceiver.removeAllListeners('progress'); + window.onunload = null; + document.l10n.formatValue('decryptingFile').then(decryptingFile => { + $('.progress-text').text(decryptingFile); + }); } else { console.log('Done decrypting'); + downloadEnd = Date.now(); } }); fileReceiver.on('hashing', isStillHashing => { // The file is being hashed to make sure a malicious user hasn't tampered with it if (isStillHashing) { - console.log('Checking file integrity'); + document.l10n.formatValue('verifyingFile').then(verifyingFile => { + $('.progress-text').text(verifyingFile); + }); } else { - console.log('Integrity check done'); + $('.progress-text').text(' '); + document.l10n + .formatValues('downloadNotification', 'downloadFinish') + .then(translated => { + notify(translated[0]); + $('.title').text(translated[1]); + }); } }); + const startTime = Date.now(); + + // record download-started by recipient + sendEvent('recipient', 'download-started', { + cm1: bytelength, + cm4: timeToExpiry, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads + }); + fileReceiver .download() - .catch(() => { - document.l10n.formatValue('expiredPageHeader') - .then(translated => { - $('.title').text(translated); - }); + .catch(err => { + // record download-stopped (errored) by recipient + sendEvent('recipient', 'download-stopped', { + cm1: bytelength, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd2: 'errored', + cd6: err + }); + + document.l10n.formatValue('expiredPageHeader').then(translated => { + $('.title').text(translated); + }); $('#download-btn').attr('hidden', true); $('#expired-img').removeAttr('hidden'); console.log('The file has expired, or has already been deleted.'); return; }) .then(([decrypted, fname]) => { + const endTime = Date.now(); + const totalTime = endTime - startTime; + const downloadTime = endTime - downloadEnd; + const downloadSpeed = bytelength / (downloadTime / 1000); + + storage.referrer = 'completed-download'; + // record download-stopped (completed) by recipient + sendEvent('recipient', 'download-stopped', { + cm1: bytelength, + cm2: totalTime, + cm3: downloadSpeed, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd2: 'completed' + }); + const dataView = new DataView(decrypted); const blob = new Blob([dataView]); const downloadUrl = URL.createObjectURL(blob); diff --git a/frontend/src/fileReceiver.js b/frontend/src/fileReceiver.js index 155a8dd9..882dcb2b 100644 --- a/frontend/src/fileReceiver.js +++ b/frontend/src/fileReceiver.js @@ -58,41 +58,47 @@ class FileReceiver extends EventEmitter { true, ['encrypt', 'decrypt'] ) - ]).then(([fdata, key]) => { - this.emit('decrypting', true); - return Promise.all([ - window.crypto.subtle.decrypt( - { - name: 'AES-GCM', - iv: hexToArray(fdata.iv), - additionalData: hexToArray(fdata.aad) - }, - key, - fdata.data - ).then(decrypted => { - this.emit('decrypting', false); - return Promise.resolve(decrypted) - }), - fdata.filename, - hexToArray(fdata.aad) - ]); - }).then(([decrypted, fname, proposedHash]) => { - this.emit('hashing', true); - return window.crypto.subtle.digest('SHA-256', decrypted).then(calculatedHash => { - this.emit('hashing', false); - const integrity = new Uint8Array(calculatedHash).toString() === proposedHash.toString(); - if (!integrity) { - this.emit('unsafe', true) - return Promise.reject(); - } else { - this.emit('safe', true); - return Promise.all([ - decrypted, - decodeURIComponent(fname) - ]); - } + ]) + .then(([fdata, key]) => { + this.emit('decrypting', true); + return Promise.all([ + window.crypto.subtle + .decrypt( + { + name: 'AES-GCM', + iv: hexToArray(fdata.iv), + additionalData: hexToArray(fdata.aad), + tagLength: 128 + }, + key, + fdata.data + ) + .then(decrypted => { + this.emit('decrypting', false); + return Promise.resolve(decrypted); + }), + fdata.filename, + hexToArray(fdata.aad) + ]); }) - }) + .then(([decrypted, fname, proposedHash]) => { + this.emit('hashing', true); + return window.crypto.subtle + .digest('SHA-256', decrypted) + .then(calculatedHash => { + this.emit('hashing', false); + const integrity = + new Uint8Array(calculatedHash).toString() === + proposedHash.toString(); + if (!integrity) { + this.emit('unsafe', true); + return Promise.reject(); + } else { + this.emit('safe', true); + return Promise.all([decrypted, decodeURIComponent(fname)]); + } + }); + }); } } diff --git a/frontend/src/fileSender.js b/frontend/src/fileSender.js index a16b1849..97c76deb 100644 --- a/frontend/src/fileSender.js +++ b/frontend/src/fileSender.js @@ -118,14 +118,16 @@ class FileSender extends EventEmitter { xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { - // uuid field and url field - const responseObj = JSON.parse(xhr.responseText); - resolve({ - url: responseObj.url, - fileId: responseObj.id, - secretKey: keydata.k, - deleteToken: responseObj.delete - }); + if (xhr.status === 200) { + const responseObj = JSON.parse(xhr.responseText); + return resolve({ + url: responseObj.url, + fileId: responseObj.id, + secretKey: keydata.k, + deleteToken: responseObj.delete + }); + } + reject(xhr.status); } }; diff --git a/frontend/src/main.js b/frontend/src/main.js deleted file mode 100644 index 12c05a38..00000000 --- a/frontend/src/main.js +++ /dev/null @@ -1,5 +0,0 @@ -window.Raven = require('raven-js'); -window.Raven.config(window.dsn).install(); -window.dsn = undefined; -require('./upload'); -require('./download'); diff --git a/frontend/src/storage.js b/frontend/src/storage.js new file mode 100644 index 00000000..c95d93a6 --- /dev/null +++ b/frontend/src/storage.js @@ -0,0 +1,66 @@ +const { isFile } = require('./utils'); + +class Storage { + constructor(engine) { + this.engine = engine; + } + + get totalDownloads() { + return Number(this.engine.getItem('totalDownloads')); + } + set totalDownloads(n) { + this.engine.setItem('totalDownloads', n); + } + get totalUploads() { + return Number(this.engine.getItem('totalUploads')); + } + set totalUploads(n) { + this.engine.setItem('totalUploads', n); + } + get referrer() { + return this.engine.getItem('referrer'); + } + set referrer(str) { + this.engine.setItem('referrer', str); + } + + get files() { + const fs = []; + for (let i = 0; i < this.engine.length; i++) { + const k = this.engine.key(i); + if (isFile(k)) { + fs.push(JSON.parse(this.engine.getItem(k))); // parse or whatever else + } + } + return fs; + } + + get numFiles() { + let length = 0; + for (let i = 0; i < this.engine.length; i++) { + const k = this.engine.key(i); + if (isFile(k)) { + length += 1; + } + } + return length; + } + + getFileById(id) { + return this.engine.getItem(id); + } + + has(property) { + return this.engine.hasOwnProperty(property); + } + + remove(property) { + this.engine.removeItem(property); + } + + addFile(id, file) { + this.engine.setItem(id, JSON.stringify(file)); + } +} + +module.exports = Storage; diff --git a/frontend/src/upload.js b/frontend/src/upload.js index 82f6c55b..84279760 100644 --- a/frontend/src/upload.js +++ b/frontend/src/upload.js @@ -1,17 +1,74 @@ +/* global MAXFILESIZE EXPIRE_SECONDS */ +require('./common'); const FileSender = require('./fileSender'); -const { notify, gcmCompliant } = require('./utils'); +const { + notify, + gcmCompliant, + findMetric, + sendEvent, + ONE_DAY_IN_MS +} = require('./utils'); +const bytes = require('bytes'); +const Storage = require('./storage'); +const storage = new Storage(localStorage); + const $ = require('jquery'); require('jquery-circle-progress'); const Raven = window.Raven; +if (storage.has('referrer')) { + window.referrer = storage.referrer; + storage.remove('referrer'); +} else { + window.referrer = 'external'; +} + $(document).ready(function() { gcmCompliant().catch(err => { $('#page-one').attr('hidden', true); - $('#unsupported-browser').removeAttr('hidden'); + sendEvent('sender', 'unsupported', { + cd6: err + }).then(() => { + location.replace('/unsupported'); + }); }); $('#file-upload').change(onUpload); + + $('.legal-links a, .social-links a, #dl-firefox').click(function(target) { + target.preventDefault(); + const metric = findMetric(target.currentTarget.href); + // record exited event by recipient + sendEvent('sender', 'exited', { + cd3: metric + }).then(() => { + location.href = target.currentTarget.href; + }); + }); + + $('#send-new-completed').click(function(target) { + target.preventDefault(); + // record restarted event + sendEvent('sender', 'restarted', { + cd2: 'completed' + }).then(() => { + storage.referrer = 'completed-upload'; + location.href = target.currentTarget.href; + }); + }); + + $('#send-new-error').click(function(target) { + target.preventDefault(); + // record restarted event + sendEvent('sender', 'restarted', { + cd2: 'errored' + }).then(() => { + storage.referrer = 'errored-upload'; + location.href = target.currentTarget.href; + }); + }); + $('body').on('dragover', allowDrop).on('drop', onUpload); // reset copy button const $copyBtn = $('#copy-btn'); @@ -19,18 +76,23 @@ $(document).ready(function() { $('#link').attr('disabled', false); $copyBtn.attr('data-l10n-id', 'copyUrlFormButton'); - if (localStorage.length === 0) { + const files = storage.files; + if (files.length === 0) { toggleHeader(); } else { - for (let i = 0; i < localStorage.length; i++) { - const id = localStorage.key(i); - //check if file exists before adding to list - checkExistence(id, true); + for (const index in files) { + const id = files[index].fileId; + //check if file still exists before adding to list + checkExistence(id, files[index], true); } } // copy link to clipboard $copyBtn.click(() => { + // record copied event from success screen + sendEvent('sender', 'copied', { + cd4: 'success-screen' + }); const aux = document.createElement('input'); aux.setAttribute('value', $('#link').attr('value')); document.body.appendChild(aux); @@ -40,7 +102,9 @@ $(document).ready(function() { //disable button for 3s $copyBtn.attr('disabled', true); $('#link').attr('disabled', true); - $copyBtn.html(''); + $copyBtn.html( + '' + ); window.setTimeout(() => { $copyBtn.attr('disabled', false); $('#link').attr('disabled', false); @@ -69,14 +133,28 @@ $(document).ready(function() { // on file upload by browse or drag & drop function onUpload(event) { event.preventDefault(); + + // don't allow upload if not on upload page + if ($('#page-one').attr('hidden')){ + return; + } + + storage.totalUploads += 1; + let file = ''; if (event.type === 'drop') { - if (event.originalEvent.dataTransfer.files.length > 1 || event.originalEvent.dataTransfer.files[0].size === 0){ + if (!event.originalEvent.dataTransfer.files[0]) { $('.upload-window').removeClass('ondrag'); - document.l10n.formatValue('uploadPageMultipleFilesAlert') - .then(str => { - alert(str); - }); + return; + } + if ( + event.originalEvent.dataTransfer.files.length > 1 || + event.originalEvent.dataTransfer.files[0].size === 0 + ) { + $('.upload-window').removeClass('ondrag'); + document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => { + alert(str); + }); return; } file = event.originalEvent.dataTransfer.files[0]; @@ -84,21 +162,39 @@ $(document).ready(function() { file = event.target.files[0]; } + if (file.size > MAXFILESIZE) { + return document.l10n + .formatValue('fileTooBig', { size: bytes(MAXFILESIZE) }) + .then(alert); + } + $('#page-one').attr('hidden', true); $('#upload-error').attr('hidden', true); $('#upload-progress').removeAttr('hidden'); + document.l10n.formatValue('importingFile').then(importingFile => { + $('.progress-text').text(importingFile); + }); //don't allow drag and drop when not on page-one $('body').off('drop', onUpload); - const expiration = 24 * 60 * 60 * 1000; //will eventually come from a field const fileSender = new FileSender(file); $('#cancel-upload').click(() => { fileSender.cancel(); location.reload(); - document.l10n.formatValue('uploadCancelNotification') - .then(str => { - notify(str); - }); + document.l10n.formatValue('uploadCancelNotification').then(str => { + notify(str); + }); + storage.referrer = 'cancelled-upload'; + + // record upload-stopped (cancelled) by sender + sendEvent('sender', 'upload-stopped', { + cm1: file.size, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd1: event.type === 'drop' ? 'drop' : 'click', + cd2: 'cancelled' + }); }); fileSender.on('progress', progress => { @@ -106,101 +202,152 @@ $(document).ready(function() { // update progress bar $('#ul-progress').circleProgress('value', percent); $('#ul-progress').circleProgress().on('circle-animation-end', function() { - $('.percent-number').html(`${Math.floor(percent * 100)}`); + $('.percent-number').text(`${Math.floor(percent * 100)}`); }); - if (progress[1] < 1000000) { - $('.progress-text').text( - `${file.name} (${(progress[0] / 1000).toFixed(1)}KB of ${(progress[1] / 1000).toFixed(1)}KB)` - ); - } else if (progress[1] < 1000000000) { - $('.progress-text').text( - `${file.name} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000).toFixed(1)}MB)` - ); - } else { - $('.progress-text').text( - `${file.name} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000000).toFixed(1)}GB)` - ); - } - }); - - fileSender.on('loading', isStillLoading => { - // The file is loading into Firefox at this stage - if (isStillLoading) { - console.log('Processing'); - } else { - console.log('Finished processing'); - } + $('.progress-text').text( + `${file.name} (${bytes(progress[0], { + decimalPlaces: 1, + fixedDecimals: true + })} of ${bytes(progress[1], { decimalPlaces: 1 })})` + ); }); fileSender.on('hashing', isStillHashing => { // The file is being hashed if (isStillHashing) { - console.log('Hashing'); + document.l10n.formatValue('verifyingFile').then(verifyingFile => { + $('.progress-text').text(verifyingFile); + }); } else { console.log('Finished hashing'); } }); + let uploadStart; fileSender.on('encrypting', isStillEncrypting => { // The file is being encrypted if (isStillEncrypting) { - console.log('Encrypting'); + document.l10n.formatValue('encryptingFile').then(encryptingFile => { + $('.progress-text').text(encryptingFile); + }); } else { console.log('Finished encrypting'); + uploadStart = Date.now(); } }); - let t = ''; - fileSender - .upload() - .then(info => { - const fileData = { - name: file.name, - fileId: info.fileId, - url: info.url, - secretKey: info.secretKey, - deleteToken: info.deleteToken, - creationDate: new Date(), - expiry: expiration - }; - localStorage.setItem(info.fileId, JSON.stringify(fileData)); - $('#upload-filename').attr('data-l10n-id', 'uploadSuccessConfirmHeader'); - t = window.setTimeout(() => { + + let t; + const startTime = Date.now(); + const unexpiredFiles = storage.numFiles + 1; + + // record upload-started event by sender + sendEvent('sender', 'upload-started', { + cm1: file.size, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd1: event.type === 'drop' ? 'drop' : 'click', + cd5: window.referrer + }); + + // For large files we need to give the ui a tick to breathe and update + // before we kick off the FileSender + setTimeout(() => { + fileSender + .upload() + .then(info => { + const endTime = Date.now(); + const totalTime = endTime - startTime; + const uploadTime = endTime - uploadStart; + const uploadSpeed = file.size / (uploadTime / 1000); + const expiration = EXPIRE_SECONDS * 1000; + + // record upload-stopped (completed) by sender + sendEvent('sender', 'upload-stopped', { + cm1: file.size, + cm2: totalTime, + cm3: uploadSpeed, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd1: event.type === 'drop' ? 'drop' : 'click', + cd2: 'completed' + }); + + const fileData = { + name: file.name, + size: file.size, + fileId: info.fileId, + url: info.url, + secretKey: info.secretKey, + deleteToken: info.deleteToken, + creationDate: new Date(), + expiry: expiration, + totalTime: totalTime, + typeOfUpload: event.type === 'drop' ? 'drop' : 'click', + uploadSpeed: uploadSpeed + }; + + storage.addFile(info.fileId, fileData); + $('#upload-filename').attr( + 'data-l10n-id', + 'uploadSuccessConfirmHeader' + ); + t = window.setTimeout(() => { + $('#page-one').attr('hidden', true); + $('#upload-progress').attr('hidden', true); + $('#upload-error').attr('hidden', true); + $('#share-link').removeAttr('hidden'); + }, 1000); + + populateFileList(fileData); + document.l10n.formatValue('notifyUploadDone').then(str => { + notify(str); + }); + }) + .catch(err => { + // 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); $('#page-one').attr('hidden', true); $('#upload-progress').attr('hidden', true); - $('#upload-error').attr('hidden', true); - $('#share-link').removeAttr('hidden'); - }, 1000); + $('#upload-error').removeAttr('hidden'); + window.clearTimeout(t); - populateFileList(JSON.stringify(fileData)); - document.l10n.formatValue('notifyUploadDone') - .then(str => { - notify(str); - }); - }) - .catch(err => { - Raven.captureException(err); - console.log(err); - $('#page-one').attr('hidden', true); - $('#upload-progress').attr('hidden', true); - $('#upload-error').removeAttr('hidden'); - window.clearTimeout(t); - }); + // record upload-stopped (errored) by sender + sendEvent('sender', 'upload-stopped', { + cm1: file.size, + cm5: storage.totalUploads, + cm6: unexpiredFiles, + cm7: storage.totalDownloads, + cd1: event.type === 'drop' ? 'drop' : 'click', + cd2: 'errored', + cd6: err + }); + }); + }, 10); } function allowDrop(ev) { ev.preventDefault(); } - function checkExistence(id, populate) { + function checkExistence(id, file, populate) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { if (populate) { - populateFileList(localStorage.getItem(id)); + populateFileList(file); } } else if (xhr.status === 404) { - localStorage.removeItem(id); + storage.remove(id); + if (storage.numFiles === 0) { + toggleHeader(); + } } } }; @@ -208,21 +355,23 @@ $(document).ready(function() { xhr.send(); } - //update file table with current files in localStorage + //update file table with current files in storage function populateFileList(file) { - try { - file = JSON.parse(file); - } catch (e) { - return; - } - const row = document.createElement('tr'); const name = document.createElement('td'); const link = document.createElement('td'); - const $copyIcon = $('', { src: '/resources/copy-16.svg', class: 'icon-copy', 'data-l10n-id': 'copyUrlHover'}); + const $copyIcon = $('', { + src: '/resources/copy-16.svg', + class: 'icon-copy', + 'data-l10n-id': 'copyUrlHover' + }); const expiry = document.createElement('td'); const del = document.createElement('td'); - const $delIcon = $('', { src: '/resources/close-16.svg', class: 'icon-delete', 'data-l10n-id': 'deleteButtonHover' }); + const $delIcon = $('', { + src: '/resources/close-16.svg', + class: 'icon-delete', + 'data-l10n-id': 'deleteButtonHover' + }); const popupDiv = document.createElement('div'); const $popupText = $('