Merge pull request #1496 from dannycoates/dear-nice-things-too
Begin implementing a reporting mechanism
This commit is contained in:
commit
0a8663aa51
17
app/api.js
17
app/api.js
@ -127,10 +127,10 @@ export async function metadata(id, keychain) {
|
||||
return {
|
||||
size: meta.size,
|
||||
ttl: data.ttl,
|
||||
iv: meta.iv,
|
||||
name: meta.name,
|
||||
type: meta.type,
|
||||
manifest: meta.manifest
|
||||
manifest: meta.manifest,
|
||||
flagged: data.flagged
|
||||
};
|
||||
}
|
||||
throw new Error(result.response.status);
|
||||
@ -438,3 +438,16 @@ export async function getConstants() {
|
||||
|
||||
throw new Error(response.status);
|
||||
}
|
||||
|
||||
export async function reportLink(id, key, reason) {
|
||||
const response = await fetch(
|
||||
getApiUrl(`/api/report/${id}`),
|
||||
post({ key, reason })
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(response.status);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import FileSender from './fileSender';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { reportLink } from './api';
|
||||
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
|
||||
import * as metrics from './metrics';
|
||||
import { bytes, locale } from './utils';
|
||||
@ -306,6 +307,26 @@ export default function(state, emitter) {
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('report', async ({ reason }) => {
|
||||
try {
|
||||
const file = state.fileInfo;
|
||||
if (!file) {
|
||||
// TODO
|
||||
emitter.emit('pushState', '/error');
|
||||
return render();
|
||||
}
|
||||
await reportLink(file.id, file.secretKey, reason);
|
||||
render();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err.message === '404') {
|
||||
state.fileInfo = { reported: true };
|
||||
return render();
|
||||
}
|
||||
emitter.emit('pushState', '/error');
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
// poll for updates of the upload list
|
||||
if (!state.modal && state.route === '/') {
|
||||
|
@ -47,9 +47,9 @@ export default class FileReceiver extends Nanobus {
|
||||
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||
this.fileInfo.name = meta.name;
|
||||
this.fileInfo.type = meta.type;
|
||||
this.fileInfo.iv = meta.iv;
|
||||
this.fileInfo.size = +meta.size;
|
||||
this.fileInfo.manifest = meta.manifest;
|
||||
this.fileInfo.flagged = meta.flagged;
|
||||
this.state = 'ready';
|
||||
}
|
||||
|
||||
|
74
app/main.css
74
app/main.css
@ -55,6 +55,12 @@ body {
|
||||
@apply bg-blue-70;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@apply bg-grey-transparent;
|
||||
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply leading-normal;
|
||||
@apply select-none;
|
||||
@ -138,21 +144,6 @@ footer li:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.feedback-link {
|
||||
background-color: #000;
|
||||
background-image: url('../assets/feedback.svg');
|
||||
background-position: 0.125rem 0.25rem;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.125rem;
|
||||
color: #fff;
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
line-height: 0.75rem;
|
||||
padding: 0.375rem 0.375rem 0.375rem 1.25rem;
|
||||
text-indent: 0.125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.link-blue {
|
||||
@apply text-blue-60;
|
||||
}
|
||||
@ -175,6 +166,10 @@ footer li:hover {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.dl-bg {
|
||||
filter: grayscale(1) opacity(0.15);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@ -322,6 +317,10 @@ select {
|
||||
@apply bg-blue-50;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@apply bg-grey-80;
|
||||
}
|
||||
|
||||
.link-blue {
|
||||
@apply text-blue-40;
|
||||
}
|
||||
@ -392,48 +391,3 @@ select {
|
||||
.signin:hover:active {
|
||||
transform: scale(0.9375);
|
||||
}
|
||||
|
||||
/* begin signin button color experiment */
|
||||
|
||||
.white-blue {
|
||||
@apply border-blue-60;
|
||||
@apply border-2;
|
||||
@apply text-blue-60;
|
||||
}
|
||||
|
||||
.white-blue:hover,
|
||||
.white-blue:focus {
|
||||
@apply bg-blue-60;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.blue {
|
||||
@apply bg-blue-60;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.white-violet {
|
||||
@apply border-violet;
|
||||
@apply border-2;
|
||||
@apply text-violet;
|
||||
}
|
||||
|
||||
.white-violet:hover,
|
||||
.white-violet:focus {
|
||||
@apply bg-violet;
|
||||
@apply text-white;
|
||||
|
||||
background-image: var(--violet-gradient);
|
||||
}
|
||||
|
||||
.violet {
|
||||
@apply bg-violet;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.violet:hover,
|
||||
.violet:focus {
|
||||
background-image: var(--violet-gradient);
|
||||
}
|
||||
|
||||
/* end signin button color experiment */
|
||||
|
@ -14,6 +14,7 @@ module.exports = function(app = choo({ hash: true })) {
|
||||
emit('authenticate', state.query.code, state.query.state);
|
||||
});
|
||||
app.route('/login', body(require('./ui/home')));
|
||||
app.route('/report', body(require('./ui/report')));
|
||||
app.route('*', body(require('./ui/notFound')));
|
||||
return app;
|
||||
};
|
||||
|
@ -482,6 +482,11 @@ module.exports.empty = function(state, emit) {
|
||||
>
|
||||
${state.translate('addFilesButton')}
|
||||
</label>
|
||||
<p
|
||||
class="font-normal text-sm text-grey-50 dark:text-grey-40 my-6 mx-12 text-center max-w-sm leading-loose"
|
||||
>
|
||||
${state.translate('trustWarningMessage')}
|
||||
</p>
|
||||
${upsell}
|
||||
</send-upload-area>
|
||||
`;
|
||||
@ -517,13 +522,27 @@ module.exports.preview = function(state, emit) {
|
||||
`;
|
||||
return html`
|
||||
<send-archive
|
||||
class="flex flex-col max-h-full bg-white p-4 w-full md:w-128 dark:bg-grey-90"
|
||||
class="flex flex-col max-h-full bg-white w-full dark:bg-grey-90"
|
||||
>
|
||||
<div class="border rounded py-3 px-6 dark:border-grey-70">
|
||||
<div class="border rounded py-3 px-4 dark:border-grey-70">
|
||||
${archiveInfo(archive)} ${details}
|
||||
</div>
|
||||
<div class="checkbox inline-block mt-6 mx-auto">
|
||||
<input
|
||||
id="trust-download"
|
||||
type="checkbox"
|
||||
autocomplete="off"
|
||||
onchange="${toggleDownloadEnabled}"
|
||||
/>
|
||||
<label for="trust-download">
|
||||
${state.translate('downloadTrustCheckbox', {
|
||||
count: archive.manifest.files.length
|
||||
})}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
id="download-btn"
|
||||
disabled
|
||||
class="btn rounded-lg mt-4 w-full flex-shrink-0 focus:outline"
|
||||
title="${state.translate('downloadButtonLabel')}"
|
||||
onclick=${download}
|
||||
@ -533,6 +552,13 @@ module.exports.preview = function(state, emit) {
|
||||
</send-archive>
|
||||
`;
|
||||
|
||||
function toggleDownloadEnabled(event) {
|
||||
event.stopPropagation();
|
||||
const checked = event.target.checked;
|
||||
const btn = document.getElementById('download-btn');
|
||||
btn.disabled = !checked;
|
||||
}
|
||||
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
event.target.disabled = true;
|
||||
|
@ -10,11 +10,9 @@ module.exports = function(name, url) {
|
||||
<h1 class="text-3xl font-bold my-4">
|
||||
${state.translate('notifyUploadEncryptDone')}
|
||||
</h1>
|
||||
<p
|
||||
class="font-normal leading-normal text-grey-80 word-break-all dark:text-grey-40"
|
||||
>
|
||||
<p class="font-normal leading-normal text-grey-80 dark:text-grey-40">
|
||||
${state.translate('copyLinkDescription')} <br />
|
||||
${name}
|
||||
<span class="word-break-all">${name}</span>
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* global downloadMetadata */
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const archiveTile = require('./archiveTile');
|
||||
const modal = require('./modal');
|
||||
const noStreams = require('./noStreams');
|
||||
@ -31,22 +32,53 @@ function downloading(state, emit) {
|
||||
}
|
||||
|
||||
function preview(state, emit) {
|
||||
if (state.fileInfo.flagged) {
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-col w-full max-w-md h-full mx-auto items-center justify-center"
|
||||
>
|
||||
<h1 class="text-xl font-bold">${state.translate('downloadFlagged')}</h1>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (!state.capabilities.streamDownload && state.fileInfo.size > BIG_SIZE) {
|
||||
return noStreams(state, emit);
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-col w-full max-w-md h-full mx-auto items-center justify-center"
|
||||
class="w-full overflow-hidden md:flex md:flex-row items-stretch md:flex-1"
|
||||
>
|
||||
<h1 class="text-3xl font-bold mb-4">
|
||||
${state.translate('downloadTitle')}
|
||||
</h1>
|
||||
<p
|
||||
class="w-full text-grey-80 text-center leading-normal dark:text-grey-40"
|
||||
<div
|
||||
class="px-2 w-full md:px-0 flex-half md:flex md:flex-col mt-12 md:pr-8 pb-4"
|
||||
>
|
||||
${state.translate('downloadDescription')}
|
||||
</p>
|
||||
${archiveTile.preview(state, emit)}
|
||||
<h1 class="text-3xl font-bold mb-4 text-center md:text-left">
|
||||
${state.translate('downloadTitle')}
|
||||
</h1>
|
||||
<p
|
||||
class="text-grey-80 leading-normal dark:text-grey-40 mb-4 text-center md:text-left"
|
||||
>
|
||||
${state.translate('downloadDescription')}
|
||||
</p>
|
||||
<p
|
||||
class="text-grey-80 leading-normal dark:text-grey-40 font-semibold text-center md:mb-8 md:text-left"
|
||||
>
|
||||
${state.translate('downloadConfirmDescription')}
|
||||
</p>
|
||||
<img
|
||||
class="hidden md:block dl-bg w-full"
|
||||
src="${assets.get('intro.svg')}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex-half flex-half md:flex md:flex-col md:justify-center"
|
||||
>
|
||||
${archiveTile.preview(state, emit)}
|
||||
<a href="/report" class="link-blue mt-4 text-center block"
|
||||
>${state.translate('reportFile', {
|
||||
count: state.fileInfo.manifest.files.length
|
||||
})}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -83,7 +115,7 @@ module.exports = function(state, emit) {
|
||||
<main class="main">
|
||||
${state.modal && modal(state, emit)}
|
||||
<section
|
||||
class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big"
|
||||
class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big md:flex md:flex-col"
|
||||
>
|
||||
${content}
|
||||
</section>
|
||||
|
@ -10,7 +10,7 @@ module.exports = function(state) {
|
||||
<h1 class="text-center text-3xl font-bold my-2">
|
||||
${state.translate('downloadFinish')}
|
||||
</h1>
|
||||
<img src="${assets.get('completed.svg')}" class="my-12 h-48" />
|
||||
<img src="${assets.get('completed.svg')}" class="my-8 h-48" />
|
||||
<p class="text-grey-80 leading-normal dark:text-grey-40">
|
||||
${state.translate('trySendDescription')}
|
||||
</p>
|
||||
@ -19,6 +19,9 @@ module.exports = function(state) {
|
||||
>${state.translate('sendYourFilesLink')}</a
|
||||
>
|
||||
</p>
|
||||
<p class="">
|
||||
<a href="/report" class="link-blue">${state.translate('reportFile')}</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
58
app/ui/downloadDialog.js
Normal file
58
app/ui/downloadDialog.js
Normal file
@ -0,0 +1,58 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function() {
|
||||
return function(state, emit, close) {
|
||||
const archive = state.fileInfo;
|
||||
return html`
|
||||
<send-download-dialog
|
||||
class="flex flex-col w-full max-w-sm h-full mx-auto items-center justify-center"
|
||||
>
|
||||
<h1 class="text-3xl font-bold mb-4">
|
||||
${state.translate('downloadConfirmTitle')}
|
||||
</h1>
|
||||
<p
|
||||
class="w-full text-grey-80 text-center leading-normal dark:text-grey-40 mb-8"
|
||||
>
|
||||
${state.translate('downloadConfirmDescription')}
|
||||
</p>
|
||||
<div class="checkbox inline-block mr-3 mb-8">
|
||||
<input
|
||||
id="trust-download"
|
||||
type="checkbox"
|
||||
autocomplete="off"
|
||||
onchange="${toggleDownloadEnabled}"
|
||||
/>
|
||||
<label for="trust-download">
|
||||
${state.translate('downloadTrustCheckbox')}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
id="download-btn"
|
||||
disabled
|
||||
class="btn rounded-lg w-full flex-shrink-0"
|
||||
onclick="${download}"
|
||||
title="${state.translate('downloadButtonLabel')}"
|
||||
>
|
||||
${state.translate('downloadButtonLabel')}
|
||||
</button>
|
||||
<a href="/report" class="link-blue mt-8"
|
||||
>${state.translate('reportFile')}</a
|
||||
>
|
||||
</send-download-dialog>
|
||||
`;
|
||||
|
||||
function toggleDownloadEnabled(event) {
|
||||
event.stopPropagation();
|
||||
const checked = event.target.checked;
|
||||
const btn = document.getElementById('download-btn');
|
||||
btn.disabled = !checked;
|
||||
}
|
||||
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
close();
|
||||
event.target.disabled = true;
|
||||
emit('download', archive);
|
||||
}
|
||||
};
|
||||
};
|
@ -1,7 +1,5 @@
|
||||
const html = require('choo/html');
|
||||
const Component = require('choo/component');
|
||||
const version = require('../../package.json').version;
|
||||
const { browserName } = require('../utils');
|
||||
|
||||
class Footer extends Component {
|
||||
constructor(name, state) {
|
||||
@ -15,8 +13,6 @@ class Footer extends Component {
|
||||
|
||||
createElement() {
|
||||
const translate = this.state.translate;
|
||||
const browser = browserName();
|
||||
const feedbackUrl = `https://qsurvey.mozilla.com/s3/Firefox-Send-Product-Feedback?ver=${version}&browser=${browser}`;
|
||||
return html`
|
||||
<footer
|
||||
class="flex flex-col md:flex-row items-start w-full flex-none self-start p-6 md:p-8 font-medium text-xs text-grey-60 dark:text-grey-40 md:items-center justify-between"
|
||||
@ -43,17 +39,6 @@ class Footer extends Component {
|
||||
<li class="m-2">
|
||||
<a href="https://github.com/mozilla/send">GitHub </a>
|
||||
</li>
|
||||
<li class="m-2">
|
||||
<a
|
||||
href="${feedbackUrl}"
|
||||
rel="noreferrer noopener"
|
||||
class="feedback-link"
|
||||
alt="Feedback"
|
||||
target="_blank"
|
||||
>
|
||||
${translate('siteFeedback')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
`;
|
||||
|
@ -5,6 +5,9 @@ const modal = require('./modal');
|
||||
const intro = require('./intro');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.user.loginRequired && !state.user.loggedIn) {
|
||||
emit('signup-cta', 'required');
|
||||
}
|
||||
const archives = state.storage.files
|
||||
.filter(archive => !archive.expired)
|
||||
.map(archive => archiveTile(state, emit, archive));
|
||||
|
@ -21,6 +21,11 @@ module.exports = function(state, emit) {
|
||||
>${state.translate('sendYourFilesLink')}</a
|
||||
>
|
||||
</p>
|
||||
<p class="">
|
||||
<a href="/report" class="link-blue"
|
||||
>${state.translate('reportFile')}</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
|
132
app/ui/report.js
Normal file
132
app/ui/report.js
Normal file
@ -0,0 +1,132 @@
|
||||
const html = require('choo/html');
|
||||
const raw = require('choo/html/raw');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
const REPORTABLES = ['Malware', 'Pii', 'Abuse'];
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
let submitting = false;
|
||||
const file = state.fileInfo;
|
||||
if (!file) {
|
||||
return html`
|
||||
<main class="main">
|
||||
<section
|
||||
class="flex flex-col items-center justify-center h-full w-full p-6 md:p-8 overflow-hidden md:rounded-xl md:shadow-big"
|
||||
>
|
||||
<p class="mb-4 leading-normal">
|
||||
${state.translate('reportUnknownDescription')}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
if (file.reported) {
|
||||
return html`
|
||||
<main class="main">
|
||||
<section
|
||||
class="flex flex-col items-center justify-center h-full w-full p-6 md:p-8 overflow-hidden md:rounded-xl md:shadow-big"
|
||||
>
|
||||
<h1 class="text-center text-3xl font-bold my-2">
|
||||
${state.translate('reportedTitle')}
|
||||
</h1>
|
||||
<p class="max-w-md text-center text-grey-80 leading-normal">
|
||||
${state.translate('reportedDescription')}
|
||||
</p>
|
||||
<img src="${assets.get('notFound.svg')}" class="my-12" />
|
||||
<p class="my-5">
|
||||
<a href="/" class="btn rounded-lg flex items-center" role="button"
|
||||
>${state.translate('okButton')}</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<main class="main">
|
||||
<section
|
||||
class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col w-full max-w-sm h-full mx-auto items-center justify-center"
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-4">
|
||||
${state.translate('reportFile')}
|
||||
</h1>
|
||||
<p class="mb-4 leading-normal font-semibold">
|
||||
${state.translate('reportDescription')}
|
||||
</p>
|
||||
<form onsubmit="${report}" data-no-csrf>
|
||||
<fieldset onchange="${optionChanged}">
|
||||
<ul
|
||||
class="list-none p-4 mb-6 rounded-sm bg-grey-10 dark:bg-black"
|
||||
>
|
||||
${REPORTABLES.map(
|
||||
reportable =>
|
||||
html`
|
||||
<li class="mb-2 leading-normal">
|
||||
<label
|
||||
for="${reportable.toLowerCase()}"
|
||||
class="flex items-center"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="reason"
|
||||
id="${reportable.toLowerCase()}"
|
||||
value="${reportable.toLowerCase()}"
|
||||
class="mr-2 my-2 w-4 h-4"
|
||||
/>
|
||||
${state.translate(`reportReason${reportable}`)}
|
||||
</label>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
<li class="mt-4 mb-2 leading-normal">
|
||||
${raw(
|
||||
replaceLinks(state.translate('reportReasonCopyright'), [
|
||||
'https://www.mozilla.org/about/legal/report-infringement/'
|
||||
])
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
<input
|
||||
type="submit"
|
||||
disabled
|
||||
class="btn rounded-lg w-full flex-shrink-0 focus:outline"
|
||||
title="${state.translate('reportButton')}"
|
||||
value="${state.translate('reportButton')}"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
|
||||
function optionChanged(event) {
|
||||
event.stopPropagation();
|
||||
const button = event.currentTarget.nextElementSibling;
|
||||
button.disabled = false;
|
||||
}
|
||||
|
||||
function report(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
submitting = true;
|
||||
state.fileInfo.reported = true;
|
||||
const form = event.target;
|
||||
emit('report', { reason: form.reason.value });
|
||||
}
|
||||
|
||||
function replaceLinks(str, urls) {
|
||||
let i = 0;
|
||||
const s = str.replace(
|
||||
/<a>([^<]+)<\/a>/g,
|
||||
(m, v) => `<a class="text-blue" href="${urls[i++]}">${v}</a>`
|
||||
);
|
||||
return `<p>${s}</p>`;
|
||||
}
|
||||
};
|
@ -9,11 +9,9 @@ module.exports = function(name, url) {
|
||||
<h1 class="text-3xl font-bold my-4">
|
||||
${state.translate('notifyUploadEncryptDone')}
|
||||
</h1>
|
||||
<p
|
||||
class="font-normal leading-normal text-grey-80 word-break-all dark:text-grey-40"
|
||||
>
|
||||
<p class="font-normal leading-normal text-grey-80 dark:text-grey-40">
|
||||
${state.translate('shareLinkDescription')}<br />
|
||||
${name}
|
||||
<span class="word-break-all">${name}</span>
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -94,6 +94,10 @@ export default class User {
|
||||
: this.limits.ANON.MAX_DOWNLOADS;
|
||||
}
|
||||
|
||||
get loginRequired() {
|
||||
return this.authConfig.fxa_required;
|
||||
}
|
||||
|
||||
async metricId() {
|
||||
return this.loggedIn ? hashId(this.info.uid) : undefined;
|
||||
}
|
||||
|
188
package-lock.json
generated
188
package-lock.json
generated
@ -1144,6 +1144,56 @@
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@peculiar/asn1-schema": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.5.tgz",
|
||||
"integrity": "sha512-VIKJjsgMkv+yyWx3C+D4xo6/NeCg0XFBgNlavtkxELijV+aKAq53du5KkOJbeZtm1nn9CinQKny2PqL8zCfpeA==",
|
||||
"requires": {
|
||||
"@types/asn1js": "^0.0.1",
|
||||
"asn1js": "^2.0.26",
|
||||
"pvtsutils": "^1.0.10",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"@peculiar/json-schema": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.10.tgz",
|
||||
"integrity": "sha512-kbpnG9CkF1y6wwGkW7YtSA+yYK4X5uk4rAwsd1hxiaYE3Hkw2EsGlbGh/COkMLyFf+Fe830BoFiMSB3QnC/ItA==",
|
||||
"requires": {
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"@peculiar/webcrypto": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.1.1.tgz",
|
||||
"integrity": "sha512-Bu2XgOvzirnLcojZYs4KQ8hOLf2ETpa0NL6btQt5NgsAwctI6yVkzgYP+EcG7Mm579RBP+V0LM5rXyMlTVx23A==",
|
||||
"requires": {
|
||||
"@peculiar/asn1-schema": "^2.0.3",
|
||||
"@peculiar/json-schema": "^1.1.10",
|
||||
"pvtsutils": "^1.0.10",
|
||||
"tslib": "^1.11.2",
|
||||
"webcrypto-core": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
|
||||
},
|
||||
"webcrypto-core": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.1.2.tgz",
|
||||
"integrity": "sha512-LxM/dTcXr/ZnwwKLox0tGEOIqvP7KIJ4Hk/fFPX20tr1EgqTmpEFZinmu4FzoGVbs6e4jI1priQKCDrOBD3L6w==",
|
||||
"requires": {
|
||||
"@peculiar/asn1-schema": "^2.0.1",
|
||||
"@peculiar/json-schema": "^1.1.10",
|
||||
"asn1js": "^2.0.26",
|
||||
"pvtsutils": "^1.0.10",
|
||||
"tslib": "^1.11.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@samverschueren/stream-to-observable": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
|
||||
@ -1296,6 +1346,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="
|
||||
},
|
||||
"@types/asn1js": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/asn1js/-/asn1js-0.0.1.tgz",
|
||||
"integrity": "sha1-74uflwjLFjKhw6nNJ3F8qr55O8I=",
|
||||
"requires": {
|
||||
"@types/pvutils": "*"
|
||||
}
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
@ -1343,6 +1401,11 @@
|
||||
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/pvutils": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/pvutils/-/pvutils-0.0.2.tgz",
|
||||
"integrity": "sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w=="
|
||||
},
|
||||
"@types/q": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
|
||||
@ -1966,6 +2029,14 @@
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"asn1js": {
|
||||
"version": "2.0.26",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz",
|
||||
"integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==",
|
||||
"requires": {
|
||||
"pvutils": "^1.0.17"
|
||||
}
|
||||
},
|
||||
"assert": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
|
||||
@ -3662,6 +3733,11 @@
|
||||
"randomfill": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
|
||||
},
|
||||
"css-blank-pseudo": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
|
||||
@ -4277,6 +4353,11 @@
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"denque": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
|
||||
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
@ -4662,11 +4743,6 @@
|
||||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"double-ended-queue": {
|
||||
"version": "2.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
|
||||
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
|
||||
@ -6875,6 +6951,19 @@
|
||||
"stream-events": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"configstore": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
|
||||
"integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
|
||||
"requires": {
|
||||
"dot-prop": "^5.2.0",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"make-dir": "^3.0.0",
|
||||
"unique-string": "^2.0.0",
|
||||
"write-file-atomic": "^3.0.0",
|
||||
"xdg-basedir": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"gaxios": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz",
|
||||
@ -6886,6 +6975,30 @@
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
|
||||
"integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
|
||||
"requires": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-typedarray": "^1.0.0",
|
||||
"signal-exit": "^3.0.2",
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -7109,8 +7222,7 @@
|
||||
"graceful-fs": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
|
||||
},
|
||||
"growl": {
|
||||
"version": "1.10.5",
|
||||
@ -7994,8 +8106,7 @@
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
|
||||
"dev": true
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
@ -13030,6 +13141,19 @@
|
||||
"yargs": "^14.0.0"
|
||||
}
|
||||
},
|
||||
"pvtsutils": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.0.10.tgz",
|
||||
"integrity": "sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw==",
|
||||
"requires": {
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"pvutils": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz",
|
||||
"integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ=="
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||
@ -13361,13 +13485,14 @@
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
|
||||
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz",
|
||||
"integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==",
|
||||
"requires": {
|
||||
"double-ended-queue": "^2.1.0-0",
|
||||
"redis-commands": "^1.2.0",
|
||||
"redis-parser": "^2.6.0"
|
||||
"denque": "^1.4.1",
|
||||
"redis-commands": "^1.5.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"redis-commands": {
|
||||
@ -13375,6 +13500,11 @@
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
|
||||
"integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
|
||||
},
|
||||
"redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
|
||||
},
|
||||
"redis-mock": {
|
||||
"version": "0.47.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.47.0.tgz",
|
||||
@ -13382,9 +13512,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"redis-parser": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
|
||||
"integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
|
||||
"requires": {
|
||||
"redis-errors": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"reduce-css-calc": {
|
||||
"version": "2.1.7",
|
||||
@ -14143,8 +14276,7 @@
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
@ -15885,6 +16017,14 @@
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||
"requires": {
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.21",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
|
||||
@ -16025,6 +16165,14 @@
|
||||
"imurmurhash": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"unique-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||
"integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
|
||||
"requires": {
|
||||
"crypto-random-string": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"unist-util-find-all-after": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz",
|
||||
|
@ -23,11 +23,11 @@
|
||||
"release": "npm-run-all contributors changelog",
|
||||
"test": "npm-run-all test:*",
|
||||
"test:backend": "nyc --reporter=lcovonly mocha --reporter=min test/backend",
|
||||
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js",
|
||||
"test:frontend": "cross-env NODE_ENV=development FXA_REQUIRED=false node test/frontend/runner.js",
|
||||
"test:report": "nyc report --reporter=html",
|
||||
"test-integration": "cross-env NODE_ENV=development wdio test/wdio.docker.conf.js",
|
||||
"circleci-test-integration": "echo 'webdriverio tests need to be updated to node 12'",
|
||||
"start": "npm run clean && cross-env NODE_ENV=development L10N_DEV=true FXA_CLIENT_ID=fced6b5e3f4c66b9 BASE_URL=http://localhost:8080 webpack-dev-server --mode=development",
|
||||
"start": "npm run clean && cross-env NODE_ENV=development L10N_DEV=true FXA_CLIENT_ID=fced6b5e3f4c66b9 BASE_URL=http://localhost:1337 webpack-dev-server --port=1337 --mode=development",
|
||||
"android": "cross-env ANDROID=1 npm start",
|
||||
"prod": "node server/bin/prod.js"
|
||||
},
|
||||
@ -134,6 +134,7 @@
|
||||
"@fluent/bundle": "^0.13.0",
|
||||
"@fluent/langneg": "^0.3.0",
|
||||
"@google-cloud/storage": "^4.1.1",
|
||||
"@peculiar/webcrypto": "^1.1.1",
|
||||
"@sentry/node": "^5.8.0",
|
||||
"aws-sdk": "^2.568.0",
|
||||
"body-parser": "^1.19.0",
|
||||
@ -147,7 +148,7 @@
|
||||
"mkdirp": "^0.5.1",
|
||||
"mozlog": "^2.2.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"redis": "^2.8.0",
|
||||
"redis": "^3.0.2",
|
||||
"selenium-standalone": "^6.15.6",
|
||||
"ua-parser-js": "^0.7.20"
|
||||
},
|
||||
|
@ -1,6 +1,5 @@
|
||||
# Firefox Send is a brand name and should not be localized.
|
||||
title = Firefox Send
|
||||
siteFeedback = Feedback
|
||||
importingFile = Importing…
|
||||
encryptingFile = Encrypting…
|
||||
decryptingFile = Decrypting…
|
||||
@ -109,6 +108,7 @@ legalDateStamp = Version 1.0, dated March 12, 2019
|
||||
# A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m"
|
||||
expiresDaysHoursMinutes = { $days }d { $hours }h { $minutes }m
|
||||
addFilesButton = Select files to upload
|
||||
trustWarningMessage = Make sure you trust your recipient when sharing sensitive data.
|
||||
uploadButton = Upload
|
||||
# the first part of the string 'Drag and drop files or click to send up to 1GB'
|
||||
dragAndDropFiles = Drag and drop files
|
||||
@ -145,3 +145,33 @@ shareLinkButton = Share link
|
||||
shareMessage = Download “{ $name }” with { -send-brand }: simple, safe file sharing
|
||||
trailheadPromo = There is a way to protect your privacy. Join Firefox.
|
||||
learnMore = Learn more.
|
||||
downloadFlagged = This link has been disabled for violating the terms of service.
|
||||
downloadConfirmTitle = One more thing
|
||||
downloadConfirmDescription = Make sure you trust the person who sent you this file because we can’t verify that it will not harm your device.
|
||||
# This string has a special case for '1' and [other] (default). If necessary for
|
||||
# your language, you can add {$count} to your translations and use the
|
||||
# standard CLDR forms, or only use the form for [other] if both strings should
|
||||
# be identical.
|
||||
downloadTrustCheckbox =
|
||||
{ $count ->
|
||||
[one] I trust the person who sent this file
|
||||
*[other] I trust the person who sent these files
|
||||
}
|
||||
# This string has a special case for '1' and [other] (default). If necessary for
|
||||
# your language, you can add {$count} to your translations and use the
|
||||
# standard CLDR forms, or only use the form for [other] if both strings should
|
||||
# be identical.
|
||||
reportFile =
|
||||
{ $count ->
|
||||
[one] Report this file as suspicious
|
||||
*[other] Report these files as suspicious
|
||||
}
|
||||
reportDescription = Help us understand what’s going on. What do you think is wrong with these files?
|
||||
reportUnknownDescription = Please go to the url of the link you wish to report and click “{ reportFile }”.
|
||||
reportButton = Report
|
||||
reportReasonMalware = These files contain malware or are part of a phishing attack.
|
||||
reportReasonPii = These files contain personally identifiable information about me.
|
||||
reportReasonAbuse = These files contain illegal or abusive content.
|
||||
reportReasonCopyright = To report copyright or trademark infringement, use the process described at <a>this page</a>.
|
||||
reportedTitle = Files Reported
|
||||
reportedDescription = Thank you. We have received your report on these files.
|
||||
|
@ -96,6 +96,28 @@ function statDeleteEvent(data) {
|
||||
return sendBatch([event]);
|
||||
}
|
||||
|
||||
function statReportEvent(data) {
|
||||
const loc = location(data.ip);
|
||||
const event = {
|
||||
session_id: -1,
|
||||
country: loc.country,
|
||||
region: loc.state,
|
||||
user_id: userId(data.id, data.owner),
|
||||
app_version: pkg.version,
|
||||
time: truncateToHour(Date.now()),
|
||||
event_type: 'server_report',
|
||||
event_properties: {
|
||||
reason: data.reason,
|
||||
agent: data.agent,
|
||||
download_limit: data.dlimit,
|
||||
download_count: data.download_count,
|
||||
ttl: data.ttl
|
||||
},
|
||||
event_id: data.download_count + 1
|
||||
};
|
||||
return sendBatch([event]);
|
||||
}
|
||||
|
||||
function clientEvent(event, ua, language, session_id, deltaT, platform, ip) {
|
||||
const loc = location(ip);
|
||||
const ep = event.event_properties || {};
|
||||
@ -173,6 +195,7 @@ module.exports = {
|
||||
statUploadEvent,
|
||||
statDownloadEvent,
|
||||
statDeleteEvent,
|
||||
statReportEvent,
|
||||
clientEvent,
|
||||
sendBatch
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ module.exports = function(app, devServer) {
|
||||
expressWs(wsapp, null, { perMessageDeflate: false });
|
||||
routes(wsapp);
|
||||
wsapp.ws('/api/ws', require('../routes/ws'));
|
||||
wsapp.listen(8081, config.listen_address);
|
||||
wsapp.listen(1338, config.listen_address);
|
||||
|
||||
assets.setMiddleware(devServer.middleware);
|
||||
app.use(morgan('dev', { stream: process.stderr }));
|
||||
|
@ -120,6 +120,11 @@ const conf = convict({
|
||||
default: '',
|
||||
env: 'SENTRY_DSN'
|
||||
},
|
||||
sentry_host: {
|
||||
format: String,
|
||||
default: 'https://sentry.prod.mozaws.net',
|
||||
env: 'SENTRY_HOST'
|
||||
},
|
||||
env: {
|
||||
format: ['production', 'development', 'test'],
|
||||
default: 'development',
|
||||
@ -150,9 +155,14 @@ const conf = convict({
|
||||
default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`,
|
||||
env: 'FILE_DIR'
|
||||
},
|
||||
fxa_required: {
|
||||
format: Boolean,
|
||||
default: true,
|
||||
env: 'FXA_REQUIRED'
|
||||
},
|
||||
fxa_url: {
|
||||
format: 'url',
|
||||
default: 'https://send-fxa.dev.lcip.org',
|
||||
default: 'http://localhost:3030',
|
||||
env: 'FXA_URL'
|
||||
},
|
||||
fxa_client_id: {
|
||||
|
53
server/keychain.js
Normal file
53
server/keychain.js
Normal file
@ -0,0 +1,53 @@
|
||||
const { Crypto } = require('@peculiar/webcrypto');
|
||||
const crypto = new Crypto();
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
module.exports = class Keychain {
|
||||
constructor(secretKeyB64) {
|
||||
if (secretKeyB64) {
|
||||
this.rawSecret = new Uint8Array(Buffer.from(secretKeyB64, 'base64'));
|
||||
} else {
|
||||
throw new Error('key is required');
|
||||
}
|
||||
this.secretKeyPromise = crypto.subtle.importKey(
|
||||
'raw',
|
||||
this.rawSecret,
|
||||
'HKDF',
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('metadata'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async decryptMetadata(ciphertext) {
|
||||
const metaKey = await this.metaKeyPromise;
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: new Uint8Array(12),
|
||||
tagLength: 128
|
||||
},
|
||||
metaKey,
|
||||
ciphertext
|
||||
);
|
||||
return JSON.parse(decoder.decode(plaintext));
|
||||
}
|
||||
};
|
@ -7,6 +7,9 @@ class Metadata {
|
||||
this.metadata = obj.metadata;
|
||||
this.auth = obj.auth;
|
||||
this.nonce = obj.nonce;
|
||||
this.flagged = !!obj.flagged;
|
||||
this.dead = !!obj.dead;
|
||||
this.key = obj.key;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ module.exports = {
|
||||
if (id && ownerToken) {
|
||||
try {
|
||||
req.meta = await storage.metadata(id);
|
||||
if (!req.meta) {
|
||||
if (!req.meta || req.meta.dead) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const metaOwner = Buffer.from(req.meta.owner, 'utf8');
|
||||
|
@ -6,7 +6,7 @@ module.exports = async function(req, res) {
|
||||
const id = req.params.id;
|
||||
const meta = req.meta;
|
||||
const ttl = await storage.ttl(id);
|
||||
await storage.del(id);
|
||||
await storage.kill(id);
|
||||
res.sendStatus(200);
|
||||
statDeleteEvent({
|
||||
id,
|
||||
|
@ -7,6 +7,9 @@ module.exports = async function(req, res) {
|
||||
const id = req.params.id;
|
||||
try {
|
||||
const meta = req.meta;
|
||||
if (meta.dead || meta.flagged) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const fileStream = await storage.get(id);
|
||||
let cancelled = false;
|
||||
|
||||
@ -33,7 +36,7 @@ module.exports = async function(req, res) {
|
||||
});
|
||||
try {
|
||||
if (dl >= dlimit) {
|
||||
await storage.del(id);
|
||||
await storage.kill(id);
|
||||
} else {
|
||||
await storage.incrementField(id, 'dl');
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ const storage = require('../storage');
|
||||
module.exports = async (req, res) => {
|
||||
try {
|
||||
const meta = await storage.metadata(req.params.id);
|
||||
if (!meta || meta.dead) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
|
||||
res.send({
|
||||
requiresPassword: meta.pwd
|
||||
|
@ -32,55 +32,57 @@ module.exports = function(app) {
|
||||
});
|
||||
if (!IS_DEV) {
|
||||
let csp = {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
'wss://*.dev.lcip.org',
|
||||
'wss://*.send.nonprod.cloudops.mozgcp.net',
|
||||
config.base_url.replace(/^https:\/\//, 'wss://'),
|
||||
'https://*.dev.lcip.org',
|
||||
'https://accounts.firefox.com',
|
||||
'https://*.accounts.firefox.com',
|
||||
'https://sentry.prod.mozaws.net'
|
||||
],
|
||||
imgSrc: [
|
||||
"'self'",
|
||||
'https://*.dev.lcip.org',
|
||||
'https://firefoxusercontent.com',
|
||||
'https://secure.gravatar.com'
|
||||
],
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
function(req) {
|
||||
return `'nonce-${req.cspNonce}'`;
|
||||
}
|
||||
],
|
||||
formAction: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
reportUri: '/__cspreport__'
|
||||
}
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
config.base_url.replace(/^https:\/\//, 'wss://')
|
||||
],
|
||||
imgSrc: ["'self'"],
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
function(req) {
|
||||
return `'nonce-${req.cspNonce}'`;
|
||||
}
|
||||
],
|
||||
formAction: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
reportUri: '/__cspreport__'
|
||||
}
|
||||
|
||||
csp.directives.connectSrc.push(config.base_url.replace(/^https:\/\//,'wss://'))
|
||||
if(config.fxa_csp_oauth_url != ""){
|
||||
csp.directives.connectSrc.push(config.fxa_csp_oauth_url)
|
||||
};
|
||||
if (config.fxa_client_id) {
|
||||
csp.directives.connectSrc.push('https://accounts.firefox.com');
|
||||
csp.directives.connectSrc.push('https://*.accounts.firefox.com');
|
||||
csp.directives.imgSrc.push('https://firefoxusercontent.com');
|
||||
csp.directives.imgSrc.push('https://secure.gravatar.com');
|
||||
}
|
||||
if(config.fxa_csp_content_url != "" ){
|
||||
csp.directives.connectSrc.push(config.fxa_csp_content_url)
|
||||
if (config.sentry_id) {
|
||||
csp.directives.connectSrc.push(config.sentry_host);
|
||||
}
|
||||
if(config.fxa_csp_profile_url != "" ){
|
||||
csp.directives.connectSrc.push(config.fxa_csp_profile_url)
|
||||
if (
|
||||
config.base_url.test(/^https:\/\/.*\.dev\.lcip\.org$/) ||
|
||||
config.base_url.test(
|
||||
/^https:\/\/.*\.send\.nonprod\.cloudops\.mozgcp\.net$/
|
||||
)
|
||||
) {
|
||||
csp.directives.connectSrc.push('https://*.dev.lcip.org');
|
||||
csp.directives.imgSrc.push('https://*.dev.lcip.org');
|
||||
}
|
||||
if(config.fxa_csp_profileimage_url != ""){
|
||||
csp.directives.imgSrc.push(config.fxa_csp_profileimage_url)
|
||||
if (config.fxa_csp_oauth_url != '') {
|
||||
csp.directives.connectSrc.push(config.fxa_csp_oauth_url);
|
||||
}
|
||||
if (config.fxa_csp_content_url != '') {
|
||||
csp.directives.connectSrc.push(config.fxa_csp_content_url);
|
||||
}
|
||||
if (config.fxa_csp_profile_url != '') {
|
||||
csp.directives.connectSrc.push(config.fxa_csp_profile_url);
|
||||
}
|
||||
if (config.fxa_csp_profileimage_url != '') {
|
||||
csp.directives.imgSrc.push(config.fxa_csp_profileimage_url);
|
||||
}
|
||||
|
||||
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy(csp)
|
||||
);
|
||||
app.use(helmet.contentSecurityPolicy(csp));
|
||||
}
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
@ -101,6 +103,7 @@ module.exports = function(app) {
|
||||
app.get('/oauth', language, pages.blank);
|
||||
app.get('/legal', language, pages.legal);
|
||||
app.get('/login', language, pages.index);
|
||||
app.get('/report', language, pages.blank);
|
||||
app.get('/app.webmanifest', language, require('./webmanifest'));
|
||||
app.get(`/download/:id${ID_REGEX}`, language, pages.download);
|
||||
app.get('/unsupported/:reason', language, pages.unsupported);
|
||||
@ -114,7 +117,7 @@ module.exports = function(app) {
|
||||
app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata'));
|
||||
app.get('/api/filelist/:id([\\w-]{16})', auth.fxa, filelist.get);
|
||||
app.post('/api/filelist/:id([\\w-]{16})', auth.fxa, filelist.post);
|
||||
app.post('/api/upload', auth.fxa, require('./upload'));
|
||||
// app.post('/api/upload', auth.fxa, require('./upload'));
|
||||
app.post(`/api/delete/:id${ID_REGEX}`, auth.owner, require('./delete'));
|
||||
app.post(`/api/password/:id${ID_REGEX}`, auth.owner, require('./password'));
|
||||
app.post(
|
||||
@ -124,6 +127,7 @@ module.exports = function(app) {
|
||||
require('./params')
|
||||
);
|
||||
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
|
||||
app.post(`/api/report/:id${ID_REGEX}`, require('./report'));
|
||||
app.post('/api/metrics', require('./metrics'));
|
||||
app.get('/__version__', function(req, res) {
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
|
@ -4,9 +4,13 @@ module.exports = async function(req, res) {
|
||||
const id = req.params.id;
|
||||
const meta = req.meta;
|
||||
try {
|
||||
if (meta.dead && !meta.flagged) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const ttl = await storage.ttl(id);
|
||||
res.send({
|
||||
metadata: meta.metadata,
|
||||
flagged: !!meta.flagged,
|
||||
finalDownload: meta.dl + 1 === meta.dlimit,
|
||||
ttl
|
||||
});
|
||||
|
@ -23,14 +23,17 @@ module.exports = {
|
||||
const id = req.params.id;
|
||||
const appState = await state(req);
|
||||
try {
|
||||
const { nonce, pwd } = await storage.metadata(id);
|
||||
const { nonce, pwd, dead, flagged } = await storage.metadata(id);
|
||||
if (dead && !flagged) {
|
||||
return next();
|
||||
}
|
||||
res.set('WWW-Authenticate', `send-v1 ${nonce}`);
|
||||
res.send(
|
||||
stripEvents(
|
||||
routes().toString(
|
||||
`/download/${id}`,
|
||||
Object.assign(appState, {
|
||||
downloadMetadata: { nonce, pwd }
|
||||
downloadMetadata: { nonce, pwd, flagged }
|
||||
})
|
||||
)
|
||||
)
|
||||
|
39
server/routes/report.js
Normal file
39
server/routes/report.js
Normal file
@ -0,0 +1,39 @@
|
||||
const storage = require('../storage');
|
||||
const Keychain = require('../keychain');
|
||||
const { statReportEvent } = require('../amplitude');
|
||||
|
||||
module.exports = async function(req, res) {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const meta = await storage.metadata(id);
|
||||
if (meta.flagged) {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
try {
|
||||
const key = req.body.key;
|
||||
const keychain = new Keychain(key);
|
||||
const metadata = await keychain.decryptMetadata(
|
||||
Buffer.from(meta.metadata, 'base64')
|
||||
);
|
||||
if (metadata.manifest) {
|
||||
storage.flag(id, key);
|
||||
statReportEvent({
|
||||
id,
|
||||
ip: req.ip,
|
||||
owner: meta.owner,
|
||||
reason: req.body.reason,
|
||||
download_limit: meta.dlimit,
|
||||
download_count: meta.dl,
|
||||
agent: req.ua.browser.name || req.ua.ua.substring(0, 6)
|
||||
});
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
res.sendStatus(400);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.sendStatus(400);
|
||||
}
|
||||
} catch (e) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
};
|
@ -46,7 +46,8 @@ module.exports = function(ws, req) {
|
||||
!auth ||
|
||||
timeLimit <= 0 ||
|
||||
timeLimit > maxExpireSeconds ||
|
||||
dlimit > maxDownloads
|
||||
dlimit > maxDownloads ||
|
||||
(config.fxa_required && !user)
|
||||
) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
|
@ -15,7 +15,11 @@ module.exports = async function(req) {
|
||||
try {
|
||||
authConfig = await getFxaConfig();
|
||||
authConfig.client_id = config.fxa_client_id;
|
||||
authConfig.fxa_required = config.fxa_required;
|
||||
} catch (e) {
|
||||
if (config.auth_required) {
|
||||
throw new Error('fxa_required is set but no config was found');
|
||||
}
|
||||
// continue without accounts
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,15 @@ class DB {
|
||||
}
|
||||
|
||||
async getPrefixedId(id) {
|
||||
const prefix = await this.redis.hgetAsync(id, 'prefix');
|
||||
const [prefix, dead, flagged] = await this.redis.hmgetAsync(
|
||||
id,
|
||||
'prefix',
|
||||
'dead',
|
||||
'flagged'
|
||||
);
|
||||
if (dead || flagged) {
|
||||
throw new Error('id not available');
|
||||
}
|
||||
return `${prefix}-${id}`;
|
||||
}
|
||||
|
||||
@ -51,9 +59,10 @@ class DB {
|
||||
const prefix = getPrefix(expireSeconds);
|
||||
const filePath = `${prefix}-${id}`;
|
||||
await this.storage.set(filePath, file);
|
||||
this.redis.hset(id, 'prefix', prefix);
|
||||
if (meta) {
|
||||
this.redis.hmset(id, meta);
|
||||
this.redis.hmset(id, { prefix, ...meta });
|
||||
} else {
|
||||
this.redis.hset(id, 'prefix', prefix);
|
||||
}
|
||||
this.redis.expire(id, expireSeconds);
|
||||
}
|
||||
@ -66,6 +75,16 @@ class DB {
|
||||
this.redis.hincrby(id, key, increment);
|
||||
}
|
||||
|
||||
kill(id) {
|
||||
this.redis.hset(id, 'dead', 1);
|
||||
}
|
||||
|
||||
async flag(id, key) {
|
||||
// this.redis.persist(id);
|
||||
this.redis.hmset(id, { flagged: 1, key });
|
||||
this.redis.sadd('flagged', id);
|
||||
}
|
||||
|
||||
async del(id) {
|
||||
const filePath = await this.getPrefixedId(id);
|
||||
this.storage.del(filePath);
|
||||
|
@ -23,6 +23,8 @@ module.exports = function(config) {
|
||||
client.ttlAsync = promisify(client.ttl);
|
||||
client.hgetallAsync = promisify(client.hgetall);
|
||||
client.hgetAsync = promisify(client.hget);
|
||||
client.hmgetAsync = promisify(client.hmget);
|
||||
client.pingAsync = promisify(client.ping);
|
||||
client.existsAsync = promisify(client.exists);
|
||||
return client;
|
||||
};
|
||||
|
@ -259,6 +259,14 @@ module.exports = {
|
||||
full: '100%',
|
||||
screen: '100vh'
|
||||
},
|
||||
flex: {
|
||||
'1': '1 1 0%',
|
||||
auto: '1 1 auto',
|
||||
initial: '0 1 auto',
|
||||
none: 'none',
|
||||
half: '0 0 50%',
|
||||
full: '0 0 100%'
|
||||
},
|
||||
minWidth: {
|
||||
'0': '0',
|
||||
full: '100%'
|
||||
|
@ -2,7 +2,7 @@ const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire').noCallThru();
|
||||
|
||||
const storage = {
|
||||
del: sinon.stub(),
|
||||
kill: sinon.stub(),
|
||||
ttl: sinon.stub()
|
||||
};
|
||||
|
||||
@ -24,19 +24,19 @@ const delRoute = proxyquire('../../server/routes/delete', {
|
||||
|
||||
describe('/api/delete', function() {
|
||||
afterEach(function() {
|
||||
storage.del.reset();
|
||||
storage.kill.reset();
|
||||
});
|
||||
|
||||
it('calls storage.del with the id parameter', async function() {
|
||||
it('calls storage.kill with the id parameter', async function() {
|
||||
const req = request('x');
|
||||
const res = response();
|
||||
await delRoute(req, res);
|
||||
sinon.assert.calledWith(storage.del, 'x');
|
||||
sinon.assert.calledWith(storage.kill, 'x');
|
||||
sinon.assert.calledWith(res.sendStatus, 200);
|
||||
});
|
||||
|
||||
it('sends a 404 on failure', async function() {
|
||||
storage.del.returns(Promise.reject(new Error()));
|
||||
storage.kill.returns(Promise.reject(new Error()));
|
||||
const res = response();
|
||||
await delRoute(request('x'), res);
|
||||
sinon.assert.calledWith(res.sendStatus, 404);
|
||||
|
@ -6,7 +6,7 @@ const storage = {
|
||||
length: sinon.stub()
|
||||
};
|
||||
|
||||
function request(id, meta) {
|
||||
function request(id, meta = {}) {
|
||||
return {
|
||||
params: { id },
|
||||
meta
|
||||
|
@ -133,7 +133,12 @@ describe('Storage', function() {
|
||||
};
|
||||
await storage.set('x', null, m);
|
||||
const meta = await storage.metadata('x');
|
||||
assert.deepEqual(meta, m);
|
||||
assert.deepEqual(meta, {
|
||||
...m,
|
||||
dead: false,
|
||||
flagged: false,
|
||||
key: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -207,7 +207,7 @@ const web = {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api/ws': {
|
||||
target: 'ws://localhost:8081',
|
||||
target: 'ws://localhost:1338',
|
||||
ws: true,
|
||||
secure: false
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user